Skip to content

Commit

Permalink
Merge pull request #22 from nocarryr/global-dispatcher
Browse files Browse the repository at this point in the history
Add "Global Dispatcher"
  • Loading branch information
nocarryr authored Jun 22, 2023
2 parents 33aafaa + e33b432 commit 9413c09
Show file tree
Hide file tree
Showing 19 changed files with 521 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:

runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]

Expand Down
10 changes: 10 additions & 0 deletions doc/source/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest

@pytest.fixture(autouse=True)
def dispatcher_cleanup():
import pydispatch
pydispatch._GLOBAL_DISPATCHER._Dispatcher__events.clear()
pydispatch.decorators._CACHED_CALLBACKS.cache.clear()
yield
pydispatch._GLOBAL_DISPATCHER._Dispatcher__events.clear()
pydispatch.decorators._CACHED_CALLBACKS.cache.clear()
108 changes: 108 additions & 0 deletions doc/source/global-dispatcher.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
.. _global-dispatcher:

Global Dispatcher
=================

.. currentmodule:: pydispatch

.. versionadded:: 0.2.2

At the module-level, most of the functionality of :class:`~.dispatch.Dispatcher`
can be used directly as a `singleton`_ instance. Note that this interface only
supports event dispatching (not :ref:`properties`).

When used this way, the concept is similar to the `Signals Framework`_ found in Django.

Basic Usage
-----------

Events can be registered using :func:`register_event`, connected to callbacks
using :func:`bind` and dispatched with the :func:`emit` function.

>>> import pydispatch

>>> def my_callback(message, **kwargs):
... print(f'my_callback: "{message}"')

>>> # register 'event_a' as an event and bind it to my_callback
>>> pydispatch.register_event('event_a')
>>> pydispatch.bind(event_a=my_callback)

>>> # emit the event
>>> pydispatch.emit('event_a', 'hello')
my_callback: "hello"

>>> # unbind the callback
>>> pydispatch.unbind(my_callback)
>>> pydispatch.emit('event_a', 'still there?')

>>> # (Nothing printed)


The @receiver Decorator
-----------------------

To simplify binding callbacks to events, the :func:`~decorators.receiver` decorator may be used.

>>> from pydispatch import receiver

>>> @receiver('event_a')
... def my_callback(message, **kwargs):
... print(f'my_callback: "{message}"')

>>> pydispatch.emit('event_a', 'hello again!')
my_callback: "hello again!"

Note that there is currently no way to :func:`unbind` a callback defined in this way.


Arguments
^^^^^^^^^

If the event name has not been registered beforehand, the ``cache`` and ``auto_register``
arguments to :func:`~decorators.receiver` may be used

