diff --git a/doc/qubes-events.rst b/doc/qubes-events.rst index 1f4b0705..37aa3573 100644 --- a/doc/qubes-events.rst +++ b/doc/qubes-events.rst @@ -40,29 +40,7 @@ which the event was fired and the second one is the event name. The rest are passed from :py:meth:`qubes.events.Emitter.fire_event` as described previously. One callable can handle more than one event. -The easiest way to hook an event is to invoke -:py:meth:`qubes.events.Emitter.add_handler` classmethod. - -.. code-block:: python - - import qubes.events - - class MyClass(qubes.events.Emitter): - pass - - def event_handler(subject, event): - if event == 'event1': - print('Got event 1') - elif event == 'event2': - print('Got event 2') - - MyClass.add_handler('event1', event_handler) - MyClass.add_handler('event2', event_handler) - - o = MyClass() - o.fire_event('event1') - -If you wish to define handler in the class definition, the best way is to use +The easiest way to hook an event is to use :py:func:`qubes.events.handler` decorator. .. code-block:: python @@ -80,7 +58,10 @@ If you wish to define handler in the class definition, the best way is to use o = MyClass() o.fire_event('event1') +Note that your handler will be called for all instances of this class. + .. TODO: extensions +.. TODO: add/remove_handler Handling events with variable signature @@ -100,7 +81,8 @@ every other python function with variable signature. else: print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue)) - qubes.Qubes.add_handler('property-set:default_netvm') + app = qubes.Qubes() + app.add_handler('property-set:default_netvm') If you expect :py:obj:`None` to be a reasonable value of the property, you have a problem. One way to solve it is to invent your very own, magic @@ -117,7 +99,8 @@ a problem. One way to solve it is to invent your very own, magic else: print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue)) - qubes.Qubes.add_handler('property-set:default_netvm') + app = qubes.Qubes() + app.add_handler('property-set:default_netvm') There is no possible way of collision other than intentionally passing this very object (not even passing similar featureless ``object()``), because ``is`` diff --git a/qubes/events.py b/qubes/events.py index 2520529e..ca111876 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -27,6 +27,8 @@ etc. import collections +import itertools + def handler(*events): '''Event handler decorator factory. @@ -88,7 +90,7 @@ class EmitterMeta(type): continue for event in attr.ha_events: - cls.add_handler(event, attr) + cls.__handlers__[event].add(attr) class Emitter(object, metaclass=EmitterMeta): @@ -102,10 +104,10 @@ class Emitter(object, metaclass=EmitterMeta): super(Emitter, self).__init__(*args, **kwargs) if not hasattr(self, 'events_enabled'): self.events_enabled = False + self.__handlers__ = collections.defaultdict(set) - @classmethod - def add_handler(cls, event, func): + def add_handler(self, event, func): '''Add event handler to subject's class. This is class method, it is invalid to call it on object instance. @@ -115,10 +117,9 @@ class Emitter(object, metaclass=EmitterMeta): ''' # pylint: disable=no-member - cls.__handlers__[event].add(func) + self.__handlers__[event].add(func) - @classmethod - def remove_handler(cls, event, func): + def remove_handler(self, event, func): '''Remove event handler from subject's class. This is class method, it is invalid to call it on object instance. @@ -131,8 +132,7 @@ class Emitter(object, metaclass=EmitterMeta): ''' # pylint: disable=no-member - cls.__handlers__[event].remove(func) - + self.__handlers__[event].remove(func) def _fire_event_in_order(self, order, event, kwargs): '''Fire event for classes in given order. @@ -145,12 +145,14 @@ class Emitter(object, metaclass=EmitterMeta): return [] effects = [] - for cls in order: - if not hasattr(cls, '__handlers__'): + for i in order: + try: + handlers_dict = i.__handlers__ + except AttributeError: continue - handlers = cls.__handlers__[event] - if '*' in cls.__handlers__: - handlers = cls.__handlers__['*'] | handlers + handlers = handlers_dict.get(event, set()) + if '*' in handlers_dict: + handlers = handlers_dict['*'] | handlers for func in sorted(handlers, key=(lambda handler: hasattr(handler, 'ha_bound')), reverse=True): @@ -159,7 +161,6 @@ class Emitter(object, metaclass=EmitterMeta): effects.extend(effect) return effects - def fire_event(self, event, **kwargs): '''Call all handlers for an event. @@ -178,7 +179,8 @@ class Emitter(object, metaclass=EmitterMeta): events. ''' - return self._fire_event_in_order(reversed(self.__class__.__mro__), + return self._fire_event_in_order( + itertools.chain(reversed(self.__class__.__mro__), (self,)), event, kwargs) @@ -199,5 +201,6 @@ class Emitter(object, metaclass=EmitterMeta): events. ''' - return self._fire_event_in_order(self.__class__.__mro__, + return self._fire_event_in_order( + itertools.chain((self,), self.__class__.__mro__), event, kwargs) diff --git a/qubes/ext/__init__.py b/qubes/ext/__init__.py index c02011b2..f9bcd81a 100644 --- a/qubes/ext/__init__.py +++ b/qubes/ext/__init__.py @@ -45,11 +45,12 @@ class Extension(object): if attr.ha_vm is not None: for event in attr.ha_events: - attr.ha_vm.add_handler(event, attr) + attr.ha_vm.__handlers__[event].add(attr) else: # global hook for event in attr.ha_events: - qubes.Qubes.add_handler(event, attr) + # pylint: disable=no-member + qubes.Qubes.__handlers__[event].add(attr) return cls._instance diff --git a/qubes/tests/events.py b/qubes/tests/events.py index d102e67a..57101347 100644 --- a/qubes/tests/events.py +++ b/qubes/tests/events.py @@ -103,3 +103,35 @@ class TC_00_Emitter(qubes.tests.QubesTestCase): self.assertEqual(testevent_fired[0], 3) emitter.fire_event('bar') self.assertEqual(testevent_fired[0], 4) + + def test_005_instance_handlers(self): + class TestEmitter(qubes.events.Emitter): + @qubes.events.handler('testevent') + def on_testevent_1(self, event): + yield 'testevent_1' + + def on_testevent_2(subject, event): + yield 'testevent_2' + + emitter = TestEmitter() + emitter.add_handler('testevent', on_testevent_2) + emitter.events_enabled = True + + emitter2 = TestEmitter() + emitter2.events_enabled = True + + with self.subTest('fire_event'): + effect = emitter.fire_event('testevent') + effect2 = emitter2.fire_event('testevent') + self.assertEqual(list(effect), + ['testevent_1', 'testevent_2']) + self.assertEqual(list(effect2), + ['testevent_1']) + + with self.subTest('fire_event_pre'): + effect = emitter.fire_event_pre('testevent') + effect2 = emitter2.fire_event_pre('testevent') + self.assertEqual(list(effect), + ['testevent_2', 'testevent_1']) + self.assertEqual(list(effect2), + ['testevent_1'])