123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- # Copyright (c) 2016 Anki, Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License in the file LICENSE.txt or at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
-
- '''Event dispatch system.
-
- The SDK is based around the dispatch and observation of events.
- Objects inheriting from the :class:`Dispatcher` generate and
- dispatch events as the state of the robot and its world are updated.
-
- For example the :class:`cozmo.objects.LightCube` class generates an
- :class:`~cozmo.objects.EvtObjectTapped` event anytime the cube the object
- represents is tapped.
-
- The event can be observed in a number of different ways:
-
- #. By calling the :meth:`~Dispatcher.wait_for` method on the object to observe.
- This will wait until the specific event has been sent to that object and
- return the generated event.
- #. By calling :meth:`~Dispatcher.add_event_handler` on the object
- to observe, which will cause the supplied function to be called every time
- the specified event occurs (use the :func:`oneshot` decorator
- to only have the handler called once)
- #. By sub-classing a type and implementing a receiver method.
- For example, subclass the :class:`cozmo.objects.LightCube` type and implement `evt_object_tapped`.
- Note that the factory attribute would need to be updated on the
- generating class for your type to be used by the SDK.
- For example, :attr:`~cozmo.world.World.light_cube_factory` in this example.
- #. By subclassing a type and implementing a default receiver method.
- Events not dispatched to an explicit receiver method are dispatched to
- `recv_default_handler`.
-
- Events are dispatched to a target object (by calling :meth:`dispatch_event`
- on the receiving object). In line with the above, upon receiving an event,
- the object will:
-
- #. Dispatch the event to any handlers which have explicitly registered interest
- in the event (or a superclass of the event) via
- :meth:`~Dispatcher.add_event_handler` or via :meth:`Dispatcher.wait_for`
- #. Dispatch the event to any "children" of the object (see below)
- #. Dispatch the event to method handlers on the receiving object, or the
- `recv_default_handler` if it has no matching handler
- #. Dispatch the event to the parent of the object (if any), and in turn onto
- the parent's parents.
-
- Any handler may raise a :class:`~cozmo.exceptions.StopPropogation` exception
- to prevent the event reaching any subsequent handlers (but generally should
- have no need to do so).
-
- Child objects receive all events that are sent to the originating object
- (which may have multiple children).
-
- Originating objects may have one parent object, which receives all events sent
- to its child.
-
- For example, :class:`cozmo.robot.Robot` creates a :class:`cozmo.world.World`
- object and sets itself as a parent and the World as the child; both receive
- events sent to the other.
-
- The World class creates individual :class:`cozmo.objects.ObservableObject` objects
- as they are discovered and makes itself a parent, so as to receive all events
- sent to the child. However, it does not make those ObservableObject objects children
- for the sake of message dispatch as they only need to receive a small subset
- of messages the World object receives.
- '''
-
- # __all__ should order by constants, event classes, other classes, functions.
- __all__ = ['Event', 'Dispatcher', 'Filter', 'Handler',
- 'oneshot', 'filter_handler', 'wait_for_first']
-
-
- import asyncio
- import collections
- import inspect
- import re
- import weakref
-
- from . import base
- from . import exceptions
- from . import logger
-
-
- # from https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
- _first_cap_re = re.compile('(.)([A-Z][a-z]+)')
- _all_cap_re = re.compile('([a-z0-9])([A-Z])')
- def _uncamelcase(name):
- s1 = _first_cap_re.sub(r'\1_\2', name)
- return _all_cap_re.sub(r'\1_\2', s1).lower()
-
- registered_events = {}
-
- active_dispatchers = weakref.WeakSet()
-
- class _rprop:
- def __init__(self, value):
- self._value = value
- def __get__(self, instance, owner):
- return self._value
-
- class docstr(str):
- @property
- def __doc__(self):
- return self.__str__()
-
-
- class _AutoRegister(type):
- '''helper to automatically register event classes wherever they're defined
- without requiring a class decorator'''
-
- def __new__(mcs, name, bases, attrs, **kw):
- if name in ('Event',):
- return super().__new__(mcs, name, bases, attrs, **kw)
-
- if not (name.startswith('Evt') or name.startswith('_Evt') or name.startswith('_Msg')):
- raise ValueError('Event class names must begin with "Evt (%s)"' % name)
-
- if '__doc__' not in attrs:
- raise ValueError('Event classes must have a docstring')
-
- props = set()
- for base in bases:
- if hasattr(base, '_props'):
- props.update(base._props)
-
- newattrs = {'_internal': False}
- for k, v in attrs.items():
- if k[0] == '_':
- newattrs[k] = v
- continue
- if k in props:
- raise ValueError("Event class %s duplicates property %s defined in superclass" % (mcs, k))
- props.add(k)
- newattrs[k] = docstr(v)
- newattrs['_props'] = props
- newattrs['_props_sorted'] = sorted(props)
-
- if name[0] == '_':
- newattrs['_internal'] = True
- name = name[1:]
-
- # create a read only property for the event name
- newattrs['event_name'] = _rprop(name)
- return super().__new__(mcs, name, bases, newattrs, **kw)
-
- def __init__(cls, name, bases, attrs, **kw):
- if name in registered_events:
- raise ValueError("Duplicate event name %s (%s duplicated by %s)"
- % (name, _full_qual_name(cls), _full_qual_name(registered_events[name])))
- registered_events[name] = cls
- super().__init__(name, bases, attrs, **kw)
-
-
- def _full_qual_name(obj):
- return obj.__module__ + '.' + obj.__qualname__
-
-
- class Event(metaclass=_AutoRegister):
- '''An event representing an action that has occurred.
-
- Instances of an Event have attributes set to values passed to the event.
-
- For example, :class:`cozmo.objects.EvtObjectTapped` defines obj and tap_count
- parameters which can be accessed as ``evt.obj`` and ``evt.tap_count``.
- '''
-
- #_first_raised_by = "The object that generated the event"
- #_last_raised_by = "The object that last relayed the event to the dispatched handler"
-
- #pylint: disable=no-member
- # Event Metaclass raises "no-member" pylint errors in pylint within this scope.
-
- def __init__(self, **kwargs):
- unset = self._props.copy()
- for k, v in kwargs.items():
- if k not in self._props:
- raise ValueError("Event %s has no parameter called %s" % (self.event_name, k))
- setattr(self, k, v)
- unset.remove(k)
- for k in unset:
- setattr(self, k, None)
- self._delivered_to = set()
-
- def __repr__(self):
- kvs = {'name': self.event_name}
- for k in self._props_sorted:
- kvs[k] = getattr(self, k)
- return '<%s %s>' % (self.__class__.__name__, ' '.join(['%s=%s' % kv for kv in kvs.items()]),)
-
- def _params(self):
- return {k: getattr(self, k) for k in self._props}
-
- @classmethod
- def _handler_method_name(cls):
- name = 'recv_' + _uncamelcase(cls.event_name)
- if cls._internal:
- name = '_' + name
- return name
-
- def _dispatch_to_func(self, f):
- return f(self, **self._params())
-
- def _dispatch_to_obj(self, obj, fallback_to_default=True):
- for cls in self._parent_event_classes():
- f = getattr(obj, cls._handler_method_name(), None)
- if f and not self._is_filtered(f):
- return self._dispatch_to_func(f)
-
- if fallback_to_default:
- name = 'recv_default_handler'
- if self._internal:
- name = '_' + name
- f = getattr(obj, name, None)
- if f and not self._is_filtered(f):
- return f(self, **self._params())
-
- def _dispatch_to_future(self, fut):
- if not fut.done():
- fut.set_result(self)
-
- def _is_filtered(self, f):
- filters = getattr(f, '_handler_filters', None)
- if filters is None:
- return False
- for filter in filters:
- if filter(self):
- return False
- return True
-
- def _parent_event_classes(self):
- for cls in self.__class__.__mro__:
- if cls != Event and issubclass(cls, Event):
- yield cls
-
-
- def _register_dynamic_event_type(event_name, attrs):
- return type(event_name, (Event,), attrs)
-
-
- class Handler(collections.namedtuple('Handler', 'obj evt f')):
- '''A Handler is returned by :meth:`Dispatcher.add_event_handler`
-
- The handler can be disabled at any time by calling its :meth:`disable`
- method.
- '''
- __slots__ = ()
-
- def disable(self):
- '''Removes the handler from the object it was originally registered with.'''
- return self.obj.remove_event_handler(self.evt, self.f)
-
- @property
- def oneshot(self):
- '''bool: True if the wrapped handler function will only be called once.'''
- return getattr(self.f, '_oneshot_handler', False)
-
-
- class NullHandler(Handler):
- def disable(self):
- pass
-
-
- class Dispatcher(base.Base):
- '''Mixin to provide event dispatch handling.'''
-
- def __init__(self, *a, dispatch_parent=None, loop=None, **kw):
- super().__init__(**kw)
- active_dispatchers.add(self)
- self._dispatch_parent = dispatch_parent
- self._dispatch_children = []
- self._dispatch_handlers = collections.defaultdict(list)
- if not loop:
- raise ValueError("Loop was not supplied to "+self.__class__.__name__)
- self._loop = loop or asyncio.get_event_loop()
- self._dispatcher_running = True
-
- def _set_parent_dispatcher(self, parent):
- self._dispatch_parent = parent
-
- def _add_child_dispatcher(self, child):
- self._dispatch_children.append(child)
-
- def _stop_dispatcher(self):
- """Stop dispatching events - call before closing the connection to prevent stray dispatched events"""
- self._dispatcher_running = False
-
- def add_event_handler(self, event, f):
- """Register an event handler to be notified when this object receives a type of Event.
-
- Expects a subclass of Event as the first argument. If the class has
- subclasses then the handler will be notified for events of that subclass too.
- For example, adding a handler for :class:`~cozmo.action.EvtActionCompleted`
- will cause the handler to also be notified for
- :class:`~cozmo.anim.EvtAnimationCompleted` as it's a subclass.
-
- Callable handlers (e.g. functions) are called with a first argument
- containing an Event instance and the remaining keyword arguments set as
- the event parameters.
-
- For example, ``def my_ontap_handler(evt, *, obj, tap_count, **kwargs)``
- or ``def my_ontap_handler(evt, obj=None, tap_count=None, **kwargs)``
-
- It's recommended that a ``**kwargs`` parameter be included in the
- definition so that future expansion of event parameters do not cause
- the handler to fail.
-
- Callable handlers may raise an events.StopProgation exception to prevent
- other handlers listening to the same event from being triggered.
-
- :class:`asyncio.Future` handlers are called with a result set to the event.
-
- Args:
- event (:class:`Event`): A subclass of :class:`Event` (not an instance of that class)
- f (callable): A callable or :class:`asyncio.Future` to execute when the event is received
- Raises:
- :class:`TypeError`: An invalid event type was supplied
-
- """
- if not issubclass(event, Event):
- raise TypeError("event must be a subclass of Event (not an instance)")
-
- if not self._dispatcher_running:
- return NullHandler(self, event, f)
-
- if isinstance(f, asyncio.Future):
- # futures can only be called once.
- f = oneshot(f)
-
- handler = Handler(self, event, f)
- self._dispatch_handlers[event.event_name].append(handler)
- return handler
-
- def remove_event_handler(self, event, f):
- """Remove an event handler for this object.
-
- Args:
- event (:class:`Event`): The event class, or an instance thereof,
- used with register_event_handler.
- f (callable or :class:`Handler`): The callable object that was
- passed as a handler to :meth:`add_event_handler`, or a
- :class:`Handler` instance that was returned by
- :meth:`add_event_handler`.
- Raises:
- :class:`ValueError`: No matching handler found.
- """
- if not (isinstance(event, Event) or (isinstance(event, type) and issubclass(event, Event))):
- raise TypeError("event must be a subclasss or instance of Event")
-
- if isinstance(f, Handler):
- for i, h in enumerate(self._dispatch_handlers[event.event_name]):
- if h == f:
- del self._dispatch_handlers[event.event_name][i]
- return
- else:
- for i, h in enumerate(self._dispatch_handlers[event.event_name]):
- if h.f == f:
- del self._dispatch_handlers[event.event_name][i]
- return
- raise ValueError("No matching handler found for %s (%s)" % (event.event_name, f) )
-
- def dispatch_event(self, event, **kw):
- '''Dispatches a single event to registered handlers.
-
- Not generally called from user-facing code.
-
- Args:
- event (:class:`Event`): An class or instance of :class:`Event`
- kw (dict): If a class is passed to event, then the remaining keywords
- are passed to it to create an instance of the event.
- Returns:
- A :class:`asyncio.Task` or :class:`asyncio.Future` that will
- complete once all event handlers have been called.
- Raises:
- :class:`TypeError` if an invalid event is supplied.
- '''
-
- if not self._dispatcher_running:
- return
-
- event_cls = event
- if not isinstance(event, Event):
- if not isinstance(event, type) or not issubclass(event, Event):
- raise TypeError("events must be a subclass or instance of Event")
- # create an instance of the event if passed a class
- event = event(**kw)
- else:
- event_cls = event.__class__
-
- if id(self) in event._delivered_to:
- return
- event._delivered_to.add(id(self))
-
- handlers = set()
- for cls in event._parent_event_classes():
- for handler in self._dispatch_handlers[cls.event_name]:
- if event._is_filtered(handler.f):
- continue
-
- if getattr(handler.f, '_oneshot_handler', False):
- # Disable oneshot events prior to actual dispatch
- handler.disable()
- handlers.add(handler)
-
- return asyncio.ensure_future(self._dispatch_event(event, handlers), loop=self._loop)
-
- async def _dispatch_event(self, event, handlers):
- # iterate through events from child->parent
- # update the dispatched_to set for each event so each handler
- # only receives the most specific event if they are monitoring for both.
-
- try:
- # dispatch to local handlers
- for handler in handlers:
- if isinstance(handler.f, asyncio.Future):
- event._dispatch_to_future(handler.f)
- else:
- result = event._dispatch_to_func(handler.f)
- if asyncio.iscoroutine(result):
- await result
-
- # dispatch to children
- for child in self._dispatch_children:
- child.dispatch_event(event)
-
- # dispatch to self methods
- result = event._dispatch_to_obj(self)
- if asyncio.iscoroutine(result):
- await result
-
- # dispatch to parent dispatcher
- if self._dispatch_parent:
- self._dispatch_parent.dispatch_event(event)
-
- except exceptions.StopPropogation:
- pass
-
- def _abort_event_futures(self, exc):
- '''Sets an exception on all pending Future handlers
-
- This prevents coroutines awaiting a Future from blocking forever
- should a hard failure occur with the connection.
- '''
- handlers = set()
- for evh in self._dispatch_handlers.values():
- for h in evh:
- handlers.add(h)
-
- for handler in handlers:
- if isinstance(handler.f, asyncio.Future):
- if not handler.f.done():
- handler.f.set_exception(exc)
- handler.disable()
-
- async def wait_for(self, event_or_filter, timeout=30):
- '''Waits for the specified event to be sent to the current object.
-
- Args:
- event_or_filter (:class:`Event`): Either a :class:`Event` class
- or a :class:`Filter` instance to wait to trigger
- timeout: Maximum time to wait for the event. Pass None to wait indefinitely.
- Returns:
- The :class:`Event` instance that was dispatched
- Raises:
- :class:`asyncio.TimeoutError`
- '''
- f = asyncio.Future(loop=self._loop) # replace with loop.create_future in 3.5.2
- # TODO: add a timer that logs every 5 seconds that the event is still being
- # waited on. Will help novice programmers realize why their program is hanging.
- f = oneshot(f)
-
- if isinstance(event_or_filter, Filter):
- f = filter_handler(event_or_filter)(f)
- event = event_or_filter._event
- else:
- event = event_or_filter
-
- self.add_event_handler(event, f)
- if timeout:
- return await asyncio.wait_for(f, timeout, loop=self._loop)
- return await f
-
-
- def oneshot(f):
- '''Event handler decorator; causes the handler to only be dispatched to once.'''
- f._oneshot_handler = True
- return f
-
-
- def filter_handler(event, **filters):
- '''Decorates a handler function or Future to only be called if a filter is matched.
-
- A handler may apply multiple separate filters; the handlers will be called
- if any of those filters matches.
-
- For example::
-
- # Handle only if the anim_majorwin animation completed
- @filter_handler(cozmo.anim.EvtAnimationCompleted, animation_name="anim_majorwin")
-
- # Handle only when the observed object is a LightCube
- @filter_handler(cozmo.objects.EvtObjectObserved, obj=lambda obj: isinstance(cozmo.objects.LightCube))
-
- Args:
- event (:class:`Event`): The event class to match on
- filters (dict): Zero or more event parameters to filter on. Values may
- be either strings for exact matches, or functions which accept the
- value as the first argument and return a bool indicating whether
- the value passes the filter.
- '''
-
- if isinstance(event, Filter):
- if len(filters) != 0:
- raise ValueError("Cannot supply filter values when passing a Filter as the first argument")
- filter = event
- else:
- filter = Filter(event, **filters)
-
- def filter_property(f):
- if hasattr(f, '_handler_filters'):
- f._handler_filters.append(filter)
- else:
- f._handler_filters = [filter]
- return f
- return filter_property
-
-
- class Filter:
- """Provides fine-grain filtering of events for dispatch.
-
- See the ::func::`filter_handler` method for further details.
- """
-
- def __init__(self, event, **filters):
- if not issubclass(event, Event):
- raise TypeError("event must be a subclass of Event (not an instance)")
- self._event = event
- self._filters = filters
- for key in self._filters.keys():
- if not hasattr(event, key):
- raise AttributeError("Event %s does not define property %s", event.__name__, key)
-
- def __setattr__(self, key, val):
- if key[0] == '_':
- return super().__setattr__(key, val)
- if not hasattr(self._event, key):
- raise AttributeError("Event %s does not define property %s", self._event.__name__, key)
- self._filters[key] = val
-
- def __call__(self, evt):
- for prop, filter in self._filters.items():
- val = getattr(evt, prop)
- if callable(filter):
- if not filter(val):
- return False
- elif val != filter:
- return False
- return True
-
-
- async def wait_for_first(*futures, discard_remaining=True, loop=None):
- '''Wait the first of a set of futures to complete.
-
- Eg::
-
- event = cozmo.event.wait_for_first(
- coz.world.wait_for_new_cube(),
- playing_anim.wait_for(cozmo.anim.EvtAnimationCompleted)
- )
-
- If more than one completes during a single event loop run, then
- if any of those results are not exception, one of them will be selected
- (at random, as determined by ``set.pop``) to be returned, else one
- of the result exceptions will be raised instead.
-
- Args:
- futures (list of :class:`asyncio.Future`): The futures or coroutines to wait on.
- discard_remaining (bool): Cancel or discard the results of the futures
- that did not return first.
- loop (:class:`asyncio.BaseEventLoop`): The event loop to wait on.
- Returns:
- The first result, or raised exception
- '''
- done, pending = await asyncio.wait(futures, loop=loop, return_when=asyncio.FIRST_COMPLETED)
-
- # collect the results from all "done" futures; only one will be returned
- result = None
- for fut in done:
- try:
- fut_result = fut.result()
- if result is None or isinstance(result, BaseException):
- result = fut_result
- except Exception as exc:
- if result is None:
- result = exc
-
- if discard_remaining:
- # cancel the pending futures
- for fut in pending:
- fut.cancel()
-
- if isinstance(result, BaseException):
- raise result
- return result
-
-
- def _abort_futures(exc):
- '''Trigger the exception handler for all pending Future handlers.'''
- for obj in active_dispatchers:
- obj._abort_event_futures(exc)
|