New Upstream Release - blinker
Ready changes
Summary
Merged new upstream version: 1.6.2 (was: 1.5).
Diff
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..c7af9ee
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,24 @@
+[flake8]
+extend-select =
+ # bugbear
+ B
+ # bugbear opinions
+ B9
+ # implicit str concat
+ ISC
+extend-ignore =
+ # slice notation whitespace, invalid
+ E203
+ # import at top, too many circular import fixes
+ E402
+ # line length, handled by bugbear B950
+ E501
+ # bare except, handled by bugbear B001
+ E722
+ # zip with strict=, requires python >= 3.10
+ B905
+ # string formatting opinion, B028 renamed to B907
+ B028
+ B907
+# up to 88 allowed by bugbear B950
+max-line-length = 80
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 521431d..9141cd7 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -3,9 +3,19 @@ on:
push:
branches:
- main
+ - '*.x'
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ - '*.rst'
pull_request:
branches:
- main
+ - '*.x'
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ - '*.rst'
jobs:
tests:
name: ${{ matrix.name }}
@@ -14,23 +24,27 @@ jobs:
fail-fast: false
matrix:
include:
- - {name: '3.11', python: '3.11-dev', tox: py311}
- - {name: '3.10', python: '3.10', tox: py310}
- - {name: '3.9', python: '3.9', tox: py39}
- - {name: '3.8', python: '3.8', tox: py38}
- - {name: '3.7', python: '3.7', tox: py37}
- - {name: 'PyPy', python: 'pypy-3.9', tox: pypy39}
+ - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311}
+ - {name: Windows, python: '3.11', os: windows-latest, tox: py311}
+ - {name: Mac, python: '3.11', os: macos-latest, tox: py311}
+ - {name: '3.12-dev', python: '3.12-dev', os: ubuntu-latest, tox: py312}
+ - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
+ - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
+ - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
+ - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
+ - {name: 'PyPy', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy39}
+ - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
cache: 'pip'
- cache-dependency-path: 'tests/requirements.txt'
+ cache-dependency-path: 'requirements/*.txt'
- name: update pip
run: |
pip install -U wheel
pip install -U setuptools
python -m pip install -U pip
- run: pip install tox
- - run: tox -e ${{ matrix.tox }} -s false
+ - run: tox run -e ${{ matrix.tox }}
diff --git a/.gitignore b/.gitignore
index 1ca9c16..7e222fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,9 @@ __pycache__/
*.pyc
/.pytest_cache/
/.tox/
-/*.egg-info/
+**.egg-info/
/dist/
/docs/_build/
+.python-version
+# prefix for devs private stuff kept with the project
+private_*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5391626
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,49 @@
+ci:
+ autoupdate_schedule: monthly
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.3.0
+ hooks:
+ - id: fix-byte-order-marker
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+
+ - repo: https://github.com/psf/black
+ rev: 22.12.0
+ hooks:
+ - id: black
+ args: ["--target-version", "py37"]
+
+ - repo: https://github.com/PyCQA/flake8
+ rev: 6.0.0
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - flake8-bugbear
+ - flake8-implicit-str-concat
+
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.3.1
+ hooks:
+ - id: pyupgrade
+ args: ["--py37-plus"]
+
+ - repo: https://github.com/peterdemin/pip-compile-multi
+ rev: v2.6.1
+ hooks:
+ - id: pip-compile-multi-verify
+
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: fix-byte-order-marker
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ exclude: "^tests/.*.http$"
+
+ - repo: https://github.com/asottile/reorder_python_imports
+ rev: v3.9.0
+ hooks:
+ - id: reorder-python-imports
+ name: Reorder Python imports (src, tests)
+ args: ["--application-directories", ".:src"]
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index db1749d..20ef9d8 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -5,9 +5,10 @@ build:
python: "3.10"
python:
install:
- - requirements: docs/requirements.txt
+ - requirements: requirements/docs.txt
- method: pip
path: .
+
sphinx:
builder: dirhtml
fail_on_warning: true
diff --git a/CHANGES.rst b/CHANGES.rst
index d278ca0..463736a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,35 @@
+Version 1.6.2
+-------------
+
+Released 2023-04-12
+
+- Type annotations are not evaluated at runtime. typing-extensions is not a runtime
+ dependency. :pr:`94`
+
+Version 1.6.1
+-------------
+
+Released 2023-04-09
+
+- Ensure that py.typed is present in the distributions (to enable other
+ projects to use blinker's typing).
+- Require typing-extensions > 4.2 to ensure it includes
+ ParamSpec. :issue:`90`
+
+Version 1.6
+-----------
+
+Released 2023-04-02
+
+- Add a muted context manager to temporarily turn off a
+ signal. :pr:`84`
+- Allow int senders (alongside existing string senders). :pr:`83`
+- Add a send_async method to the Signal to allow signals to send to
+ coroutine receivers. :pr:`76`
+- Update and modernise the project structure to match that used by the
+ pallets projects. :pr:`77`
+- Add an intial set of type hints for the project.
+
Version 1.5
-----------
diff --git a/MANIFEST.in b/MANIFEST.in
index cab5663..41180b4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
include CHANGES.rst
include tox.ini
+include src/blinker/py.typed
graft docs
prune docs/_build
graft tests
diff --git a/README.rst b/README.rst
index 883dded..e3bc6d4 100644
--- a/README.rst
+++ b/README.rst
@@ -12,12 +12,12 @@ sent by any sender.
>>> from blinker import signal
>>> started = signal('round-started')
>>> def each(round):
- ... print "Round %s!" % round
+ ... print(f"Round {round}")
...
>>> started.connect(each)
>>> def round_two(round):
- ... print "This is round two."
+ ... print("This is round two.")
...
>>> started.connect(round_two, sender=2)
diff --git a/blinker/__init__.py b/blinker/__init__.py
deleted file mode 100644
index bed55ca..0000000
--- a/blinker/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from blinker.base import (
- ANY,
- NamedSignal,
- Namespace,
- Signal,
- WeakNamespace,
- receiver_connected,
- signal,
-)
-
-__all__ = [
- 'ANY',
- 'NamedSignal',
- 'Namespace',
- 'Signal',
- 'WeakNamespace',
- 'receiver_connected',
- 'signal',
- ]
-
-
-__version__ = '1.5'
diff --git a/blinker/_utilities.py b/blinker/_utilities.py
deleted file mode 100644
index 133c57a..0000000
--- a/blinker/_utilities.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from weakref import ref
-
-from blinker._saferef import BoundMethodWeakref
-
-
-try:
- callable
-except NameError:
- def callable(object):
- return hasattr(object, '__call__')
-
-
-try:
- from collections import defaultdict
-except:
- class defaultdict(dict):
-
- def __init__(self, default_factory=None, *a, **kw):
- if (default_factory is not None and
- not hasattr(default_factory, '__call__')):
- raise TypeError('first argument must be callable')
- dict.__init__(self, *a, **kw)
- self.default_factory = default_factory
-
- def __getitem__(self, key):
- try:
- return dict.__getitem__(self, key)
- except KeyError:
- return self.__missing__(key)
-
- def __missing__(self, key):
- if self.default_factory is None:
- raise KeyError(key)
- self[key] = value = self.default_factory()
- return value
-
- def __reduce__(self):
- if self.default_factory is None:
- args = ()
- else:
- args = self.default_factory,
- return type(self), args, None, None, self.items()
-
- def copy(self):
- return self.__copy__()
-
- def __copy__(self):
- return type(self)(self.default_factory, self)
-
- def __deepcopy__(self, memo):
- import copy
- return type(self)(self.default_factory,
- copy.deepcopy(self.items()))
-
- def __repr__(self):
- return 'defaultdict({}, {})'.format(self.default_factory,
- dict.__repr__(self))
-
-
-class _symbol(object):
-
- def __init__(self, name):
- """Construct a new named symbol."""
- self.__name__ = self.name = name
-
- def __reduce__(self):
- return symbol, (self.name,)
-
- def __repr__(self):
- return self.name
-_symbol.__name__ = 'symbol'
-
-
-class symbol(object):
- """A constant symbol.
-
- >>> symbol('foo') is symbol('foo')
- True
- >>> symbol('foo')
- foo
-
- A slight refinement of the MAGICCOOKIE=object() pattern. The primary
- advantage of symbol() is its repr(). They are also singletons.
-
- Repeated calls of symbol('name') will all return the same instance.
-
- """
- symbols = {}
-
- def __new__(cls, name):
- try:
- return cls.symbols[name]
- except KeyError:
- return cls.symbols.setdefault(name, _symbol(name))
-
-
-try:
- text = (str, unicode)
-except NameError:
- text = str
-
-
-def hashable_identity(obj):
- if hasattr(obj, '__func__'):
- return (id(obj.__func__), id(obj.__self__))
- elif hasattr(obj, 'im_func'):
- return (id(obj.im_func), id(obj.im_self))
- elif isinstance(obj, text):
- return obj
- else:
- return id(obj)
-
-
-WeakTypes = (ref, BoundMethodWeakref)
-
-
-class annotatable_weakref(ref):
- """A weakref.ref that supports custom instance attributes."""
-
-
-def reference(object, callback=None, **annotations):
- """Return an annotated weak ref."""
- if callable(object):
- weak = callable_reference(object, callback)
- else:
- weak = annotatable_weakref(object, callback)
- for key, value in annotations.items():
- setattr(weak, key, value)
- return weak
-
-
-def callable_reference(object, callback=None):
- """Return an annotated weak ref, supporting bound instance methods."""
- if hasattr(object, 'im_self') and object.im_self is not None:
- return BoundMethodWeakref(target=object, on_delete=callback)
- elif hasattr(object, '__self__') and object.__self__ is not None:
- return BoundMethodWeakref(target=object, on_delete=callback)
- return annotatable_weakref(object, callback)
-
-
-class lazy_property(object):
- """A @property that is only evaluated once."""
-
- def __init__(self, deferred):
- self._deferred = deferred
- self.__doc__ = deferred.__doc__
-
- def __get__(self, obj, cls):
- if obj is None:
- return self
- value = self._deferred(obj)
- setattr(obj, self._deferred.__name__, value)
- return value
diff --git a/debian/changelog b/debian/changelog
index 7b2966e..3ee142a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+blinker (1.6.2-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Wed, 07 Jun 2023 14:09:39 -0000
+
blinker (1.5-1) unstable; urgency=medium
* Team upload.
diff --git a/docs/conf.py b/docs/conf.py
index 5bbd200..3f09b72 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,10 +8,12 @@ release, version = get_version("blinker", placeholder=None)
extensions = [
"sphinx.ext.autodoc",
"pallets_sphinx_themes",
+ "sphinx_issues",
]
autoclass_content = "both"
autodoc_member_order = "groupwise"
+issues_github_path = "pallets-eco/blinker"
html_theme = "flask"
html_theme_options = {"index_sidebar_logo": False}
diff --git a/docs/index.rst b/docs/index.rst
index 2853115..c2f111a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,8 +21,7 @@ The core of Blinker is quite small but provides powerful features:
- thread safety
Blinker was written by Jason Kirtand and is provided under the MIT
-License. The library supports Python 2.7 and Python 3.5 or later;
-or Jython 2.7 or later; or PyPy 2.7 or later.
+License. The library supports Python 3.7 or later; or PyPy3.9 or later.
Decoupling With Named Signals
@@ -53,7 +52,7 @@ object that caused the signal to be emitted.
.. code-block:: python
>>> def subscriber(sender):
- ... print("Got a signal sent by %r" % sender)
+ ... print(f"Got a signal sent by {sender!r}")
...
>>> ready = signal('ready')
>>> ready.connect(subscriber)
@@ -85,7 +84,7 @@ that particular instance was responsible for emitting the signal.
... complete.send(self)
...
... def __repr__(self):
- ... return '<Processor %s>' % self.name
+ ... return f'<Processor {self.name}>'
...
>>> processor_a = Processor('a')
>>> processor_a.go()
@@ -142,7 +141,7 @@ These will in turn be passed to the connected functions:
>>> send_data = signal('send-data')
>>> @send_data.connect
... def receive_data(sender, **kw):
- ... print("Caught signal from %r, data %r" % (sender, kw))
+ ... print(f"Caught signal from {sender!r}, data {kw!r}")
... return 'received!'
...
>>> result = send_data.send('anonymous', abc=123)
@@ -158,6 +157,19 @@ value``) pairs:
[(<function receive_data at 0x...>, 'received!')]
+Muting signals
+--------------
+
+To mute a signal, as may be required when testing, the
+:meth:`~Signal.muted` can be used as a context decorator:
+
+.. code-block:: python
+
+ sig = signal('send-data')
+ with sig.muted():
+ ...
+
+
Anonymous Signals
-----------------
@@ -182,7 +194,7 @@ processing signals as class attributes:
... self.on_complete.send(self)
...
... def __repr__(self):
- ... return '<AltProcessor %s>' % self.name
+ ... return f'<AltProcessor {self.name}>'
...
``connect`` as a Decorator
@@ -197,7 +209,7 @@ be used as a decorator on functions:
>>> apc = AltProcessor('c')
>>> @apc.on_complete.connect
... def completed(sender):
- ... print "AltProcessor %s completed!" % sender.name
+ ... print f"AltProcessor {sender.name} completed!"
...
>>> apc.go()
Alternate processing.
@@ -214,7 +226,7 @@ function. For this, :meth:`~Signal.connect_via` can be used:
... @dice_roll.connect_via(3)
... @dice_roll.connect_via(5)
... def odd_subscriber(sender):
- ... print("Observed dice roll %r." % sender)
+ ... print(f"Observed dice roll {sender!r}.")
...
>>> result = dice_roll.send(3)
Observed dice roll 3.
@@ -260,6 +272,69 @@ See the documentation of the :obj:`receiver_connected` built-in signal
for an example.
+Async receivers
+---------------
+
+Receivers can be coroutine functions which can be called and awaited
+via the :meth:`~Signal.send_async` method:
+
+.. code-block:: python
+
+ sig = blinker.Signal()
+
+ async def receiver():
+ ...
+
+ sig.connect(receiver)
+ await sig.send_async()
+
+This however requires that all receivers are awaitable which then
+precludes the usage of :meth:`~Signal.send`. To mix and match the
+:meth:`~Signal.send_async` method takes a ``_sync_wrapper`` argument
+such as:
+
+.. code-block:: python
+
+ sig = blinker.Signal()
+
+ def receiver():
+ ...
+
+ sig.connect(receiver)
+
+ def wrapper(func):
+
+ async def inner(*args, **kwargs):
+ func(*args, **kwargs)
+
+ return inner
+
+ await sig.send_async(_sync_wrapper=wrapper)
+
+The equivalent usage for :meth:`~Signal.send` is via the
+``_async_wrapper`` argument. This usage is will depend on your event
+loop, and in the simple case whereby you aren't running within an
+event loop the following example can be used:
+
+.. code-block:: python
+
+ sig = blinker.Signal()
+
+ async def receiver():
+ ...
+
+ sig.connect(receiver)
+
+ def wrapper(func):
+
+ def inner(*args, **kwargs):
+ asyncio.run(func(*args, **kwargs))
+
+ return inner
+
+ await sig.send(_async_wrapper=wrapper)
+
+
API Documentation
-----------------
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c58c26d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[build-system]
+requires = ["setuptools>=61.2"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "blinker"
+license = {text = "MIT License"}
+authors = [{name = "Jason Kirtland", email = "jek@discorporate.us"}]
+maintainers = [{name = "Pallets Ecosystem", email = "contact@palletsprojects.com"}]
+description = "Fast, simple object-to-object and broadcast signaling"
+keywords = [
+ "signal",
+ "emit",
+ "events",
+ "broadcast",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries",
+]
+requires-python = ">= 3.7"
+dynamic = ["version"]
+
+[project.urls]
+Homepage = "https://blinker.readthedocs.io"
+Documentation = "https://blinker.readthedocs.io"
+"Source Code" = "https://github.com/pallets-eco/blinker/"
+"Issue Tracker" = "https://github.com/pallets-eco/blinker/issues/"
+Chat = "https://discord.gg/pallets"
+
+[project.readme]
+file = "README.rst"
+content-type = "text/x-rst"
+
+[tool.mypy]
+python_version = "3.7"
+files = ["src/blinker"]
+show_error_codes = true
+pretty = true
+#strict = true
+allow_redefinition = true
+disallow_subclassing_any = true
+#disallow_untyped_calls = true
+#disallow_untyped_defs = true
+disallow_incomplete_defs = true
+no_implicit_optional = true
+local_partial_types = true
+no_implicit_reexport = true
+strict_equality = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = true
+warn_return_any = true
+#warn_unreachable = True
+
+[tool.setuptools]
+license-files = ["LICENSE.rst"]
+
+[tool.setuptools.dynamic]
+version = {attr = "blinker.__version__"}
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
+filterwarnings = ["error"]
diff --git a/requirements/dev.in b/requirements/dev.in
new file mode 100644
index 0000000..95fefe0
--- /dev/null
+++ b/requirements/dev.in
@@ -0,0 +1,2 @@
+tox
+pre-commit
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..1e67c09
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,67 @@
+# SHA1:6af58729dc01f2483688d6bb82d851a0975313fb
+#
+# This file is autogenerated by pip-compile-multi
+# To update, run:
+#
+# pip-compile-multi
+#
+cachetools==5.2.1
+ # via tox
+cfgv==3.3.1
+ # via pre-commit
+chardet==5.1.0
+ # via tox
+colorama==0.4.6
+ # via tox
+distlib==0.3.6
+ # via virtualenv
+filelock==3.9.0
+ # via
+ # tox
+ # virtualenv
+identify==2.5.13
+ # via pre-commit
+importlib-metadata==6.0.0
+ # via
+ # pluggy
+ # pre-commit
+ # tox
+ # virtualenv
+nodeenv==1.7.0
+ # via pre-commit
+packaging==23.0
+ # via
+ # pyproject-api
+ # tox
+platformdirs==2.6.2
+ # via
+ # tox
+ # virtualenv
+pluggy==1.0.0
+ # via tox
+pre-commit==2.21.0
+ # via -r requirements/dev.in
+pyproject-api==1.5.0
+ # via tox
+pyyaml==6.0
+ # via pre-commit
+tomli==2.0.1
+ # via
+ # pyproject-api
+ # tox
+tox==4.3.5
+ # via -r requirements/dev.in
+typing-extensions==4.4.0
+ # via
+ # importlib-metadata
+ # platformdirs
+ # tox
+virtualenv==20.17.1
+ # via
+ # pre-commit
+ # tox
+zipp==3.11.0
+ # via importlib-metadata
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/docs/requirements.in b/requirements/docs.in
similarity index 67%
rename from docs/requirements.in
rename to requirements/docs.in
index 6bb1491..0ace8b2 100644
--- a/docs/requirements.in
+++ b/requirements/docs.in
@@ -1,2 +1,3 @@
-Sphinx
Pallets-Sphinx-Themes
+Sphinx
+sphinx-issues
diff --git a/docs/requirements.txt b/requirements/docs.txt
similarity index 52%
rename from docs/requirements.txt
rename to requirements/docs.txt
index ab73fb8..e038098 100644
--- a/docs/requirements.txt
+++ b/requirements/docs.txt
@@ -1,52 +1,54 @@
+# SHA1:443ee977856867aeaba570acaa0e03086aca933c
#
-# This file is autogenerated by pip-compile with python 3.10
+# This file is autogenerated by pip-compile-multi
# To update, run:
#
-# pip-compile docs/requirements.in
+# pip-compile-multi
#
-alabaster==0.7.12
+alabaster==0.7.13
# via sphinx
-babel==2.10.3
+babel==2.11.0
# via sphinx
-certifi==2022.6.15
+certifi==2022.12.7
# via requests
-charset-normalizer==2.1.0
+charset-normalizer==3.0.1
# via requests
-docutils==0.18.1
+docutils==0.19
# via sphinx
-idna==3.3
+idna==3.4
# via requests
imagesize==1.4.1
# via sphinx
jinja2==3.1.2
# via sphinx
-markupsafe==2.1.1
+markupsafe==2.1.2
# via jinja2
-packaging==21.3
+packaging==23.0
# via
# pallets-sphinx-themes
# sphinx
-pallets-sphinx-themes==2.0.2
- # via -r docs/requirements.in
-pygments==2.12.0
+pallets-sphinx-themes==2.0.3
+ # via -r requirements/docs.in
+pygments==2.14.0
# via sphinx
-pyparsing==3.0.9
- # via packaging
-pytz==2022.1
+pytz==2022.7.1
# via babel
-requests==2.28.1
+requests==2.28.2
# via sphinx
snowballstemmer==2.2.0
# via sphinx
-sphinx==5.0.2
+sphinx==6.1.3
# via
- # -r docs/requirements.in
+ # -r requirements/docs.in
# pallets-sphinx-themes
-sphinxcontrib-applehelp==1.0.2
+ # sphinx-issues
+sphinx-issues==3.0.1
+ # via -r requirements/docs.in
+sphinxcontrib-applehelp==1.0.4
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
-sphinxcontrib-htmlhelp==2.0.0
+sphinxcontrib-htmlhelp==2.0.1
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
@@ -54,5 +56,5 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
-urllib3==1.26.10
+urllib3==1.26.14
# via requests
diff --git a/requirements/tests.in b/requirements/tests.in
new file mode 100644
index 0000000..ee4ba01
--- /dev/null
+++ b/requirements/tests.in
@@ -0,0 +1,2 @@
+pytest
+pytest-asyncio
diff --git a/requirements/tests.txt b/requirements/tests.txt
new file mode 100644
index 0000000..edcf4fb
--- /dev/null
+++ b/requirements/tests.txt
@@ -0,0 +1,35 @@
+# SHA1:738f1ea95febe383951f6eb64bdad13fefc1a97a
+#
+# This file is autogenerated by pip-compile-multi
+# To update, run:
+#
+# pip-compile-multi
+#
+attrs==22.2.0
+ # via pytest
+exceptiongroup==1.1.0
+ # via pytest
+importlib-metadata==6.0.0
+ # via
+ # pluggy
+ # pytest
+iniconfig==2.0.0
+ # via pytest
+packaging==23.0
+ # via pytest
+pluggy==1.0.0
+ # via pytest
+pytest==7.2.1
+ # via
+ # -r requirements/tests.in
+ # pytest-asyncio
+pytest-asyncio==0.20.3
+ # via -r requirements/tests.in
+tomli==2.0.1
+ # via pytest
+typing-extensions==4.4.0
+ # via
+ # importlib-metadata
+ # pytest-asyncio
+zipp==3.11.0
+ # via importlib-metadata
diff --git a/requirements/typing.in b/requirements/typing.in
new file mode 100644
index 0000000..f0aa93a
--- /dev/null
+++ b/requirements/typing.in
@@ -0,0 +1 @@
+mypy
diff --git a/requirements/typing.txt b/requirements/typing.txt
new file mode 100644
index 0000000..074101b
--- /dev/null
+++ b/requirements/typing.txt
@@ -0,0 +1,13 @@
+# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
+#
+# This file is autogenerated by pip-compile-multi
+# To update, run:
+#
+# pip-compile-multi
+#
+mypy==1.0.1
+ # via -r requirements/typing.in
+mypy-extensions==1.0.0
+ # via mypy
+typing-extensions==4.5.0
+ # via mypy
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 01aaea5..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[bdist_wheel]
-universal = 1
-
-[tool:pytest]
-testpaths = tests
-filterwarnings =
- error
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 462ccd3..0000000
--- a/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-try:
- from setuptools import setup
-except ImportError:
- from distutils.core import setup
-
-readme = open('README.rst').read()
-import blinker
-version = blinker.__version__
-
-setup(name="blinker",
- version=version,
- packages=['blinker'],
- author='Jason Kirtland',
- author_email='jek@discorporate.us',
- maintainer="Pallets Ecosystem",
- maintainer_email="contact@palletsprojects.com",
- description='Fast, simple object-to-object and broadcast signaling',
- keywords='signal emit events broadcast',
- long_description=readme,
- long_description_content_type="text/x-rst",
- license='MIT License',
- url='https://blinker.readthedocs.io',
- project_urls={
- 'Source': 'https://github.com/pallets-eco/blinker',
- },
- python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
- classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Topic :: Software Development :: Libraries',
- ],
-)
diff --git a/src/blinker/__init__.py b/src/blinker/__init__.py
new file mode 100644
index 0000000..71d66d3
--- /dev/null
+++ b/src/blinker/__init__.py
@@ -0,0 +1,19 @@
+from blinker.base import ANY
+from blinker.base import NamedSignal
+from blinker.base import Namespace
+from blinker.base import receiver_connected
+from blinker.base import Signal
+from blinker.base import signal
+from blinker.base import WeakNamespace
+
+__all__ = [
+ "ANY",
+ "NamedSignal",
+ "Namespace",
+ "Signal",
+ "WeakNamespace",
+ "receiver_connected",
+ "signal",
+]
+
+__version__ = "1.6.2"
diff --git a/blinker/_saferef.py b/src/blinker/_saferef.py
similarity index 88%
rename from blinker/_saferef.py
rename to src/blinker/_saferef.py
index 081173d..dcb70c1 100644
--- a/blinker/_saferef.py
+++ b/src/blinker/_saferef.py
@@ -33,26 +33,14 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
"""Refactored 'safe reference from dispatcher.py"""
-
import operator
import sys
import traceback
import weakref
-try:
- callable
-except NameError:
- def callable(object):
- return hasattr(object, '__call__')
-
-
-if sys.version_info < (3,):
- get_self = operator.attrgetter('im_self')
- get_func = operator.attrgetter('im_func')
-else:
- get_self = operator.attrgetter('__self__')
- get_func = operator.attrgetter('__func__')
+get_self = operator.attrgetter("__self__")
+get_func = operator.attrgetter("__func__")
def safe_ref(target, on_delete=None):
@@ -78,14 +66,15 @@ def safe_ref(target, on_delete=None):
if im_self is not None:
# Turn a bound method into a BoundMethodWeakref instance.
# Keep track of these instances for lookup by disconnect().
- assert hasattr(target, 'im_func') or hasattr(target, '__func__'), (
- "safe_ref target %r has im_self, but no im_func, "
- "don't know how to create reference" % target)
+ assert hasattr(target, "im_func") or hasattr(target, "__func__"), (
+ f"safe_ref target {target!r} has im_self, but no im_func, "
+ "don't know how to create reference"
+ )
reference = BoundMethodWeakref(target=target, on_delete=on_delete)
return reference
-class BoundMethodWeakref(object):
+class BoundMethodWeakref:
"""'Safe' and reusable weak references to instance methods.
BoundMethodWeakref objects provide a mechanism for referencing a
@@ -119,13 +108,13 @@ class BoundMethodWeakref(object):
produce the same BoundMethodWeakref instance.
"""
- _all_instances = weakref.WeakValueDictionary()
+ _all_instances = weakref.WeakValueDictionary() # type: ignore[var-annotated]
def __new__(cls, target, on_delete=None, *arguments, **named):
"""Create new instance or return current instance.
Basically this method of construction allows us to
- short-circuit creation of references to already- referenced
+ short-circuit creation of references to already-referenced
instance methods. The key corresponding to the target is
calculated, and if there is already an existing reference,
that is returned, with its deletion_methods attribute updated.
@@ -138,7 +127,7 @@ class BoundMethodWeakref(object):
current.deletion_methods.append(on_delete)
return current
else:
- base = super(BoundMethodWeakref, cls).__new__(cls)
+ base = super().__new__(cls)
cls._all_instances[key] = base
base.__init__(target, on_delete, *arguments, **named)
return base
@@ -159,6 +148,7 @@ class BoundMethodWeakref(object):
single argument, which will be passed a pointer to this
object.
"""
+
def remove(weak, self=self):
"""Set self.isDead to True when method or instance is destroyed."""
methods = self.deletion_methods[:]
@@ -176,8 +166,11 @@ class BoundMethodWeakref(object):
traceback.print_exc()
except AttributeError:
e = sys.exc_info()[1]
- print ('Exception during saferef %s '
- 'cleanup function %s: %s' % (self, function, e))
+ print(
+ f"Exception during saferef {self} "
+ f"cleanup function {function}: {e}"
+ )
+
self.deletion_methods = [on_delete]
self.key = self.calculate_key(target)
im_self = get_self(target)
@@ -187,6 +180,7 @@ class BoundMethodWeakref(object):
self.self_name = str(im_self)
self.func_name = str(im_func.__name__)
+ @classmethod
def calculate_key(cls, target):
"""Calculate the reference key for this reference.
@@ -194,7 +188,6 @@ class BoundMethodWeakref(object):
object and the target function respectively.
"""
return (id(get_self(target)), id(get_func(target)))
- calculate_key = classmethod(calculate_key)
def __str__(self):
"""Give a friendly representation of the object."""
@@ -202,19 +195,22 @@ class BoundMethodWeakref(object):
self.__class__.__name__,
self.self_name,
self.func_name,
- )
+ )
__repr__ = __str__
+ def __hash__(self):
+ return hash((self.self_name, self.key))
+
def __nonzero__(self):
"""Whether we are still a valid reference."""
return self() is not None
- def __cmp__(self, other):
+ def __eq__(self, other):
"""Compare with another reference."""
if not isinstance(other, self.__class__):
- return cmp(self.__class__, type(other))
- return cmp(self.key, other.key)
+ return operator.eq(self.__class__, type(other))
+ return operator.eq(self.key, other.key)
def __call__(self):
"""Return a strong reference to the bound method.
diff --git a/src/blinker/_utilities.py b/src/blinker/_utilities.py
new file mode 100644
index 0000000..068d94c
--- /dev/null
+++ b/src/blinker/_utilities.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import asyncio
+import inspect
+import sys
+import typing as t
+from functools import partial
+from weakref import ref
+
+from blinker._saferef import BoundMethodWeakref
+
+IdentityType = t.Union[t.Tuple[int, int], str, int]
+
+
+class _symbol:
+ def __init__(self, name):
+ """Construct a new named symbol."""
+ self.__name__ = self.name = name
+
+ def __reduce__(self):
+ return symbol, (self.name,)
+
+ def __repr__(self):
+ return self.name
+
+
+_symbol.__name__ = "symbol"
+
+
+class symbol:
+ """A constant symbol.
+
+ >>> symbol('foo') is symbol('foo')
+ True
+ >>> symbol('foo')
+ foo
+
+ A slight refinement of the MAGICCOOKIE=object() pattern. The primary
+ advantage of symbol() is its repr(). They are also singletons.
+
+ Repeated calls of symbol('name') will all return the same instance.
+
+ """
+
+ symbols = {} # type: ignore[var-annotated]
+
+ def __new__(cls, name):
+ try:
+ return cls.symbols[name]
+ except KeyError:
+ return cls.symbols.setdefault(name, _symbol(name))
+
+
+def hashable_identity(obj: object) -> IdentityType:
+ if hasattr(obj, "__func__"):
+ return (id(obj.__func__), id(obj.__self__)) # type: ignore[attr-defined]
+ elif hasattr(obj, "im_func"):
+ return (id(obj.im_func), id(obj.im_self)) # type: ignore[attr-defined]
+ elif isinstance(obj, (int, str)):
+ return obj
+ else:
+ return id(obj)
+
+
+WeakTypes = (ref, BoundMethodWeakref)
+
+
+class annotatable_weakref(ref):
+ """A weakref.ref that supports custom instance attributes."""
+
+ receiver_id: t.Optional[IdentityType]
+ sender_id: t.Optional[IdentityType]
+
+
+def reference( # type: ignore[no-untyped-def]
+ object, callback=None, **annotations
+) -> annotatable_weakref:
+ """Return an annotated weak ref."""
+ if callable(object):
+ weak = callable_reference(object, callback)
+ else:
+ weak = annotatable_weakref(object, callback)
+ for key, value in annotations.items():
+ setattr(weak, key, value)
+ return weak # type: ignore[no-any-return]
+
+
+def callable_reference(object, callback=None):
+ """Return an annotated weak ref, supporting bound instance methods."""
+ if hasattr(object, "im_self") and object.im_self is not None:
+ return BoundMethodWeakref(target=object, on_delete=callback)
+ elif hasattr(object, "__self__") and object.__self__ is not None:
+ return BoundMethodWeakref(target=object, on_delete=callback)
+ return annotatable_weakref(object, callback)
+
+
+class lazy_property:
+ """A @property that is only evaluated once."""
+
+ def __init__(self, deferred):
+ self._deferred = deferred
+ self.__doc__ = deferred.__doc__
+
+ def __get__(self, obj, cls):
+ if obj is None:
+ return self
+ value = self._deferred(obj)
+ setattr(obj, self._deferred.__name__, value)
+ return value
+
+
+def is_coroutine_function(func: t.Any) -> bool:
+ # Python < 3.8 does not correctly determine partially wrapped
+ # coroutine functions are coroutine functions, hence the need for
+ # this to exist. Code taken from CPython.
+ if sys.version_info >= (3, 8):
+ return asyncio.iscoroutinefunction(func)
+ else:
+ # Note that there is something special about the AsyncMock
+ # such that it isn't determined as a coroutine function
+ # without an explicit check.
+ try:
+ from unittest.mock import AsyncMock # type: ignore[attr-defined]
+
+ if isinstance(func, AsyncMock):
+ return True
+ except ImportError:
+ # Not testing, no asynctest to import
+ pass
+
+ while inspect.ismethod(func):
+ func = func.__func__
+ while isinstance(func, partial):
+ func = func.func
+ if not inspect.isfunction(func):
+ return False
+
+ if func.__code__.co_flags & inspect.CO_COROUTINE:
+ return True
+
+ acic = asyncio.coroutines._is_coroutine # type: ignore[attr-defined]
+ return getattr(func, "_is_coroutine", None) is acic
diff --git a/blinker/base.py b/src/blinker/base.py
similarity index 64%
rename from blinker/base.py
rename to src/blinker/base.py
index 8a3e4f9..80e24e2 100644
--- a/blinker/base.py
+++ b/src/blinker/base.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8; fill-column: 76 -*-
"""Signals and events.
A small implementation of signals, inspired by a snippet of Django signal
@@ -8,26 +7,40 @@ each manages its own receivers and message emission.
The :func:`signal` function provides singleton behavior for named signals.
"""
+from __future__ import annotations
+
+import typing as t
+from collections import defaultdict
from contextlib import contextmanager
from warnings import warn
from weakref import WeakValueDictionary
-from blinker._utilities import (
- WeakTypes,
- defaultdict,
- hashable_identity,
- lazy_property,
- reference,
- symbol,
- )
+from blinker._utilities import annotatable_weakref
+from blinker._utilities import hashable_identity
+from blinker._utilities import IdentityType
+from blinker._utilities import is_coroutine_function
+from blinker._utilities import lazy_property
+from blinker._utilities import reference
+from blinker._utilities import symbol
+from blinker._utilities import WeakTypes
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ T_callable = t.TypeVar("T_callable", bound=t.Callable[..., t.Any])
+
+ T = t.TypeVar("T")
+ P = te.ParamSpec("P")
+ AsyncWrapperType = t.Callable[[t.Callable[P, T]], t.Callable[P, t.Awaitable[T]]]
+ SyncWrapperType = t.Callable[[t.Callable[P, t.Awaitable[T]]], t.Callable[P, T]]
-ANY = symbol('ANY')
+ANY = symbol("ANY")
ANY.__doc__ = 'Token for "any sender".'
ANY_ID = 0
-class Signal(object):
+class Signal:
"""A notification emitter."""
#: An :obj:`ANY` convenience synonym, allows ``Signal.ANY``
@@ -35,7 +48,7 @@ class Signal(object):
ANY = ANY
@lazy_property
- def receiver_connected(self):
+ def receiver_connected(self) -> Signal:
"""Emitted after each :meth:`connect`.
The signal sender is the signal instance, and the :meth:`connect`
@@ -47,7 +60,7 @@ class Signal(object):
return Signal(doc="Emitted after a receiver connects.")
@lazy_property
- def receiver_disconnected(self):
+ def receiver_disconnected(self) -> Signal:
"""Emitted after :meth:`disconnect`.
The sender is the signal instance, and the :meth:`disconnect` arguments
@@ -70,7 +83,7 @@ class Signal(object):
"""
return Signal(doc="Emitted after a receiver disconnects.")
- def __init__(self, doc=None):
+ def __init__(self, doc: str | None = None) -> None:
"""
:param doc: optional. If provided, will be assigned to the signal's
__doc__ attribute.
@@ -84,12 +97,15 @@ class Signal(object):
#: internal :class:`Signal` implementation, however the boolean value
#: of the mapping is useful as an extremely efficient check to see if
#: any receivers are connected to the signal.
- self.receivers = {}
- self._by_receiver = defaultdict(set)
- self._by_sender = defaultdict(set)
- self._weak_senders = {}
-
- def connect(self, receiver, sender=ANY, weak=True):
+ self.receivers: dict[IdentityType, t.Callable | annotatable_weakref] = {}
+ self.is_muted = False
+ self._by_receiver: dict[IdentityType, set[IdentityType]] = defaultdict(set)
+ self._by_sender: dict[IdentityType, set[IdentityType]] = defaultdict(set)
+ self._weak_senders: dict[IdentityType, annotatable_weakref] = {}
+
+ def connect(
+ self, receiver: T_callable, sender: t.Any = ANY, weak: bool = True
+ ) -> T_callable:
"""Connect *receiver* to signal events sent by *sender*.
:param receiver: A callable. Will be invoked by :meth:`send` with
@@ -109,11 +125,14 @@ class Signal(object):
"""
receiver_id = hashable_identity(receiver)
+ receiver_ref: T_callable | annotatable_weakref
+
if weak:
receiver_ref = reference(receiver, self._cleanup_receiver)
receiver_ref.receiver_id = receiver_id
else:
receiver_ref = receiver
+ sender_id: IdentityType
if sender is ANY:
sender_id = ANY_ID
else:
@@ -136,28 +155,27 @@ class Signal(object):
del sender_ref
# broadcast this connection. if receivers raise, disconnect.
- if ('receiver_connected' in self.__dict__ and
- self.receiver_connected.receivers):
+ if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
try:
- self.receiver_connected.send(self,
- receiver=receiver,
- sender=sender,
- weak=weak)
- except:
+ self.receiver_connected.send(
+ self, receiver=receiver, sender=sender, weak=weak
+ )
+ except TypeError as e:
self.disconnect(receiver, sender)
- raise
+ raise e
if receiver_connected.receivers and self is not receiver_connected:
try:
- receiver_connected.send(self,
- receiver_arg=receiver,
- sender_arg=sender,
- weak_arg=weak)
- except:
+ receiver_connected.send(
+ self, receiver_arg=receiver, sender_arg=sender, weak_arg=weak
+ )
+ except TypeError as e:
self.disconnect(receiver, sender)
- raise
+ raise e
return receiver
- def connect_via(self, sender, weak=False):
+ def connect_via(
+ self, sender: t.Any, weak: bool = False
+ ) -> t.Callable[[T_callable], T_callable]:
"""Connect the decorated function as a receiver for *sender*.
:param sender: Any object or :obj:`ANY`. The decorated function
@@ -178,13 +196,17 @@ class Signal(object):
.. versionadded:: 1.1
"""
- def decorator(fn):
+
+ def decorator(fn: T_callable) -> T_callable:
self.connect(fn, sender, weak)
return fn
+
return decorator
@contextmanager
- def connected_to(self, receiver, sender=ANY):
+ def connected_to(
+ self, receiver: t.Callable, sender: t.Any = ANY
+ ) -> t.Generator[None, None, None]:
"""Execute a block with the signal temporarily connected to *receiver*.
:param receiver: a receiver callable
@@ -207,13 +229,28 @@ class Signal(object):
self.connect(receiver, sender=sender, weak=False)
try:
yield None
- except:
+ except Exception as e:
self.disconnect(receiver)
- raise
+ raise e
else:
self.disconnect(receiver)
- def temporarily_connected_to(self, receiver, sender=ANY):
+ @contextmanager
+ def muted(self) -> t.Generator[None, None, None]:
+ """Context manager for temporarily disabling signal.
+ Useful for test purposes.
+ """
+ self.is_muted = True
+ try:
+ yield None
+ except Exception as e:
+ raise e
+ finally:
+ self.is_muted = False
+
+ def temporarily_connected_to(
+ self, receiver: t.Callable, sender: t.Any = ANY
+ ) -> t.ContextManager[None]:
"""An alias for :meth:`connected_to`.
:param receiver: a receiver callable
@@ -226,12 +263,18 @@ class Signal(object):
deprecated in 1.2 and will be removed in a subsequent version.
"""
- warn("temporarily_connected_to is deprecated; "
- "use connected_to instead.",
- DeprecationWarning)
+ warn(
+ "temporarily_connected_to is deprecated; use connected_to instead.",
+ DeprecationWarning,
+ )
return self.connected_to(receiver, sender)
- def send(self, *sender, **kwargs):
+ def send(
+ self,
+ *sender: t.Any,
+ _async_wrapper: AsyncWrapperType | None = None,
+ **kwargs: t.Any,
+ ) -> list[tuple[t.Callable, t.Any]]:
"""Emit this signal on behalf of *sender*, passing on ``kwargs``.
Returns a list of 2-tuples, pairing receivers with their return
@@ -239,15 +282,65 @@ class Signal(object):
:param sender: Any object or ``None``. If omitted, synonymous
with ``None``. Only accepts one positional argument.
+ :param _async_wrapper: A callable that should wrap a coroutine
+ receiver and run it when called synchronously.
+
+ :param kwargs: Data to be sent to receivers.
+ """
+ if self.is_muted:
+ return []
+
+ sender = self._extract_sender(sender)
+ results = []
+ for receiver in self.receivers_for(sender):
+ if is_coroutine_function(receiver):
+ if _async_wrapper is None:
+ raise RuntimeError("Cannot send to a coroutine function")
+ receiver = _async_wrapper(receiver)
+ result = receiver(sender, **kwargs) # type: ignore[call-arg]
+ results.append((receiver, result))
+ return results
+
+ async def send_async(
+ self,
+ *sender: t.Any,
+ _sync_wrapper: SyncWrapperType | None = None,
+ **kwargs: t.Any,
+ ) -> list[tuple[t.Callable, t.Any]]:
+ """Emit this signal on behalf of *sender*, passing on ``kwargs``.
+
+ Returns a list of 2-tuples, pairing receivers with their return
+ value. The ordering of receiver notification is undefined.
+
+ :param sender: Any object or ``None``. If omitted, synonymous
+ with ``None``. Only accepts one positional argument.
+ :param _sync_wrapper: A callable that should wrap a synchronous
+ receiver and run it when awaited.
:param kwargs: Data to be sent to receivers.
"""
+ if self.is_muted:
+ return []
+
+ sender = self._extract_sender(sender)
+ results = []
+ for receiver in self.receivers_for(sender):
+ if not is_coroutine_function(receiver):
+ if _sync_wrapper is None:
+ raise RuntimeError("Cannot send to a non-coroutine function")
+ receiver = _sync_wrapper(receiver) # type: ignore[arg-type]
+ result = await receiver(sender, **kwargs) # type: ignore[call-arg, misc]
+ results.append((receiver, result))
+ return results
+
+ def _extract_sender(self, sender: t.Any) -> t.Any:
if not self.receivers:
# Ensure correct signature even on no-op sends, disable with -O
# for lowest possible cost.
if __debug__ and sender and len(sender) > 1:
- raise TypeError('send() accepts only one positional '
- 'argument, %s given' % len(sender))
+ raise TypeError(
+ f"send() accepts only one positional argument, {len(sender)} given"
+ )
return []
# Using '*sender' rather than 'sender=None' allows 'sender' to be
@@ -256,14 +349,14 @@ class Signal(object):
if len(sender) == 0:
sender = None
elif len(sender) > 1:
- raise TypeError('send() accepts only one positional argument, '
- '%s given' % len(sender))
+ raise TypeError(
+ f"send() accepts only one positional argument, {len(sender)} given"
+ )
else:
sender = sender[0]
- return [(receiver, receiver(sender, **kwargs))
- for receiver in self.receivers_for(sender)]
+ return sender
- def has_receivers_for(self, sender):
+ def has_receivers_for(self, sender: t.Any) -> bool:
"""True if there is probably a receiver for *sender*.
Performs an optimistic check only. Does not guarantee that all
@@ -279,14 +372,15 @@ class Signal(object):
return False
return hashable_identity(sender) in self._by_sender
- def receivers_for(self, sender):
+ def receivers_for(
+ self, sender: t.Any
+ ) -> t.Generator[t.Callable | annotatable_weakref, None, None]:
"""Iterate all live receivers listening for *sender*."""
# TODO: test receivers_for(ANY)
if self.receivers:
sender_id = hashable_identity(sender)
if sender_id in self._by_sender:
- ids = (self._by_sender[ANY_ID] |
- self._by_sender[sender_id])
+ ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
else:
ids = self._by_sender[ANY_ID].copy()
for receiver_id in ids:
@@ -299,9 +393,9 @@ class Signal(object):
self._disconnect(receiver_id, ANY_ID)
continue
receiver = strong
- yield receiver
+ yield receiver # type: ignore[misc]
- def disconnect(self, receiver, sender=ANY):
+ def disconnect(self, receiver: t.Callable, sender: t.Any = ANY) -> None:
"""Disconnect *receiver* from this signal's events.
:param receiver: a previously :meth:`connected<connect>` callable
@@ -310,6 +404,7 @@ class Signal(object):
to disconnect from all senders. Defaults to ``ANY``.
"""
+ sender_id: IdentityType
if sender is ANY:
sender_id = ANY_ID
else:
@@ -317,13 +412,13 @@ class Signal(object):
receiver_id = hashable_identity(receiver)
self._disconnect(receiver_id, sender_id)
- if ('receiver_disconnected' in self.__dict__ and
- self.receiver_disconnected.receivers):
- self.receiver_disconnected.send(self,
- receiver=receiver,
- sender=sender)
+ if (
+ "receiver_disconnected" in self.__dict__
+ and self.receiver_disconnected.receivers
+ ):
+ self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
- def _disconnect(self, receiver_id, sender_id):
+ def _disconnect(self, receiver_id: IdentityType, sender_id: IdentityType) -> None:
if sender_id == ANY_ID:
if self._by_receiver.pop(receiver_id, False):
for bucket in self._by_sender.values():
@@ -333,19 +428,19 @@ class Signal(object):
self._by_sender[sender_id].discard(receiver_id)
self._by_receiver[receiver_id].discard(sender_id)
- def _cleanup_receiver(self, receiver_ref):
+ def _cleanup_receiver(self, receiver_ref: annotatable_weakref) -> None:
"""Disconnect a receiver from all senders."""
- self._disconnect(receiver_ref.receiver_id, ANY_ID)
+ self._disconnect(t.cast(IdentityType, receiver_ref.receiver_id), ANY_ID)
- def _cleanup_sender(self, sender_ref):
+ def _cleanup_sender(self, sender_ref: annotatable_weakref) -> None:
"""Disconnect all receivers from a sender."""
- sender_id = sender_ref.sender_id
+ sender_id = t.cast(IdentityType, sender_ref.sender_id)
assert sender_id != ANY_ID
self._weak_senders.pop(sender_id, None)
for receiver_id in self._by_sender.pop(sender_id, ()):
self._by_receiver[receiver_id].discard(sender_id)
- def _cleanup_bookkeeping(self):
+ def _cleanup_bookkeeping(self) -> None:
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
Connecting & disconnecting leave behind a small amount of bookkeeping
@@ -371,7 +466,7 @@ class Signal(object):
if not bucket:
mapping.pop(_id, None)
- def _clear_state(self):
+ def _clear_state(self) -> None:
"""Throw away all signal state. Useful for unit tests."""
self._weak_senders.clear()
self.receivers.clear()
@@ -379,7 +474,8 @@ class Signal(object):
self._by_receiver.clear()
-receiver_connected = Signal("""\
+receiver_connected = Signal(
+ """\
Sent by a :class:`Signal` after a receiver connects.
:argument: the Signal that was connected to
@@ -394,36 +490,38 @@ As of 1.2, individual signals have their own private
:attr:`~Signal.receiver_disconnected` signals with a slightly simplified
call signature. This global signal is planned to be removed in 1.6.
-""")
+"""
+)
class NamedSignal(Signal):
"""A named generic notification emitter."""
- def __init__(self, name, doc=None):
+ def __init__(self, name: str, doc: str | None = None) -> None:
Signal.__init__(self, doc)
#: The name of this signal.
self.name = name
- def __repr__(self):
+ def __repr__(self) -> str:
base = Signal.__repr__(self)
- return "{}; {!r}>".format(base[:-1], self.name)
+ return f"{base[:-1]}; {self.name!r}>"
class Namespace(dict):
"""A mapping of signal names to signals."""
- def signal(self, name, doc=None):
+ def signal(self, name: str, doc: str | None = None) -> NamedSignal:
"""Return the :class:`NamedSignal` *name*, creating it if required.
Repeated calls to this function will return the same signal object.
"""
try:
- return self[name]
+ return self[name] # type: ignore[no-any-return]
except KeyError:
- return self.setdefault(name, NamedSignal(name, doc))
+ result = self.setdefault(name, NamedSignal(name, doc))
+ return result # type: ignore[no-any-return]
class WeakNamespace(WeakValueDictionary):
@@ -437,16 +535,17 @@ class WeakNamespace(WeakValueDictionary):
"""
- def signal(self, name, doc=None):
+ def signal(self, name: str, doc: str | None = None) -> NamedSignal:
"""Return the :class:`NamedSignal` *name*, creating it if required.
Repeated calls to this function will return the same signal object.
"""
try:
- return self[name]
+ return self[name] # type: ignore[no-any-return]
except KeyError:
- return self.setdefault(name, NamedSignal(name, doc))
+ result = self.setdefault(name, NamedSignal(name, doc))
+ return result # type: ignore[no-any-return]
signal = Namespace().signal
diff --git a/src/blinker/py.typed b/src/blinker/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/tests/requirements.in b/tests/requirements.in
deleted file mode 100644
index e079f8a..0000000
--- a/tests/requirements.in
+++ /dev/null
@@ -1 +0,0 @@
-pytest
diff --git a/tests/requirements.txt b/tests/requirements.txt
deleted file mode 100644
index 07ddd98..0000000
--- a/tests/requirements.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-#
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
-#
-# pip-compile tests/requirements.in
-#
-attrs==21.4.0
- # via pytest
-iniconfig==1.1.1
- # via pytest
-packaging==21.3
- # via pytest
-pluggy==1.0.0
- # via pytest
-py==1.11.0
- # via pytest
-pyparsing==3.0.9
- # via packaging
-pytest==7.1.2
- # via -r tests/requirements.in
-tomli==2.0.1
- # via pytest
diff --git a/tests/test_context.py b/tests/test_context.py
index 7521ac3..f45cba7 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -1,5 +1,3 @@
-from __future__ import with_statement
-
from blinker import Signal
@@ -7,7 +5,9 @@ def test_temp_connection():
sig = Signal()
canary = []
- receiver = lambda sender: canary.append(sender)
+
+ def receiver(sender):
+ canary.append(sender)
sig.send(1)
with sig.connected_to(receiver):
@@ -22,7 +22,9 @@ def test_temp_connection_for_sender():
sig = Signal()
canary = []
- receiver = lambda sender: canary.append(sender)
+
+ def receiver(sender):
+ canary.append(sender)
with sig.connected_to(receiver, sender=2):
sig.send(1)
@@ -36,7 +38,9 @@ def test_temp_connection_failure():
sig = Signal()
canary = []
- receiver = lambda sender: canary.append(sender)
+
+ def receiver(sender):
+ canary.append(sender)
class Failure(Exception):
pass
diff --git a/tests/test_saferef.py b/tests/test_saferef.py
index 0517fe5..123183b 100644
--- a/tests/test_saferef.py
+++ b/tests/test_saferef.py
@@ -37,7 +37,7 @@ import unittest
from blinker._saferef import safe_ref
-class _Sample1(object):
+class _Sample1:
def x(self):
pass
@@ -46,7 +46,7 @@ def _sample2(obj):
pass
-class _Sample3(object):
+class _Sample3:
def __call__(self, obj):
pass
@@ -60,14 +60,14 @@ class TestSaferef(unittest.TestCase):
def setUp(self):
ts = []
ss = []
- for x in range(100):
+ for _ in range(100):
t = _Sample1()
ts.append(t)
s = safe_ref(t.x, self._closure)
ss.append(s)
ts.append(_sample2)
ss.append(safe_ref(_sample2, self._closure))
- for x in range(30):
+ for _ in range(30):
t = _Sample3()
ts.append(t)
s = safe_ref(t, self._closure)
@@ -77,9 +77,9 @@ class TestSaferef(unittest.TestCase):
self.closure_count = 0
def tearDown(self):
- if hasattr(self, 'ts'):
+ if hasattr(self, "ts"):
del self.ts
- if hasattr(self, 'ss'):
+ if hasattr(self, "ss"):
del self.ss
def test_In(self):
@@ -98,7 +98,7 @@ class TestSaferef(unittest.TestCase):
for s in self.ss:
sd[s] = 1
for t in self.ts:
- if hasattr(t, 'x'):
+ if hasattr(t, "x"):
assert safe_ref(t.x) in sd
else:
assert safe_ref(t) in sd
diff --git a/tests/test_signals.py b/tests/test_signals.py
index e74db47..f06ebc0 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -2,13 +2,13 @@ import gc
import sys
import time
-import blinker
-
import pytest
+import blinker
+
-jython = sys.platform.startswith('java')
-pypy = hasattr(sys, 'pypy_version_info')
+jython = sys.platform.startswith("java")
+pypy = hasattr(sys, "pypy_version_info")
def collect_acyclic_refs():
@@ -28,14 +28,17 @@ class Sentinel(list):
When connected to a signal, appends (key, sender, kw) to the Sentinel.
"""
+
def receiver(*sentby, **kw):
self.append((key, sentby[0], kw))
- receiver.func_name = 'receiver_%s' % key
+
+ receiver.func_name = "receiver_%s" % key
return receiver
def test_meta_connect():
sentinel = []
+
def meta_received(sender, **kw):
sentinel.append(dict(kw, sender=sender))
@@ -45,13 +48,18 @@ def test_meta_connect():
def receiver(sender, **kw):
pass
+
sig = blinker.Signal()
sig.connect(receiver)
- assert sentinel == [{'sender': sig,
- 'receiver_arg': receiver,
- 'sender_arg': blinker.ANY,
- 'weak_arg': True}]
+ assert sentinel == [
+ {
+ "sender": sig,
+ "receiver_arg": receiver,
+ "sender_arg": blinker.ANY,
+ "weak_arg": True,
+ }
+ ]
blinker.receiver_connected._clear_state()
@@ -60,10 +68,10 @@ def _test_signal_signals(sender):
sentinel = Sentinel()
sig = blinker.Signal()
- connected = sentinel.make_receiver('receiver_connected')
- disconnected = sentinel.make_receiver('receiver_disconnected')
- receiver1 = sentinel.make_receiver('receiver1')
- receiver2 = sentinel.make_receiver('receiver2')
+ connected = sentinel.make_receiver("receiver_connected")
+ disconnected = sentinel.make_receiver("receiver_disconnected")
+ receiver1 = sentinel.make_receiver("receiver1")
+ receiver2 = sentinel.make_receiver("receiver2")
assert not sig.receiver_connected.receivers
assert not sig.receiver_disconnected.receivers
@@ -76,25 +84,27 @@ def _test_signal_signals(sender):
for receiver, weak in [(receiver1, True), (receiver2, False)]:
sig.connect(receiver, sender=sender, weak=weak)
- expected = ('receiver_connected',
- sig,
- {'receiver': receiver, 'sender': sender, 'weak': weak})
+ expected = (
+ "receiver_connected",
+ sig,
+ {"receiver": receiver, "sender": sender, "weak": weak},
+ )
assert sentinel[-1] == expected
# disconnect from explicit sender
sig.disconnect(receiver1, sender=sender)
- expected = ('receiver_disconnected',
- sig,
- {'receiver': receiver1, 'sender': sender})
+ expected = ("receiver_disconnected", sig, {"receiver": receiver1, "sender": sender})
assert sentinel[-1] == expected
# disconnect from ANY and all senders (implicit disconnect signature)
sig.disconnect(receiver2)
- assert sentinel[-1] == ('receiver_disconnected',
- sig,
- {'receiver': receiver2, 'sender': blinker.ANY})
+ assert sentinel[-1] == (
+ "receiver_disconnected",
+ sig,
+ {"receiver": receiver2, "sender": blinker.ANY},
+ )
def test_signal_signals_any_sender():
@@ -111,10 +121,10 @@ def test_signal_weak_receiver_vanishes():
sentinel = Sentinel()
sig = blinker.Signal()
- connected = sentinel.make_receiver('receiver_connected')
- disconnected = sentinel.make_receiver('receiver_disconnected')
- receiver1 = sentinel.make_receiver('receiver1')
- receiver2 = sentinel.make_receiver('receiver2')
+ connected = sentinel.make_receiver("receiver_connected")
+ disconnected = sentinel.make_receiver("receiver_disconnected")
+ receiver1 = sentinel.make_receiver("receiver1")
+ receiver2 = sentinel.make_receiver("receiver2")
sig.receiver_connected.connect(connected)
sig.receiver_disconnected.connect(disconnected)
@@ -124,7 +134,7 @@ def test_signal_weak_receiver_vanishes():
sig.disconnect(receiver1)
assert len(sentinel) == 2
- assert sentinel[-1][2]['receiver'] is receiver1
+ assert sentinel[-1][2]["receiver"] is receiver1
del sentinel[:]
sig.connect(receiver2, weak=True)
@@ -138,7 +148,7 @@ def test_signal_weak_receiver_vanishes():
assert len(sentinel) == 0
# and everything really is disconnected
- sig.send('abc')
+ sig.send("abc")
assert len(sentinel) == 0
@@ -146,12 +156,12 @@ def test_signal_signals_weak_sender():
sentinel = Sentinel()
sig = blinker.Signal()
- connected = sentinel.make_receiver('receiver_connected')
- disconnected = sentinel.make_receiver('receiver_disconnected')
- receiver1 = sentinel.make_receiver('receiver1')
- receiver2 = sentinel.make_receiver('receiver2')
+ connected = sentinel.make_receiver("receiver_connected")
+ disconnected = sentinel.make_receiver("receiver_disconnected")
+ receiver1 = sentinel.make_receiver("receiver1")
+ receiver2 = sentinel.make_receiver("receiver2")
- class Sender(object):
+ class Sender:
"""A weakref-able object."""
sig.receiver_connected.connect(connected)
@@ -176,22 +186,29 @@ def test_signal_signals_weak_sender():
assert len(sentinel) == 1
# and everything really is disconnected
- sig.send('abc')
+ sig.send("abc")
assert len(sentinel) == 1
def test_empty_bucket_growth():
+ def senders():
+ return (
+ len(sig._by_sender),
+ sum(len(i) for i in sig._by_sender.values()),
+ )
+
+ def receivers():
+ return (
+ len(sig._by_receiver),
+ sum(len(i) for i in sig._by_receiver.values()),
+ )
+
sentinel = Sentinel()
sig = blinker.Signal()
- senders = lambda: (len(sig._by_sender),
- sum(len(i) for i in sig._by_sender.values()))
- receivers = lambda: (len(sig._by_receiver),
- sum(len(i) for i in sig._by_receiver.values()))
-
- receiver1 = sentinel.make_receiver('receiver1')
- receiver2 = sentinel.make_receiver('receiver2')
+ receiver1 = sentinel.make_receiver("receiver1")
+ receiver2 = sentinel.make_receiver("receiver2")
- class Sender(object):
+ class Sender:
"""A weakref-able object."""
sender = Sender()
@@ -218,13 +235,14 @@ def test_empty_bucket_growth():
def test_meta_connect_failure():
def meta_received(sender, **kw):
- raise TypeError('boom')
+ raise TypeError("boom")
assert not blinker.receiver_connected.receivers
blinker.receiver_connected.connect(meta_received)
def receiver(sender, **kw):
pass
+
sig = blinker.Signal()
pytest.raises(TypeError, sig.connect, receiver)
@@ -238,37 +256,38 @@ def test_meta_connect_failure():
def test_weak_namespace():
ns = blinker.WeakNamespace()
assert not ns
- s1 = ns.signal('abc')
- assert s1 is ns.signal('abc')
- assert s1 is not ns.signal('def')
- assert 'abc' in ns
+ s1 = ns.signal("abc")
+ assert s1 is ns.signal("abc")
+ assert s1 is not ns.signal("def")
+ assert "abc" in ns
collect_acyclic_refs()
# weak by default, already out of scope
- assert 'def' not in ns
+ assert "def" not in ns
del s1
collect_acyclic_refs()
- assert 'abc' not in ns
+ assert "abc" not in ns
def test_namespace():
ns = blinker.Namespace()
assert not ns
- s1 = ns.signal('abc')
- assert s1 is ns.signal('abc')
- assert s1 is not ns.signal('def')
- assert 'abc' in ns
+ s1 = ns.signal("abc")
+ assert s1 is ns.signal("abc")
+ assert s1 is not ns.signal("def")
+ assert "abc" in ns
del s1
collect_acyclic_refs()
- assert 'def' in ns
- assert 'abc' in ns
+ assert "def" in ns
+ assert "abc" in ns
def test_weak_receiver():
sentinel = []
+
def received(sender, **kw):
sentinel.append(kw)
@@ -295,8 +314,10 @@ def test_weak_receiver():
def test_strong_receiver():
sentinel = []
+
def received(sender):
sentinel.append(sender)
+
fn_id = id(received)
sig = blinker.Signal()
@@ -311,12 +332,39 @@ def test_strong_receiver():
assert [id(fn) for fn in sig.receivers.values()] == [fn_id]
+async def test_async_receiver():
+ sentinel = []
+
+ async def received_async(sender):
+ sentinel.append(sender)
+
+ def received(sender):
+ sentinel.append(sender)
+
+ def wrapper(func):
+ async def inner(*args, **kwargs):
+ func(*args, **kwargs)
+
+ return inner
+
+ sig = blinker.Signal()
+ sig.connect(received)
+ sig.connect(received_async)
+
+ await sig.send_async(_sync_wrapper=wrapper)
+ assert len(sentinel) == 2
+
+ with pytest.raises(RuntimeError):
+ sig.send()
+
+
def test_instancemethod_receiver():
sentinel = []
- class Receiver(object):
+ class Receiver:
def __init__(self, bucket):
self.bucket = bucket
+
def received(self, sender):
self.bucket.append(sender)
@@ -337,6 +385,7 @@ def test_instancemethod_receiver():
def test_filtered_receiver():
sentinel = []
+
def received(sender):
sentinel.append(sender)
@@ -367,11 +416,13 @@ def test_filtered_receiver():
def test_filtered_receiver_weakref():
sentinel = []
+
def received(sender):
sentinel.append(sender)
- class Object(object):
+ class Object:
pass
+
obj = Object()
sig = blinker.Signal()
@@ -394,8 +445,9 @@ def test_filtered_receiver_weakref():
def test_decorated_receiver():
sentinel = []
- class Object(object):
+ class Object:
pass
+
obj = Object()
sig = blinker.Signal()
@@ -419,6 +471,7 @@ def test_decorated_receiver():
def test_no_double_send():
sentinel = []
+
def received(sender):
sentinel.append(sender)
@@ -437,19 +490,21 @@ def test_no_double_send():
def test_has_receivers():
- received = lambda sender: None
+ def received(_):
+ return None
sig = blinker.Signal()
assert not sig.has_receivers_for(None)
assert not sig.has_receivers_for(blinker.ANY)
- sig.connect(received, 'xyz')
+ sig.connect(received, "xyz")
assert not sig.has_receivers_for(None)
assert not sig.has_receivers_for(blinker.ANY)
- assert sig.has_receivers_for('xyz')
+ assert sig.has_receivers_for("xyz")
- class Object(object):
+ class Object:
pass
+
o = Object()
sig.connect(received, o)
@@ -458,31 +513,63 @@ def test_has_receivers():
del received
collect_acyclic_refs()
- assert not sig.has_receivers_for('xyz')
- assert list(sig.receivers_for('xyz')) == []
+ assert not sig.has_receivers_for("xyz")
+ assert list(sig.receivers_for("xyz")) == []
assert list(sig.receivers_for(o)) == []
sig.connect(lambda sender: None, weak=False)
- assert sig.has_receivers_for('xyz')
+ assert sig.has_receivers_for("xyz")
assert sig.has_receivers_for(o)
assert sig.has_receivers_for(None)
assert sig.has_receivers_for(blinker.ANY)
- assert sig.has_receivers_for('xyz')
+ assert sig.has_receivers_for("xyz")
def test_instance_doc():
- sig = blinker.Signal(doc='x')
- assert sig.__doc__ == 'x'
+ sig = blinker.Signal(doc="x")
+ assert sig.__doc__ == "x"
- sig = blinker.Signal('x')
- assert sig.__doc__ == 'x'
+ sig = blinker.Signal("x")
+ assert sig.__doc__ == "x"
def test_named_blinker():
- sig = blinker.NamedSignal('squiznart')
- assert 'squiznart' in repr(sig)
+ sig = blinker.NamedSignal("squiznart")
+ assert "squiznart" in repr(sig)
+
+
+def test_mute_signal():
+ sentinel = []
+
+ def received(sender):
+ sentinel.append(sender)
+
+ sig = blinker.Signal()
+ sig.connect(received)
+
+ sig.send(123)
+ assert 123 in sentinel
+
+ with sig.muted():
+ sig.send(456)
+ assert 456 not in sentinel
def values_are_empty_sets_(dictionary):
for val in dictionary.values():
assert val == set()
+
+
+def test_int_sender():
+ sentinel = []
+
+ def received(sender):
+ sentinel.append(sender)
+
+ sig = blinker.Signal()
+
+ sig.connect(received, sender=123456789)
+ sig.send(123456789)
+ assert len(sentinel) == 1
+ sig.send(123456789 + 0) # Forces a new id with CPython
+ assert len(sentinel) == 2
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 4f802f4..a07a53a 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -4,21 +4,21 @@ from blinker._utilities import symbol
def test_symbols():
- foo = symbol('foo')
- assert foo.name == 'foo'
- assert foo is symbol('foo')
+ foo = symbol("foo")
+ assert foo.name == "foo"
+ assert foo is symbol("foo")
- bar = symbol('bar')
+ bar = symbol("bar")
assert foo is not bar
assert foo != bar
assert not foo == bar
- assert repr(foo) == 'foo'
+ assert repr(foo) == "foo"
def test_pickled_symbols():
- foo = symbol('foo')
+ foo = symbol("foo")
- for protocol in 0, 1, 2:
+ for _ in 0, 1, 2:
roundtrip = pickle.loads(pickle.dumps(foo))
assert roundtrip is foo
diff --git a/tox.ini b/tox.ini
index 72fdd8e..75f9933 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,27 @@
[tox]
envlist =
- py3{11,10,9,8,7}
+ py3{12,11,10,9,8,7}
pypy3{9,8,7}
+ docs
+ lint
+ typing
skip_missing_interpreters = true
[testenv]
-deps = -r tests/requirements.txt
+deps = -r requirements/tests.txt
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests}
+
+[testenv:docs]
+deps = -r requirements/docs.txt
+commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
+
+[testenv:lint]
+skip_install = true
+deps =
+ pre-commit>=2.20
+commands =
+ pre-commit run --all-files --show-diff-on-failure {tty:--color=always} {posargs}
+
+[testenv:typing]
+deps = -r requirements/typing.txt
+commands = mypy