pax_global_header00006660000000000000000000000064145205452260014517gustar00rootroot0000000000000052 comment=2adbe03d2243ba7bf07fad09728bc1f179999ae2 blinker-1.7.0/000077500000000000000000000000001452054522600131525ustar00rootroot00000000000000blinker-1.7.0/.flake8000066400000000000000000000005171452054522600143300ustar00rootroot00000000000000[flake8] extend-select = # bugbear B # bugbear opinions B9 # implicit str concat ISC extend-ignore = # slice notation whitespace, invalid E203 # line length, handled by bugbear B950 E501 # bare except, handled by bugbear B001 E722 # up to 88 allowed by bugbear B950 max-line-length = 80 blinker-1.7.0/.github/000077500000000000000000000000001452054522600145125ustar00rootroot00000000000000blinker-1.7.0/.github/dependabot.yml000066400000000000000000000002471452054522600173450ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" day: "monday" time: "16:00" timezone: "UTC" blinker-1.7.0/.github/workflows/000077500000000000000000000000001452054522600165475ustar00rootroot00000000000000blinker-1.7.0/.github/workflows/lock.yaml000066400000000000000000000011671452054522600203700ustar00rootroot00000000000000name: '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. We find 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@be8aa5be94131386884a6da4189effda9b14aa21 with: issue-inactive-days: 14 pr-inactive-days: 14 blinker-1.7.0/.github/workflows/publish.yaml000066400000000000000000000047151452054522600211100ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 with: python-version: '3.x' cache: 'pip' cache-dependency-path: 'requirements/*.txt' - run: pip install build # 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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 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@v1.9.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@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - 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: 'publish' runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e with: repository-url: https://test.pypi.org/legacy/ packages-dir: artifact/ - uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e with: packages-dir: artifact/ blinker-1.7.0/.github/workflows/tests.yaml000066400000000000000000000021021452054522600205700ustar00rootroot00000000000000name: Tests 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 }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: '3.12', python: '3.12', tox: py312} - {name: '3.11', python: '3.11', 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: 'Typing', python: '3.12', tox: typing} steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: 'requirements/*.txt' - run: pip install tox - run: tox run -e ${{ matrix.tox }} blinker-1.7.0/.gitignore000066400000000000000000000002251452054522600151410ustar00rootroot00000000000000/.idea/ /.vscode/ /.venv/ __pycache__/ /dist/ *.egg-info/ /.pytest_cache/ /.coverage /.coverage.* /htmlcov/ /.mypy_cache/ /.tox/ /docs/_build/ /.env blinker-1.7.0/.pre-commit-config.yaml000066400000000000000000000016321452054522600174350ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade rev: v3.7.0 hooks: - id: pyupgrade args: ["--py38-plus"] - repo: https://github.com/asottile/reorder-python-imports rev: v3.10.0 hooks: - id: reorder-python-imports args: ["--application-directories", "src:tests"] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - 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/peterdemin/pip-compile-multi rev: v2.6.3 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 blinker-1.7.0/.readthedocs.yaml000066400000000000000000000003221452054522600163760ustar00rootroot00000000000000version: 2 build: os: ubuntu-20.04 tools: python: "3.10" python: install: - requirements: requirements/docs.txt - method: pip path: . sphinx: builder: dirhtml fail_on_warning: true blinker-1.7.0/CHANGES.rst000066400000000000000000000067221452054522600147630ustar00rootroot00000000000000Version 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 ``signal.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` - 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 ----------- 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.7.0/LICENSE.rst000066400000000000000000000020361452054522600147670ustar00rootroot00000000000000Copyright 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.7.0/README.rst000066400000000000000000000017361452054522600146500ustar00rootroot00000000000000Blinker ======= Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events, or "signals". Signal receivers can subscribe to specific senders or receive signals sent by any sender. .. code-block:: 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! Links ----- - Documentation: https://blinker.readthedocs.io/ - Changes: https://blinker.readthedocs.io/#changes - PyPI Releases: https://pypi.org/project/blinker/ - Source Code: https://github.com/pallets-eco/blinker/ - Issue Tracker: https://github.com/pallets-eco/blinker/issues/ blinker-1.7.0/docs/000077500000000000000000000000001452054522600141025ustar00rootroot00000000000000blinker-1.7.0/docs/Makefile000066400000000000000000000011721452054522600155430ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) blinker-1.7.0/docs/_static/000077500000000000000000000000001452054522600155305ustar00rootroot00000000000000blinker-1.7.0/docs/_static/blinker-named.png000066400000000000000000000070531452054522600207530ustar00rootroot00000000000000PNG  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.7.0/docs/conf.py000066400000000000000000000022531452054522600154030ustar00rootroot00000000000000from 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) 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} 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.7.0/docs/index.rst000066400000000000000000000240211452054522600157420ustar00rootroot00000000000000.. rst-class:: hide-header Blinker Documentation ===================== .. image:: _static/blinker-named.png :align: center 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. 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) 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 .. currentmodule:: blinker.base Basic Signals +++++++++++++ .. autodata:: blinker.base.ANY .. autodata:: blinker.base.receiver_connected .. autoclass:: Signal :members: :undoc-members: Named Signals +++++++++++++ .. function:: signal(name, doc=None) Return the :class:`NamedSignal` *name*, creating it if required. Repeated calls to this function will return the same signal object. Signals are created in a global :class:`Namespace`. .. autoclass:: NamedSignal :show-inheritance: :members: .. autoclass:: Namespace :show-inheritance: :members: signal .. autoclass:: WeakNamespace :show-inheritance: :members: signal Changes ======= .. include:: ../CHANGES.rst MIT License =========== .. include:: ../LICENSE.rst blinker-1.7.0/docs/make.bat000066400000000000000000000013751452054522600155150ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd blinker-1.7.0/pyproject.toml000066400000000000000000000034161452054522600160720ustar00rootroot00000000000000[project] name = "blinker" license = {file = "LICENSE.rst"} 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" readme = "README.rst" 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.8" 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" [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] asyncio_mode = "auto" testpaths = ["tests"] filterwarnings = ["error"] [tool.mypy] python_version = "3.8" 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 blinker-1.7.0/requirements/000077500000000000000000000000001452054522600156755ustar00rootroot00000000000000blinker-1.7.0/requirements/dev.in000066400000000000000000000000411452054522600167760ustar00rootroot00000000000000tox pre-commit pip-compile-multi blinker-1.7.0/requirements/dev.txt000066400000000000000000000023341452054522600172160ustar00rootroot00000000000000# SHA1:39179f2c476f94362aa0705be059a488d7e38b6d # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # build==1.0.3 # via pip-tools cachetools==5.3.2 # via tox cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via tox click==8.1.7 # via # pip-compile-multi # pip-tools colorama==0.4.6 # via tox distlib==0.3.7 # via virtualenv filelock==3.13.1 # via # tox # virtualenv identify==2.5.31 # via pre-commit nodeenv==1.8.0 # via pre-commit packaging==23.2 # via # build # pyproject-api # tox pip-compile-multi==2.6.3 # via -r requirements/dev.in pip-tools==7.3.0 # via pip-compile-multi platformdirs==3.11.0 # via # tox # virtualenv pluggy==1.3.0 # via tox pre-commit==3.5.0 # via -r requirements/dev.in pyproject-api==1.6.1 # via tox pyproject-hooks==1.0.0 # via build pyyaml==6.0.1 # via pre-commit toposort==1.10 # via pip-compile-multi tox==4.11.3 # via -r requirements/dev.in virtualenv==20.24.6 # via # pre-commit # tox wheel==0.41.3 # via pip-tools # The following packages are considered to be unsafe in a requirements file: # pip # setuptools blinker-1.7.0/requirements/docs.in000066400000000000000000000000531452054522600171530ustar00rootroot00000000000000Pallets-Sphinx-Themes Sphinx sphinx-issues blinker-1.7.0/requirements/docs.txt000066400000000000000000000026301452054522600173670ustar00rootroot00000000000000# SHA1:443ee977856867aeaba570acaa0e03086aca933c # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # alabaster==0.7.13 # via sphinx babel==2.13.1 # via sphinx certifi==2023.7.22 # via requests charset-normalizer==3.3.2 # via requests docutils==0.20.1 # via sphinx idna==3.4 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx markupsafe==2.1.3 # via jinja2 packaging==23.2 # via # pallets-sphinx-themes # sphinx pallets-sphinx-themes==2.1.1 # via -r requirements/docs.in pygments==2.16.1 # via sphinx requests==2.31.0 # via sphinx snowballstemmer==2.2.0 # via sphinx sphinx==7.2.6 # via # -r requirements/docs.in # pallets-sphinx-themes # sphinx-issues # sphinxcontrib-applehelp # sphinxcontrib-devhelp # sphinxcontrib-htmlhelp # sphinxcontrib-qthelp # sphinxcontrib-serializinghtml sphinx-issues==3.0.1 # via -r requirements/docs.in sphinxcontrib-applehelp==1.0.7 # via sphinx sphinxcontrib-devhelp==1.0.5 # via sphinx sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.6 # via sphinx sphinxcontrib-serializinghtml==1.1.9 # via sphinx urllib3==2.0.7 # via requests # The following packages are considered to be unsafe in a requirements file: # setuptools blinker-1.7.0/requirements/tests.in000066400000000000000000000000261452054522600173650ustar00rootroot00000000000000pytest pytest-asyncio blinker-1.7.0/requirements/tests.txt000066400000000000000000000005751452054522600176070ustar00rootroot00000000000000# SHA1:738f1ea95febe383951f6eb64bdad13fefc1a97a # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # iniconfig==2.0.0 # via pytest packaging==23.2 # via pytest pluggy==1.3.0 # via pytest pytest==7.4.3 # via # -r requirements/tests.in # pytest-asyncio pytest-asyncio==0.21.1 # via -r requirements/tests.in blinker-1.7.0/requirements/typing.in000066400000000000000000000000051452054522600175320ustar00rootroot00000000000000mypy blinker-1.7.0/requirements/typing.txt000066400000000000000000000004171452054522600177520ustar00rootroot00000000000000# SHA1:7983aaa01d64547827c20395d77e248c41b2572f # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # mypy==1.6.1 # via -r requirements/typing.in mypy-extensions==1.0.0 # via mypy typing-extensions==4.8.0 # via mypy blinker-1.7.0/src/000077500000000000000000000000001452054522600137415ustar00rootroot00000000000000blinker-1.7.0/src/blinker/000077500000000000000000000000001452054522600153675ustar00rootroot00000000000000blinker-1.7.0/src/blinker/__init__.py000066400000000000000000000006301452054522600174770ustar00rootroot00000000000000from 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.7.0" blinker-1.7.0/src/blinker/_saferef.py000066400000000000000000000216101452054522600175130ustar00rootroot00000000000000# extracted from Louie, http://pylouie.org/ # updated for Python 3 # # Copyright (c) 2006 Patrick K. O'Brien, Mike C. Fletcher, # Matthew R. Scott # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # * Neither the name of the nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # 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 get_self = operator.attrgetter("__self__") get_func = operator.attrgetter("__func__") def safe_ref(target, on_delete=None): """Return a *safe* weak reference to a callable target. - ``target``: The object to be weakly referenced, if it's a bound method reference, will create a BoundMethodWeakref, otherwise creates a simple weakref. - ``on_delete``: If provided, will have a hard reference stored to the callable to be called after the safe reference goes out of scope with the reference object, (either a weakref or a BoundMethodWeakref) as argument. """ try: im_self = get_self(target) except AttributeError: if callable(on_delete): return weakref.ref(target, on_delete) else: return weakref.ref(target) else: 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__"), ( 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: """'Safe' and reusable weak references to instance methods. BoundMethodWeakref objects provide a mechanism for referencing a bound method without requiring that the method object itself (which is normally a transient object) is kept alive. Instead, the BoundMethodWeakref object keeps weak references to both the object and the function which together define the instance method. Attributes: - ``key``: The identity key for the reference, calculated by the class's calculate_key method applied to the target instance method. - ``deletion_methods``: Sequence of callable objects taking single argument, a reference to this object which will be called when *either* the target object or target function is garbage collected (i.e. when this object becomes invalid). These are specified as the on_delete parameters of safe_ref calls. - ``weak_self``: Weak reference to the target object. - ``weak_func``: Weak reference to the target function. Class Attributes: - ``_all_instances``: Class attribute pointing to all live BoundMethodWeakref objects indexed by the class's calculate_key(target) method applied to the target objects. This weak value dictionary is used to short-circuit creation so that multiple references to the same (object, function) pair produce the same BoundMethodWeakref instance. """ _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 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. Otherwise the new instance is created and registered in the table of already-referenced methods. """ key = cls.calculate_key(target) current = cls._all_instances.get(key) if current is not None: current.deletion_methods.append(on_delete) return current else: base = super().__new__(cls) cls._all_instances[key] = base base.__init__(target, on_delete, *arguments, **named) return base def __init__(self, target, on_delete=None): """Return a weak-reference-like instance for a bound method. - ``target``: The instance-method target for the weak reference, must have im_self and im_func attributes and be reconstructable via the following, which is true of built-in instance methods:: target.im_func.__get__( target.im_self ) - ``on_delete``: Optional callback which will be called when this weak reference ceases to be valid (i.e. either the object or the function is garbage collected). Should take a 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[:] del self.deletion_methods[:] try: del self.__class__._all_instances[self.key] except KeyError: pass for function in methods: try: if callable(function): function(self) except Exception: try: traceback.print_exc() except AttributeError: e = sys.exc_info()[1] 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) im_func = get_func(target) self.weak_self = weakref.ref(im_self, remove) self.weak_func = weakref.ref(im_func, remove) 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. Currently this is a two-tuple of the id()'s of the target object and the target function respectively. """ return (id(get_self(target)), id(get_func(target))) def __str__(self): """Give a friendly representation of the object.""" return "{}({}.{})".format( 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 __eq__(self, other): """Compare with another reference.""" if not isinstance(other, self.__class__): 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. If the target cannot be retrieved, then will return None, otherwise returns a bound instance method for our object and function. Note: You may call this method any number of times, as it does not invalidate the reference. """ target = self.weak_self() if target is not None: function = self.weak_func() if function is not None: return function.__get__(target) return None blinker-1.7.0/src/blinker/_utilities.py000066400000000000000000000054501452054522600201170ustar00rootroot00000000000000from __future__ import annotations import typing as t 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 blinker-1.7.0/src/blinker/base.py000066400000000000000000000501001452054522600166470ustar00rootroot00000000000000"""Signals and events. A small implementation of signals, inspired by a snippet of Django signal API client code seen in a blog post. Signals are first-class objects and 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 inspect import iscoroutinefunction from warnings import warn from weakref import WeakValueDictionary from blinker._utilities import annotatable_weakref from blinker._utilities import hashable_identity from blinker._utilities import IdentityType 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.Awaitable[T]]], t.Callable[P, T]] SyncWrapperType = t.Callable[[t.Callable[P, T]], t.Callable[P, t.Awaitable[T]]] ANY = symbol("ANY") ANY.__doc__ = 'Token for "any sender".' ANY_ID = 0 # NOTE: We need a reference to cast for use in weakref callbacks otherwise # t.cast may have already been set to None during finalization. cast = t.cast class Signal: """A notification emitter.""" #: An :obj:`ANY` convenience synonym, allows ``Signal.ANY`` #: without an additional import. ANY = ANY set_class: type[set] = set @lazy_property def receiver_connected(self) -> Signal: """Emitted after each :meth:`connect`. 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.") @lazy_property def receiver_disconnected(self) -> Signal: """Emitted after :meth:`disconnect`. The sender is the signal instance, and the :meth:`disconnect` arguments are passed through: *receiver* and *sender*. Note, this signal is emitted **only** when :meth:`disconnect` is called explicitly. The disconnect signal can not be emitted by an automatic disconnect (due to a weakly referenced receiver or sender going out of scope), as the receiver and/or sender instances are no longer available for use at the time this signal would be emitted. 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: """ :param doc: optional. If provided, will be assigned to the signal's __doc__ attribute. """ if doc: self.__doc__ = doc #: A mapping of connected receivers. #: #: The values of this mapping are not meaningful outside of the #: 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: dict[IdentityType, t.Callable | annotatable_weakref] = {} self.is_muted = False self._by_receiver: dict[IdentityType, set[IdentityType]] = defaultdict( self.set_class ) self._by_sender: dict[IdentityType, set[IdentityType]] = defaultdict( self.set_class ) 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 `sender=` as a single positional argument and any ``kwargs`` that were provided to a call to :meth:`send`. :param sender: Any object or :obj:`ANY`, defaults to ``ANY``. Restricts notifications delivered to *receiver* to only those :meth:`send` emissions sent by *sender*. If ``ANY``, the receiver will always be notified. A *receiver* may be connected to multiple *sender* values on the same Signal through multiple calls to :meth:`connect`. :param weak: If true, the Signal will hold a weakref to *receiver* and automatically disconnect when *receiver* goes out of scope or is garbage collected. Defaults to True. """ 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: sender_id = hashable_identity(sender) self.receivers.setdefault(receiver_id, receiver_ref) self._by_sender[sender_id].add(receiver_id) self._by_receiver[receiver_id].add(sender_id) del receiver_ref if sender is not ANY and sender_id not in self._weak_senders: # wire together a cleanup for weakref-able senders try: sender_ref = reference(sender, self._cleanup_sender) sender_ref.sender_id = sender_id except TypeError: pass else: self._weak_senders.setdefault(sender_id, sender_ref) del sender_ref # broadcast this connection. if receivers raise, disconnect. 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 as e: self.disconnect(receiver, sender) 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 TypeError as e: self.disconnect(receiver, sender) raise e return receiver 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 will only receive :meth:`send` emissions sent by *sender*. If ``ANY``, the receiver will always be notified. A function may be decorated multiple times with differing *sender* values. :param weak: If true, the Signal will hold a weakref to the decorated function and automatically disconnect when *receiver* goes out of scope or is garbage collected. Unlike :meth:`connect`, this defaults to False. The decorated function will be invoked by :meth:`send` with `sender=` as a single positional argument and any ``kwargs`` that were provided to the call to :meth:`send`. .. versionadded:: 1.1 """ def decorator(fn: T_callable) -> T_callable: self.connect(fn, sender, weak) return fn return decorator @contextmanager 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 :param sender: optional, a sender to filter on This is a context manager for use in the ``with`` statement. It can be useful in unit tests. *receiver* is connected to the signal for the duration of the ``with`` block, and will be disconnected automatically when exiting the block: .. code-block:: python with on_ready.connected_to(receiver): # do stuff on_ready.send(123) .. versionadded:: 1.1 """ self.connect(receiver, sender=sender, weak=False) try: yield None finally: self.disconnect(receiver) @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 :param sender: optional, a sender to filter on .. versionadded:: 0.9 .. versionchanged:: 1.1 Renamed to :meth:`connected_to`. ``temporarily_connected_to`` was deprecated in 1.2 and will be removed in a subsequent version. """ warn( "temporarily_connected_to is deprecated; use connected_to instead.", DeprecationWarning, ) return self.connected_to(receiver, sender) 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 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 _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 iscoroutinefunction(receiver): if _async_wrapper is None: raise RuntimeError("Cannot send to a coroutine function") receiver = _async_wrapper(receiver) result = receiver(sender, **kwargs) 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 iscoroutinefunction(receiver): if _sync_wrapper is None: raise RuntimeError("Cannot send to a non-coroutine function") receiver = _sync_wrapper(receiver) result = await receiver(sender, **kwargs) 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( f"send() accepts only one positional argument, {len(sender)} given" ) return [] # Using '*sender' rather than 'sender=None' allows 'sender' to be # used as a keyword argument- i.e. it's an invisible name in the # function signature. if len(sender) == 0: sender = None elif len(sender) > 1: raise TypeError( f"send() accepts only one positional argument, {len(sender)} given" ) else: sender = sender[0] return 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 weakly referenced receivers are still alive. See :meth:`receivers_for` for a stronger search. """ if not self.receivers: return False if self._by_sender[ANY_ID]: return True if sender is ANY: return False return hashable_identity(sender) in self._by_sender def receivers_for( self, sender: t.Any ) -> t.Generator[t.Callable[[t.Any], t.Any], 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] 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, WeakTypes): strong = receiver() if strong is None: self._disconnect(receiver_id, ANY_ID) continue receiver = strong yield receiver # type: ignore[misc] def disconnect(self, receiver: t.Callable, sender: t.Any = ANY) -> None: """Disconnect *receiver* from this signal's events. :param receiver: a previously :meth:`connected` callable :param sender: a specific sender to disconnect from, or :obj:`ANY` to disconnect from all senders. Defaults to ``ANY``. """ sender_id: IdentityType if sender is ANY: sender_id = ANY_ID else: sender_id = hashable_identity(sender) 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) 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(): 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 _cleanup_receiver(self, receiver_ref: annotatable_weakref) -> None: """Disconnect a receiver from all senders.""" self._disconnect(cast(IdentityType, receiver_ref.receiver_id), ANY_ID) def _cleanup_sender(self, sender_ref: annotatable_weakref) -> None: """Disconnect all receivers from a sender.""" sender_id = 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) -> None: """Prune unused sender/receiver bookkeeping. Not threadsafe. Connecting & disconnecting leave behind a small amount of bookkeeping for the receiver and sender values. 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 Python process performing dynamic signal routing with high volume- e.g. connecting to function closures, "senders" are all unique object instances, and doing all of this over and over- you may see memory usage will 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 _id, bucket in list(mapping.items()): if not bucket: mapping.pop(_id, None) def _clear_state(self) -> None: """Throw away all signal state. Useful for unit 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 As of 1.2, individual signals have their own private :attr:`~Signal.receiver_connected` and :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: str, doc: str | None = None) -> None: Signal.__init__(self, doc) #: The name of this signal. self.name = name def __repr__(self) -> str: base = Signal.__repr__(self) return f"{base[:-1]}; {self.name!r}>" # noqa: E702 class Namespace(dict): """A mapping of signal names to signals.""" 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] # type: ignore[no-any-return] except KeyError: result = self.setdefault(name, NamedSignal(name, doc)) return result # type: ignore[no-any-return] class WeakNamespace(WeakValueDictionary): """A weak mapping of signal names to signals. Automatically cleans up unused Signals when the last reference goes out of scope. This namespace implementation exists for a measure of legacy compatibility with Blinker <= 1.2, and may be dropped in the future. .. versionadded:: 1.3 """ 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] # type: ignore[no-any-return] except KeyError: result = self.setdefault(name, NamedSignal(name, doc)) return result # type: ignore[no-any-return] signal = Namespace().signal blinker-1.7.0/src/blinker/py.typed000066400000000000000000000000001452054522600170540ustar00rootroot00000000000000blinker-1.7.0/tests/000077500000000000000000000000001452054522600143145ustar00rootroot00000000000000blinker-1.7.0/tests/test_context.py000066400000000000000000000021751452054522600174160ustar00rootroot00000000000000import pytest from blinker import Signal def test_temp_connection(): sig = Signal() canary = [] def receiver(sender): 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(): sig = Signal() canary = [] def receiver(sender): 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): sig = Signal() canary = [] def receiver(sender): canary.append(sender) class Failure(Exception): pass with pytest.raises(exc_type): # noqa: B908 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.7.0/tests/test_saferef.py000066400000000000000000000072421452054522600173450ustar00rootroot00000000000000# extracted from Louie, http://pylouie.org/ # updated for Python 3 # # Copyright (c) 2006 Patrick K. O'Brien, Mike C. Fletcher, # Matthew R. Scott # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # * Neither the name of the nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import unittest from blinker._saferef import safe_ref class _Sample1: def x(self): pass def _sample2(obj): pass class _Sample3: def __call__(self, obj): pass class TestSaferef(unittest.TestCase): # XXX: The original tests had a test for closure, and it had an # off-by-one problem, perhaps due to scope issues. It has been # removed from this test suite. def setUp(self): ts = [] ss = [] 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 _ in range(30): t = _Sample3() ts.append(t) s = safe_ref(t, self._closure) ss.append(s) self.ts = ts self.ss = ss self.closure_count = 0 def tearDown(self): if hasattr(self, "ts"): del self.ts if hasattr(self, "ss"): del self.ss def test_In(self): """Test the `in` operator for safe references (cmp)""" for t in self.ts[:50]: assert safe_ref(t.x) in self.ss def test_Valid(self): """Test that the references are valid (return instance methods)""" for s in self.ss: assert s() def test_ShortCircuit(self): """Test that creation short-circuits to reuse existing references""" sd = {} for s in self.ss: sd[s] = 1 for t in self.ts: if hasattr(t, "x"): assert safe_ref(t.x) in sd else: assert safe_ref(t) in sd def test_Representation(self): """Test that the reference object's representation works XXX Doesn't currently check the results, just that no error is raised """ repr(self.ss[-1]) def _closure(self, ref): """Dumb utility mechanism to increment deletion counter""" self.closure_count += 1 blinker-1.7.0/tests/test_signals.py000066400000000000000000000312421452054522600173670ustar00rootroot00000000000000import gc import sys import time import pytest import blinker jython = sys.platform.startswith("java") pypy = hasattr(sys, "pypy_version_info") def collect_acyclic_refs(): # cpython releases these immediately without a collection if jython or pypy: gc.collect() if jython: time.sleep(0.1) class Sentinel(list): """A signal receipt accumulator.""" def make_receiver(self, key): """Return a generic signal receiver function logging as *key* 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 return receiver def test_meta_connect(): sentinel = [] def meta_received(sender, **kw): sentinel.append(dict(kw, sender=sender)) assert not blinker.receiver_connected.receivers blinker.receiver_connected.connect(meta_received) assert not sentinel 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, } ] blinker.receiver_connected._clear_state() 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") 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(): _test_signal_signals(blinker.ANY) def test_signal_signals_strong_sender(): _test_signal_signals("squiznart") def test_signal_weak_receiver_vanishes(): # 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(): 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(): 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() 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_meta_connect_failure(): def meta_received(sender, **kw): 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) assert not sig.receivers assert not sig._by_receiver assert sig._by_sender == {blinker.base.ANY_ID: set()} blinker.receiver_connected._clear_state() 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 collect_acyclic_refs() # weak by default, already out of scope assert "def" not in ns del s1 collect_acyclic_refs() 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 del s1 collect_acyclic_refs() assert "def" in ns assert "abc" in ns def test_weak_receiver(): sentinel = [] def received(sender, **kw): sentinel.append(kw) sig = blinker.Signal() # XXX: weirdly, under jython an explicit weak=True causes this test # to fail, leaking a strong ref to the receiver somewhere. # http://bugs.jython.org/issue1586 if jython: sig.connect(received) # weak=True by default. else: 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(): sentinel = [] def received(sender): 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(): 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: def __init__(self, bucket): self.bucket = bucket def received(self, sender): 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(): sentinel = [] def received(sender): 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(): sentinel = [] def received(sender): 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(): sentinel = [] class Object: pass obj = Object() sig = blinker.Signal() @sig.connect_via(obj) def receiver(sender, **kw): 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(): sentinel = [] def received(sender): 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(): 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") 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(): sig = blinker.Signal(doc="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) 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 blinker-1.7.0/tests/test_utilities.py000066400000000000000000000007031452054522600177400ustar00rootroot00000000000000import pickle from blinker._utilities import symbol def test_symbols(): 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(): foo = symbol("foo") for _ in 0, 1, 2: roundtrip = pickle.loads(pickle.dumps(foo)) assert roundtrip is foo blinker-1.7.0/tox.ini000066400000000000000000000010551452054522600144660ustar00rootroot00000000000000[tox] envlist = py3{12,11,10,9,8} pypy310 docs style typing skip_missing_interpreters = true [testenv] package = wheel wheel_build_env = .pkg deps = -r requirements/tests.txt commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests} [testenv:style] skip_install = true deps = pre-commit commands = pre-commit run --all-files [testenv:typing] deps = -r requirements/typing.txt commands = mypy [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html