Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5
Expand All @@ -31,6 +31,8 @@ jobs:
run: |
uv run pytest --cov --cov-config=.coveragerc tests pydispatch doc README.md
- name: Test python-dispatch-sphinx
# TODO: Remove 3.14 exclusion when 3.14 support is added to sphinx-plugin
if: ${{ matrix.python-version != '3.14' }}
run: |
uv run pytest --cov --cov-append --cov-config=sphinx-plugin/.coveragerc sphinx-plugin/tests
- name: Upload to Coveralls
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/dist-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]
dist-type: [sdist, wheel]
fail-fast: false

Expand Down Expand Up @@ -103,6 +103,8 @@ jobs:
- name: Test pydispatch distribution
run: py.test tests/
- name: Test pydispatch_sphinx distribution
# TODO: Remove 3.14 exclusion when 3.14 support is added to sphinx-plugin
if: ${{ matrix.python-version != '3.14' }}
run: py.test sphinx-plugin/tests/

deploy:
Expand Down
54 changes: 31 additions & 23 deletions doc/source/async.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ bind_async method
... async def wait_for_event(self):
... await self.event_received.wait()

>>> loop = asyncio.get_event_loop()
>>> emitter = MyEmitter()
>>> listener = MyAsyncListener()

>>> # Pass the event loop as first argument to "bind_async"
>>> emitter.bind_async(loop, on_state=listener.on_emitter_state)

>>> loop.run_until_complete(emitter.trigger())
>>> loop.run_until_complete(listener.wait_for_event())
>>> async def main():
... loop = asyncio.get_running_loop()
... emitter = MyEmitter()
... listener = MyAsyncListener()
...
... # Pass the event loop as first argument to "bind_async"
... emitter.bind_async(loop, on_state=listener.on_emitter_state)
...
... await emitter.trigger()
... await listener.wait_for_event()

>>> asyncio.run(main())
received on_state event

bind (with keyword argument)
Expand All @@ -76,15 +79,17 @@ bind (with keyword argument)
... async def wait_for_event(self):
... await self.event_received.wait()

>>> loop = asyncio.get_event_loop()
>>> emitter = MyEmitter()
>>> listener = MyAsyncListener()

>>> # Pass the event loop using __aio_loop__
>>> emitter.bind(on_state=listener.on_emitter_state, __aio_loop__=loop)

>>> loop.run_until_complete(emitter.trigger())
>>> loop.run_until_complete(listener.wait_for_event())
>>> async def main():
... loop = asyncio.get_running_loop()
... emitter = MyEmitter()
... listener = MyAsyncListener()
...
... # Pass the event loop using __aio_loop__
... emitter.bind(on_state=listener.on_emitter_state, __aio_loop__=loop)
... await emitter.trigger()
... await listener.wait_for_event()

>>> asyncio.run(main())
received on_state event

Async (awaitable) Events
Expand Down Expand Up @@ -131,11 +136,14 @@ This can also be done with :any:`Property` objects
... break
... return 'done'

>>> loop = asyncio.get_event_loop()
>>> emitter = MyEmitter()
>>> listener = MyAsyncListener()
>>> coros = [emitter.change_values(), listener.wait_for_values(emitter)]
>>> loop.run_until_complete(asyncio.gather(*coros))
>>> async def main():
... loop = asyncio.get_running_loop()
... emitter = MyEmitter()
... listener = MyAsyncListener()
... coros = [emitter.change_values(), listener.wait_for_values(emitter)]
... return await asyncio.gather(*coros)

>>> asyncio.run(main())
0
1
2
Expand Down
24 changes: 21 additions & 3 deletions pydispatch/aioutils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import sys
import asyncio
import threading
from _weakref import ref
from _weakrefset import _IterationGuard
from weakref import ref
from _weakref import _remove_dead_weakref # type: ignore[import]
if sys.version_info < (3, 14):
from _weakrefset import _IterationGuard
else:
# _IterationGuard was removed in Python 3.14. We define a no-op version here
class _IterationGuard:
def __init__(self, obj):
pass
def __enter__(self):
pass
def __exit__(self, *args):
pass


from pydispatch.utils import (
WeakMethodContainer,
isfunction,
get_method_vars,
_remove_dead_weakref,
)


Expand Down Expand Up @@ -192,6 +204,12 @@ def remove(wr, selfref=ref(self)):
_remove_dead_weakref(self.data, wr.key)
self._on_weakref_fin(wr.key)
self._remove = remove

# `_pending_removals` and `_iterating` were removed in Python 3.14.
# To maintain compatibility with earlier versions, we reintroduce them here.
self._pending_removals = []
self._iterating = set()

self.event_loop_map = {}
def add_method(self, loop, callback):
"""Add a coroutine function
Expand Down
18 changes: 10 additions & 8 deletions pydispatch/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,16 @@ class Foo(Dispatcher):
... async def on_foo_test_event(self, *args, **kwargs):
... self.got_foo_event.set()

>>> loop = asyncio.get_event_loop()
>>> foo = Foo()
>>> bar = Bar()
>>> foo.bind(test_event=bar.on_foo_test_event, __aio_loop__=loop)
>>> fut = asyncio.ensure_future(bar.wait_for_foo())

>>> foo.emit('test_event')
>>> loop.run_until_complete(fut)
>>> async def main():
... loop = asyncio.get_running_loop()
... foo = Foo()
... bar = Bar()
... foo.bind(test_event=bar.on_foo_test_event, __aio_loop__=loop)
... fut = asyncio.create_task(bar.wait_for_foo())
... foo.emit('test_event')
... await fut

>>> asyncio.run(main())
got foo!

This can also be done using :meth:`bind_async`.
Expand Down
10 changes: 8 additions & 2 deletions pydispatch/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import weakref
from weakref import ref, _remove_dead_weakref
from _weakref import ref
from weakref import ref
from _weakref import _remove_dead_weakref # type: ignore[import]
import types
import asyncio

Expand Down Expand Up @@ -114,6 +114,12 @@ def remove(wr, selfref=ref(self)):
_remove_dead_weakref(self.data, wr.key)
self._data_del_callback(wr.key)
self._remove = remove

# `_pending_removals` and `_iterating` were removed in Python 3.14.
# To maintain compatibility with earlier versions, we reintroduce them here.
self._pending_removals = []
self._iterating = set()

self.data = InformativeDict()
self.data.del_callback = self._data_del_callback
def _data_del_callback(self, key):
Expand Down