events: add support for per-instance handlers

This commit is contained in:
Marek Marczykowski-Górecki 2017-05-12 13:53:30 +02:00
parent bd1f84fcec
commit da7496794a
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
4 changed files with 62 additions and 43 deletions

View File

@ -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. passed from :py:meth:`qubes.events.Emitter.fire_event` as described previously.
One callable can handle more than one event. One callable can handle more than one event.
The easiest way to hook an event is to invoke The easiest way to hook an event is to use
: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
:py:func:`qubes.events.handler` decorator. :py:func:`qubes.events.handler` decorator.
.. code-block:: python .. 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 = MyClass()
o.fire_event('event1') o.fire_event('event1')
Note that your handler will be called for all instances of this class.
.. TODO: extensions .. TODO: extensions
.. TODO: add/remove_handler
Handling events with variable signature Handling events with variable signature
@ -100,7 +81,8 @@ every other python function with variable signature.
else: else:
print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue)) 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 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 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: else:
print('Property {} changed {!r} -> {!r}'.format(name, oldvalue, newvalue)) 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 There is no possible way of collision other than intentionally passing this very
object (not even passing similar featureless ``object()``), because ``is`` object (not even passing similar featureless ``object()``), because ``is``

View File

@ -27,6 +27,8 @@ etc.
import collections import collections
import itertools
def handler(*events): def handler(*events):
'''Event handler decorator factory. '''Event handler decorator factory.
@ -88,7 +90,7 @@ class EmitterMeta(type):
continue continue
for event in attr.ha_events: for event in attr.ha_events:
cls.add_handler(event, attr) cls.__handlers__[event].add(attr)
class Emitter(object, metaclass=EmitterMeta): class Emitter(object, metaclass=EmitterMeta):
@ -102,10 +104,10 @@ class Emitter(object, metaclass=EmitterMeta):
super(Emitter, self).__init__(*args, **kwargs) super(Emitter, self).__init__(*args, **kwargs)
if not hasattr(self, 'events_enabled'): if not hasattr(self, 'events_enabled'):
self.events_enabled = False self.events_enabled = False
self.__handlers__ = collections.defaultdict(set)
@classmethod def add_handler(self, event, func):
def add_handler(cls, event, func):
'''Add event handler to subject's class. '''Add event handler to subject's class.
This is class method, it is invalid to call it on object instance. 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 # pylint: disable=no-member
cls.__handlers__[event].add(func) self.__handlers__[event].add(func)
@classmethod def remove_handler(self, event, func):
def remove_handler(cls, event, func):
'''Remove event handler from subject's class. '''Remove event handler from subject's class.
This is class method, it is invalid to call it on object instance. 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 # pylint: disable=no-member
cls.__handlers__[event].remove(func) self.__handlers__[event].remove(func)
def _fire_event_in_order(self, order, event, kwargs): def _fire_event_in_order(self, order, event, kwargs):
'''Fire event for classes in given order. '''Fire event for classes in given order.
@ -145,12 +145,14 @@ class Emitter(object, metaclass=EmitterMeta):
return [] return []
effects = [] effects = []
for cls in order: for i in order:
if not hasattr(cls, '__handlers__'): try:
handlers_dict = i.__handlers__
except AttributeError:
continue continue
handlers = cls.__handlers__[event] handlers = handlers_dict.get(event, set())
if '*' in cls.__handlers__: if '*' in handlers_dict:
handlers = cls.__handlers__['*'] | handlers handlers = handlers_dict['*'] | handlers
for func in sorted(handlers, for func in sorted(handlers,
key=(lambda handler: hasattr(handler, 'ha_bound')), key=(lambda handler: hasattr(handler, 'ha_bound')),
reverse=True): reverse=True):
@ -159,7 +161,6 @@ class Emitter(object, metaclass=EmitterMeta):
effects.extend(effect) effects.extend(effect)
return effects return effects
def fire_event(self, event, **kwargs): def fire_event(self, event, **kwargs):
'''Call all handlers for an event. '''Call all handlers for an event.
@ -178,7 +179,8 @@ class Emitter(object, metaclass=EmitterMeta):
events. 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) event, kwargs)
@ -199,5 +201,6 @@ class Emitter(object, metaclass=EmitterMeta):
events. events.
''' '''
return self._fire_event_in_order(self.__class__.__mro__, return self._fire_event_in_order(
itertools.chain((self,), self.__class__.__mro__),
event, kwargs) event, kwargs)

View File

@ -45,11 +45,12 @@ class Extension(object):
if attr.ha_vm is not None: if attr.ha_vm is not None:
for event in attr.ha_events: for event in attr.ha_events:
attr.ha_vm.add_handler(event, attr) attr.ha_vm.__handlers__[event].add(attr)
else: else:
# global hook # global hook
for event in attr.ha_events: 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 return cls._instance

View File

@ -103,3 +103,35 @@ class TC_00_Emitter(qubes.tests.QubesTestCase):
self.assertEqual(testevent_fired[0], 3) self.assertEqual(testevent_fired[0], 3)
emitter.fire_event('bar') emitter.fire_event('bar')
self.assertEqual(testevent_fired[0], 4) 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'])