{ "info": { "author": "Alberto Berti", "author_email": "alberto@metapensiero.it", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5" ], "description": ".. -*- coding: utf-8 -*-\n.. :Project: metapensiero.signal -- An event framework that is asyncio aware\n.. :Created: dom 09 ago 2015 12:57:35 CEST\n.. :Author: Alberto Berti \n.. :License: GNU General Public License version 3 or later\n.. :Copyright: Copyright (C) 2015 Alberto Berti\n..\n\n=====================\n metapensiero.signal\n=====================\n\n :author: Alberto Berti\n :contact: alberto@metapensiero.it\n :license: GNU General Public License version 3 or later\n\nAn event framework that is asyncio aware\n========================================\n\n.. contents::\n\nGoal\n----\n\nThis package implements a light event system that is able to deal with\nboth synchronous and asynchronous event handlers. It can be used as-is\nor as member of a class.\n\nIf you use it on Python 2.7 you'll get just synchronous handlers\nmanagement, but there is a way to bind it to external event systems in\na generic way. Check out the ``external.py`` submodule and the tests\nfor more info.\n\nInstallation\n------------\n\nTo install the package execute the following command::\n\n $ pip install metapensiero.signal\n\nUsage\n-----\n\nBasic functionality\n~~~~~~~~~~~~~~~~~~~\n\nThe most significant component provided by this package is the class\n``Signal``:\n\n.. code:: python\n\n from metapensiero.signal import Signal\n\n signal = Signal()\n called1 = False\n called2 = False\n\n def handler1(arg, kw):\n nonlocal called1\n called1 = (arg, kw)\n\n def handler2(arg, kw):\n nonlocal called2\n called2 = (arg, kw)\n\n signal.connect(handler1)\n signal.connect(handler2)\n\n signal.notify(1, kw='a')\n assert called1 == (1, 'a')\n assert called2 == (1, 'a')\n\nAs you can see, to have a function or method called when a signal is\n*fired* you just have to call the ``connect()`` method of the signal\ninstance. To remove that same method you can use the ``disconnect()``\nmethod.\n\nAs you can see above, the way to fire an event is by calling the\n``notify()`` method and any argument or keyword argument passed to\nthat function will be added to the handlers call.\n\nIt's possible to remove all the connected handlers by invoking the\n``clear()`` method.\n\nAsynchronous signal handlers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nNot only you can have synchronous handlers, but you can have\nasynchronous handlers as well:\n\n.. code:: python\n\n import asyncio\n from metapensiero.signal import Signal\n\n @asyncio.coroutine\n def test_with_mixed_handlers():\n signal = Signal()\n called1 = False\n called2 = False\n h1 = asyncio.Event()\n\n @asyncio.coroutine\n def handler1(arg, kw):\n nonlocal called1\n called1 = (arg, kw)\n h1.set()\n\n def handler2(arg, kw):\n nonlocal called2\n called2 = (arg, kw)\n\n signal.connect(handler1)\n signal.connect(handler2)\n\n signal.notify(1, kw='a')\n assert called2 == (1, 'a')\n assert called1 == False\n yield from h1.wait()\n assert called1 == (1, 'a')\n\n loop = asyncio.get_event_loop()\n loop.run_until_complete(test_with_mixed_handlers())\n\nAs you can see in this example the var ``called2`` immediately after\nthe notify has the expected value but the value of the var ``called1``\nhasn't. To have it the code has to suspend itself and wait for the\nflag event to be set. This is because ``handler1()`` is scheduled to\nbe executed with ``asyncio.ensure_future()`` but it isn't waited for a\nresult by the ``notify()`` method.\n\nThe usage of a flag to synchronize is a bit silly, what if we have\nmore than one async handler? Do we have to create an ``asyncio.Event``\ninstance for all of them and then wait for everyone of those? And if\nthe actual amount of async handlers isn't known in advance, what\nshould we do?\n\nTransaction support\n~~~~~~~~~~~~~~~~~~~\n\nThis is exactly where the sister package\n`metapensiero.asyncio.transaction`__ comes handy. The ``Signal`` class\nworks with it to ensure that two coroutines (the one calling\n``notify()`` and ``handler1()``) can be synchronized.\n\nTo do that the *outer* code has just to start a *transaction* and\nif it is in place, the ``Signal`` class' code will automatically add\nany async event handler to it.\n\nTo summarize this feature the previous example can be written also\nas:\n\n.. code:: python\n\n import asyncio\n from metapensiero.signal import Signal\n from metapensiero.asyncio import transaction\n\n @asyncio.coroutine\n def test_with_mixed_handlers():\n signal = Signal()\n called1 = False\n called2 = False\n\n @asyncio.coroutine\n def handler1(arg, kw):\n nonlocal called1\n called1 = (arg, kw)\n h1.set()\n\n def handler2(arg, kw):\n nonlocal called2\n called2 = (arg, kw)\n\n signal.connect(handler1)\n signal.connect(handler2)\n\n trans = transaction.begin()\n signal.notify(1, kw='a')\n assert called2 == (1, 'a')\n assert called1 == False\n yield from trans.end()\n assert called1 == (1, 'a')\n\n loop = asyncio.get_event_loop()\n loop.run_until_complete(test_with_mixed_handlers())\n\nOr, with python 3.5, we can use async context managers, so it becomes:\n\n.. code:: python\n\n import asyncio\n from metapensiero.signal import Signal\n from metapensiero.asyncio import transaction\n\n async def test_with_mixed_handlers():\n signal = Signal()\n called1 = False\n called2 = False\n\n async def handler1(arg, kw):\n nonlocal called1\n called1 = (arg, kw)\n h1.set()\n\n def handler2(arg, kw):\n nonlocal called2\n called2 = (arg, kw)\n\n signal.connect(handler1)\n signal.connect(handler2)\n\n async with transaction.begin():\n signal.notify(1, kw='a')\n assert called2 == (1, 'a')\n assert called1 == False\n assert called1 == (1, 'a')\n\n loop = asyncio.get_event_loop()\n loop.run_until_complete(test_with_mixed_handlers())\n\n__ https://pypi.python.org/pypi/metapensiero.asyncio.transaction\n\nThis way the calling context has a generic and scalable way of\nsynchronize the block of code that runs ``notify()`` with the side effects,\neven when they are async and their number is unknown.\n\nUse signals with classes\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nA ``Signal`` instance class can also be used as a member of a\nclass. When this is the case a decorator is provided to declare\nclass-level handlers. To let this feature work, the user class has to\nhave a specific metaclass:\n\n.. code:: python\n\n from metapensiero.signal import Signal, SignalAndHandlerInitMeta, handler\n\n class A(metaclass=SignalAndHandlerInitMeta):\n\n click = Signal()\n\n def __init__(self):\n self.called = False\n\n @handler('click')\n def onclick(self, arg, kw):\n self.called = (arg, kw)\n\n a1 = A()\n assert a1.called == False\n a1.click.notify(1, kw='a')\n assert a1.called == (1, 'a')\n\nOf course a class-level handler can be async:\n\n.. code:: python\n\n import asyncio\n\n from metapensiero.asyncio import transaction\n from metapensiero.signal import Signal, SignalAndHandlerInitMeta, handler\n\n class A(metaclass=SignalAndHandlerInitMeta):\n\n click = Signal()\n\n def __init__(self):\n self.called = False\n self.called2 = False\n\n @handler('click')\n def onclick(self, arg, kw):\n self.called = (arg, kw)\n\n @handler('click')\n @asyncio.coroutine\n def click2(self, arg, kw):\n self.called2 = (arg, kw)\n\n a1 = A()\n\n @asyncio.coroutine\n def runner():\n assert a1.called == False\n assert a1.called2 == False\n\n trans = transaction.begin()\n a1.click.notify(1, kw='a')\n assert a1.called == (1, 'a')\n assert a1.called2 == False\n yield from trans.end()\n assert a1.called2 == (1, 'a')\n\n loop = asyncio.get_event_loop()\n loop.run_until_complete(runner())\n\nOf course, you can use the ``Signal`` class without user class\ninstrumentation, but you will have to do per-instance subscriptions by\nyourself:\n\n.. code:: python\n\n class B:\n\n # the name here is needed for classes that don't explicitly support\n # signals\n click = Signal('click')\n\n def __init__(self):\n self.called = False\n self.click.connect(self.onclick)\n\n def onclick(self, arg, kw):\n self.called = (arg, kw)\n\n b = B()\n assert b.called == False\n b.onclick.notify(1, kw='b')\n assert b.called == (1, 'b')\n\nExtensibility\n~~~~~~~~~~~~~\n\nSignals support two way to extend their functionality. The first is\nglobal and is intended as a way to plug in signals into other event\nsystems. Please have a look at the code in ``external.py`` and the\ncorrisponding tests to learn how to use those abstract classes, they\ngive you a way to tap into signal's machinery do your stuff.\n\nThe second way is per-signal and allows you to define three functions\nto wrap around ``notify()``, ``connect()``, ``disconnect()`` and\nattach them to each instance of signal via decorators, much like with\nbuiltins properties.\n\nEach of these functions will receive all the relevant arguments to\ncustomize the behavior of the internal signal methods and will receive\nanother argument that every function has to call in order to trigger\nthe default behavior. The return value of your wrapper function will\nbe returned to the calling context instead of default return values.\n\nHere is an example, pay attention to the signature of each wrapper\nbeacuse you have to respect that:\n\n.. code:: python\n\n from metapensiero.signal import Signal, SignalAndHandlerInitMeta, handler\n\n c = dict(called=0, connnect_handler=None, handler_called=0, handler_args=None,\n disconnnect_handler=None, handler2_called=0, handler2_args=None)\n\n class A(metaclass=SignalAndHandlerInitMeta):\n\n @Signal\n def click(self, subscribers, notify, *args, **kwargs):\n c['called'] += 1\n c['wrap_args'] = (args, kwargs)\n assert len(subscribers) == 2\n assert isinstance(self, A)\n notify('foo', k=2)\n return 'mynotify'\n\n @click.on_connect\n def click(self, handler, subscribers, connect):\n c['called'] += 1\n c['connect_handler'] = handler\n assert len(subscribers) == 0\n connect(handler)\n return 'myconnect'\n\n @click.on_disconnect\n def click(self, handler, subscribers, disconnect):\n c['called'] += 1\n c['disconnect_handler'] = handler\n assert len(subscribers) == 1\n disconnect(handler)\n return 'mydisconnect'\n\n @handler('click')\n def handler(self, *args, **kwargs):\n c['handler_called'] += 1\n c['handler_args'] = (args, kwargs)\n\n a = A()\n\n def handler2(*args, **kwargs):\n c['handler2_called'] += 1\n c['handler2_args'] = (args, kwargs)\n\n res = a.click.connect(handler2)\n assert res == 'myconnect'\n res = a.click.notify('bar', k=1)\n assert res == 'mynotify'\n res = a.click.disconnect(handler2)\n assert res == 'mydisconnect'\n assert c['called'] == 3\n assert c['wrap_args'] == (('bar',), {'k': 1})\n assert c['handler_called'] == 1\n assert c['handler_args'] == (('foo',), {'k': 2})\n assert c['handler2_called'] == 1\n assert c['handler2_args'] == (('foo',), {'k': 2})\n assert c['disconnect_handler'] == handler2\n assert c['connect_handler'] == handler2\n\nAs you can see, with this way of managing wrappers to default\nbehaviors, you can modify arguments, return customized values or even\navoid triggering the default behavior.\n\nThere are cases when you want to notify the callback during\n``on_connect`` or ``on_disconnect`` wrappers, for example when your\nhandler has the chance of being connected too late to the signal,\nwhere a notification has been delivered already. In such cases you may\nwant to check for this situation in the wrapper and immediately notify\nthe late callback if it's the case.\n\nThe system offers a facility for doing exactly that using ``Signal``'s\ninternal machinery, so handling possible coroutines by appending them\nto the on-going transaction. The ``connect`` and ``disconnect``\nwrappers parameter of the preceding example have another member, a\nfunction ``notify()`` that will take the callback as first argument,\nand then any other argument that will be passed to the handler. So,\nlet's see and example:\n\n.. code:: python\n\n class A(metaclass=SignalAndHandlerInitMeta):\n\n click = Signal()\n\n @click.on_connect\n def click(self, handler, subscribers, connect):\n if self.clicked:\n connect.notify(handler)\n connect(handler)\n\n def __init__(self):\n self.clicked = False\n\n @handler\n def click_handler(self):\n self.clicked = True\n\n\nTesting\n-------\n\nTo run the tests you should run the following at the package root::\n\n python setup.py test\n\n\nBuild status\n------------\n\n.. image:: https://travis-ci.org/azazel75/metapensiero.signal.svg?branch=master\n :target: https://travis-ci.org/azazel75/metapensiero.signal\n\n\n.. -*- coding: utf-8 -*-\n\nChanges\n-------\n\n0.1 (2015-12-27)\n~~~~~~~~~~~~~~~~\n\n- Move here code from rocky\n- Add documentation\n- Move here tests also\n\n0.2 (2015-12-27)\n~~~~~~~~~~~~~~~~\n\n- Documentation fixes\n\n0.3 (2016-01-22)\n~~~~~~~~~~~~~~~~\n\n- Add generic plugin infrastructure\n- Python 2.7 compatibility for the sync handling\n\n0.4 (2016-01-22)\n~~~~~~~~~~~~~~~~\n\n- Allow ``notify()``, ``connect()`` and ``disconnect()`` costomization\n via wrapper functions and decorators\n\n0.5 (2016-01-22)\n~~~~~~~~~~~~~~~~\n\n- make PyPI happy\n\n0.6 (2016-01-28)\n~~~~~~~~~~~~~~~~\n\n- add ``clear()`` method\n\n0.7 (2016-02-03)\n~~~~~~~~~~~~~~~~\n\n- force re-raise of captured exceptions during notify\n\n0.8 (2016-03-24)\n~~~~~~~~~~~~~~~~\n\n- improvements to the external plugin, now used in raccoon.\n\n0.9 (2016-03-26)\n~~~~~~~~~~~~~~~~\n\n- better parameters naming\n- pass additional ``notify()`` function to wrappers\n- updated documentation", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/azazel75/metapensiero.signal", "keywords": "signal event asyncio framework", "license": "GPLv3+", "maintainer": null, "maintainer_email": null, "name": "metapensiero.signal", "package_url": "https://pypi.org/project/metapensiero.signal/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/metapensiero.signal/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/azazel75/metapensiero.signal" }, "release_url": "https://pypi.org/project/metapensiero.signal/0.9/", "requires_dist": null, "requires_python": null, "summary": "A event framework that is asyncio aware", "version": "0.9" }, "last_serial": 2028970, "releases": { "0.8": [ { "comment_text": "", "digests": { "md5": "dc193d50e6762430b876cfb9afc19569", "sha256": "4067b27738764b69cd5ff3dbca4e6d1128cb56c2adbadfe7c205ce6c09d4e420" }, "downloads": -1, "filename": "metapensiero.signal-0.8.tar.gz", "has_sig": false, "md5_digest": "dc193d50e6762430b876cfb9afc19569", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11247, "upload_time": "2016-03-24T15:36:09", "url": "https://files.pythonhosted.org/packages/1d/dd/021b95ee01820150f75e4a5f82a69c41a8bd4d67eb778ebae11b29c0e57b/metapensiero.signal-0.8.tar.gz" } ], "0.9": [ { "comment_text": "", "digests": { "md5": "a935c645ab82ed78cf5ae9070d2c2d40", "sha256": "4fb420de5a3e09cd7364d4bcb900c2fa6d86809722dfbe056b05471115412dc5" }, "downloads": -1, "filename": "metapensiero.signal-0.9.tar.gz", "has_sig": false, "md5_digest": "a935c645ab82ed78cf5ae9070d2c2d40", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12104, "upload_time": "2016-03-26T22:57:16", "url": "https://files.pythonhosted.org/packages/27/9a/63056ea06c9613c2c1d98ada4c7c0a350ec753d635fac3359e8ae62a4773/metapensiero.signal-0.9.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "a935c645ab82ed78cf5ae9070d2c2d40", "sha256": "4fb420de5a3e09cd7364d4bcb900c2fa6d86809722dfbe056b05471115412dc5" }, "downloads": -1, "filename": "metapensiero.signal-0.9.tar.gz", "has_sig": false, "md5_digest": "a935c645ab82ed78cf5ae9070d2c2d40", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12104, "upload_time": "2016-03-26T22:57:16", "url": "https://files.pythonhosted.org/packages/27/9a/63056ea06c9613c2c1d98ada4c7c0a350ec753d635fac3359e8ae62a4773/metapensiero.signal-0.9.tar.gz" } ] }