Package: metapensiero.signal
author:Alberto Berticontact:alberto@metapensiero.itlicense:GNU General Public License version 3 or laterAn event framework that is asyncio awareContentsAn event framework that is asyncio awareGoalInstallationUsageBasic functionalityAsynchronous signal handlersTransaction supportUse signals with classesExtensibilityTestingBuild statusChanges0.1 (2015-12-27)0.2 (2015-12-27)0.3 (2016-01-22)0.4 (2016-01-22)0.5 (2016-01-22)0.6 (2016-01-28)0.7 (2016-02-03)0.8 (2016-03-24)0.9 (2016-03-26)GoalThis package implements a light event system that is able to deal with
both synchronous and asynchronous event handlers. It can be used as-is
or as member of a class.If you use it on Python 2.7 you’ll get just synchronous handlers
management, but there is a way to bind it to external event systems in
a generic way. Check out theexternal.pysubmodule and the tests
for more info.InstallationTo install the package execute the following command:$ pip install metapensiero.signalUsageBasic functionalityThe most significant component provided by this package is the classSignal:frommetapensiero.signalimportSignalsignal=Signal()called1=Falsecalled2=Falsedefhandler1(arg,kw):nonlocalcalled1called1=(arg,kw)defhandler2(arg,kw):nonlocalcalled2called2=(arg,kw)signal.connect(handler1)signal.connect(handler2)signal.notify(1,kw='a')assertcalled1==(1,'a')assertcalled2==(1,'a')As you can see, to have a function or method called when a signal isfiredyou just have to call theconnect()method of the signal
instance. To remove that same method you can use thedisconnect()method.As you can see above, the way to fire an event is by calling thenotify()method and any argument or keyword argument passed to
that function will be added to the handlers call.It’s possible to remove all the connected handlers by invoking theclear()method.Asynchronous signal handlersNot only you can have synchronous handlers, but you can have
asynchronous handlers as well:importasynciofrommetapensiero.signalimportSignal@asyncio.coroutinedeftest_with_mixed_handlers():signal=Signal()called1=Falsecalled2=Falseh1=asyncio.Event()@asyncio.coroutinedefhandler1(arg,kw):nonlocalcalled1called1=(arg,kw)h1.set()defhandler2(arg,kw):nonlocalcalled2called2=(arg,kw)signal.connect(handler1)signal.connect(handler2)signal.notify(1,kw='a')assertcalled2==(1,'a')assertcalled1==Falseyield fromh1.wait()assertcalled1==(1,'a')loop=asyncio.get_event_loop()loop.run_until_complete(test_with_mixed_handlers())As you can see in this example the varcalled2immediately after
the notify has the expected value but the value of the varcalled1hasn’t. To have it the code has to suspend itself and wait for the
flag event to be set. This is becausehandler1()is scheduled to
be executed withasyncio.ensure_future()but it isn’t waited for a
result by thenotify()method.The usage of a flag to synchronize is a bit silly, what if we have
more than one async handler? Do we have to create anasyncio.Eventinstance for all of them and then wait for everyone of those? And if
the actual amount of async handlers isn’t known in advance, what
should we do?Transaction supportThis is exactly where the sister packagemetapensiero.asyncio.transactioncomes handy. TheSignalclass
works with it to ensure that two coroutines (the one callingnotify()andhandler1()) can be synchronized.To do that theoutercode has just to start atransactionand
if it is in place, theSignalclass’ code will automatically add
any async event handler to it.To summarize this feature the previous example can be written also
as:importasynciofrommetapensiero.signalimportSignalfrommetapensiero.asyncioimporttransaction@asyncio.coroutinedeftest_with_mixed_handlers():signal=Signal()called1=Falsecalled2=False@asyncio.coroutinedefhandler1(arg,kw):nonlocalcalled1called1=(arg,kw)h1.set()defhandler2(arg,kw):nonlocalcalled2called2=(arg,kw)signal.connect(handler1)signal.connect(handler2)trans=transaction.begin()signal.notify(1,kw='a')assertcalled2==(1,'a')assertcalled1==Falseyield fromtrans.end()assertcalled1==(1,'a')loop=asyncio.get_event_loop()loop.run_until_complete(test_with_mixed_handlers())Or, with python 3.5, we can use async context managers, so it becomes:importasynciofrommetapensiero.signalimportSignalfrommetapensiero.asyncioimporttransactionasyncdeftest_with_mixed_handlers():signal=Signal()called1=Falsecalled2=Falseasyncdefhandler1(arg,kw):nonlocalcalled1called1=(arg,kw)h1.set()defhandler2(arg,kw):nonlocalcalled2called2=(arg,kw)signal.connect(handler1)signal.connect(handler2)asyncwithtransaction.begin():signal.notify(1,kw='a')assertcalled2==(1,'a')assertcalled1==Falseassertcalled1==(1,'a')loop=asyncio.get_event_loop()loop.run_until_complete(test_with_mixed_handlers())This way the calling context has a generic and scalable way of
synchronize the block of code that runsnotify()with the side effects,
even when they are async and their number is unknown.Use signals with classesASignalinstance class can also be used as a member of a
class. When this is the case a decorator is provided to declare
class-level handlers. To let this feature work, the user class has to
have a specific metaclass:frommetapensiero.signalimportSignal,SignalAndHandlerInitMeta,handlerclassA(metaclass=SignalAndHandlerInitMeta):click=Signal()def__init__(self):self.called=False@handler('click')defonclick(self,arg,kw):self.called=(arg,kw)a1=A()asserta1.called==Falsea1.click.notify(1,kw='a')asserta1.called==(1,'a')Of course a class-level handler can be async:importasynciofrommetapensiero.asyncioimporttransactionfrommetapensiero.signalimportSignal,SignalAndHandlerInitMeta,handlerclassA(metaclass=SignalAndHandlerInitMeta):click=Signal()def__init__(self):self.called=Falseself.called2=False@handler('click')defonclick(self,arg,kw):self.called=(arg,kw)@handler('click')@asyncio.coroutinedefclick2(self,arg,kw):self.called2=(arg,kw)a1=A()@asyncio.coroutinedefrunner():asserta1.called==Falseasserta1.called2==Falsetrans=transaction.begin()a1.click.notify(1,kw='a')asserta1.called==(1,'a')asserta1.called2==Falseyield fromtrans.end()asserta1.called2==(1,'a')loop=asyncio.get_event_loop()loop.run_until_complete(runner())Of course, you can use theSignalclass without user class
instrumentation, but you will have to do per-instance subscriptions by
yourself:classB:# the name here is needed for classes that don't explicitly support# signalsclick=Signal('click')def__init__(self):self.called=Falseself.click.connect(self.onclick)defonclick(self,arg,kw):self.called=(arg,kw)b=B()assertb.called==Falseb.onclick.notify(1,kw='b')assertb.called==(1,'b')ExtensibilitySignals support two way to extend their functionality. The first is
global and is intended as a way to plug in signals into other event
systems. Please have a look at the code inexternal.pyand the
corrisponding tests to learn how to use those abstract classes, they
give you a way to tap into signal’s machinery do your stuff.The second way is per-signal and allows you to define three functions
to wrap aroundnotify(),connect(),disconnect()and
attach them to each instance of signal via decorators, much like with
builtins properties.Each of these functions will receive all the relevant arguments to
customize the behavior of the internal signal methods and will receive
another argument that every function has to call in order to trigger
the default behavior. The return value of your wrapper function will
be returned to the calling context instead of default return values.Here is an example, pay attention to the signature of each wrapper
beacuse you have to respect that:frommetapensiero.signalimportSignal,SignalAndHandlerInitMeta,handlerc=dict(called=0,connnect_handler=None,handler_called=0,handler_args=None,disconnnect_handler=None,handler2_called=0,handler2_args=None)classA(metaclass=SignalAndHandlerInitMeta):@Signaldefclick(self,subscribers,notify,*args,**kwargs):c['called']+=1c['wrap_args']=(args,kwargs)assertlen(subscribers)==2assertisinstance(self,A)notify('foo',k=2)return'mynotify'@click.on_connectdefclick(self,handler,subscribers,connect):c['called']+=1c['connect_handler']=handlerassertlen(subscribers)==0connect(handler)return'myconnect'@click.on_disconnectdefclick(self,handler,subscribers,disconnect):c['called']+=1c['disconnect_handler']=handlerassertlen(subscribers)==1disconnect(handler)return'mydisconnect'@handler('click')defhandler(self,*args,**kwargs):c['handler_called']+=1c['handler_args']=(args,kwargs)a=A()defhandler2(*args,**kwargs):c['handler2_called']+=1c['handler2_args']=(args,kwargs)res=a.click.connect(handler2)assertres=='myconnect'res=a.click.notify('bar',k=1)assertres=='mynotify'res=a.click.disconnect(handler2)assertres=='mydisconnect'assertc['called']==3assertc['wrap_args']==(('bar',),{'k':1})assertc['handler_called']==1assertc['handler_args']==(('foo',),{'k':2})assertc['handler2_called']==1assertc['handler2_args']==(('foo',),{'k':2})assertc['disconnect_handler']==handler2assertc['connect_handler']==handler2As you can see, with this way of managing wrappers to default
behaviors, you can modify arguments, return customized values or even
avoid triggering the default behavior.There are cases when you want to notify the callback duringon_connectoron_disconnectwrappers, for example when your
handler has the chance of being connected too late to the signal,
where a notification has been delivered already. In such cases you may
want to check for this situation in the wrapper and immediately notify
the late callback if it’s the case.The system offers a facility for doing exactly that usingSignal’s
internal machinery, so handling possible coroutines by appending them
to the on-going transaction. Theconnectanddisconnectwrappers parameter of the preceding example have another member, a
functionnotify()that will take the  callback as first argument,
and then any other argument that will be passed to the handler. So,
let’s see and example:classA(metaclass=SignalAndHandlerInitMeta):click=Signal()@click.on_connectdefclick(self,handler,subscribers,connect):ifself.clicked:connect.notify(handler)connect(handler)def__init__(self):self.clicked=False@handlerdefclick_handler(self):self.clicked=TrueTestingTo run the tests you should run the following at the package root:python setup.py testBuild statusChanges0.1 (2015-12-27)Move here code from rockyAdd documentationMove here tests also0.2 (2015-12-27)Documentation fixes0.3 (2016-01-22)Add generic plugin infrastructurePython 2.7 compatibility for the sync handling0.4 (2016-01-22)Allownotify(),connect()anddisconnect()costomization
via wrapper functions and decorators0.5 (2016-01-22)make PyPI happy0.6 (2016-01-28)addclear()method0.7 (2016-02-03)force re-raise of captured exceptions during notify0.8 (2016-03-24)improvements to the external plugin, now used in raccoon.0.9 (2016-03-26)better parameters namingpass additionalnotify()function to wrappersupdated documentation