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.
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``

View File

@ -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)

View File

@ -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

View File

@ -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'])