pax_global_header00006660000000000000000000000064141773020540014515gustar00rootroot0000000000000052 comment=0078dc71fafb4620a52d849acac01906f4d29518 pyee-9.0.4/000077500000000000000000000000001417730205400124715ustar00rootroot00000000000000pyee-9.0.4/.github/000077500000000000000000000000001417730205400140315ustar00rootroot00000000000000pyee-9.0.4/.github/workflows/000077500000000000000000000000001417730205400160665ustar00rootroot00000000000000pyee-9.0.4/.github/workflows/qa.yaml000066400000000000000000000016041417730205400173540ustar00rootroot00000000000000name: QA on: pull_request jobs: qa: name: Run QA checks runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Set up Node.js @latest uses: actions/setup-node@v2 with: node-version: 16 - name: Install the world run: | python -m pip install --upgrade pip wheel pip install -r requirements.txt pip install -r requirements_dev.txt pip install -e . npm i - name: Run linting run: | make lint - name: Run type checking run: | make check - name: Run tests run: make test pyee-9.0.4/.gitignore000066400000000000000000000002221417730205400144550ustar00rootroot00000000000000*.pyc docs/_build dist/* build/* MANIFEST README .cache .eggs .python-version pyee.egg-info/ version.txt scratchpad.ipynb .tox/ node_modules venv pyee-9.0.4/.readthedocs.yaml000066400000000000000000000002231417730205400157150ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: - pdf python: version: "3.8" install: - requirements: requirements_docs.txt pyee-9.0.4/CHANGELOG.rst000066400000000000000000000153561417730205400145240ustar00rootroot000000000000002022/02/04 Version 9.0.4 ------------------------ - Add ``py.typed`` file to ``MANIFEST.in`` (ensures mypy actually respects the type annotations) 2022/01/18 Version 9.0.3 ------------------------ - Improve type safety of ``EventEmitter#on``, ``EventEmitter#add_listener`` and ``EventEmitter#listens_to`` by parameterizing the ``Handler`` - Minor fixes to documentation 2022/01/17 Version 9.0.2 ------------------------ - Add ``tests_require`` to setup.py, fixing COPR build - Install as an editable package in ``environment.yml`` and ``requirements_docs.txt``, fixing Conda workflows and ReadTheDocs respectively 2022/01/17 Version 9.0.1 ------------------------ - Fix regression where ``EventEmitter#listeners`` began crashing when called with uninitialized listeners 2022/01/17 Version 9.0.0 ------------------------ Compatibility: - Drop 3.6 support New features: - New ``EventEmitter.event_names()`` method (see PR #96) - Type annotations and type checking with ``pyright`` - Exprimental ``pyee.cls`` module exposing an ``@evented`` class decorator and a ``@on`` method decorator (see PR #84) Moved/deprecated interfaces: - ``pyee.TwistedEventEmitter`` -> ``pyee.twisted.TwistedEventEmitter`` - ``pyee.AsyncIOEventEmitter`` -> ``pyee.asyncio.AsyncIOEventEmitter`` - ``pyee.ExecutorEventEmitter`` -> ``pyee.executor.ExecutorEventEmitter`` - ``pyee.TrioEventEmitter`` -> ``pyee.trio.TrioEventEmitter`` Removed interfaces: - ``pyee.CompatEventEmitter`` Documentation fixes: - Add docstring to ``BaseEventEmitter`` - Update docstrings to reference ``EventEmitter`` instead of ``BaseEventEmitter`` throughout Developer Setup & CI: - Migrated builds from Travis to GitHub Actions - Refactor developer setup to use a local virtualenv 2021/8/14 Version 8.2.2 ----------------------- - Correct version in docs 2021/8/14 Version 8.2.1 ----------------------- - Add .readthedocs.yaml file - Remove vcversioner dependency from docs build 2021/8/14 Version 8.2.0 ----------------------- - Remove test_requires and setup_requires directives from setup.py (closing #82) - Remove vcversioner from dependencies - Streamline requirements.txt and environment.yml files - Update and extend CONTRIBUTING.rst - CI with GitHub Actions instead of Travis (closing #56) - Format all code with black - Switch default branch to ``main`` - Add the CHANGELOG to Sphinx docs (closing #51) - Updated copyright information 2020/10/08 Version 8.1.0 ------------------------ - Improve thread safety in base EventEmitter - Documentation fix in ExecutorEventEmitter 2020/09/20 Version 8.0.1 ------------------------ - Update README to reflect new API 2020/09/20 Version 8.0.0 ------------------------ - Drop support for Python 2.7 - Remove CompatEventEmitter and rename BaseEventEmitter to EventEmitter - Create an alias for BaseEventEmitter with a deprecation warning 2020/09/20 Version 7.0.4 ------------------------ - setup_requires vs tests_require now correct - tests_require updated to pass in tox - 3.7 testing removed from tox - 2.7 testing removed from Travis 2020/09/04 Version 7.0.3 ------------------------ - Tag license as MIT in setup.py - Update requirements and environment to pip -e the package 2020/05/12 Version 7.0.2 ------------------------ - Support Python 3.8 by attempting to import TimeoutError from ``asyncio.exceptions`` - Add LICENSE to package manifest - Add trio testing to tox - Add Python 3.8 to tox - Fix Python 2.7 in tox 2020/01/30 Version 7.0.1 ------------------------ - Some tweaks to the docs 2020/01/30 Version 7.0.0 ------------------------ - Added a ``TrioEventEmitter`` class for intended use with trio - ``AsyncIOEventEmitter`` now correctly handles cancellations - Add a new experimental ``pyee.uplift`` API for adding new functionality to existing event emitters 2019/04/11 Version 6.0.0 ------------------------ - Added a ``BaseEventEmitter`` class which is entirely synchronous and intended for simple use and for subclassing - Added an ``AsyncIOEventEmitter`` class for intended use with asyncio - Added a ``TwistedEventEmitter`` class for intended use with twisted - Added an ``ExecutorEventEmitter`` class which runs events in an executor - Deprecated ``EventEmitter`` (use one of the new classes) 2017/11/18 Version 5.0.0 ------------------------ - CHANGELOG.md reformatted to CHANGELOG.rst - Added CONTRIBUTORS.rst - The `listeners` method no longer returns the raw list of listeners, and instead returns a list of unwrapped listeners; This means that mutating listeners on the EventEmitter by mutating the list returned by this method isn't possible anymore, and that for once handlers this method returns the unwrapped handler rather than the wrapped handler - `once` API now returns the unwrapped handler in both decorator and non-decorator cases - Possible to remove once handlers with unwrapped handlers - Internally, listeners are now stored on a OrderedDict rather than a list - Minor stylistic tweaks to make code more pythonic 2017/11/17 Version 4.0.1 ------------------------ - Fix bug in setup.py; Now publishable 2017/11/17 Version 4.0.0 ------------------------ - Coroutines now work with .once - Wrapped listener is removed prior to hook execution rather than after for synchronous .once handlers 2017/02/12 Version 3.0.3 ------------------------ - Add universal wheel 2017/02/10 Version 3.0.2 ------------------------ - EventEmitter now inherits from object 2016/10/02 Version 3.0.1 ------------------------ - Fixes/Updates to pyee docs - Uses vcversioner for managing version information 2016/10/02 Version 3.0.0 ------------------------ - Errors resulting from async functions are now proxied to the "error" event, rather than being lost into the aether. 2016/10/01 Version 2.0.3 ------------------------ - Fix setup.py broken in python 2.7 - Add link to CHANGELOG in README 2016/10/01 Version 2.0.2 ------------------------ - Fix RST render warnings in README 2016/10/01 Version 2.0.1 ------------------------ - Add README contents as long\_description inside setup.py 2016/10/01 Version 2.0.0 ------------------------ - Drop support for pythons 3.2, 3.3 and 3.4 (support 2.7 and 3.5) - Use pytest instead of nose - Removed Event\_emitter alias - Code passes flake8 - Use setuptools (no support for users without setuptools) - Reogranized docs, hosted on readthedocs.org - Support for scheduling coroutine functions passed to `@ee.on` 2016/02/15 Version 1.0.2 ------------------------ - Make copy of event handlers array before iterating on emit 2015/09/21 Version 1.0.1 ------------------------ - Change URLs to reference jfhbrook 2015/09/20 Version 1.0.0 ------------------------ - Decorators return original function for `on` and `once` - Explicit python 3 support - Addition of legit license file - Addition of CHANGELOG.md - Now properly using semver pyee-9.0.4/CONTRIBUTORS.rst000066400000000000000000000010061417730205400151550ustar00rootroot00000000000000General format is: contributor, github handle, email. Listed in no particular order: - Josh Holbrook @jfhbrook - Gleicon Moraes @gleicon - Zack Do @doboy - @Zearin - René Kijewski @Kijewski - Gabe Appleton @gappleto97 - Daniel M. Capella @polyzen - Fabian Affolter @fabaff - Anton Bolshakov @blshkv - Åke Forslund @forslund - Ivan Gretchka @leirons - Max Schmitt @mxschmitt pyee-9.0.4/DEVELOPMENT.rst000066400000000000000000000047261417730205400150160ustar00rootroot00000000000000Development And Publishing ========================== Environment Setup ----------------- To create a local virtualenv, run:: make setup This will create a virtualenv at ``./venv``, install dependencies with pip, and install pyright using npm. To activate the environment in your shell:: . ./venv/bin/activate Alternately, run everything with the make tasks, which source the activate script before running commands. conda ~~~~~ To create a Conda environment, run:: conda env create npm i To update the environment, run:: conda env update npm i --update To activate the environment, run:: conda activate pyee The other Makefile tasks should operate normally if the environment is activated. Formatting, Linting and Testing ------------------------------- The basics are wrapped with a Makefile:: make format # runs black make lint # runs flake8 make test # runs pytest Generating Docs --------------- Docs for published projects are automatically generated by readthedocs, but you can also preview them locally by running:: make build_docs Then, you can serve them with Python's dev server with:: make serve_docs Publishing ---------- Do a Final Check ~~~~~~~~~~~~~~~~ Make sure that formatting looks good and that linting and testing are passing. Update the Changelog ~~~~~~~~~~~~~~~~~~~~ Update the CHANGELOG.rst file to detail the changes being rolled into the new version. Update the Version in setup.py ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This project *used* to use ``vcversioner`` and versioning of the package would automatically leverage the appropriate git tag, but that is no longer the case. I do my best to follow `semver ` when updating versions. Add a Git Tag ~~~~~~~~~~~~~ This project uses git tags to tag versions:: git tag -a {version} -m 'Release {version}' You don't need to prefix the version with a ``v``. Build and Publish ~~~~~~~~~~~~~~~~~ To package everything, run:: make package To publish:: make publish Push the Tag to GitHub ~~~~~~~~~~~~~~~~~~~~~~ :: git push origin main --tags Check on RTD ~~~~~~~~~~~~ RTD should build automatically but I find there's a delay so I like to kick it off manually. Log into `RTD `, log in, then go to `the pyee project page ` and build latest and stable. Announce on Twitter ~~~~~~~~~~~~~~~~~~~ It's not official, but I like to announce the release on Twitter. pyee-9.0.4/LICENSE000066400000000000000000000020701417730205400134750ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021 Josh Holbrook 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. pyee-9.0.4/MANIFEST.in000066400000000000000000000002611417730205400142260ustar00rootroot00000000000000include LICENSE include README.rst include CHANGELOG.rst include CONTRIBUTORS.rst include DEVELOPMENT.rst include version.txt include pyee/py.typed recursive-include tests *.py pyee-9.0.4/Makefile000066400000000000000000000031121417730205400141260ustar00rootroot00000000000000.PHONY: setup setup-conda package upload check test tox lint format build_docs serve_docs clean setup: python3 -m venv venv if [ -d venv ]; then . ./venv/bin/activate; fi; pip install pip wheel --upgrade if [ -d venv ]; then . ./venv/bin/activate; fi; pip install -r requirements.txt if [ -d venv ]; then . ./venv/bin/activate; fi; pip install -r requirements_dev.txt if [ -d venv ]; then . ./venv/bin/activate; fi; pip install -e . npm i package: test lint if [ -d venv ]; then . ./venv/bin/activate; fi; python setup.py check if [ -d venv ]; then . ./venv/bin/activate; fi; python setup.py sdist if [ -d venv ]; then . ./venv/bin/activate; fi; python setup.py bdist_wheel --universal upload: if [ -d venv ]; then . ./venv/bin/activate; fi; twine upload dist/* check: if [ -d venv ]; then . ./venv/bin/activate; fi; npm run pyright test: if [ -d venv ]; then . ./venv/bin/activate; fi; pytest ./tests tox: if [ -d venv ]; then . ./venv/bin/activate; fi; tox lint: if [ -d venv ]; then . ./venv/bin/activate; fi; flake8 ./pyee setup.py ./tests ./docs format: if [ -d venv ]; then . ./venv/bin/activate; fi; black ./pyee setup.py ./tests ./docs if [ -d venv ]; then . ./venv/bin/activate; fi; isort ./pyee setup.py ./tests ./docs build_docs: if [ -d venv ]; then . ./venv/bin/activate; fi; cd docs && make html serve_docs: build_docs if [ -d venv ]; then . ./venv/bin/activate; fi; cd docs/_build/html && python -m http.server clean: rm -rf .tox rm -rf dist rm -rf pyee.egg-info rm -rf pyee/*.pyc rm -rf pyee/__pycache__ rm -rf pytest_runner-*.egg rm -rf tests/__pycache__ pyee-9.0.4/README.rst000066400000000000000000000014531417730205400141630ustar00rootroot00000000000000pyee ==== .. image:: https://travis-ci.org/jfhbrook/pyee.png :target: https://travis-ci.org/jfhbrook/pyee .. image:: https://readthedocs.org/projects/pyee/badge/?version=latest :target: https://pyee.readthedocs.io pyee supplies a ``EventEmitter`` object that is similar to the ``EventEmitter`` class from Node.js. It also supplies a number of subclasses with added support for async and threaded programming in python, such as async/await as seen in python 3.5+. Docs: ----- Autogenerated API docs, including basic installation directions and examples, can be found at https://pyee.readthedocs.io . Development: ------------ See ``DEVELOPMENT.rst``. Changelog: ---------- See ``CHANGELOG.rst``. Contributors: ------------- See ``CONTRIBUTORS.rst``. License: -------- MIT/X11, see ``LICENSE``. pyee-9.0.4/docs/000077500000000000000000000000001417730205400134215ustar00rootroot00000000000000pyee-9.0.4/docs/Makefile000066400000000000000000000166561417730205400150770ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyee.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyee.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pyee" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyee" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." pyee-9.0.4/docs/conf.py000066400000000000000000000231461417730205400147260ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # pyee documentation build configuration file, created by # sphinx-quickstart on Sat Oct 1 15:15:23 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "pyee" copyright = "2021, Josh Holbrook" author = "Josh Holbrook" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "9.0.4" # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "bizstyle" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = 'pyee v1.0.2' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or # 32x32 pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "pyeedoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "pyee.tex", "pyee Documentation", "Josh Holbrook", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "pyee", "pyee Documentation", [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "pyee", "pyee Documentation", author, "pyee", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False pyee-9.0.4/docs/index.rst000066400000000000000000000022521417730205400152630ustar00rootroot00000000000000pyee ==== pyee is a rough port of `node.js's EventEmitter `_. Unlike its namesake, it includes a number of subclasses useful for implementing async and threaded programming in python, such as async/await as seen in python 3.5+. Install: -------- You can install this project into your environment of choice using ``pip``:: pip install pyee API Docs: --------- .. toctree:: :maxdepth: 2 .. automodule:: pyee .. autoclass:: pyee.EventEmitter :members: .. autoclass:: pyee.asyncio.AsyncIOEventEmitter :members: .. autoclass:: pyee.twisted.TwistedEventEmitter :members: .. autoclass:: pyee.executor.ExecutorEventEmitter :members: .. autoclass:: pyee.trio.TrioEventEmitter :members: .. autoclass:: BaseEventEmitter :members: .. autoexception:: pyee.PyeeException .. autofunction:: pyee.uplift.uplift .. autofunction:: pyee.cls.on .. autofunction:: pyee.cls.evented Some Links ========== * `Fork Me On GitHub! `_ * `These Very Docs on readthedocs.io `_ * :ref:`genindex` * :ref:`modindex` * :ref:`search` Changelog ========= .. include:: ../CHANGELOG.rst pyee-9.0.4/environment.yml000066400000000000000000000003361417730205400155620ustar00rootroot00000000000000name: pyee channels: - conda-forge - default dependencies: - python=3.8.3 - pip=20.2.3 - trio=0.17.0 - twine=3.2.0 - twisted=20.3.0 - pip: - -r requirements.txt - -r requirements_dev.txt - -e . pyee-9.0.4/package-lock.json000066400000000000000000000017621417730205400157130ustar00rootroot00000000000000{ "name": "pyee-devtools", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pyee-devtools", "version": "1.0.0", "license": "MIT", "devDependencies": { "pyright": "^1.1.159" } }, "node_modules/pyright": { "version": "1.1.203", "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.203.tgz", "integrity": "sha512-BglTVxjj6iQBRvqxsQbm9pz8ZMQzBt1GJxxyW4QRJ3utbaXiPQJMpB4UGLIQI6c5S30lcObEdkLicHeWtQYvuQ==", "dev": true, "bin": { "pyright": "index.js", "pyright-langserver": "langserver.index.js" }, "engines": { "node": ">=12.0.0" } } }, "dependencies": { "pyright": { "version": "1.1.203", "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.203.tgz", "integrity": "sha512-BglTVxjj6iQBRvqxsQbm9pz8ZMQzBt1GJxxyW4QRJ3utbaXiPQJMpB4UGLIQI6c5S30lcObEdkLicHeWtQYvuQ==", "dev": true } } } pyee-9.0.4/package.json000066400000000000000000000010131417730205400147520ustar00rootroot00000000000000{ "name": "pyee-devtools", "version": "1.0.0", "description": "Node.js tools to support developing pyee", "main": "index.js", "scripts": { "pyright": "pyright ./pyee ./tests" }, "repository": { "type": "git", "url": "git+ssh://git@github.com/jfhbrook/pyee.git" }, "author": "Josh Holbrook", "license": "MIT", "bugs": { "url": "https://github.com/jfhbrook/pyee/issues" }, "homepage": "https://github.com/jfhbrook/pyee#readme", "devDependencies": { "pyright": "^1.1.159" } } pyee-9.0.4/pyee/000077500000000000000000000000001417730205400134335ustar00rootroot00000000000000pyee-9.0.4/pyee/__init__.py000066400000000000000000000072201417730205400155450ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ pyee supplies a ``EventEmitter`` class that is similar to the ``EventEmitter`` class from Node.js. In addition, it supplies the subclasses ``AsyncIOEventEmitter``, ``TwistedEventEmitter`` and ``ExecutorEventEmitter`` for supporting async and threaded execution with asyncio, twisted, and concurrent.futures Executors respectively, as supported by the environment. Example ------- :: In [1]: from pyee.base import EventEmitter In [2]: ee = EventEmitter() In [3]: @ee.on('event') ...: def event_handler(): ...: print('BANG BANG') ...: In [4]: ee.emit('event') BANG BANG In [5]: """ from warnings import warn from pyee.base import EventEmitter as EventEmitter from pyee.base import PyeeException class BaseEventEmitter(EventEmitter): """ BaseEventEmitter is deprecated and an alias for EventEmitter. """ def __init__(self): warn( DeprecationWarning( "pyee.BaseEventEmitter is deprecated and will be removed in a " "future major version; you should instead use pyee.EventEmitter." ) ) super(BaseEventEmitter, self).__init__() __all__ = ["BaseEventEmitter", "EventEmitter", "PyeeException"] try: from pyee.asyncio import AsyncIOEventEmitter as _AsyncIOEventEmitter # noqa class AsyncIOEventEmitter(_AsyncIOEventEmitter): """ AsyncIOEventEmitter has been moved to the pyee.asyncio module. """ def __init__(self, loop=None): warn( DeprecationWarning( "pyee.AsyncIOEventEmitter has been moved to the pyee.asyncio " "module." ) ) super(AsyncIOEventEmitter, self).__init__(loop=loop) __all__.append("AsyncIOEventEmitter") except ImportError: pass try: from pyee.twisted import TwistedEventEmitter as _TwistedEventEmitter # noqa class TwistedEventEmitter(_TwistedEventEmitter): """ TwistedEventEmitter has been moved to the pyee.twisted module. """ def __init__(self): warn( DeprecationWarning( "pyee.TwistedEventEmitter has been moved to the pyee.twisted " "module." ) ) super(TwistedEventEmitter, self).__init__() __all__.append("TwistedEventEmitter") except ImportError: pass try: from pyee.executor import ExecutorEventEmitter as _ExecutorEventEmitter # noqa class ExecutorEventEmitter(_ExecutorEventEmitter): """ ExecutorEventEmitter has been moved to the pyee.executor module. """ def __init__(self, executor=None): warn( DeprecationWarning( "pyee.ExecutorEventEmitter has been moved to the pyee.executor " "module." ) ) super(ExecutorEventEmitter, self).__init__(executor=executor) __all__.append("ExecutorEventEmitter") except ImportError: pass try: from pyee.trio import TrioEventEmitter as _TrioEventEmitter # noqa class TrioEventEmitter(_TrioEventEmitter): """ TrioEventEmitter has been moved to the pyee.trio module. """ def __init__(self, nursery=None, manager=None): warn( DeprecationWarning( "pyee.TrioEventEmitter has been moved to the pyee.trio module." ) ) super(TrioEventEmitter, self).__init__(nursery=nursery, manager=manager) __all__.append("TrioEventEmitter") except (ImportError, SyntaxError): pass pyee-9.0.4/pyee/asyncio.py000066400000000000000000000050571417730205400154610ustar00rootroot00000000000000# -*- coding: utf-8 -*- from asyncio import AbstractEventLoop, ensure_future, Future, iscoroutine from typing import Any, Callable, cast, Dict, Optional, Tuple from pyee.base import EventEmitter __all__ = ["AsyncIOEventEmitter"] class AsyncIOEventEmitter(EventEmitter): """An event emitter class which can run asyncio coroutines in addition to synchronous blocking functions. For example:: @ee.on('event') async def async_handler(*args, **kwargs): await returns_a_future() On emit, the event emitter will automatically schedule the coroutine using ``asyncio.ensure_future`` and the configured event loop (defaults to ``asyncio.get_event_loop()``). Unlike the case with the EventEmitter, all exceptions raised by event handlers are automatically emitted on the ``error`` event. This is important for asyncio coroutines specifically but is also handled for synchronous functions for consistency. When ``loop`` is specified, the supplied event loop will be used when scheduling work with ``ensure_future``. Otherwise, the default asyncio event loop is used. For asyncio coroutine event handlers, calling emit is non-blocking. In other words, you do not have to await any results from emit, and the coroutine is scheduled in a fire-and-forget fashion. """ def __init__(self, loop: Optional[AbstractEventLoop] = None): super(AsyncIOEventEmitter, self).__init__() self._loop: Optional[AbstractEventLoop] = loop def _emit_run( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ): try: coro: Any = f(*args, **kwargs) except Exception as exc: self.emit("error", exc) else: if iscoroutine(coro): if self._loop: # ensure_future is *extremely* cranky about the types here, # but this is relatively well-tested and I think the types # are more strict than they should be fut: Any = ensure_future(cast(Any, coro), loop=self._loop) else: fut = ensure_future(cast(Any, coro)) elif isinstance(coro, Future): fut = cast(Any, coro) else: return def callback(f): if f.cancelled(): return exc: Exception = f.exception() if exc: self.emit("error", exc) fut.add_done_callback(callback) pyee-9.0.4/pyee/base.py000066400000000000000000000171471417730205400147310ustar00rootroot00000000000000# -*- coding: utf-8 -*- from collections import OrderedDict from threading import Lock from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union class PyeeException(Exception): """An exception internal to pyee.""" Handler = TypeVar(name="Handler", bound=Callable) class EventEmitter: """The base event emitter class. All other event emitters inherit from this class. Most events are registered with an emitter via the ``on`` and ``once`` methods, and fired with the ``emit`` method. However, pyee event emitters have two *special* events: - ``new_listener``: Fires whenever a new listener is created. Listeners for this event do not fire upon their own creation. - ``error``: When emitted raises an Exception by default, behavior can be overridden by attaching callback to the event. For example:: @ee.on('error') def on_error(message): logging.err(message) ee.emit('error', Exception('something blew up')) All callbacks are handled in a synchronous, blocking manner. As in node.js, raised exceptions are not automatically handled for you---you must catch your own exceptions, and treat them accordingly. """ def __init__(self) -> None: self._events: Dict[ str, "OrderedDict[Callable, Callable]", ] = dict() self._lock: Lock = Lock() def on( self, event: str, f: Optional[Handler] = None ) -> Union[Handler, Callable[[Handler], Handler]]: """Registers the function ``f`` to the event name ``event``, if provided. If ``f`` isn't provided, this method calls ``EventEmitter#listens_to`, and otherwise calls ``EventEmitter#add_listener``. In other words, you may either use it as a decorator:: @ee.on('data') def data_handler(data): print(data) Or directly:: ee.on('data', data_handler) In both the decorated and undecorated forms, the event handler is returned. The upshot of this is that you can call decorated handlers directly, as well as use them in remove_listener calls. Note that this method's return type is a union type. If you are using mypy or pyright, you will probably want to use either ``EventEmitter#listens_to`` or ``EventEmitter#add_listener``. """ if f is None: return self.listens_to(event) else: return self.add_listener(event, f) def listens_to(self, event: str) -> Callable[[Handler], Handler]: """Returns a decorator which will register the decorated function to the event name ``event``:: @ee.listens_to("event") def data_handler(data): print(data) By only supporting the decorator use case, this method has improved type safety over ``EventEmitter#on``. """ def on(f: Handler) -> Handler: self._add_event_handler(event, f, f) return f return on def add_listener(self, event: str, f: Handler) -> Handler: """Register the function ``f`` to the event name ``event``:: def data_handler(data): print(data) h = ee.add_listener("event", data_handler) By not supporting the decorator use case, this method has improved type safety over ``EventEmitter#on``. """ self._add_event_handler(event, f, f) return f def _add_event_handler(self, event: str, k: Callable, v: Callable): # Fire 'new_listener' *before* adding the new listener! self.emit("new_listener", event, k) # Add the necessary function # Note that k and v are the same for `on` handlers, but # different for `once` handlers, where v is a wrapped version # of k which removes itself before calling k with self._lock: if event not in self._events: self._events[event] = OrderedDict() self._events[event][k] = v def _emit_run( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> None: f(*args, **kwargs) def event_names(self) -> Set[str]: """Get a set of events that this emitter is listening to.""" return set(self._events.keys()) def _emit_handle_potential_error(self, event: str, error: Any) -> None: if event == "error": if isinstance(error, Exception): raise error else: raise PyeeException(f"Uncaught, unspecified 'error' event: {error}") def _call_handlers( self, event: str, args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> bool: handled = False with self._lock: funcs = list(self._events.get(event, OrderedDict()).values()) for f in funcs: self._emit_run(f, args, kwargs) handled = True return handled def emit( self, event: str, *args: Any, **kwargs: Any, ) -> bool: """Emit ``event``, passing ``*args`` and ``**kwargs`` to each attached function. Returns ``True`` if any functions are attached to ``event``; otherwise returns ``False``. Example:: ee.emit('data', '00101001') Assuming ``data`` is an attached function, this will call ``data('00101001')'``. """ handled = self._call_handlers(event, args, kwargs) if not handled: self._emit_handle_potential_error(event, args[0] if args else None) return handled def once( self, event: str, f: Callable = None, ) -> Callable: """The same as ``ee.on``, except that the listener is automatically removed after being called. """ def _wrapper(f: Callable) -> Callable: def g( *args: Any, **kwargs: Any, ) -> Any: with self._lock: # Check that the event wasn't removed already right # before the lock if event in self._events and f in self._events[event]: self._remove_listener(event, f) else: return None # f may return a coroutine, so we need to return that # result here so that emit can schedule it return f(*args, **kwargs) self._add_event_handler(event, f, g) return f if f is None: return _wrapper else: return _wrapper(f) def _remove_listener(self, event: str, f: Callable) -> None: """Naked unprotected removal.""" self._events[event].pop(f) if not len(self._events[event]): del self._events[event] def remove_listener(self, event: str, f: Callable) -> None: """Removes the function ``f`` from ``event``.""" with self._lock: self._remove_listener(event, f) def remove_all_listeners(self, event: Optional[str] = None) -> None: """Remove all listeners attached to ``event``. If ``event`` is ``None``, remove all listeners on all events. """ with self._lock: if event is not None: self._events[event] = OrderedDict() else: self._events = dict() def listeners(self, event: str) -> List[Callable]: """Returns a list of all listeners registered to the ``event``.""" return list(self._events.get(event, OrderedDict()).keys()) pyee-9.0.4/pyee/cls.py000066400000000000000000000055411417730205400145730ustar00rootroot00000000000000from dataclasses import dataclass from functools import wraps from typing import Callable, List, Type, TypeVar from pyee import EventEmitter @dataclass class Handler: event: str method: Callable class Handlers: def __init__(self): self._handlers: List[Handler] = [] def append(self, handler): self._handlers.append(handler) def __iter__(self): return iter(self._handlers) def reset(self): self._handlers = [] _handlers = Handlers() def on(event: str) -> Callable[[Callable], Callable]: """ Register an event handler on an evented class. See the ``evented`` class decorator for a full example. """ def decorator(method: Callable) -> Callable: _handlers.append(Handler(event=event, method=method)) return method return decorator def _bind(self, method): @wraps(method) def bound(*args, **kwargs): return method(self, *args, **kwargs) return bound Cls = TypeVar(name="Cls", bound=Type) def evented(cls: Cls) -> Cls: """ Configure an evented class. Evented classes are classes which use an EventEmitter to call instance methods during runtime. To achieve this without this helper, you would instantiate an ``EventEmitter`` in the ``__init__`` method and then call ``event_emitter.on`` for every method on ``self``. This decorator and the ``on`` function help make things look a little nicer by defining the event handler on the method in the class and then adding the ``__init__`` hook in a wrapper:: from pyee.cls import evented, on @evented class Evented: @on("event") def event_handler(self, *args, **kwargs): print(self, args, kwargs) evented_obj = Evented() evented_obj.event_emitter.emit( "event", "hello world", numbers=[1, 2, 3] ) The ``__init__`` wrapper will create a ``self.event_emitter: EventEmitter`` automatically but you can also define your own event_emitter inside your class's unwrapped ``__init__`` method. For example, to use this decorator with a ``TwistedEventEmitter``:: @evented class Evented: def __init__(self): self.event_emitter = TwistedEventEmitter() @on("event") async def event_handler(self, *args, **kwargs): await self.some_async_action(*args, **kwargs) """ handlers: List[Handler] = list(_handlers) _handlers.reset() og_init: Callable = cls.__init__ @wraps(cls.__init__) def init(self, *args, **kwargs): og_init(self, *args, **kwargs) if not hasattr(self, "event_emitter"): self.event_emitter = EventEmitter() for h in handlers: self.event_emitter.on(h.event, _bind(self, h.method)) cls.__init__ = init return cls pyee-9.0.4/pyee/executor.py000066400000000000000000000046371417730205400156550ustar00rootroot00000000000000# -*- coding: utf-8 -*- from concurrent.futures import Executor, Future, ThreadPoolExecutor from types import TracebackType from typing import Any, Callable, Dict, Optional, Tuple, Type from pyee.base import EventEmitter __all__ = ["ExecutorEventEmitter"] class ExecutorEventEmitter(EventEmitter): """An event emitter class which runs handlers in a ``concurrent.futures`` executor. By default, this class creates a default ``ThreadPoolExecutor``, but a custom executor may also be passed in explicitly to, for instance, use a ``ProcessPoolExecutor`` instead. This class runs all emitted events on the configured executor. Errors captured by the resulting Future are automatically emitted on the ``error`` event. This is unlike the EventEmitter, which have no error handling. The underlying executor may be shut down by calling the ``shutdown`` method. Alternately you can treat the event emitter as a context manager:: with ExecutorEventEmitter() as ee: # Underlying executor open @ee.on('data') def handler(data): print(data) ee.emit('event') # Underlying executor closed Since the function call is scheduled on an executor, emit is always non-blocking. No effort is made to ensure thread safety, beyond using an executor. """ def __init__(self, executor: Executor = None): super(ExecutorEventEmitter, self).__init__() if executor: self._executor: Executor = executor else: self._executor = ThreadPoolExecutor() def _emit_run( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ): future: Future = self._executor.submit(f, *args, **kwargs) @future.add_done_callback def _callback(f: Future) -> None: exc: Optional[BaseException] = f.exception() if isinstance(exc, Exception): self.emit("error", exc) elif exc is not None: raise exc def shutdown(self, wait: bool = True) -> None: """Call ``shutdown`` on the internal executor.""" self._executor.shutdown(wait=wait) def __enter__(self) -> "ExecutorEventEmitter": return self def __exit__( self, type: Type[Exception], value: Exception, traceback: TracebackType ) -> Optional[bool]: self.shutdown() pyee-9.0.4/pyee/py.typed000066400000000000000000000000001417730205400151200ustar00rootroot00000000000000pyee-9.0.4/pyee/trio.py000066400000000000000000000106501417730205400147640ustar00rootroot00000000000000# -*- coding: utf-8 -*- from contextlib import AbstractAsyncContextManager, asynccontextmanager from types import TracebackType from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, Tuple, Type import trio from pyee.base import EventEmitter, PyeeException __all__ = ["TrioEventEmitter"] Nursery = trio.Nursery class TrioEventEmitter(EventEmitter): """An event emitter class which can run trio tasks in a trio nursery. By default, this class will lazily create both a nursery manager (the object returned from ``trio.open_nursery()`` and a nursery (the object yielded by using the nursery manager as an async context manager). It is also possible to supply an existing nursery manager via the ``manager`` argument, or an existing nursery via the ``nursery`` argument. Instances of TrioEventEmitter are themselves async context managers, so that they may manage the lifecycle of the underlying trio nursery. For example, typical usage of this library may look something like this:: async with TrioEventEmitter() as ee: # Underlying nursery is instantiated and ready to go @ee.on('data') async def handler(data): print(data) ee.emit('event') # Underlying nursery and manager have been cleaned up Unlike the case with the EventEmitter, all exceptions raised by event handlers are automatically emitted on the ``error`` event. This is important for trio coroutines specifically but is also handled for synchronous functions for consistency. For trio coroutine event handlers, calling emit is non-blocking. In other words, you should not attempt to await emit; the coroutine is scheduled in a fire-and-forget fashion. """ def __init__( self, nursery: Nursery = None, manager: "AbstractAsyncContextManager[trio.Nursery]" = None, ): super(TrioEventEmitter, self).__init__() self._nursery: Optional[Nursery] = None self._manager: Optional["AbstractAsyncContextManager[trio.Nursery]"] = None if nursery: if manager: raise PyeeException( "You may either pass a nursery or a nursery manager " "but not both" ) self._nursery = nursery elif manager: self._manager = manager else: self._manager = trio.open_nursery() def _async_runner( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> Callable[[], Awaitable[None]]: async def runner() -> None: try: await f(*args, **kwargs) except Exception as exc: self.emit("error", exc) return runner def _emit_run( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> None: if not self._nursery: raise PyeeException("Uninitialized trio nursery") self._nursery.start_soon(self._async_runner(f, args, kwargs)) @asynccontextmanager async def context( self, ) -> AsyncGenerator["TrioEventEmitter", None]: """Returns an async contextmanager which manages the underlying nursery to the EventEmitter. The ``TrioEventEmitter``'s async context management methods are implemented using this function, but it may also be used directly for clarity. """ if self._nursery is not None: yield self elif self._manager is not None: async with self._manager as nursery: self._nursery = nursery yield self else: raise PyeeException("Uninitialized nursery or nursery manager") async def __aenter__(self) -> "TrioEventEmitter": self._context: Optional[ AbstractAsyncContextManager["TrioEventEmitter"] ] = self.context() return await self._context.__aenter__() async def __aexit__( self, type: Optional[Type[BaseException]], value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: if self._context is None: raise PyeeException("Attempting to exit uninitialized context") rv = await self._context.__aexit__(type, value, traceback) self._context = None self._nursery = None self._manager = None return rv pyee-9.0.4/pyee/twisted.py000066400000000000000000000056431417730205400155000ustar00rootroot00000000000000# -*- coding: utf-8 -*- from typing import Any, Callable, Dict, Tuple from twisted.internet.defer import Deferred, ensureDeferred from twisted.python.failure import Failure from pyee.base import EventEmitter, PyeeException try: from asyncio import iscoroutine except ImportError: iscoroutine = None __all__ = ["TwistedEventEmitter"] class TwistedEventEmitter(EventEmitter): """An event emitter class which can run twisted coroutines and handle returned Deferreds, in addition to synchronous blocking functions. For example:: @ee.on('event') @inlineCallbacks def async_handler(*args, **kwargs): yield returns_a_deferred() or:: @ee.on('event') async def async_handler(*args, **kwargs): await returns_a_deferred() When async handlers fail, Failures are first emitted on the ``failure`` event. If there are no ``failure`` handlers, the Failure's associated exception is then emitted on the ``error`` event. If there are no ``error`` handlers, the exception is raised. For consistency, when handlers raise errors synchronously, they're captured, wrapped in a Failure and treated as an async failure. This is unlike the behavior of EventEmitter, which have no special error handling. For twisted coroutine event handlers, calling emit is non-blocking. In other words, you do not have to await any results from emit, and the coroutine is scheduled in a fire-and-forget fashion. Similar behavior occurs for "sync" functions which return Deferreds. """ def __init__(self): super(TwistedEventEmitter, self).__init__() def _emit_run( self, f: Callable, args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> None: d = None try: result = f(*args, **kwargs) except Exception: self.emit("failure", Failure()) else: if iscoroutine and iscoroutine(result): d: Deferred[Any] = ensureDeferred(result) elif isinstance(result, Deferred): d = result else: return def errback(failure: Failure) -> None: if failure: self.emit("failure", failure) d.addErrback(errback) def _emit_handle_potential_error(self, event: str, error: Any) -> None: if event == "failure": if isinstance(error, Failure): try: error.raiseException() except Exception as exc: self.emit("error", exc) elif isinstance(error, Exception): self.emit("error", error) else: self.emit("error", PyeeException(f"Unexpected failure object: {error}")) else: (super(TwistedEventEmitter, self))._emit_handle_potential_error( event, error ) pyee-9.0.4/pyee/uplift.py000066400000000000000000000147231417730205400153170ustar00rootroot00000000000000# -*- coding: utf-8 -*- from functools import wraps from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar, Union import warnings from typing_extensions import Literal from pyee.base import EventEmitter UpliftingEventEmitter = TypeVar(name="UpliftingEventEmitter", bound=EventEmitter) EMIT_WRAPPERS: Dict[EventEmitter, Callable[[], None]] = dict() def unwrap(event_emitter: EventEmitter) -> None: """Unwrap an uplifted EventEmitter, returning it to its prior state.""" if event_emitter in EMIT_WRAPPERS: EMIT_WRAPPERS[event_emitter]() def _wrap( left: EventEmitter, right: EventEmitter, error_handler: Any, proxy_new_listener: bool, ) -> None: left_emit = left.emit left_unwrap: Optional[Callable[[], None]] = EMIT_WRAPPERS.get(left) @wraps(left_emit) def wrapped_emit(event: str, *args: Any, **kwargs: Any) -> bool: left_handled: bool = left._call_handlers(event, args, kwargs) # Do it for the right side if proxy_new_listener or event != "new_listener": right_handled = right._call_handlers(event, args, kwargs) else: right_handled = False handled = left_handled or right_handled # Use the error handling on ``error_handler`` (should either be # ``left`` or ``right``) if not handled: error_handler._emit_handle_potential_error(event, args[0] if args else None) return handled def _unwrap() -> None: warnings.warn( DeprecationWarning( "Patched ee.unwrap() is deprecated and will be removed in a " "future release. Use pyee.uplift.unwrap instead." ) ) unwrap(left) def unwrap_hook() -> None: left.emit = left_emit if left_unwrap: EMIT_WRAPPERS[left] = left_unwrap else: del EMIT_WRAPPERS[left] del left.unwrap # type: ignore left.emit = left_emit unwrap(right) left.emit = wrapped_emit EMIT_WRAPPERS[left] = unwrap_hook left.unwrap = _unwrap # type: ignore _PROXY_NEW_LISTENER_SETTINGS: Dict[str, Tuple[bool, bool]] = dict( forward=(False, True), backward=(True, False), both=(True, True), neither=(False, False), ) ErrorStrategy = Union[Literal["new"], Literal["underlying"], Literal["neither"]] ProxyStrategy = Union[ Literal["forward"], Literal["backward"], Literal["both"], Literal["neither"] ] def uplift( cls: Type[UpliftingEventEmitter], underlying: EventEmitter, error_handling: ErrorStrategy = "new", proxy_new_listener: ProxyStrategy = "forward", *args: Any, **kwargs: Any ) -> UpliftingEventEmitter: """A helper to create instances of an event emitter ``cls`` that inherits event behavior from an ``underlying`` event emitter instance. This is mostly helpful if you have a simple underlying event emitter that you don't have direct control over, but you want to use that event emitter in a new context - for example, you may want to ``uplift`` a ``EventEmitter`` supplied by a third party library into an ``AsyncIOEventEmitter`` so that you may register async event handlers in your ``asyncio`` app but still be able to receive events from the underlying event emitter and call the underlying event emitter's existing handlers. When called, ``uplift`` instantiates a new instance of ``cls``, passing along any unrecognized arguments, and overwrites the ``emit`` method on the ``underlying`` event emitter to also emit events on the new event emitter and vice versa. In both cases, they return whether the ``emit`` method was handled by either emitter. Execution order prefers the event emitter on which ``emit`` was called. The ``unwrap`` function may be called on either instance; this will unwrap both ``emit`` methods. The ``error_handling`` flag can be configured to control what happens to unhandled errors: - 'new': Error handling for the new event emitter is always used and the underlying library's non-event-based error handling is inert. - 'underlying': Error handling on the underlying event emitter is always used and the new event emitter can not implement non-event-based error handling. - 'neither': Error handling for the new event emitter is used if the handler was registered on the new event emitter, and vice versa. Tuning this option can be useful depending on how the underlying event emitter does error handling. The default is 'new'. The ``proxy_new_listener`` option can be configured to control how ``new_listener`` events are treated: - 'forward': ``new_listener`` events are propagated from the underlying - 'both': ``new_listener`` events are propagated as with other events. - 'neither': ``new_listener`` events are only fired on their respective event emitters. event emitter to the new event emitter but not vice versa. - 'backward': ``new_listener`` events are propagated from the new event emitter to the underlying event emitter, but not vice versa. Tuning this option can be useful depending on how the ``new_listener`` event is used by the underlying event emitter, if at all. The default is 'forward', since ``underlying`` may not know how to handle certain handlers, such as asyncio coroutines. Each event emitter tracks its own internal table of handlers. ``remove_listener``, ``remove_all_listeners`` and ``listeners`` all work independently. This means you will have to remember which event emitter an event handler was added to! Note that both the new event emitter returned by ``cls`` and the underlying event emitter should inherit from ``EventEmitter``, or at least implement the interface for the undocumented ``_call_handlers`` and ``_emit_handle_potential_error`` methods. """ ( new_proxy_new_listener, underlying_proxy_new_listener, ) = _PROXY_NEW_LISTENER_SETTINGS[proxy_new_listener] new: UpliftingEventEmitter = cls(*args, **kwargs) uplift_error_handlers: Dict[str, Tuple[EventEmitter, EventEmitter]] = dict( new=(new, new), underlying=(underlying, underlying), neither=(new, underlying) ) new_error_handler, underlying_error_handler = uplift_error_handlers[error_handling] _wrap(new, underlying, new_error_handler, new_proxy_new_listener) _wrap(underlying, new, underlying_error_handler, underlying_proxy_new_listener) return new pyee-9.0.4/pyproject.toml000066400000000000000000000001421417730205400154020ustar00rootroot00000000000000[tool.isort] profile = "appnexus" known_application = "pyee" [tool.pyright] include = ["python"] pyee-9.0.4/pytest.ini000066400000000000000000000000621417730205400145200ustar00rootroot00000000000000[pytest] addopts = --verbose -s testpaths = tests pyee-9.0.4/requirements.txt000066400000000000000000000000311417730205400157470ustar00rootroot00000000000000typing-extensions==4.0.1 pyee-9.0.4/requirements_dev.txt000066400000000000000000000004431417730205400166140ustar00rootroot00000000000000mock==4.0.2 flake8==3.8.3 flake8-black==0.2.3 pytest==6.2.5 pytest-asyncio==0.12.0; python_version >= '3.4' pytest-trio==0.6.0; python_version >= '3.7' trio==0.17.0; python_version > '3.6' twisted==20.3.0 Sphinx==3.2.1 black==21.7b0 isort==5.10.1 trio-typing==0.7.0 tox==3.20.0 twine==3.2.0 pyee-9.0.4/requirements_docs.txt000066400000000000000000000000611417730205400167620ustar00rootroot00000000000000-r requirements.txt -r requirements_dev.txt -e . pyee-9.0.4/setup.cfg000066400000000000000000000000631417730205400143110ustar00rootroot00000000000000[flake8] max-line-length = 88 extend-ignore = E203 pyee-9.0.4/setup.py000066400000000000000000000024541417730205400142100ustar00rootroot00000000000000# -*- coding: utf-8 -*- from os import path from setuptools import find_packages, setup README_rst = path.join(path.abspath(path.dirname(__file__)), "README.rst") with open(README_rst, "r") as f: long_description = f.read() setup( name="pyee", version="9.0.4", packages=find_packages(), include_package_data=True, description="A port of node.js's EventEmitter to python.", long_description=long_description, author="Josh Holbrook", author_email="josh.holbrook@gmail.com", url="https://github.com/jfhbrook/pyee", license="MIT", keywords=["events", "emitter", "node.js", "node", "eventemitter", "event_emitter"], install_requires=["typing-extensions"], tests_require=["twisted", "trio"], classifiers=[ "Programming Language :: Python", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Other/Nonlisted Topic", ], ) pyee-9.0.4/tests/000077500000000000000000000000001417730205400136335ustar00rootroot00000000000000pyee-9.0.4/tests/conftest.py000066400000000000000000000003531417730205400160330ustar00rootroot00000000000000# -*- coding: utf-8 -*- from sys import version_info as v collect_ignore = [] if not (v[0] >= 3 and v[1] >= 5): collect_ignore.append("test_async.py") if not (v[0] >= 3 and v[1] >= 7): collect_ignore.append("test_trio.py") pyee-9.0.4/tests/test_async.py000066400000000000000000000076101417730205400163650ustar00rootroot00000000000000# -*- coding: utf-8 -*- from asyncio import Future, wait_for import pytest import pytest_asyncio.plugin # noqa try: from asyncio.exceptions import TimeoutError # type: ignore except ImportError: from concurrent.futures import TimeoutError # type: ignore from mock import Mock from twisted.internet.defer import succeed from pyee import AsyncIOEventEmitter, TwistedEventEmitter class PyeeTestError(Exception): pass @pytest.mark.asyncio async def test_asyncio_emit(event_loop): """Test that AsyncIOEventEmitter can handle wrapping coroutines """ ee = AsyncIOEventEmitter(loop=event_loop) should_call = Future(loop=event_loop) @ee.on("event") async def event_handler(): should_call.set_result(True) ee.emit("event") result = await wait_for(should_call, 0.1) assert result is True @pytest.mark.asyncio async def test_asyncio_once_emit(event_loop): """Test that AsyncIOEventEmitter also wrap coroutines when using once """ ee = AsyncIOEventEmitter(loop=event_loop) should_call = Future(loop=event_loop) @ee.once("event") async def event_handler(): should_call.set_result(True) ee.emit("event") result = await wait_for(should_call, 0.1) assert result is True @pytest.mark.asyncio async def test_asyncio_error(event_loop): """Test that AsyncIOEventEmitter can handle errors when wrapping coroutines """ ee = AsyncIOEventEmitter(loop=event_loop) should_call = Future(loop=event_loop) @ee.on("event") async def event_handler(): raise PyeeTestError() @ee.on("error") def handle_error(exc): should_call.set_result(exc) ee.emit("event") result = await wait_for(should_call, 0.1) assert isinstance(result, PyeeTestError) @pytest.mark.asyncio async def test_asyncio_cancellation(event_loop): """Test that AsyncIOEventEmitter can handle Future cancellations""" cancel_me = Future(loop=event_loop) should_not_call = Future(loop=event_loop) ee = AsyncIOEventEmitter(loop=event_loop) @ee.on("event") async def event_handler(): cancel_me.cancel() @ee.on("error") def handle_error(exc): should_not_call.set_result(None) ee.emit("event") try: await wait_for(should_not_call, 0.1) except TimeoutError: pass else: raise PyeeTestError() @pytest.mark.asyncio async def test_sync_error(event_loop): """Test that regular functions have the same error handling as coroutines""" ee = AsyncIOEventEmitter(loop=event_loop) should_call = Future(loop=event_loop) @ee.on("event") def sync_handler(): raise PyeeTestError() @ee.on("error") def handle_error(exc): should_call.set_result(exc) ee.emit("event") result = await wait_for(should_call, 0.1) assert isinstance(result, PyeeTestError) def test_twisted_emit(): """Test that TwistedEventEmitter can handle wrapping coroutines """ ee = TwistedEventEmitter() should_call = Mock() @ee.on("event") async def event_handler(): _ = await succeed("yes!") should_call(True) ee.emit("event") should_call.assert_called_once() def test_twisted_once(): """Test that TwistedEventEmitter also wraps coroutines for once """ ee = TwistedEventEmitter() should_call = Mock() @ee.once("event") async def event_handler(): _ = await succeed("yes!") should_call(True) ee.emit("event") should_call.assert_called_once() def test_twisted_error(): """Test that TwistedEventEmitters handle Failures when wrapping coroutines.""" ee = TwistedEventEmitter() should_call = Mock() @ee.on("event") async def event_handler(): raise PyeeTestError() @ee.on("failure") def handle_error(e): should_call(e) ee.emit("event") should_call.assert_called_once() pyee-9.0.4/tests/test_cls.py000066400000000000000000000017411417730205400160300ustar00rootroot00000000000000# -*- coding: utf-8 -*- from mock import Mock import pytest from pyee import EventEmitter from pyee.cls import evented, on @evented class EventedFixture: def __init__(self): self.call_me = Mock() @on("event") def event_handler(self, *args, **kwargs): self.call_me(self, *args, **kwargs) _custom_event_emitter = EventEmitter() @evented class CustomEmitterFixture: def __init__(self): self.call_me = Mock() self.event_emitter = _custom_event_emitter @on("event") def event_handler(self, *args, **kwargs): self.call_me(self, *args, **kwargs) class InheritedFixture(EventedFixture): pass @pytest.mark.parametrize( "cls", [EventedFixture, CustomEmitterFixture, InheritedFixture] ) def test_evented_decorator(cls): inst = cls() inst.event_emitter.emit("event", "emitter is emitted!") inst.call_me.assert_called_once_with(inst, "emitter is emitted!") _custom_event_emitter.remove_all_listeners() pyee-9.0.4/tests/test_executor.py000066400000000000000000000023141417730205400171020ustar00rootroot00000000000000# -*- coding: utf-8 -*- from time import sleep from mock import Mock from pyee import ExecutorEventEmitter class PyeeTestError(Exception): pass def test_executor_emit(): """Test that ExecutorEventEmitters can emit events.""" with ExecutorEventEmitter() as ee: should_call = Mock() @ee.on("event") def event_handler(): should_call(True) ee.emit("event") sleep(0.1) should_call.assert_called_once() def test_executor_once(): """Test that ExecutorEventEmitters also emit events for once.""" with ExecutorEventEmitter() as ee: should_call = Mock() @ee.once("event") def event_handler(): should_call(True) ee.emit("event") sleep(0.1) should_call.assert_called_once() def test_executor_error(): """Test that ExecutorEventEmitters handle errors.""" with ExecutorEventEmitter() as ee: should_call = Mock() @ee.on("event") def event_handler(): raise PyeeTestError() @ee.on("error") def handle_error(e): should_call(e) ee.emit("event") sleep(0.1) should_call.assert_called_once() pyee-9.0.4/tests/test_sync.py000066400000000000000000000137061417730205400162270ustar00rootroot00000000000000# -*- coding: utf-8 -*- from collections import OrderedDict from mock import Mock from pytest import raises from pyee import EventEmitter class PyeeTestException(Exception): pass def test_emit_sync(): """Basic synchronous emission works""" call_me = Mock() ee = EventEmitter() @ee.on("event") def event_handler(data, **kwargs): call_me() assert data == "emitter is emitted!" assert ee.event_names() == {"event"} # Making sure data is passed propers ee.emit("event", "emitter is emitted!", error=False) call_me.assert_called_once() def test_emit_error(): """Errors raise with no event handler, otherwise emit on handler""" call_me = Mock() ee = EventEmitter() test_exception = PyeeTestException("lololol") with raises(PyeeTestException): ee.emit("error", test_exception) @ee.on("error") def on_error(exc): call_me() assert ee.event_names() == {"error"} # No longer raises and error instead return True indicating handled assert ee.emit("error", test_exception) is True call_me.assert_called_once() def test_emit_return(): """Emit returns True when handlers are registered on an event, and false otherwise. """ call_me = Mock() ee = EventEmitter() assert ee.event_names() == set() # make sure emitting without a callback returns False assert not ee.emit("data") # add a callback ee.on("data")(call_me) # should return True now assert ee.emit("data") def test_new_listener_event(): """The 'new_listener' event fires whenever a new listener is added.""" call_me = Mock() ee = EventEmitter() ee.on("new_listener", call_me) # Should fire new_listener event @ee.on("event") def event_handler(data): pass assert ee.event_names() == {"new_listener", "event"} call_me.assert_called_once_with("event", event_handler) def test_listener_removal(): """Removing listeners removes the correct listener from an event.""" ee = EventEmitter() # Some functions to pass to the EE def first(): return 1 ee.on("event", first) @ee.on("event") def second(): return 2 @ee.on("event") def third(): return 3 def fourth(): return 4 ee.on("event", fourth) assert ee.event_names() == {"event"} assert ee._events["event"] == OrderedDict( [(first, first), (second, second), (third, third), (fourth, fourth)] ) ee.remove_listener("event", second) assert ee._events["event"] == OrderedDict( [(first, first), (third, third), (fourth, fourth)] ) ee.remove_listener("event", first) assert ee._events["event"] == OrderedDict([(third, third), (fourth, fourth)]) ee.remove_all_listeners("event") assert "event" not in ee._events["event"] def test_listener_removal_on_emit(): """Test that a listener removed during an emit is called inside the current emit cycle. """ call_me = Mock() ee = EventEmitter() def should_remove(): ee.remove_listener("remove", call_me) ee.on("remove", should_remove) ee.on("remove", call_me) assert ee.event_names() == {"remove"} ee.emit("remove") call_me.assert_called_once() call_me.reset_mock() # Also test with the listeners added in the opposite order ee = EventEmitter() ee.on("remove", call_me) ee.on("remove", should_remove) assert ee.event_names() == {"remove"} ee.emit("remove") call_me.assert_called_once() def test_once(): """Test that `once()` method works propers.""" # very similar to "test_emit" but also makes sure that the event # gets removed afterwards call_me = Mock() ee = EventEmitter() def once_handler(data): assert data == "emitter is emitted!" call_me() # Tests to make sure that after event is emitted that it's gone. ee.once("event", once_handler) assert ee.event_names() == {"event"} ee.emit("event", "emitter is emitted!") call_me.assert_called_once() assert ee.event_names() == set() assert "event" not in ee._events def test_once_removal(): """Removal of once functions works""" ee = EventEmitter() def once_handler(data): pass handle = ee.once("event", once_handler) assert handle == once_handler assert ee.event_names() == {"event"} ee.remove_listener("event", handle) assert "event" not in ee._events assert ee.event_names() == set() def test_listeners(): """`listeners()` returns a copied list of listeners.""" call_me = Mock() ee = EventEmitter() @ee.on("event") def event_handler(): pass @ee.once("event") def once_handler(): pass listeners = ee.listeners("event") assert listeners[0] == event_handler assert listeners[1] == once_handler # listeners is a copy, you can't mutate the innards this way listeners[0] = call_me ee.emit("event") call_me.assert_not_called() def test_listeners_does_work_with_unknown_listeners(): """`listeners()` should not throw.""" ee = EventEmitter() listeners = ee.listeners("event") assert listeners == [] def test_properties_preserved(): """Test that the properties of decorated functions are preserved.""" call_me = Mock() call_me_also = Mock() ee = EventEmitter() @ee.on("always") def always_event_handler(): """An event handler.""" call_me() @ee.once("once") def once_event_handler(): """Another event handler.""" call_me_also() assert always_event_handler.__doc__ == "An event handler." assert once_event_handler.__doc__ == "Another event handler." always_event_handler() call_me.assert_called_once() once_event_handler() call_me_also.assert_called_once() call_me_also.reset_mock() # Calling the event handler directly doesn't clear the handler ee.emit("once") call_me_also.assert_called_once() pyee-9.0.4/tests/test_trio.py000066400000000000000000000046101417730205400162220ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pytest import pytest_trio.plugin # noqa import trio from pyee import TrioEventEmitter class PyeeTestError(Exception): pass @pytest.mark.trio async def test_trio_emit(): """Test that the trio event emitter can handle wrapping coroutines """ async with TrioEventEmitter() as ee: should_call = trio.Event() @ee.on("event") async def event_handler(): should_call.set() ee.emit("event") result = False with trio.move_on_after(0.1): await should_call.wait() result = True assert result @pytest.mark.trio async def test_trio_once_emit(): """Test that trio event emitters also wrap coroutines when using once """ async with TrioEventEmitter() as ee: should_call = trio.Event() @ee.once("event") async def event_handler(): should_call.set() ee.emit("event") result = False with trio.move_on_after(0.1): await should_call.wait() result = True assert result @pytest.mark.trio async def test_trio_error(): """Test that trio event emitters can handle errors when wrapping coroutines """ async with TrioEventEmitter() as ee: send, rcv = trio.open_memory_channel(1) @ee.on("event") async def event_handler(): raise PyeeTestError() @ee.on("error") async def handle_error(exc): async with send: await send.send(exc) ee.emit("event") result = None with trio.move_on_after(0.1): async with rcv: result = await rcv.__anext__() assert isinstance(result, PyeeTestError) @pytest.mark.trio async def test_sync_error(event_loop): """Test that regular functions have the same error handling as coroutines""" async with TrioEventEmitter() as ee: send, rcv = trio.open_memory_channel(1) @ee.on("event") def sync_handler(): raise PyeeTestError() @ee.on("error") async def handle_error(exc): async with send: await send.send(exc) ee.emit("event") result = None with trio.move_on_after(0.1): async with rcv: result = await rcv.__anext__() assert isinstance(result, PyeeTestError) pyee-9.0.4/tests/test_twisted.py000066400000000000000000000031271417730205400167320ustar00rootroot00000000000000# -*- coding: utf-8 -*- from mock import Mock from twisted.internet.defer import inlineCallbacks from twisted.python.failure import Failure from pyee import TwistedEventEmitter class PyeeTestError(Exception): pass def test_propagates_failure(): """Test that TwistedEventEmitters can propagate failures from twisted Deferreds """ ee = TwistedEventEmitter() should_call = Mock() @ee.on("event") @inlineCallbacks def event_handler(): yield Failure(PyeeTestError()) @ee.on("failure") def handle_failure(f): assert isinstance(f, Failure) should_call(f) ee.emit("event") should_call.assert_called_once() def test_propagates_sync_failure(): """Test that TwistedEventEmitters can propagate failures from twisted Deferreds """ ee = TwistedEventEmitter() should_call = Mock() @ee.on("event") def event_handler(): raise PyeeTestError() @ee.on("failure") def handle_failure(f): assert isinstance(f, Failure) should_call(f) ee.emit("event") should_call.assert_called_once() def test_propagates_exception(): """Test that TwistedEventEmitters propagate failures as exceptions to the error event when no failure handler """ ee = TwistedEventEmitter() should_call = Mock() @ee.on("event") @inlineCallbacks def event_handler(): yield Failure(PyeeTestError()) @ee.on("error") def handle_error(exc): assert isinstance(exc, Exception) should_call(exc) ee.emit("event") should_call.assert_called_once() pyee-9.0.4/tests/test_uplift.py000066400000000000000000000142441417730205400165540ustar00rootroot00000000000000# -*- coding: utf-8 -*- from mock import call, Mock import pytest from pyee import EventEmitter from pyee.uplift import unwrap, uplift class UpliftedEventEmitter(EventEmitter): pass def test_uplift_emit(): call_me = Mock() base_ee = EventEmitter() @base_ee.on("base_event") def base_handler(): call_me("base event on base emitter") @base_ee.on("shared_event") def shared_base_handler(): call_me("shared event on base emitter") uplifted_ee = uplift(UpliftedEventEmitter, base_ee) assert isinstance(uplifted_ee, UpliftedEventEmitter), "Returns an uplifted emitter" @uplifted_ee.on("uplifted_event") def uplifted_handler(): call_me("uplifted event on uplifted emitter") @uplifted_ee.on("shared_event") def shared_uplifted_handler(): call_me("shared event on uplifted emitter") # Events on uplifted proxy correctly assert uplifted_ee.emit("base_event") assert uplifted_ee.emit("shared_event") assert uplifted_ee.emit("uplifted_event") call_me.assert_has_calls( [ call("base event on base emitter"), call("shared event on uplifted emitter"), call("shared event on base emitter"), call("uplifted event on uplifted emitter"), ] ) call_me.reset_mock() # Events on underlying proxy correctly assert base_ee.emit("base_event") assert base_ee.emit("shared_event") assert base_ee.emit("uplifted_event") call_me.assert_has_calls( [ call("base event on base emitter"), call("shared event on base emitter"), call("shared event on uplifted emitter"), call("uplifted event on uplifted emitter"), ] ) call_me.reset_mock() # Quick check for unwrap unwrap(uplifted_ee) with pytest.raises(AttributeError): getattr(uplifted_ee, "unwrap") with pytest.raises(AttributeError): getattr(base_ee, "unwrap") assert not uplifted_ee.emit("base_event") assert uplifted_ee.emit("shared_event") assert uplifted_ee.emit("uplifted_event") assert base_ee.emit("base_event") assert base_ee.emit("shared_event") assert not base_ee.emit("uplifted_event") call_me.assert_has_calls( [ # No listener for base event on uplifted call("shared event on uplifted emitter"), call("uplifted event on uplifted emitter"), call("base event on base emitter"), call("shared event on base emitter") # No listener for uplifted event on uplifted ] ) @pytest.mark.parametrize("error_handling", ["new", "underlying", "neither"]) def test_exception_handling(error_handling): base_ee = EventEmitter() uplifted_ee = uplift(UpliftedEventEmitter, base_ee, error_handling=error_handling) # Exception handling always prefers uplifted base_error = Exception("base error") uplifted_error = Exception("uplifted error") # Hold my beer base_error_handler = Mock() base_ee._emit_handle_potential_error = base_error_handler # Hold my other beer uplifted_error_handler = Mock() uplifted_ee._emit_handle_potential_error = uplifted_error_handler base_ee.emit("error", base_error) uplifted_ee.emit("error", uplifted_error) if error_handling == "new": base_error_handler.assert_not_called() uplifted_error_handler.assert_has_calls( [call("error", base_error), call("error", uplifted_error)] ) elif error_handling == "underlying": base_error_handler.assert_has_calls( [call("error", base_error), call("error", uplifted_error)] ) uplifted_error_handler.assert_not_called() elif error_handling == "neither": base_error_handler.assert_called_once_with("error", base_error) uplifted_error_handler.assert_called_once_with("error", uplifted_error) else: raise Exception("unrecognized setting") @pytest.mark.parametrize( "proxy_new_listener", ["both", "neither", "forward", "backward"] ) def test_proxy_new_listener(proxy_new_listener): call_me = Mock() base_ee = EventEmitter() uplifted_ee = uplift( UpliftedEventEmitter, base_ee, proxy_new_listener=proxy_new_listener ) @base_ee.on("new_listener") def base_new_listener_handler(event, f): assert event in ("event", "new_listener") call_me("base new listener handler", f) @uplifted_ee.on("new_listener") def uplifted_new_listener_handler(event, f): assert event in ("event", "new_listener") call_me("uplifted new listener handler", f) def fresh_base_handler(): pass def fresh_uplifted_handler(): pass base_ee.on("event", fresh_base_handler) uplifted_ee.on("event", fresh_uplifted_handler) if proxy_new_listener == "both": call_me.assert_has_calls( [ call("base new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_uplifted_handler), call("base new listener handler", fresh_uplifted_handler), ] ) elif proxy_new_listener == "neither": call_me.assert_has_calls( [ call("base new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_uplifted_handler), ] ) elif proxy_new_listener == "forward": call_me.assert_has_calls( [ call("base new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_uplifted_handler), ] ) elif proxy_new_listener == "backward": call_me.assert_has_calls( [ call("base new listener handler", fresh_base_handler), call("uplifted new listener handler", fresh_uplifted_handler), call("base new listener handler", fresh_uplifted_handler), ] ) else: raise Exception("unrecognized proxy_new_listener") pyee-9.0.4/tox.ini000066400000000000000000000001711417730205400140030ustar00rootroot00000000000000[tox] envlist = py38,py39,py310 [testenv] deps = -rrequirements_test.txt commands = flake8 pytest ./tests pyee-9.0.4/typings/000077500000000000000000000000001417730205400141665ustar00rootroot00000000000000pyee-9.0.4/typings/twisted/000077500000000000000000000000001417730205400156515ustar00rootroot00000000000000pyee-9.0.4/typings/twisted/python/000077500000000000000000000000001417730205400171725ustar00rootroot00000000000000pyee-9.0.4/typings/twisted/python/failure.pyi000066400000000000000000000001421417730205400213410ustar00rootroot00000000000000class Failure(BaseException): value: Exception def raiseException() -> None: ...