cache
"""""

The ``cache`` argument stores the callback and will bind it to the event
once it is registered.

>>> # No event named 'foo' exists yet
>>> @receiver('foo', cache=True)
... def on_foo(message, **kwargs):
... print(f'on_foo: "{message}"')

>>> # on_foo will be connected after the call to register_event
>>> pydispatch.register_event('foo')
>>> pydispatch.emit('foo', 'bar')
on_foo: "bar"

auto_register
"""""""""""""

The ``auto_register`` argument will immediately register the event if it does not exist

>>> @receiver('bar', auto_register=True)
... def on_bar(message, **kwargs):
... print(f'on_bar: "{message}"')

>>> pydispatch.emit('bar', 'baz')
on_bar: "baz"


Async Support
^^^^^^^^^^^^^

If the decorated callback is a :term:`coroutine function` or method,
the :class:`EventLoop <asyncio.BaseEventLoop>` returned by
:func:`asyncio.get_event_loop` will be used as the ``loop`` argument to
:func:`bind_async`.

This will in most cases be the desired behavior unless multiple event loops
(in separate threads) are being used.



.. _singleton: https://en.wikipedia.org/wiki/Singleton_pattern
.. _Signals Framework: https://docs.djangoproject.com/en/4.2/topics/signals/
1 change: 1 addition & 0 deletions doc/source/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Overview
dispatcher
properties
async
global-dispatcher
2 changes: 2 additions & 0 deletions doc/source/properties.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _properties:

Properties
==========

Expand Down
2 changes: 1 addition & 1 deletion doc/source/reference/aioutils.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pydispatch.aioutils module
:mod:`pydispatch.aioutils`
==========================

.. automodule:: pydispatch.aioutils
Expand Down
7 changes: 7 additions & 0 deletions doc/source/reference/decorators.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:mod:`pydispatch.decorators`
============================

.. versionadded:: 0.2.2

.. automodule:: pydispatch.decorators
:members:
2 changes: 1 addition & 1 deletion doc/source/reference/dispatch.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pydispatch.dispatch module
:mod:`pydispatch.dispatch`
==========================

.. automodule:: pydispatch.dispatch
Expand Down
5 changes: 1 addition & 4 deletions doc/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,4 @@ Reference
.. toctree::
:maxdepth: 3

dispatch
properties
utils
aioutils
pydispatch-package
2 changes: 1 addition & 1 deletion doc/source/reference/properties.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pydispatch.properties module
:mod:`pydispatch.properties`
============================

.. automodule:: pydispatch.properties
Expand Down
15 changes: 15 additions & 0 deletions doc/source/reference/pydispatch-package.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:mod:`pydispatch`
=================

.. automodule:: pydispatch
:members:


.. toctree::
:maxdepth: 3

dispatch
properties
decorators
utils
aioutils
2 changes: 1 addition & 1 deletion doc/source/reference/utils.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pydispatch.utils module
:mod:`pydispatch.utils`
=======================

.. automodule:: pydispatch.utils
Expand Down
54 changes: 54 additions & 0 deletions pydispatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,58 @@
UserWarning)

from pydispatch.dispatch import *
from pydispatch.dispatch import _GLOBAL_DISPATCHER
from pydispatch.properties import *
from pydispatch import decorators
from pydispatch.decorators import *


def register_event(*names):
"""Register event (or events) on the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.register_event`
.. versionadded:: 0.2.2
"""
_GLOBAL_DISPATCHER.register_event(*names)
decorators._post_register_hook(*names)

def bind(**kwargs):
"""Subscribe callbacks to events on the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.bind`
.. versionadded:: 0.2.2
"""
_GLOBAL_DISPATCHER.bind(**kwargs)

def unbind(*args):
"""Unbind callbacks from events on the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.unbind`
.. versionadded:: 0.2.2
"""
_GLOBAL_DISPATCHER.unbind(*args)

def bind_async(loop, **kwargs):
"""Bind async callbacks to events on the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.bind_async`
.. versionadded:: 0.2.2
"""
_GLOBAL_DISPATCHER.bind_async(loop, **kwargs)

def emit(name, *args, **kwargs):
"""Dispatch the event with the given *name* on the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.emit`
.. versionadded:: 0.2.2
"""
return _GLOBAL_DISPATCHER.emit(name, *args, **kwargs)

def get_dispatcher_event(name):
"""Retrieve the :class:`~.dispatch.Event` object by the given name
from the :ref:`global-dispatcher`
.. seealso:: :meth:`.Dispatcher.get_dispatcher_event`
.. versionadded:: 0.2.2
"""
return _GLOBAL_DISPATCHER.get_dispatcher_event(name)
16 changes: 12 additions & 4 deletions pydispatch/aioutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pydispatch.utils import (
WeakMethodContainer,
isfunction,
get_method_vars,
_remove_dead_weakref,
)
Expand Down Expand Up @@ -200,9 +201,13 @@ def add_method(self, loop, callback):
on which to schedule callbacks
callback: The :term:`coroutine function` to add
"""
f, obj = get_method_vars(callback)
wrkey = (f, id(obj))
self[wrkey] = obj
if isfunction(callback):
wrkey = ('function', id(callback))
self[wrkey] = callback
else:
f, obj = get_method_vars(callback)
wrkey = (f, id(obj))
self[wrkey] = obj
self.event_loop_map[wrkey] = loop
def iter_instances(self):
"""Iterate over the stored objects
Expand All @@ -221,8 +226,11 @@ def iter_methods(self):
"""
for wrkey, obj in self.iter_instances():
f, obj_id = wrkey
if f == 'function':
m = self[wrkey]
else:
m = getattr(obj, f.__name__)
loop = self.event_loop_map[wrkey]
m = getattr(obj, f.__name__)
yield loop, m
def _on_weakref_fin(self, key):
if key in self.event_loop_map:
Expand Down
Loading

0 comments on commit 9413c09

Please sign in to comment.