Snap for 10453563 from 012cd22e74d4fc6cc96e553d9feb30018bffa7cc to mainline-art-release

Change-Id: I8c2149d5fee6dc3be332ba8370feff3285332576
diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml
new file mode 100644
index 0000000..f81ebab
--- /dev/null
+++ b/.github/workflows/qa.yaml
@@ -0,0 +1,34 @@
+name: 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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..84cde9e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.pyc
+docs/_build
+dist/*
+build/*
+MANIFEST
+README
+.cache
+.eggs
+.python-version
+pyee.egg-info/
+version.txt
+scratchpad.ipynb
+.tox/
+node_modules
+venv
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..6bf9a2d
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,12 @@
+version: 2
+
+sphinx:
+  configuration: docs/conf.py
+
+formats:
+  - pdf
+
+python:
+  version: "3.8"
+  install:
+    - requirements: requirements_docs.txt
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..33439fa
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["external_python_pyee_license"],
+}
+
+license {
+    name: "external_python_pyee_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-MIT",
+    ],
+    license_text: [
+        "LICENSE",
+    ],
+}
+
+python_library_host {
+    name: "pyee",
+    srcs: [
+        "pyee/*.py",
+    ],
+    libs: [
+        "typing_extensions",
+    ],
+}
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..7b2e750
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,240 @@
+2022/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
diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst
new file mode 100644
index 0000000..231b35e
--- /dev/null
+++ b/CONTRIBUTORS.rst
@@ -0,0 +1,16 @@
+General format is: contributor, github handle, email.
+
+Listed in no particular order:
+
+- Josh Holbrook @jfhbrook <josh.holbrook@gmail.com>
+- Gleicon Moraes @gleicon <gleicon@gmail.com>
+- Zack Do @doboy <doboy0@gmail.com>
+- @Zearin
+- René Kijewski @Kijewski
+- Gabe Appleton @gappleto97
+- Daniel M. Capella @polyzen <polyzen@archlinux.org>
+- Fabian Affolter @fabaff <mail@fabian-affolter.ch>
+- Anton Bolshakov @blshkv
+- Åke Forslund @forslund <ake.forslund@gmail.com>
+- Ivan Gretchka @leirons
+- Max Schmitt @mxschmitt
diff --git a/DEVELOPMENT.rst b/DEVELOPMENT.rst
new file mode 100644
index 0000000..b350f46
--- /dev/null
+++ b/DEVELOPMENT.rst
@@ -0,0 +1,123 @@
+Development 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 <https://semver.org>` 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 <https://readthedocs.org>`, log in, then go
+to `the pyee project page <https://readthedocs.org/projects/pyee/>` and build
+latest and stable.
+
+Announce on Twitter
+~~~~~~~~~~~~~~~~~~~
+
+It's not official, but I like to announce the release on Twitter.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..67dd129
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The 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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..91b0e6b
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,8 @@
+include LICENSE
+include README.rst
+include CHANGELOG.rst
+include CONTRIBUTORS.rst
+include DEVELOPMENT.rst
+include version.txt
+include pyee/py.typed
+recursive-include tests *.py
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..71c8682
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,18 @@
+name: "pyee"
+description:
+    "A rough port of Node.js's EventEmitter to Python with a few tricks of its "
+    "own."
+
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://pyee.readthedocs.io/"
+  }
+  url {
+    type: GIT
+    value: "https://github.com/jfhbrook/pyee"
+  }
+  version: "9.0.4"
+  last_upgrade_date { year: 2022 month: 7 day: 26 }
+  license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9eba7d8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+.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__
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..69cc2e5
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,4 @@
+boccongibod@google.com
+girardier@google.com
+licorne@google.com
+uael@google.com
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..a31a220
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,38 @@
+pyee
+====
+
+.. 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``.
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..011dc88
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,225 @@
+# 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 <target>' where <target> 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."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..546b2ca
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,343 @@
+#!/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.
+# "<project> v<release> 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 <link> 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
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..ccdfb81
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,63 @@
+pyee
+====
+
+pyee is a rough port of
+`node.js's EventEmitter <https://nodejs.org/api/events.html>`_. 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! <https://github.com/jfhbrook/pyee>`_
+* `These Very Docs on readthedocs.io <https://pyee.rtfd.io>`_
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+Changelog
+=========
+
+.. include:: ../CHANGELOG.rst
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 0000000..25a2c6c
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,14 @@
+name: 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 .
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..61df80a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,37 @@
+{
+  "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
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c16a298
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+  "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"
+  }
+}
diff --git a/pyee/__init__.py b/pyee/__init__.py
new file mode 100644
index 0000000..9a4dafb
--- /dev/null
+++ b/pyee/__init__.py
@@ -0,0 +1,138 @@
+# -*- 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
diff --git a/pyee/asyncio.py b/pyee/asyncio.py
new file mode 100644
index 0000000..433001f
--- /dev/null
+++ b/pyee/asyncio.py
@@ -0,0 +1,73 @@
+# -*- 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)
diff --git a/pyee/base.py b/pyee/base.py
new file mode 100644
index 0000000..85a6cf9
--- /dev/null
+++ b/pyee/base.py
@@ -0,0 +1,239 @@
+# -*- 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())
diff --git a/pyee/cls.py b/pyee/cls.py
new file mode 100644
index 0000000..21885b4
--- /dev/null
+++ b/pyee/cls.py
@@ -0,0 +1,112 @@
+from 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
diff --git a/pyee/executor.py b/pyee/executor.py
new file mode 100644
index 0000000..25df774
--- /dev/null
+++ b/pyee/executor.py
@@ -0,0 +1,79 @@
+# -*- 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()
diff --git a/pyee/py.typed b/pyee/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pyee/py.typed
diff --git a/pyee/trio.py b/pyee/trio.py
new file mode 100644
index 0000000..e79d457
--- /dev/null
+++ b/pyee/trio.py
@@ -0,0 +1,129 @@
+# -*- 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
diff --git a/pyee/twisted.py b/pyee/twisted.py
new file mode 100644
index 0000000..2b9d20b
--- /dev/null
+++ b/pyee/twisted.py
@@ -0,0 +1,93 @@
+# -*- 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
+            )
diff --git a/pyee/uplift.py b/pyee/uplift.py
new file mode 100644
index 0000000..aa5f55a
--- /dev/null
+++ b/pyee/uplift.py
@@ -0,0 +1,178 @@
+# -*- 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
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..59293c3
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,6 @@
+[tool.isort]
+profile = "appnexus"
+known_application = "pyee"
+
+[tool.pyright]
+include = ["python"]
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..edb4554
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+addopts = --verbose -s
+testpaths = tests
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0e4bc00
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+typing-extensions==4.0.1
diff --git a/requirements_dev.txt b/requirements_dev.txt
new file mode 100644
index 0000000..cabc838
--- /dev/null
+++ b/requirements_dev.txt
@@ -0,0 +1,14 @@
+mock==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==22.10.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
diff --git a/requirements_docs.txt b/requirements_docs.txt
new file mode 100644
index 0000000..4c17431
--- /dev/null
+++ b/requirements_docs.txt
@@ -0,0 +1,3 @@
+-r requirements.txt
+-r requirements_dev.txt
+-e .
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8dd399a
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 88
+extend-ignore = E203
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..bdbe45b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,40 @@
+# -*- 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",
+    ],
+)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..18b0633
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,11 @@
+# -*- 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")
diff --git a/tests/test_async.py b/tests/test_async.py
new file mode 100644
index 0000000..d503c51
--- /dev/null
+++ b/tests/test_async.py
@@ -0,0 +1,190 @@
+# -*- 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()
diff --git a/tests/test_cls.py b/tests/test_cls.py
new file mode 100644
index 0000000..d7ca3ec
--- /dev/null
+++ b/tests/test_cls.py
@@ -0,0 +1,47 @@
+# -*- 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()
diff --git a/tests/test_executor.py b/tests/test_executor.py
new file mode 100644
index 0000000..a7fef48
--- /dev/null
+++ b/tests/test_executor.py
@@ -0,0 +1,61 @@
+# -*- 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()
diff --git a/tests/test_sync.py b/tests/test_sync.py
new file mode 100644
index 0000000..a09bf00
--- /dev/null
+++ b/tests/test_sync.py
@@ -0,0 +1,280 @@
+# -*- 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()
diff --git a/tests/test_trio.py b/tests/test_trio.py
new file mode 100644
index 0000000..3877849
--- /dev/null
+++ b/tests/test_trio.py
@@ -0,0 +1,112 @@
+# -*- 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)
diff --git a/tests/test_twisted.py b/tests/test_twisted.py
new file mode 100644
index 0000000..6a667ed
--- /dev/null
+++ b/tests/test_twisted.py
@@ -0,0 +1,80 @@
+# -*- 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()
diff --git a/tests/test_uplift.py b/tests/test_uplift.py
new file mode 100644
index 0000000..69350e0
--- /dev/null
+++ b/tests/test_uplift.py
@@ -0,0 +1,201 @@
+# -*- 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")
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..86f7fef
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,9 @@
+[tox]
+envlist = py38,py39,py310
+
+[testenv]
+deps =
+    -rrequirements_test.txt  
+commands =
+    flake8
+    pytest ./tests
diff --git a/typings/twisted/python/failure.pyi b/typings/twisted/python/failure.pyi
new file mode 100644
index 0000000..dabec96
--- /dev/null
+++ b/typings/twisted/python/failure.pyi
@@ -0,0 +1,5 @@
+class Failure(BaseException):
+    value: Exception
+
+    def raiseException() -> None:
+        ...