pax_global_header00006660000000000000000000000064146162062300014512gustar00rootroot0000000000000052 comment=0a6be69aaaf72917bbedf41643f83128c8623075 blinker-1.8.2/000077500000000000000000000000001461620623000131505ustar00rootroot00000000000000blinker-1.8.2/.editorconfig000066400000000000000000000003511461620623000156240ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] indent_size = 2 blinker-1.8.2/.github/000077500000000000000000000000001461620623000145105ustar00rootroot00000000000000blinker-1.8.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001461620623000166735ustar00rootroot00000000000000blinker-1.8.2/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000011351461620623000213030ustar00rootroot00000000000000--- name: Bug report about: Report a bug in Blinker (not other projects which depend on Blinker) --- Python version: Blinker version: blinker-1.8.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005161461620623000206650ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Questions on Discussions url: https://github.com/pallets/blinker/discussions/ about: Ask questions about your own code on the Discussions tab. - name: Questions on Chat url: https://discord.gg/pallets about: Ask questions about your own code on our Discord chat. blinker-1.8.2/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000006441461620623000223420ustar00rootroot00000000000000--- name: Feature request about: Suggest a new feature for Blinker --- blinker-1.8.2/.github/dependabot.yml000066400000000000000000000005351461620623000173430ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly groups: github-actions: patterns: - '*' - package-ecosystem: pip directory: /requirements/ schedule: interval: monthly groups: python-requirements: patterns: - '*' blinker-1.8.2/.github/pull_request_template.md000066400000000000000000000014741461620623000214570ustar00rootroot00000000000000 blinker-1.8.2/.github/workflows/000077500000000000000000000000001461620623000165455ustar00rootroot00000000000000blinker-1.8.2/.github/workflows/lock.yaml000066400000000000000000000012251461620623000203610ustar00rootroot00000000000000name: Lock inactive closed issues # Lock closed issues that have not received any further activity for two weeks. # This does not close open issues, only humans may do that. It is easier to # respond to new issues with fresh examples rather than continuing discussions # on old issues. on: schedule: - cron: '0 0 * * *' permissions: issues: write pull-requests: write concurrency: group: lock jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: issue-inactive-days: 14 pr-inactive-days: 14 discussion-inactive-days: 14 blinker-1.8.2/.github/workflows/publish.yaml000066400000000000000000000051461461620623000211050ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.x' cache: pip cache-dependency-path: requirements*/*.txt - run: pip install -r requirements/build.txt # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - run: python -m build # Generate hashes used for provenance. - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: path: ./dist provenance: needs: [build] permissions: actions: read id-token: write contents: write # Can't pin with hash due to how this workflow works. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: ${{ needs.build.outputs.hash }} create-release: # Upload the sdist, wheels, and provenance to a GitHub release. They remain # available as build artifacts for a while as well. needs: [provenance] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 - name: create release run: > gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} *.intoto.jsonl/* artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: [provenance] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: name: publish url: https://pypi.org/project/blinker/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 with: repository-url: https://test.pypi.org/legacy/ packages-dir: artifact/ - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 with: packages-dir: artifact/ blinker-1.8.2/.github/workflows/tests.yaml000066400000000000000000000030571461620623000206000ustar00rootroot00000000000000name: Tests on: push: branches: - main - '*.x' paths-ignore: - 'docs/**' - '*.md' - '*.rst' pull_request: paths-ignore: - 'docs/**' - '*.md' - '*.rst' jobs: tests: name: ${{ matrix.name || matrix.python }} runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: fail-fast: false matrix: include: - {python: '3.12'} - {python: '3.11'} - {python: '3.10'} - {python: '3.9'} - {python: '3.8'} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: ${{ matrix.python }} allow-prereleases: true cache: pip cache-dependency-path: requirements*/*.txt - run: pip install tox - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} typing: runs-on: ubuntu-latest steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.x' cache: pip cache-dependency-path: requirements*/*.txt - name: cache mypy uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: ./.mypy_cache key: mypy|${{ hashFiles('pyproject.toml') }} - run: pip install tox - run: tox run -e typing blinker-1.8.2/.gitignore000066400000000000000000000001311461620623000151330ustar00rootroot00000000000000.idea/ .vscode/ .venv*/ venv*/ __pycache__/ dist/ .coverage* htmlcov/ .tox/ docs/_build/ blinker-1.8.2/.pre-commit-config.yaml000066400000000000000000000006271461620623000174360ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.2 hooks: - id: ruff - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-merge-conflict - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer blinker-1.8.2/.readthedocs.yaml000066400000000000000000000003211461620623000163730ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: '3.12' python: install: - requirements: requirements/docs.txt - method: pip path: . sphinx: builder: dirhtml fail_on_warning: true blinker-1.8.2/CHANGES.rst000066400000000000000000000115171461620623000147570ustar00rootroot00000000000000Version 1.8.2 ------------- Released 2024-05-06 - Simplify type for ``_async_wrapper`` and ``_sync_wrapper`` arguments. :pr:`156` Version 1.8.1 ------------- Released 2024-04-28 - Restore identity handling for ``str`` and ``int`` senders. :pr:`148` - Fix deprecated ``blinker.base.WeakNamespace`` import. :pr:`149` - Fix deprecated ``blinker.base.receiver_connected import``. :pr:`153` - Use types from ``collections.abc`` instead of ``typing``. :pr:`150` - Fully specify exported types as reported by pyright. :pr:`152` Version 1.8.0 ------------- Released 2024-04-27 - Deprecate the ``__version__`` attribute. Use feature detection, or ``importlib.metadata.version("blinker")``, instead. :issue:`128` - Specify that the deprecated ``temporarily_connected_to`` will be removed in the next version. - Show a deprecation warning for the deprecated global ``receiver_connected`` signal and specify that it will be removed in the next version. - Show a deprecation warning for the deprecated ``WeakNamespace`` and specify that it will be removed in the next version. - Greatly simplify how the library uses weakrefs. This is a significant change internally but should not affect any public API. :pr:`144` - Expose the namespace used by ``signal()`` as ``default_namespace``. :pr:`145` Version 1.7.0 ------------- Released 2023-11-01 - Fixed messages printed to standard error about unraisable exceptions during signal cleanup, typically during interpreter shutdown. :pr:`123` - Allow the Signal ``set_class`` to be customised, to allow calling of receivers in registration order. :pr:`116`. - Drop Python 3.7 and support Python 3.12. :pr:`126` Version 1.6.3 ------------- Released 2023-09-23 - Fix ``SyncWrapperType`` and ``AsyncWrapperType`` :pr:`108` - Fixed issue where ``connected_to`` would not disconnect the receiver if an instance of ``BaseException`` was raised. :pr:`114` 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` - ``int`` instances with the same value will be treated as the same sender, the same as ``str`` instances. :pr:`83` - Add a ``send_async`` method 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 initial set of type hints for the project. Version 1.5 ----------- Released 2022-07-17 - Support Python >= 3.7 and PyPy. Python 2, Python < 3.7, and Jython may continue to work, but the next release will make incompatible changes. Version 1.4 ----------- Released 2015-07-23 - Verified Python 3.4 support, no changes needed. - Additional bookkeeping cleanup for non-``ANY`` connections at disconnect time. - Added ``Signal._cleanup_bookeeping()`` to prune stale bookkeeping on demand. Version 1.3 ----------- Released 2013-07-03 - The global signal stash behind ``signal()`` is now backed by a regular name-to-``Signal`` dictionary. Previously, weak references were held in the mapping and ephermal usage in code like ``signal('foo').connect(...)`` could have surprising program behavior depending on import order of modules. - ``Namespace`` is now built on a regular dict. Use ``WeakNamespace`` for the older, weak-referencing behavior. - ``Signal.connect('text-sender')`` uses an alterate hashing strategy to avoid sharp edges in text identity. Version 1.2 ----------- Released 2011-10-26 - Added ``Signal.receiver_connected`` and ``Signal.receiver_disconnected`` per-``Signal`` signals. - Deprecated the global ``receiver_connected`` signal. - Verified Python 3.2 support, no changes needed. Version 1.1 ----------- Released 2010-07-21 - Added ``@signal.connect_via(sender)`` decorator - Added ``signal.connected_to`` shorthand name for the ``temporarily_connected_to`` context manager. Version 1.0 ----------- Released 2010-03-28 - Python 3.0 and 3.1 compatibility. Version 0.9 ----------- Released 2010-02-26 - Added ``Signal.temporarily_connected_to`` context manager. - Docs! Sphinx docs, project web site. Version 0.8 ----------- Released 2010-02-14 - Initial release. - Extracted from ``flatland.util.signals``. - Added Python 2.4 compatibility. - Added nearly functional Python 3.1 compatibility. Everything except connecting to instance methods seems to work. blinker-1.8.2/LICENSE.txt000066400000000000000000000020361461620623000147740ustar00rootroot00000000000000Copyright 2010 Jason Kirtland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. blinker-1.8.2/README.md000066400000000000000000000017751461620623000144410ustar00rootroot00000000000000# Blinker Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events, or "signals". ## Pallets Community Ecosystem > [!IMPORTANT]\ > This project is part of the Pallets Community Ecosystem. Pallets is the open > source organization that maintains Flask; Pallets-Eco enables community > maintenance of related projects. If you are interested in helping maintain > this project, please reach out on [the Pallets Discord server][discord]. > > [discord]: https://discord.gg/pallets ## Example Signal receivers can subscribe to specific senders or receive signals sent by any sender. ```pycon >>> from blinker import signal >>> started = signal('round-started') >>> def each(round): ... print(f"Round {round}") ... >>> started.connect(each) >>> def round_two(round): ... print("This is round two.") ... >>> started.connect(round_two, sender=2) >>> for round in range(1, 4): ... started.send(round) ... Round 1! Round 2! This is round two. Round 3! ``` blinker-1.8.2/docs/000077500000000000000000000000001461620623000141005ustar00rootroot00000000000000blinker-1.8.2/docs/_static/000077500000000000000000000000001461620623000155265ustar00rootroot00000000000000blinker-1.8.2/docs/_static/blinker-named.png000066400000000000000000000070531461620623000207510ustar00rootroot00000000000000PNG  IHDR@\<[sRGBbKGD pHYs B(xtIME.\g+ IDATx{xŽ?d@  AQ>RXETBUl#-=+J(xJQA! 0%Hn{ ndCvyɾ;;;~3}|+!‚";9gt郞&6gCVD DAI?8,SߴՃ/z2*j8]NvnaW^w!3 ɜIѩ̋EŤ>h.So3jʔR3~0}fjE4L[>_/NoϲyL[6C}t&YnΰH>ȵh'ȯgT FM臻_w|A _ܯjMwklKjauC %yڕ?hߘk+:[g ^w=iԴ7+K494cR5 RB;!W\Á"R@J(RsF3@ GD ̠Y ~D\eOz!A>EDdlRJDCȖz}?߁yv^ZۀٹcTJMep/[`pPG3zjnRDI  yTR bs)( G g^Q@ֆIp= xM5lTJa{ۜ1?Y_ߟyca[dxS)'"_)R  `lVJ=lzHN<WJMУ2|J@yzЍְ!]˙o 8'oQ6=z N4?)g1ˏ#4$NO;~ LNmCNY q#x'|B)M gꪔow 崈<T8pjt`-Nkd-=x]'GRܛ"R}DdRjqJ}'%R=QeeTW;I 1\~sP1Vꮝt7=Nlý'z=+Mv]| )beJu1`~[^[^~{KI HHSȑGa3kWWj5l4\Uu{AѾM'VF8v\;|=\Gҽ $"2.e.Y<h.1:Ҥ&+ފm  w~L⾐#55~CJdD9$/[%9!"΀~~ 7Nczû%/9®/g8_(`@[cKIM wȓ_bu\o\N{}b=[?ˡ}Ynj&d;(6秬"{#52{5քVV3l]?/L-@TYt\K/4,ۇL+iӪEKgqeDFL[bagS؝E>Z(1<&=~S0VL#c$uѺmEW㇜VBjOߌ9GN2ʎ+:LA)\.'a$IĞ K=5u\f596xc߯ߐ?pݖ{#`2 ~=lHiOdW'Y "{{Lq^>Q?"*:pKRLTk:u:?ڥ(W\GgG ЦWr:f456˰1$)e4Vk<lS6j 9ٖ+YAqjjOK8q F FFP##jd522@ FFP#QS+ /lΖ Lm ۨIENDB`blinker-1.8.2/docs/conf.py000066400000000000000000000026021461620623000153770ustar00rootroot00000000000000from pallets_sphinx_themes import get_version from pallets_sphinx_themes import ProjectLink project = "Blinker" copyright = "2010 Jason Kirtland" release, version = get_version("blinker", placeholder=None) default_role = "code" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinxcontrib.log_cabinet", "pallets_sphinx_themes", ] autodoc_member_order = "groupwise" autodoc_typehints = "description" autodoc_preserve_defaults = True extlinks = { "issue": ("https://github.com/pallets-eco/blinker/issues/%s", "#%s"), "pr": ("https://github.com/pallets-eco/blinker/pull/%s", "#%s"), } html_theme = "flask" html_theme_options = {"index_sidebar_logo": False} html_context = { "project_links": [ ProjectLink("PyPI Releases", "https://pypi.org/project/blinker/"), ProjectLink("Source Code", "https://github.com/pallets-eco/blinker/"), ProjectLink("Issue Tracker", "https://github.com/pallets-eco/blinker/issues/"), ] } html_sidebars = { "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} html_static_path = ["_static"] html_logo = "_static/blinker-named.png" html_title = f"Blinker Documentation ({version})" html_show_sourcelink = False blinker-1.8.2/docs/index.rst000066400000000000000000000241601461620623000157440ustar00rootroot00000000000000.. rst-class:: hide-header Blinker Documentation ===================== .. image:: _static/blinker-named.png :align: center .. currentmodule:: blinker Blinker provides fast & simple object-to-object and broadcast signaling for Python objects. The core of Blinker is quite small but provides powerful features: - a global registry of named signals - anonymous signals - custom name registries - permanently or temporarily connected receivers - automatically disconnected receivers via weak referencing - sending arbitrary data payloads - collecting return values from signal receivers - thread safety Blinker was written by Jason Kirtand and is provided under the MIT License. The library supports Python 3.8 or later; or PyPy3.9 or later. Decoupling With Named Signals ----------------------------- Named signals are created with :func:`signal`: .. code-block:: python >>> from blinker import signal >>> initialized = signal('initialized') >>> initialized is signal('initialized') True Every call to ``signal('name')`` returns the same signal object, allowing unconnected parts of code (different modules, plugins, anything) to all use the same signal without requiring any code sharing or special imports. Subscribing to Signals ---------------------- :meth:`Signal.connect` registers a function to be invoked each time the signal is emitted. Connected functions are always passed the object that caused the signal to be emitted. .. code-block:: python >>> def subscriber(sender): ... print(f"Got a signal sent by {sender!r}") ... >>> ready = signal('ready') >>> ready.connect(subscriber) Emitting Signals ---------------- Code producing events of interest can :meth:`Signal.send` notifications to all connected receivers. Below, a simple ``Processor`` class emits a ``ready`` signal when it's about to process something, and ``complete`` when it is done. It passes ``self`` to the :meth:`~Signal.send` method, signifying that that particular instance was responsible for emitting the signal. .. code-block:: python >>> class Processor: ... def __init__(self, name): ... self.name = name ... ... def go(self): ... ready = signal('ready') ... ready.send(self) ... print("Processing.") ... complete = signal('complete') ... complete.send(self) ... ... def __repr__(self): ... return f'' ... >>> processor_a = Processor('a') >>> processor_a.go() Got a signal sent by Processing. Notice the ``complete`` signal in ``go()``? No receivers have connected to ``complete`` yet, and that's a-ok. Calling :meth:`~Signal.send` on a signal with no receivers will result in no notifications being sent, and these no-op sends are optimized to be as inexpensive as possible. Subscribing to Specific Senders ------------------------------- The default connection to a signal invokes the receiver function when any sender emits it. The :meth:`Signal.connect` function accepts an optional argument to restrict the subscription to one specific sending object: .. code-block:: python >>> def b_subscriber(sender): ... print("Caught signal from processor_b.") ... assert sender.name == 'b' ... >>> processor_b = Processor('b') >>> ready.connect(b_subscriber, sender=processor_b) This function has been subscribed to ``ready`` but only when sent by ``processor_b``: .. code-block:: python >>> processor_a.go() Got a signal sent by Processing. >>> processor_b.go() Got a signal sent by Caught signal from processor_b. Processing. Sending and Receiving Data Through Signals ------------------------------------------ Additional keyword arguments can be passed to :meth:`~Signal.send`. These will in turn be passed to the connected functions: .. code-block:: python >>> send_data = signal('send-data') >>> @send_data.connect ... def receive_data(sender, **kw): ... print(f"Caught signal from {sender!r}, data {kw!r}") ... return 'received!' ... >>> result = send_data.send('anonymous', abc=123) Caught signal from 'anonymous', data {'abc': 123} The return value of :meth:`~Signal.send` collects the return values of each connected function as a list of (``receiver function``, ``return value``) pairs: .. code-block:: python >>> result [(, '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 ----------------- Signals need not be named. The :class:`Signal` constructor creates a unique signal each time it is invoked. For example, an alternative implementation of the Processor from above might provide the processing signals as class attributes: .. code-block:: python >>> from blinker import Signal >>> class AltProcessor: ... on_ready = Signal() ... on_complete = Signal() ... ... def __init__(self, name): ... self.name = name ... ... def go(self): ... self.on_ready.send(self) ... print("Alternate processing.") ... self.on_complete.send(self) ... ... def __repr__(self): ... return f'' ... ``connect`` as a Decorator -------------------------- You may have noticed the return value of :meth:`~Signal.connect` in the console output in the sections above. This allows ``connect`` to be used as a decorator on functions: .. code-block:: python >>> apc = AltProcessor('c') >>> @apc.on_complete.connect ... def completed(sender): ... print f"AltProcessor {sender.name} completed!" ... >>> apc.go() Alternate processing. AltProcessor c completed! While convenient, this form unfortunately does not allow the ``sender`` or ``weak`` arguments to be customized for the connected function. For this, :meth:`~Signal.connect_via` can be used: .. code-block:: python >>> dice_roll = signal('dice_roll') >>> @dice_roll.connect_via(1) ... @dice_roll.connect_via(3) ... @dice_roll.connect_via(5) ... def odd_subscriber(sender): ... print(f"Observed dice roll {sender!r}.") ... >>> result = dice_roll.send(3) Observed dice roll 3. Optimizing Signal Sending ------------------------- Signals are optimized to send very quickly, whether receivers are connected or not. If the keyword data to be sent with a signal is expensive to compute, it can be more efficient to check to see if any receivers are connected first by testing the :attr:`~Signal.receivers` property: .. code-block:: python >>> bool(signal('ready').receivers) True >>> bool(signal('complete').receivers) False >>> bool(AltProcessor.on_complete.receivers) True Checking for a receiver listening for a particular sender is also possible: .. code-block:: python >>> signal('ready').has_receivers_for(processor_a) True Documenting Signals ------------------- Both named and anonymous signals can be passed a ``doc`` argument at construction to set the pydoc help text for the signal. This documentation will be picked up by most documentation generators (such as sphinx) and is nice for documenting any additional data parameters that will be sent down with the signal. 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) Call receivers in order of registration --------------------------------------- It can be advantageous to call a signal's receivers in the order they were registered. To achieve this the storage class for receivers should be changed from an (unordered) set to an ordered set, .. code-block:: python from blinker import Signal from ordered_set import OrderedSet Signal.set_class = OrderedSet Please note that ``ordered_set`` is a PyPI package and is not installed with blinker. API Documentation ----------------- All public API members can (and should) be imported from ``blinker``:: from blinker import ANY, signal Basic Signals +++++++++++++ .. data:: ANY Symbol for "any sender". .. autoclass:: Signal :members: Named Signals +++++++++++++ .. function:: signal(name, doc=None) Return a :class:`NamedSignal` in :data:`default_namespace` for the given name, creating it if required. Repeated calls with the same name return the same signal. :param name: The name of the signal. :type name: str :param doc: The docstring of the signal. :type doc: str | None :rtype: NamedSignal .. data:: default_namespace A default :class:`Namespace` for creating named signals. :func:`signal` creates a :class:`NamedSignal` in this namespace. .. autoclass:: NamedSignal :show-inheritance: .. autoclass:: Namespace :show-inheritance: :members: signal Changes ======= .. include:: ../CHANGES.rst MIT License =========== .. literalinclude:: ../LICENSE.txt :language: text blinker-1.8.2/pyproject.toml000066400000000000000000000032371461620623000160710ustar00rootroot00000000000000[project] name = "blinker" version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" readme = "README.md" license = { file = "LICENSE.txt" } authors = [{ name = "Jason Kirtland" }] maintainers = [{ name = "Pallets Ecosystem", email = "contact@palletsprojects.com" }] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Typing :: Typed", ] requires-python = ">=3.8" [project.urls] Documentation = "https://blinker.readthedocs.io" Source = "https://github.com/pallets-eco/blinker/" Chat = "https://discord.gg/pallets" [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.sdist] include = [ "docs/", "requirements/", "tests/", "CHANGES.rst", "tox.ini", ] exclude = [ "docs/_build/", ] [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = [ "error" ] asyncio_mode = "auto" [tool.coverage.run] branch = true source = ["blinker", "tests"] [tool.coverage.paths] source = ["src", "*/site-packages"] [tool.mypy] python_version = "3.8" files = ["src/blinker", "tests"] show_error_codes = true pretty = true strict = true [tool.pyright] pythonVersion = "3.8" include = ["src/blinker", "tests"] typeCheckingMode = "basic" [tool.ruff] src = ["src"] fix = true show-fixes = true output-format = "full" [tool.ruff.lint] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warning ] ignore-init-module-imports = true [tool.ruff.lint.isort] force-single-line = true order-by-type = false blinker-1.8.2/requirements/000077500000000000000000000000001461620623000156735ustar00rootroot00000000000000blinker-1.8.2/requirements/build.in000066400000000000000000000000061461620623000173160ustar00rootroot00000000000000build blinker-1.8.2/requirements/build.txt000066400000000000000000000003431461620623000175330ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile build.in # build==1.2.1 # via -r build.in packaging==24.0 # via build pyproject-hooks==1.0.0 # via build blinker-1.8.2/requirements/dev.in000066400000000000000000000000661461620623000170030ustar00rootroot00000000000000-r docs.txt -r tests.txt -r typing.txt pre-commit tox blinker-1.8.2/requirements/dev.txt000066400000000000000000000057021461620623000172160ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile dev.in # alabaster==0.7.16 # via # -r docs.txt # sphinx babel==2.14.0 # via # -r docs.txt # sphinx cachetools==5.3.3 # via tox certifi==2024.2.2 # via # -r docs.txt # requests cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via tox charset-normalizer==3.3.2 # via # -r docs.txt # requests colorama==0.4.6 # via tox distlib==0.3.8 # via virtualenv docutils==0.21.2 # via # -r docs.txt # sphinx filelock==3.13.4 # via # tox # virtualenv identify==2.5.36 # via pre-commit idna==3.7 # via # -r docs.txt # requests imagesize==1.4.1 # via # -r docs.txt # sphinx iniconfig==2.0.0 # via # -r tests.txt # -r typing.txt # pytest jinja2==3.1.3 # via # -r docs.txt # sphinx markupsafe==2.1.5 # via # -r docs.txt # jinja2 mypy==1.10.0 # via -r typing.txt mypy-extensions==1.0.0 # via # -r typing.txt # mypy nodeenv==1.8.0 # via # -r typing.txt # pre-commit # pyright packaging==24.0 # via # -r docs.txt # -r tests.txt # -r typing.txt # pallets-sphinx-themes # pyproject-api # pytest # sphinx # tox pallets-sphinx-themes==2.1.3 # via -r docs.txt platformdirs==4.2.1 # via # tox # virtualenv pluggy==1.5.0 # via # -r tests.txt # -r typing.txt # pytest # tox pre-commit==3.7.0 # via -r dev.in pygments==2.17.2 # via # -r docs.txt # sphinx pyproject-api==1.6.1 # via tox pyright==1.1.360 # via -r typing.txt pytest==8.2.0 # via # -r tests.txt # -r typing.txt # pytest-asyncio pytest-asyncio==0.23.6 # via -r tests.txt pyyaml==6.0.1 # via pre-commit requests==2.31.0 # via # -r docs.txt # sphinx snowballstemmer==2.2.0 # via # -r docs.txt # sphinx sphinx==7.3.7 # via # -r docs.txt # pallets-sphinx-themes # sphinxcontrib-log-cabinet sphinxcontrib-applehelp==1.0.8 # via # -r docs.txt # sphinx sphinxcontrib-devhelp==1.0.6 # via # -r docs.txt # sphinx sphinxcontrib-htmlhelp==2.0.5 # via # -r docs.txt # sphinx sphinxcontrib-jsmath==1.0.1 # via # -r docs.txt # sphinx sphinxcontrib-log-cabinet==1.0.1 # via -r docs.txt sphinxcontrib-qthelp==1.0.7 # via # -r docs.txt # sphinx sphinxcontrib-serializinghtml==1.1.10 # via # -r docs.txt # sphinx tox==4.15.0 # via -r dev.in typing-extensions==4.11.0 # via # -r typing.txt # mypy urllib3==2.2.1 # via # -r docs.txt # requests virtualenv==20.26.0 # via # pre-commit # tox # The following packages are considered to be unsafe in a requirements file: # setuptools blinker-1.8.2/requirements/docs.in000066400000000000000000000000671461620623000171560ustar00rootroot00000000000000pallets-sphinx-themes sphinx sphinxcontrib-log-cabinet blinker-1.8.2/requirements/docs.txt000066400000000000000000000021651461620623000173700ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile docs.in # alabaster==0.7.16 # via sphinx babel==2.14.0 # via sphinx certifi==2024.2.2 # via requests charset-normalizer==3.3.2 # via requests docutils==0.21.2 # via sphinx idna==3.7 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.3 # via sphinx markupsafe==2.1.5 # via jinja2 packaging==24.0 # via # pallets-sphinx-themes # sphinx pallets-sphinx-themes==2.1.3 # via -r docs.in pygments==2.17.2 # via sphinx requests==2.31.0 # via sphinx snowballstemmer==2.2.0 # via sphinx sphinx==7.3.7 # via # -r docs.in # pallets-sphinx-themes # sphinxcontrib-log-cabinet sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 # via sphinx sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-log-cabinet==1.0.1 # via -r docs.in sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx urllib3==2.2.1 # via requests blinker-1.8.2/requirements/tests.in000066400000000000000000000000261461620623000173630ustar00rootroot00000000000000pytest pytest-asyncio blinker-1.8.2/requirements/tests.txt000066400000000000000000000005131461620623000175750ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile tests.in # iniconfig==2.0.0 # via pytest packaging==24.0 # via pytest pluggy==1.5.0 # via pytest pytest==8.2.0 # via # -r tests.in # pytest-asyncio pytest-asyncio==0.23.6 # via -r tests.in blinker-1.8.2/requirements/typing.in000066400000000000000000000000241461620623000175310ustar00rootroot00000000000000mypy pyright pytest blinker-1.8.2/requirements/typing.txt000066400000000000000000000010301461620623000177400ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile typing.in # iniconfig==2.0.0 # via pytest mypy==1.10.0 # via -r typing.in mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 # via pyright packaging==24.0 # via pytest pluggy==1.5.0 # via pytest pyright==1.1.360 # via -r typing.in pytest==8.2.0 # via -r typing.in typing-extensions==4.11.0 # via mypy # The following packages are considered to be unsafe in a requirements file: # setuptools blinker-1.8.2/src/000077500000000000000000000000001461620623000137375ustar00rootroot00000000000000blinker-1.8.2/src/blinker/000077500000000000000000000000001461620623000153655ustar00rootroot00000000000000blinker-1.8.2/src/blinker/__init__.py000066400000000000000000000030511461620623000174750ustar00rootroot00000000000000from __future__ import annotations import typing as t from .base import ANY from .base import default_namespace from .base import NamedSignal from .base import Namespace from .base import Signal from .base import signal __all__ = [ "ANY", "default_namespace", "NamedSignal", "Namespace", "Signal", "signal", ] def __getattr__(name: str) -> t.Any: import warnings if name == "__version__": import importlib.metadata warnings.warn( "The '__version__' attribute is deprecated and will be removed in" " Blinker 1.9.0. Use feature detection or" " 'importlib.metadata.version(\"blinker\")' instead.", DeprecationWarning, stacklevel=2, ) return importlib.metadata.version("blinker") if name == "receiver_connected": from .base import _receiver_connected warnings.warn( "The global 'receiver_connected' signal is deprecated and will be" " removed in Blinker 1.9. Use 'Signal.receiver_connected' and" " 'Signal.receiver_disconnected' instead.", DeprecationWarning, stacklevel=2, ) return _receiver_connected if name == "WeakNamespace": from .base import _WeakNamespace warnings.warn( "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." " Use 'Namespace' instead.", DeprecationWarning, stacklevel=2, ) return _WeakNamespace raise AttributeError(name) blinker-1.8.2/src/blinker/_utilities.py000066400000000000000000000032131461620623000201100ustar00rootroot00000000000000from __future__ import annotations import collections.abc as c import inspect import typing as t from weakref import ref from weakref import WeakMethod T = t.TypeVar("T") class Symbol: """A constant symbol, nicer than ``object()``. Repeated calls return the same instance. >>> Symbol('foo') is Symbol('foo') True >>> Symbol('foo') foo """ symbols: t.ClassVar[dict[str, Symbol]] = {} def __new__(cls, name: str) -> Symbol: if name in cls.symbols: return cls.symbols[name] obj = super().__new__(cls) cls.symbols[name] = obj return obj def __init__(self, name: str) -> None: self.name = name def __repr__(self) -> str: return self.name def __getnewargs__(self) -> tuple[t.Any, ...]: return (self.name,) def make_id(obj: object) -> c.Hashable: """Get a stable identifier for a receiver or sender, to be used as a dict key or in a set. """ if inspect.ismethod(obj): # The id of a bound method is not stable, but the id of the unbound # function and instance are. return id(obj.__func__), id(obj.__self__) if isinstance(obj, (str, int)): # Instances with the same value always compare equal and have the same # hash, even if the id may change. return obj # Assume other types are not hashable but will always be the same instance. return id(obj) def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]: if inspect.ismethod(obj): return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] return ref(obj, callback) blinker-1.8.2/src/blinker/base.py000066400000000000000000000540561461620623000166630ustar00rootroot00000000000000from __future__ import annotations import collections.abc as c import typing as t import warnings import weakref from collections import defaultdict from contextlib import AbstractContextManager from contextlib import contextmanager from functools import cached_property from inspect import iscoroutinefunction from weakref import WeakValueDictionary from ._utilities import make_id from ._utilities import make_ref from ._utilities import Symbol if t.TYPE_CHECKING: F = t.TypeVar("F", bound=c.Callable[..., t.Any]) ANY = Symbol("ANY") """Symbol for "any sender".""" ANY_ID = 0 class Signal: """A notification emitter. :param doc: The docstring for the signal. """ ANY = ANY """An alias for the :data:`~blinker.ANY` sender symbol.""" set_class: type[set[t.Any]] = set """The set class to use for tracking connected receivers and senders. Python's ``set`` is unordered. If receivers must be dispatched in the order they were connected, an ordered set implementation can be used. .. versionadded:: 1.7 """ @cached_property def receiver_connected(self) -> Signal: """Emitted at the end of each :meth:`connect` call. The signal sender is the signal instance, and the :meth:`connect` arguments are passed through: ``receiver``, ``sender``, and ``weak``. .. versionadded:: 1.2 """ return Signal(doc="Emitted after a receiver connects.") @cached_property def receiver_disconnected(self) -> Signal: """Emitted at the end of each :meth:`disconnect` call. The sender is the signal instance, and the :meth:`disconnect` arguments are passed through: ``receiver`` and ``sender``. This signal is emitted **only** when :meth:`disconnect` is called explicitly. This signal cannot be emitted by an automatic disconnect when a weakly referenced receiver or sender goes out of scope, as the instance is no longer be available to be used as the sender for this signal. An alternative approach is available by subscribing to :attr:`receiver_connected` and setting up a custom weakref cleanup callback on weak receivers and senders. .. versionadded:: 1.2 """ return Signal(doc="Emitted after a receiver disconnects.") def __init__(self, doc: str | None = None) -> None: if doc: self.__doc__ = doc self.receivers: dict[ t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] ] = {} """The map of connected receivers. Useful to quickly check if any receivers are connected to the signal: ``if s.receivers:``. The structure and data is not part of the public API, but checking its boolean value is. """ self.is_muted: bool = False self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: """Connect ``receiver`` to be called when the signal is sent by ``sender``. :param receiver: The callable to call when :meth:`send` is called with the given ``sender``, passing ``sender`` as a positional argument along with any extra keyword arguments. :param sender: Any object or :data:`ANY`. ``receiver`` will only be called when :meth:`send` is called with this sender. If ``ANY``, the receiver will be called for any sender. A receiver may be connected to multiple senders by calling :meth:`connect` multiple times. :param weak: Track the receiver with a :mod:`weakref`. The receiver will be automatically disconnected when it is garbage collected. When connecting a receiver defined within a function, set to ``False``, otherwise it will be disconnected when the function scope ends. """ receiver_id = make_id(receiver) sender_id = ANY_ID if sender is ANY else make_id(sender) if weak: self.receivers[receiver_id] = make_ref( receiver, self._make_cleanup_receiver(receiver_id) ) else: self.receivers[receiver_id] = receiver self._by_sender[sender_id].add(receiver_id) self._by_receiver[receiver_id].add(sender_id) if sender is not ANY and sender_id not in self._weak_senders: # store a cleanup for weakref-able senders try: self._weak_senders[sender_id] = make_ref( sender, self._make_cleanup_sender(sender_id) ) except TypeError: pass if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: try: self.receiver_connected.send( self, receiver=receiver, sender=sender, weak=weak ) except TypeError: # TODO no explanation or test for this self.disconnect(receiver, sender) raise 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 TypeError: self.disconnect(receiver, sender) raise return receiver def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: """Connect the decorated function to be called when the signal is sent by ``sender``. The decorated function will be called when :meth:`send` is called with the given ``sender``, passing ``sender`` as a positional argument along with any extra keyword arguments. :param sender: Any object or :data:`ANY`. ``receiver`` will only be called when :meth:`send` is called with this sender. If ``ANY``, the receiver will be called for any sender. A receiver may be connected to multiple senders by calling :meth:`connect` multiple times. :param weak: Track the receiver with a :mod:`weakref`. The receiver will be automatically disconnected when it is garbage collected. When connecting a receiver defined within a function, set to ``False``, otherwise it will be disconnected when the function scope ends.= .. versionadded:: 1.1 """ def decorator(fn: F) -> F: self.connect(fn, sender, weak) return fn return decorator @contextmanager def connected_to( self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY ) -> c.Generator[None, None, None]: """A context manager that temporarily connects ``receiver`` to the signal while a ``with`` block executes. When the block exits, the receiver is disconnected. Useful for tests. :param receiver: The callable to call when :meth:`send` is called with the given ``sender``, passing ``sender`` as a positional argument along with any extra keyword arguments. :param sender: Any object or :data:`ANY`. ``receiver`` will only be called when :meth:`send` is called with this sender. If ``ANY``, the receiver will be called for any sender. .. versionadded:: 1.1 """ self.connect(receiver, sender=sender, weak=False) try: yield None finally: self.disconnect(receiver) @contextmanager def muted(self) -> c.Generator[None, None, None]: """A context manager that temporarily disables the signal. No receivers will be called if the signal is sent, until the ``with`` block exits. Useful for tests. """ self.is_muted = True try: yield None finally: self.is_muted = False def temporarily_connected_to( self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY ) -> AbstractContextManager[None]: """Deprecated alias for :meth:`connected_to`. .. deprecated:: 1.1 Renamed to ``connected_to``. Will be removed in Blinker 1.9. .. versionadded:: 0.9 """ warnings.warn( "'temporarily_connected_to' is renamed to 'connected_to'. The old name is" " deprecated and will be removed in Blinker 1.9.", DeprecationWarning, stacklevel=2, ) return self.connected_to(receiver, sender) def send( self, sender: t.Any | None = None, /, *, _async_wrapper: c.Callable[ [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] ] | None = None, **kwargs: t.Any, ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: """Call all receivers that are connected to the given ``sender`` or :data:`ANY`. Each receiver is called with ``sender`` as a positional argument along with any extra keyword arguments. Return a list of ``(receiver, return value)`` tuples. The order receivers are called is undefined, but can be influenced by setting :attr:`set_class`. If a receiver raises an exception, that exception will propagate up. This makes debugging straightforward, with an assumption that correctly implemented receivers will not raise. :param sender: Call receivers connected to this sender, in addition to those connected to :data:`ANY`. :param _async_wrapper: Will be called on any receivers that are async coroutines to turn them into sync callables. For example, could run the receiver with an event loop. :param kwargs: Extra keyword arguments to pass to each receiver. .. versionchanged:: 1.7 Added the ``_async_wrapper`` argument. """ if self.is_muted: return [] results = [] for receiver in self.receivers_for(sender): if iscoroutinefunction(receiver): if _async_wrapper is None: raise RuntimeError("Cannot send to a coroutine function.") result = _async_wrapper(receiver)(sender, **kwargs) else: result = receiver(sender, **kwargs) results.append((receiver, result)) return results async def send_async( self, sender: t.Any | None = None, /, *, _sync_wrapper: c.Callable[ [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] ] | None = None, **kwargs: t.Any, ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: """Await all receivers that are connected to the given ``sender`` or :data:`ANY`. Each receiver is called with ``sender`` as a positional argument along with any extra keyword arguments. Return a list of ``(receiver, return value)`` tuples. The order receivers are called is undefined, but can be influenced by setting :attr:`set_class`. If a receiver raises an exception, that exception will propagate up. This makes debugging straightforward, with an assumption that correctly implemented receivers will not raise. :param sender: Call receivers connected to this sender, in addition to those connected to :data:`ANY`. :param _sync_wrapper: Will be called on any receivers that are sync callables to turn them into async coroutines. For example, could call the receiver in a thread. :param kwargs: Extra keyword arguments to pass to each receiver. .. versionadded:: 1.7 """ if self.is_muted: return [] results = [] for receiver in self.receivers_for(sender): if not iscoroutinefunction(receiver): if _sync_wrapper is None: raise RuntimeError("Cannot send to a non-coroutine function.") result = await _sync_wrapper(receiver)(sender, **kwargs) else: result = await receiver(sender, **kwargs) results.append((receiver, result)) return results def has_receivers_for(self, sender: t.Any) -> bool: """Check if there is at least one receiver that will be called with the given ``sender``. A receiver connected to :data:`ANY` will always be called, regardless of sender. Does not check if weakly referenced receivers are still live. See :meth:`receivers_for` for a stronger search. :param sender: Check for receivers connected to this sender, in addition to those connected to :data:`ANY`. """ if not self.receivers: return False if self._by_sender[ANY_ID]: return True if sender is ANY: return False return make_id(sender) in self._by_sender def receivers_for( self, sender: t.Any ) -> c.Generator[c.Callable[..., t.Any], None, None]: """Yield each receiver to be called for ``sender``, in addition to those to be called for :data:`ANY`. Weakly referenced receivers that are not live will be disconnected and skipped. :param sender: Yield receivers connected to this sender, in addition to those connected to :data:`ANY`. """ # TODO: test receivers_for(ANY) if not self.receivers: return sender_id = make_id(sender) if sender_id in self._by_sender: ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] else: ids = self._by_sender[ANY_ID].copy() for receiver_id in ids: receiver = self.receivers.get(receiver_id) if receiver is None: continue if isinstance(receiver, weakref.ref): strong = receiver() if strong is None: self._disconnect(receiver_id, ANY_ID) continue yield strong else: yield receiver def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: """Disconnect ``receiver`` from being called when the signal is sent by ``sender``. :param receiver: A connected receiver callable. :param sender: Disconnect from only this sender. By default, disconnect from all senders. """ sender_id: c.Hashable if sender is ANY: sender_id = ANY_ID else: sender_id = make_id(sender) receiver_id = make_id(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) def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: if sender_id == ANY_ID: if self._by_receiver.pop(receiver_id, None) is not None: for bucket in self._by_sender.values(): bucket.discard(receiver_id) self.receivers.pop(receiver_id, None) else: self._by_sender[sender_id].discard(receiver_id) self._by_receiver[receiver_id].discard(sender_id) def _make_cleanup_receiver( self, receiver_id: c.Hashable ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: """Create a callback function to disconnect a weakly referenced receiver when it is garbage collected. """ def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: self._disconnect(receiver_id, ANY_ID) return cleanup def _make_cleanup_sender( self, sender_id: c.Hashable ) -> c.Callable[[weakref.ref[t.Any]], None]: """Create a callback function to disconnect all receivers for a weakly referenced sender when it is garbage collected. """ assert sender_id != ANY_ID def cleanup(ref: weakref.ref[t.Any]) -> None: 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) return cleanup def _cleanup_bookkeeping(self) -> None: """Prune unused sender/receiver bookkeeping. Not threadsafe. Connecting & disconnecting leaves behind a small amount of bookkeeping data. Typical workloads using Blinker, for example in most web apps, Flask, CLI scripts, etc., are not adversely affected by this bookkeeping. With a long-running process performing dynamic signal routing with high volume, e.g. connecting to function closures, senders are all unique object instances. Doing all of this over and over may cause memory usage to grow due to extraneous bookkeeping. (An empty ``set`` for each stale sender/receiver pair.) This method will prune that bookkeeping away, with the caveat that such pruning is not threadsafe. The risk is that cleanup of a fully disconnected receiver/sender pair occurs while another thread is connecting that same pair. If you are in the highly dynamic, unique receiver/sender situation that has lead you to this method, that failure mode is perhaps not a big deal for you. """ for mapping in (self._by_sender, self._by_receiver): for ident, bucket in list(mapping.items()): if not bucket: mapping.pop(ident, None) def _clear_state(self) -> None: """Disconnect all receivers and senders. Useful for tests.""" self._weak_senders.clear() self.receivers.clear() self._by_sender.clear() self._by_receiver.clear() _receiver_connected = Signal( """\ Sent by a :class:`Signal` after a receiver connects. :argument: the Signal that was connected to :keyword receiver_arg: the connected receiver :keyword sender_arg: the sender to connect to :keyword weak_arg: true if the connection to receiver_arg is a weak reference .. deprecated:: 1.2 Individual signals have their own :attr:`~Signal.receiver_connected` and :attr:`~Signal.receiver_disconnected` signals with a slightly simplified call signature. This global signal will be removed in Blinker 1.9. """ ) class NamedSignal(Signal): """A named generic notification emitter. The name is not used by the signal itself, but matches the key in the :class:`Namespace` that it belongs to. :param name: The name of the signal within the namespace. :param doc: The docstring for the signal. """ def __init__(self, name: str, doc: str | None = None) -> None: super().__init__(doc) #: The name of this signal. self.name: str = name def __repr__(self) -> str: base = super().__repr__() return f"{base[:-1]}; {self.name!r}>" # noqa: E702 if t.TYPE_CHECKING: class PNamespaceSignal(t.Protocol): def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... # Python < 3.9 _NamespaceBase = dict[str, NamedSignal] # type: ignore[misc] else: _NamespaceBase = dict class Namespace(_NamespaceBase): """A dict mapping names to signals.""" def signal(self, name: str, doc: str | None = None) -> NamedSignal: """Return the :class:`NamedSignal` for the given ``name``, creating it if required. Repeated calls with the same name return the same signal. :param name: The name of the signal. :param doc: The docstring of the signal. """ if name not in self: self[name] = NamedSignal(name, doc) return self[name] class _WeakNamespace(WeakValueDictionary): # type: ignore[type-arg] """A weak mapping of names to signals. Automatically cleans up unused signals when the last reference goes out of scope. This namespace implementation provides similar behavior to Blinker <= 1.2. .. deprecated:: 1.3 Will be removed in Blinker 1.9. .. versionadded:: 1.3 """ def __init__(self) -> None: warnings.warn( "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." " Use 'Namespace' instead.", DeprecationWarning, stacklevel=2, ) super().__init__() def signal(self, name: str, doc: str | None = None) -> NamedSignal: """Return the :class:`NamedSignal` for the given ``name``, creating it if required. Repeated calls with the same name return the same signal. :param name: The name of the signal. :param doc: The docstring of the signal. """ if name not in self: self[name] = NamedSignal(name, doc) return self[name] # type: ignore[no-any-return] default_namespace: Namespace = Namespace() """A default :class:`Namespace` for creating named signals. :func:`signal` creates a :class:`NamedSignal` in this namespace. """ signal: PNamespaceSignal = default_namespace.signal """Return a :class:`NamedSignal` in :data:`default_namespace` with the given ``name``, creating it if required. Repeated calls with the same name return the same signal. """ def __getattr__(name: str) -> t.Any: if name == "receiver_connected": warnings.warn( "The global 'receiver_connected' signal is deprecated and will be" " removed in Blinker 1.9. Use 'Signal.receiver_connected' and" " 'Signal.receiver_disconnected' instead.", DeprecationWarning, stacklevel=2, ) return _receiver_connected if name == "WeakNamespace": warnings.warn( "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." " Use 'Namespace' instead.", DeprecationWarning, stacklevel=2, ) return _WeakNamespace raise AttributeError(name) blinker-1.8.2/src/blinker/py.typed000066400000000000000000000000001461620623000170520ustar00rootroot00000000000000blinker-1.8.2/tests/000077500000000000000000000000001461620623000143125ustar00rootroot00000000000000blinker-1.8.2/tests/test_context.py000066400000000000000000000023251461620623000174110ustar00rootroot00000000000000from __future__ import annotations import typing as t import pytest from blinker import Signal def test_temp_connection() -> None: sig = Signal() canary = [] def receiver(sender: t.Any) -> None: canary.append(sender) sig.send(1) with sig.connected_to(receiver): sig.send(2) sig.send(3) assert canary == [2] assert not sig.receivers def test_temp_connection_for_sender() -> None: sig = Signal() canary = [] def receiver(sender: t.Any) -> None: canary.append(sender) with sig.connected_to(receiver, sender=2): sig.send(1) sig.send(2) assert canary == [2] assert not sig.receivers class Failure(Exception): pass class BaseFailure(BaseException): pass @pytest.mark.parametrize("exc_type", [Failure, BaseFailure]) def test_temp_connection_failure(exc_type: type[BaseException]) -> None: sig = Signal() canary = [] def receiver(sender: t.Any) -> None: canary.append(sender) with pytest.raises(exc_type): sig.send(1) with sig.connected_to(receiver): sig.send(2) raise exc_type sig.send(3) assert canary == [2] assert not sig.receivers blinker-1.8.2/tests/test_signals.py000066400000000000000000000271621461620623000173730ustar00rootroot00000000000000from __future__ import annotations import collections.abc as c import gc import sys import typing as t import pytest import blinker def collect_acyclic_refs() -> None: # cpython releases these immediately without a collection if sys.implementation.name == "pypy": gc.collect() class Sentinel(list): # type: ignore[type-arg] """A signal receipt accumulator.""" def make_receiver(self, key: t.Any) -> c.Callable[..., t.Any]: """Return a generic signal receiver function logging as *key* When connected to a signal, appends (key, sender, kw) to the Sentinel. """ def receiver(*sentby: t.Any, **kw: t.Any) -> None: self.append((key, sentby[0], kw)) return receiver def _test_signal_signals(sender: t.Any) -> None: 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") assert not sig.receiver_connected.receivers assert not sig.receiver_disconnected.receivers sig.receiver_connected.connect(connected) sig.receiver_disconnected.connect(disconnected) assert sig.receiver_connected.receivers assert not sentinel 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}, ) assert sentinel[-1] == expected # disconnect from explicit sender sig.disconnect(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}, ) def test_signal_signals_any_sender() -> None: _test_signal_signals(blinker.ANY) def test_signal_signals_strong_sender() -> None: _test_signal_signals("squiznart") def test_signal_weak_receiver_vanishes() -> None: # non-edge-case path for weak receivers is exercised in the ANY sender # test above. 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") sig.receiver_connected.connect(connected) sig.receiver_disconnected.connect(disconnected) # explicit disconnect on a weak does emit the signal sig.connect(receiver1, weak=True) sig.disconnect(receiver1) assert len(sentinel) == 2 assert sentinel[-1][2]["receiver"] is receiver1 del sentinel[:] sig.connect(receiver2, weak=True) assert len(sentinel) == 1 del sentinel[:] # holds a ref to receiver2 del receiver2 collect_acyclic_refs() # no disconnect signal is fired assert len(sentinel) == 0 # and everything really is disconnected sig.send("abc") assert len(sentinel) == 0 def test_signal_signals_weak_sender() -> None: 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") class Sender: """A weakref-able object.""" sig.receiver_connected.connect(connected) sig.receiver_disconnected.connect(disconnected) sender1 = Sender() sig.connect(receiver1, sender=sender1, weak=False) # regular disconnect of weak-able sender works fine sig.disconnect(receiver1, sender=sender1) assert len(sentinel) == 2 del sentinel[:] sender2 = Sender() sig.connect(receiver2, sender=sender2, weak=False) # force sender2 to go out of scope del sender2 collect_acyclic_refs() # no disconnect signal is fired assert len(sentinel) == 1 # and everything really is disconnected sig.send("abc") assert len(sentinel) == 1 def test_empty_bucket_growth() -> None: def senders() -> tuple[int, int]: return ( len(sig._by_sender), sum(len(i) for i in sig._by_sender.values()), ) def receivers() -> tuple[int, int]: return ( len(sig._by_receiver), sum(len(i) for i in sig._by_receiver.values()), ) sentinel = Sentinel() sig = blinker.Signal() receiver1 = sentinel.make_receiver("receiver1") receiver2 = sentinel.make_receiver("receiver2") class Sender: """A weakref-able object.""" sender = Sender() sig.connect(receiver1, sender=sender) sig.connect(receiver2, sender=sender) assert senders() == (1, 2) assert receivers() == (2, 2) sig.disconnect(receiver1, sender=sender) assert senders() == (1, 1) assert receivers() == (2, 1) sig.disconnect(receiver2, sender=sender) assert senders() == (1, 0) assert receivers() == (2, 0) sig._cleanup_bookkeeping() assert senders() == (0, 0) assert receivers() == (0, 0) def test_namespace() -> None: 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 del s1 collect_acyclic_refs() assert "def" in ns assert "abc" in ns def test_weak_receiver() -> None: sentinel = [] def received(sender: t.Any, **kw: t.Any) -> None: sentinel.append(kw) sig = blinker.Signal() sig.connect(received, weak=True) del received collect_acyclic_refs() assert not sentinel sig.send() assert not sentinel assert not sig.receivers values_are_empty_sets_(sig._by_receiver) values_are_empty_sets_(sig._by_sender) def test_strong_receiver() -> None: sentinel = [] def received(sender: t.Any) -> None: sentinel.append(sender) fn_id = id(received) sig = blinker.Signal() sig.connect(received, weak=False) del received collect_acyclic_refs() assert not sentinel sig.send() assert sentinel assert [id(fn) for fn in sig.receivers.values()] == [fn_id] async def test_async_receiver() -> None: sentinel = [] async def received_async(sender: t.Any) -> None: sentinel.append(sender) def received(sender: t.Any) -> None: sentinel.append(sender) def wrapper(func: c.Callable[..., t.Any]) -> c.Callable[..., None]: async def inner(*args: t.Any, **kwargs: t.Any) -> None: func(*args, **kwargs) return inner # type: ignore[return-value] sig = blinker.Signal() sig.connect(received) sig.connect(received_async) await sig.send_async(_sync_wrapper=wrapper) # type: ignore[arg-type] assert len(sentinel) == 2 with pytest.raises(RuntimeError): sig.send() def test_instancemethod_receiver() -> None: sentinel: list[t.Any] = [] class Receiver: def __init__(self, bucket: list[t.Any]) -> None: self.bucket = bucket def received(self, sender: t.Any) -> None: self.bucket.append(sender) receiver = Receiver(sentinel) sig = blinker.Signal() sig.connect(receiver.received) assert not sentinel sig.send() assert sentinel del receiver collect_acyclic_refs() sig.send() assert len(sentinel) == 1 def test_filtered_receiver() -> None: sentinel = [] def received(sender: t.Any) -> None: sentinel.append(sender) sig = blinker.Signal() sig.connect(received, 123) assert not sentinel sig.send() assert not sentinel sig.send(123) assert sentinel == [123] sig.send() assert sentinel == [123] sig.disconnect(received, 123) sig.send(123) assert sentinel == [123] sig.connect(received, 123) sig.send(123) assert sentinel == [123, 123] sig.disconnect(received) sig.send(123) assert sentinel == [123, 123] def test_filtered_receiver_weakref() -> None: sentinel = [] def received(sender: t.Any) -> None: sentinel.append(sender) class Object: pass obj = Object() sig = blinker.Signal() sig.connect(received, obj) assert not sentinel sig.send(obj) assert sentinel == [obj] del sentinel[:] del obj collect_acyclic_refs() # general index isn't cleaned up assert sig.receivers # but receiver/sender pairs are values_are_empty_sets_(sig._by_receiver) values_are_empty_sets_(sig._by_sender) def test_decorated_receiver() -> None: sentinel = [] class Object: pass obj = Object() sig = blinker.Signal() @sig.connect_via(obj) def receiver(sender: t.Any, **kw: t.Any) -> None: sentinel.append(kw) assert not sentinel sig.send() assert not sentinel sig.send(1) assert not sentinel sig.send(obj) assert sig.receivers del receiver collect_acyclic_refs() assert sig.receivers def test_no_double_send() -> None: sentinel = [] def received(sender: t.Any) -> None: sentinel.append(sender) sig = blinker.Signal() sig.connect(received, 123) sig.connect(received) assert not sentinel sig.send() assert sentinel == [None] sig.send(123) assert sentinel == [None, 123] sig.send() assert sentinel == [None, 123, None] def test_has_receivers() -> None: def received(_: t.Any) -> None: return None sig = blinker.Signal() assert not sig.has_receivers_for(None) assert not sig.has_receivers_for(blinker.ANY) 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") class Object: pass o = Object() sig.connect(received, o) assert sig.has_receivers_for(o) del received collect_acyclic_refs() 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(o) assert sig.has_receivers_for(None) assert sig.has_receivers_for(blinker.ANY) assert sig.has_receivers_for("xyz") def test_instance_doc() -> None: sig = blinker.Signal(doc="x") assert sig.__doc__ == "x" sig = blinker.Signal("x") assert sig.__doc__ == "x" def test_named_blinker() -> None: sig = blinker.NamedSignal("squiznart") assert "squiznart" in repr(sig) def test_mute_signal() -> None: sentinel = [] def received(sender: t.Any) -> None: 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: dict[t.Any, t.Any]) -> None: for val in dictionary.values(): assert val == set() def test_int_sender() -> None: count = 0 def received(sender: t.Any) -> None: nonlocal count count += 1 sig = blinker.Signal() sig.connect(received, sender=123456789) sig.send(123456789) # Python compiler uses same instance for same literal value. assert count == 1 sig.send(int("123456789")) # Force different instance with same value. assert count == 2 blinker-1.8.2/tests/test_symbol.py000066400000000000000000000007671461620623000172420ustar00rootroot00000000000000from __future__ import annotations import pickle from blinker._utilities import Symbol def test_symbols() -> None: foo = Symbol("foo") assert foo.name == "foo" assert foo is Symbol("foo") bar = Symbol("bar") assert foo is not bar assert foo != bar assert not foo == bar assert repr(foo) == "foo" def test_pickled_symbols() -> None: foo = Symbol("foo") for _ in 0, 1, 2: roundtrip = pickle.loads(pickle.dumps(foo)) assert roundtrip is foo blinker-1.8.2/tox.ini000066400000000000000000000021071461620623000144630ustar00rootroot00000000000000[tox] envlist = py3{12,11,10,9,8} style typing docs skip_missing_interpreters = true [testenv] package = wheel wheel_build_env = .pkg constrain_package_deps = true use_frozen_constraints = true deps = -r requirements/tests.txt commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} [testenv:style] deps = pre-commit skip_install = true commands = pre-commit run --all-files [testenv:typing] deps = -r requirements/typing.txt commands = mypy pyright pyright --verifytypes blinker --ignoreexternal [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml [testenv:update-pre_commit] labels = update deps = pre-commit skip_install = true commands = pre-commit autoupdate -j4 [testenv:update-requirements] labels = update deps = pip-tools skip_install = true change_dir = requirements commands = pip-compile build.in -q {posargs:-U} pip-compile docs.in -q {posargs:-U} pip-compile tests.in -q {posargs:-U} pip-compile typing.in -q {posargs:-U} pip-compile dev.in -q {posargs:-U}