├── .gitignore ├── .travis.yml ├── .venv └── pyvenv.cfg ├── AUTHORS ├── CHANGES ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py └── index.rst ├── events ├── __init__.py ├── events.py └── tests │ ├── __init__.py │ └── tests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.py[co] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | # SublimeText project files 31 | *.sublime-* 32 | 33 | #virtualenv 34 | Include 35 | Lib 36 | Scripts 37 | 38 | #OSX 39 | .Python 40 | .DS_Store 41 | 42 | #Sphinx 43 | _build 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | script: tox 5 | python: 6 | - 2.7 7 | - 3.4 8 | - 3.5 9 | - 3.6 10 | - 3.7 11 | - 3.8 12 | - pypy3 13 | install: 14 | - pip install tox-travis 15 | -------------------------------------------------------------------------------- /.venv/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /opt/homebrew/opt/python@3.11/bin 2 | include-system-site-packages = false 3 | version = 3.11.3 4 | executable = /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/bin/python3.11 5 | command = /opt/homebrew/opt/python@3.11/bin/python3.11 -m venv /Users/nicola/code/pyeve/events/.venv 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Eve is written and maintained by Nicola Iarocci and various contributors: 2 | 3 | Development Lead 4 | ```````````````` 5 | 6 | - Nicola Iarocci 7 | 8 | Patches and Suggestions 9 | ``````````````````````` 10 | 11 | - Anfernee Jervis 12 | - Cailean Parker 13 | - Eric Smith 14 | - Evan Klitzke 15 | - Lucas Fernando Nunes 16 | - Michael Kennedy 17 | - Thomas Hanssen Nornes 18 | - Øyvind Heddeland Instefjord 19 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Events Changelog 2 | ================ 3 | 4 | Here you can see the full list of changes between each Events release. 5 | 6 | In development 7 | -------------- 8 | 9 | Version 0.5 10 | ----------- 11 | 12 | Released on July 31, 2023 13 | 14 | - Added __getitem__ dunder to Events class. This allows the calling of events from strings, thus enabling dynamic events. See PR #25. 15 | 16 | Version 0.4 17 | ----------- 18 | 19 | Released on October 31, 2020 20 | 21 | - Add Python 3.7 and 3.8 to CI matrix. 22 | - Drop Python 2.6 and Python 3.3 and pypy from CI matrix. 23 | - Add section on unsubscribing to documentation (Michael Kennedy) 24 | - Fix Events.__len__ counting __events__ 25 | - Add event_slot_cls argument to Events to allow custom EventSlot class. 26 | 27 | Version 0.3 28 | ----------- 29 | 30 | Released on June 1, 2017. 31 | 32 | - Add Python 3.5 and 3.6 to CI. 33 | - Add support for defining event names in constructor. Closes #1 (Anfernee 34 | Jervis). 35 | 36 | Version 0.2.2 37 | ------------- 38 | 39 | Released on January 2, 2017. 40 | 41 | - Limit events to only trigger current subscribers (Thomas Hanssen Nornes) 42 | - Docs: fix (Evan Klitzke) 43 | 44 | Version 0.2.1 45 | ------------- 46 | 47 | Released on May 15 2014. 48 | 49 | - Python 3.4 support. 50 | - tox support. 51 | - Prevent overriding of '__' attributes (Petr Jašek). 52 | 53 | Version 0.2.0 54 | ------------- 55 | 56 | Released on September 22 2013. 57 | 58 | - Fixed a minor encoding issue which made installing on Windows/Python3 59 | impossible (Eric Smith). 60 | 61 | Version 0.1.0 62 | ------------- 63 | 64 | Released on March 13 2013. 65 | 66 | First public release. 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ################# 3 | 4 | Contributions are welcome! Not familiar with the codebase yet? No problem! 5 | There are many ways to contribute to open source projects: reporting bugs, 6 | helping with the documentation, spreading the word and of course, adding 7 | new features and patches. 8 | 9 | Getting Started 10 | --------------- 11 | #. Make sure you have a GitHub_ account. 12 | #. Open a `new issue`_, assuming one does not already exist. 13 | #. Clearly describe the issue including steps to reproduce when it is a bug. 14 | 15 | Making Changes 16 | -------------- 17 | * Fork_ the repository on GitHub. 18 | * Create a topic branch from where you want to base your work. 19 | * This is usually the ``master`` branch. 20 | * Please avoid working directly on ``master`` branch. 21 | * Make commits of logical units (if needed rebase your feature branch before 22 | submitting it). 23 | * Check for unnecessary whitespace with ``git diff --check`` before committing. 24 | * Make sure your commit messages are in the `proper format`_. 25 | * If your commit fixes an open issue, reference it in the commit message (#15). 26 | * Make sure your code comforms to PEP8_. 27 | * Make sure you have added the necessary tests for your changes. 28 | * Run all the tests to assure nothing else was accidentally broken. 29 | * Don't forget to add yourself to AUTHORS_. 30 | 31 | These guidelines also apply when helping with documentation (actually, for 32 | typos and minor additions you might choose to `fork and edit`_). 33 | 34 | Submitting Changes 35 | ------------------ 36 | * Push your changes to a topic branch in your fork of the repository. 37 | * Submit a `Pull Request`_. 38 | * Wait for maintainer feedback. 39 | 40 | First time contributor? 41 | ----------------------- 42 | It's alright. We've all been there. 43 | 44 | Dont' know where to start? 45 | -------------------------- 46 | There are usually several TODO comments scattered around the codebase, maybe 47 | check them out and see if you have ideas, or can help with them. Also, check 48 | the `open issues`_ in case there's something that sparks your interest. What 49 | about documentation? I suck at english so if you're fluent with it (or notice 50 | any error), why not help with that? In any case, other than GitHub help_ pages, 51 | you might want to check this excellent `Effective Guide to Pull Requests`_ 52 | 53 | .. _`the repository`: http://github.com/nicolaiarocci/cerberus 54 | .. _AUTHORS: https://github.com/nicolaiarocci/cerberus/blob/master/AUTHORS 55 | .. _`open issues`: https://github.com/nicolaiarocci/cerberus/issues 56 | .. _`new issue`: https://github.com/nicolaiarocci/cerberus/issues/new 57 | .. _GitHub: https://github.com/ 58 | .. _Fork: https://help.github.com/articles/fork-a-repo 59 | .. _`proper format`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 60 | .. _PEP8: http://www.python.org/dev/peps/pep-0008/ 61 | .. _help: https://help.github.com/ 62 | .. _`Effective Guide to Pull Requests`: http://codeinthehole.com/writing/pull-requests-and-other-good-practices-for-teams-using-github/ 63 | .. _`fork and edit`: https://github.com/blog/844-forking-with-the-edit-button 64 | .. _`Pull Request`: https://help.github.com/articles/creating-a-pull-request 65 | 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 by Nicola Iarocci and contributors. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Events 2 | ------ 3 | 4 | The C# language provides a handy way to declare, subscribe to and fire events. 5 | Technically, an event is a "slot" where callback functions (event handlers) can 6 | be attached to - a process referred to as subscribing to an event. Here is 7 | a handy package that encapsulates the core to event subscription and event 8 | firing and feels like a "natural" part of the language. 9 | 10 | :: 11 | 12 | >>> def something_changed(reason): 13 | ... print "something changed because %s" % reason 14 | 15 | >>> from events import Events 16 | >>> events = Events() 17 | >>> events.on_change += something_changed 18 | 19 | Multiple callback functions can subscribe to the same event. When the event is 20 | fired, all attached event handlers are invoked in sequence. To fire the event, 21 | perform a call on the slot: 22 | 23 | :: 24 | 25 | >>> events.on_change('it had to happen') 26 | 'something changed because it had to happen' 27 | 28 | By default, Events does not check if an event can be subscribed to and fired. 29 | You can predefine events by subclassing Events and listing them. Attempts to 30 | subscribe to or fire an undefined event will raise an EventsException. 31 | 32 | :: 33 | 34 | >>> class MyEvents(Events): 35 | ... __events__ = ('on_this', 'on_that', ) 36 | 37 | >>> events = MyEvents() 38 | 39 | # this will raise an EventsException as `on_change` is unknown to MyEvents: 40 | >>> events.on_change += something_changed 41 | 42 | You can also predefine events for a single Events instance by passing an 43 | iterator to the constructor. 44 | 45 | :: 46 | 47 | >>> events = Events(('on_this', 'on_that')) 48 | 49 | # this will raise an EventsException as `on_change` is unknown to events: 50 | >>> events.on_change += something_changed 51 | 52 | 53 | Unsubscribing 54 | ------------- 55 | There may come a time when you no longer want to be notified of an event. In 56 | this case, you unsubscribe in the natural counterpart to `+=` by using `-=`. 57 | 58 | :: 59 | 60 | # We no longer want to be notified, take us out of the event callback list 61 | >>> events.on_change -= something_changed 62 | 63 | 64 | You may also want to unsubscribe for memory management reasons. The `Events()` instance 65 | will hold a reference `something_changed`. If this is a member method of an object, 66 | and the lifetime of the `Events()` instance is greater than that object, it will keep 67 | it around longer than would be the normal case. 68 | 69 | Documentation 70 | ------------- 71 | Complete documentation is available at http://events.readthedocs.org 72 | 73 | Installing 74 | ---------- 75 | Events is on PyPI so all you need to do is: 76 | 77 | :: 78 | 79 | pip install events 80 | 81 | Testing 82 | ------- 83 | Just run: 84 | 85 | :: 86 | 87 | python setup.py test 88 | 89 | Or use tox to test the package under all supported Pythons: 2.7, 3.4+ 90 | 91 | Licensing 92 | ---------- 93 | Events is BSD licensed. See the LICENSE_ for details. 94 | 95 | Contributing 96 | ------------ 97 | Please see the `Contribution Guidelines`_. 98 | 99 | Attribution 100 | ----------- 101 | Based on the excellent recipe by `Zoran Isailovski`_, Copyright (c) 2005. 102 | 103 | .. _`Contribution Guidelines`: https://github.com/pyeve/events/blob/master/CONTRIBUTING.rst 104 | .. _LICENSE: https://github.com/pyeve/events/blob/master/LICENSE 105 | .. _`Zoran Isailovski`: http://code.activestate.com/recipes/410686/ 106 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Events.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Events.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Events" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Events" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Events documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Mar 14 17:32:20 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | #sys.path.append('../events/') 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #ys.path.insert(0, os.path.abspath('.')) 20 | sys.path.append(os.path.abspath('..')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Events' 45 | copyright = u'2014-2017, Nicola Iarocci' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The full version, including alpha/beta/rc tags. 52 | release = __import__('events').__version__ 53 | # The short X.Y version. 54 | version = release.split('-dev')[0] 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | #html_theme_path = [] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | #html_logo = None 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | #html_additional_pages = {} 140 | 141 | # If false, no module index is generated. 142 | #html_domain_indices = True 143 | 144 | # If false, no index is generated. 145 | #html_use_index = True 146 | 147 | # If true, the index is split into individual pages for each letter. 148 | #html_split_index = False 149 | 150 | # If true, links to the reST sources are added to the pages. 151 | #html_show_sourcelink = True 152 | 153 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 154 | #html_show_sphinx = True 155 | 156 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 157 | #html_show_copyright = True 158 | 159 | # If true, an OpenSearch description file will be output, and all pages will 160 | # contain a tag referring to it. The value of this option must be the 161 | # base URL from which the finished HTML is served. 162 | #html_use_opensearch = '' 163 | 164 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 165 | #html_file_suffix = None 166 | 167 | # Output file base name for HTML help builder. 168 | htmlhelp_basename = 'Eventsdoc' 169 | 170 | 171 | # -- Options for LaTeX output -------------------------------------------------- 172 | 173 | latex_elements = { 174 | # The paper size ('letterpaper' or 'a4paper'). 175 | #'papersize': 'letterpaper', 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | #'pointsize': '10pt', 179 | 180 | # Additional stuff for the LaTeX preamble. 181 | #'preamble': '', 182 | } 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'Events.tex', u'Events Documentation', 188 | u'Nicola Iarocci', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # If true, show page references after internal links. 200 | #latex_show_pagerefs = False 201 | 202 | # If true, show URL addresses after external links. 203 | #latex_show_urls = False 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'events', u'Events Documentation', 218 | [u'Nicola Iarocci'], 1) 219 | ] 220 | 221 | # If true, show URL addresses after external links. 222 | #man_show_urls = False 223 | 224 | 225 | # -- Options for Texinfo output ------------------------------------------------ 226 | 227 | # Grouping the document tree into Texinfo files. List of tuples 228 | # (source start file, target name, title, author, 229 | # dir menu entry, description, category) 230 | texinfo_documents = [ 231 | ('index', 'Events', u'Events Documentation', 232 | u'Nicola Iarocci', 'Events', 'One line description of project.', 233 | 'Miscellaneous'), 234 | ] 235 | 236 | # Documents to append as an appendix to all manuals. 237 | #texinfo_appendices = [] 238 | 239 | # If false, no module index is generated. 240 | #texinfo_domain_indices = True 241 | 242 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 243 | #texinfo_show_urls = 'footnote' 244 | 245 | 246 | # Example configuration for intersphinx: refer to the Python standard library. 247 | intersphinx_mapping = {'http://docs.python.org/': None} 248 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Events 2 | ~~~~~~ 3 | *Bringing the elegance of C# EventHandler to Python* 4 | 5 | The concept of events is heavily used in GUI libraries and is the foundation 6 | for most implementations of the MVC (Model, View, Controller) design pattern. 7 | Another prominent use of events is in communication protocol stacks, where 8 | lower protocol layers need to inform upper layers of incoming data and the 9 | like. Here is a handy class that encapsulates the core to event subscription 10 | and event firing and feels like a "natural" part of the language. 11 | 12 | The package has been tested under Python 2.6, 2.7, 3.3 and 3.4. 13 | 14 | Usage 15 | ===== 16 | The C# language provides a handy way to declare, subscribe to and fire 17 | events. Technically, an event is a "slot" where callback functions (event 18 | handlers) can be attached to - a process referred to as subscribing to an 19 | event. To subscribe to an event: :: 20 | 21 | >>> def something_changed(reason): 22 | ... print "something changed because %s" % reason 23 | ... 24 | 25 | >>> from events import Events 26 | >>> events = Events() 27 | >>> events.on_change += something_changed 28 | 29 | Multiple callback functions can subscribe to the same event. When the event is 30 | fired, all attached event handlers are invoked in sequence. To fire the event, 31 | perform a call on the slot: :: 32 | 33 | >>> events.on_change('it had to happen') 34 | something changed because it had to happen 35 | 36 | Usually, instances of :class:`~events.Events` will not hang around loosely like 37 | above, but will typically be embedded in model objects, like here: :: 38 | 39 | class MyModel(object): 40 | def __init__(self): 41 | self.events = Events() 42 | ... 43 | 44 | Similarly, view and controller objects will be the prime event subscribers: :: 45 | 46 | class MyModelView(SomeWidget): 47 | def __init__(self, model): 48 | ... 49 | self.model = model 50 | model.events.on_change += self.display_value 51 | ... 52 | 53 | def display_value(self): 54 | ... 55 | 56 | 57 | Unsubscribing 58 | ------------- 59 | 60 | There may come a time when you no longer want to be notified of an event. In this case, 61 | you unsubscribe in the natural counterpart to `+=` by using `-=`:: 62 | 63 | 64 | evts = Events() 65 | evts.on_change += something_changed 66 | 67 | # We no longer want to be notified, take us out of the event callback list 68 | evts.on_change -= something_changed 69 | 70 | 71 | You may also want to unsubscribe for memory management reasons. The `Events()` instance 72 | will hold a reference `something_changed`. If this is a member method of an object, 73 | and the lifetime of the `Events()` instance is greater than that object, it will keep 74 | it around longer than would be the normal case. 75 | 76 | 77 | Introspection 78 | ------------- 79 | The :class:`~events.Events` and :class:`~events._EventSlot` classes provide 80 | some introspection support. This is usefull for example for automatic event 81 | subscription based on method name patterns. :: 82 | 83 | >>> from events import Events 84 | >>> events = Events() 85 | >>> print events 86 | 87 | 88 | >>> def changed(): 89 | ... print "something changed" 90 | ... 91 | 92 | >>> def another_one(): 93 | ... print "something changed here too" 94 | ... 95 | 96 | >>> def deleted(): 97 | ... print "something got deleted!" 98 | ... 99 | 100 | >>> events.on_change += changed 101 | >>> events.on_change += another_one 102 | >>> events.on_delete += deleted 103 | 104 | >>> print len(events) 105 | 2 106 | 107 | >>> for event in events: 108 | ... print event.__name__ 109 | ... 110 | on_change 111 | on_delete 112 | 113 | >>> event = events.on_change 114 | >>> print event 115 | event 'on_change' 116 | 117 | >>> print len(event) 118 | 2 119 | 120 | >>> for handler in event: 121 | ... print handler.__name__ 122 | ... 123 | changed 124 | another_one 125 | 126 | >>> print event[0] 127 | 128 | 129 | >>> print event[0].__name__ 130 | changed 131 | 132 | >>> print len(events.on_delete) 133 | 1 134 | 135 | >>> events.on_change() 136 | something changed 137 | somethind changed here too 138 | 139 | >>> events.on_delete() 140 | something got deleted! 141 | 142 | 143 | Event names 144 | ----------- 145 | Note that by default :class:`~events.Events` does not check if an event that is 146 | being subscribed to can actually be fired, unless the class attribute 147 | :attr:`__events__` is defined. This can cause a problem if an event name is 148 | slightly misspelled. If this is an issue, subclass :class:`~events.Events` and 149 | list the possible events, like: :: 150 | 151 | class MyEvents(Events): 152 | __events__ = ('on_this', 'on_that', ) 153 | 154 | events = MyEvents() 155 | 156 | # this will raise an EventsException as `on_change` is unknown to MyEvents: 157 | events.on_change += changed 158 | 159 | You can also predefine events for a single :class:`~events.Events` instance by 160 | passing an iterator to the constructor. :: 161 | 162 | events = Events(('on_this', 'on_that')) 163 | 164 | # this will raise an EventsException as `on_change` is unknown to MyEvents: 165 | events.on_change += changed 166 | 167 | It is recommended to use the constructor method for one time use cases. For more 168 | complicated use cases, it is recommended to subclass :class:`~events.Events` 169 | and define :attr:`__events__`. 170 | 171 | You can also leverage both the constructor method and the :attr:`__events__` 172 | attribute to restrict events for specific instances: :: 173 | 174 | DatabaseEvents(Events): 175 | __events__ = ('insert', 'update', 'delete', 'select') 176 | 177 | audit_events = ('select') 178 | 179 | AppDatabaseEvents = DatabaseEvents() 180 | 181 | # only knows the 'select' event from DatabaseEvents 182 | AuditDatabaseEvents = DatabaseEvents(audit_events) 183 | 184 | 185 | 186 | Installing 187 | ========== 188 | Events is on PyPI so all you need to do is: :: 189 | 190 | pip install events 191 | 192 | Testing 193 | ======= 194 | Just run: :: 195 | 196 | python setup.py test 197 | 198 | The package has been tested under Python 2.6, Python 2.7 and Python 3.3. 199 | 200 | Source Code 201 | =========== 202 | Source code is available at GitHub_. 203 | 204 | Attribution 205 | =========== 206 | Based on the excellent recipe by `Zoran Isailovski`_, Copyright (c) 2005. 207 | 208 | Copyright Notice 209 | ================ 210 | This is an open source project by `Nicola Iarocci`_. See the original LICENSE_ 211 | for more informations. 212 | 213 | .. _LICENSE: https://github.com/pyeve/events/blob/master/LICENSE 214 | .. _`Zoran Isailovski`: http://code.activestate.com/recipes/410686/ 215 | .. _GitHub: https://github.com/pyeve/events 216 | .. _`Nicola Iarocci`: http://nicolaiarocci.com 217 | -------------------------------------------------------------------------------- /events/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .events import Events, EventsException 3 | 4 | __version__ = '0.5' 5 | 6 | __all__ = [ 7 | Events.__name__, 8 | EventsException.__name__, 9 | ] 10 | -------------------------------------------------------------------------------- /events/events.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Events 5 | ~~~~~~ 6 | 7 | Implements C#-Style Events. 8 | 9 | Derived from the original work by Zoran Isailovski: 10 | http://code.activestate.com/recipes/410686/ - Copyright (c) 2005 11 | 12 | :copyright: (c) 2014-2017 by Nicola Iarocci. 13 | :license: BSD, see LICENSE for more details. 14 | """ 15 | 16 | 17 | class _EventSlot: 18 | def __init__(self, name): 19 | self.targets = [] 20 | self.__name__ = name 21 | 22 | def __repr__(self): 23 | return "event '%s'" % self.__name__ 24 | 25 | def __call__(self, *a, **kw): 26 | for f in tuple(self.targets): 27 | f(*a, **kw) 28 | 29 | def __iadd__(self, f): 30 | self.targets.append(f) 31 | return self 32 | 33 | def __isub__(self, f): 34 | while f in self.targets: 35 | self.targets.remove(f) 36 | return self 37 | 38 | def __len__(self): 39 | return len(self.targets) 40 | 41 | def __iter__(self): 42 | def gen(): 43 | for target in self.targets: 44 | yield target 45 | return gen() 46 | 47 | def __getitem__(self, key): 48 | return self.targets[key] 49 | 50 | 51 | class EventsException(Exception): 52 | pass 53 | 54 | 55 | class Events: 56 | """ 57 | Encapsulates the core to event subscription and event firing, and feels 58 | like a "natural" part of the language. 59 | 60 | The class Events is there mainly for 3 reasons: 61 | 62 | - Events (Slots) are added automatically, so there is no need to 63 | declare/create them separately. This is great for prototyping. (Note 64 | that `__events__` is optional and should primarilly help detect 65 | misspelled event names.) 66 | - To provide (and encapsulate) some level of introspection. 67 | - To "steel the name" and hereby remove unneeded redundancy in a call 68 | like: 69 | 70 | xxx.OnChange = event('OnChange') 71 | """ 72 | def __init__(self, events=None, event_slot_cls=_EventSlot): 73 | self.__event_slot_cls__ = event_slot_cls 74 | 75 | if events is not None: 76 | 77 | try: 78 | for _ in events: 79 | break 80 | except: 81 | raise AttributeError("type object %s is not iterable" % 82 | (type(events))) 83 | else: 84 | self.__events__ = events 85 | 86 | def __getattr__(self, name): 87 | if name.startswith('__'): 88 | raise AttributeError("type object '%s' has no attribute '%s'" % 89 | (self.__class__.__name__, name)) 90 | 91 | if hasattr(self, '__events__'): 92 | if name not in self.__events__: 93 | raise EventsException("Event '%s' is not declared" % name) 94 | 95 | elif hasattr(self.__class__, '__events__'): 96 | if name not in self.__class__.__events__: 97 | raise EventsException("Event '%s' is not declared" % name) 98 | 99 | self.__dict__[name] = ev = self.__event_slot_cls__(name) 100 | return ev 101 | 102 | def __getitem__(self, item): 103 | return self.__dict__[item] 104 | 105 | def __repr__(self): 106 | return '<%s.%s object at %s>' % (self.__class__.__module__, 107 | self.__class__.__name__, 108 | hex(id(self))) 109 | 110 | __str__ = __repr__ 111 | 112 | def __len__(self): 113 | return len(list(self.__iter__())) 114 | 115 | def __iter__(self): 116 | def gen(dictitems=self.__dict__.items()): 117 | for attr, val in dictitems: 118 | if isinstance(val, self.__event_slot_cls__): 119 | yield val 120 | return gen() 121 | -------------------------------------------------------------------------------- /events/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/events/e8ec95bed91aa6f86d39a72cbcfb16a9dce819a4/events/tests/__init__.py -------------------------------------------------------------------------------- /events/tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | import events 5 | from events import Events, EventsException 6 | 7 | 8 | class TestBase(unittest.TestCase): 9 | def setUp(self): 10 | self.events = Events() 11 | 12 | def callback1(self): 13 | pass 14 | 15 | def callback2(self): 16 | pass 17 | 18 | def callback3(self): 19 | pass 20 | 21 | 22 | class TestEvents(TestBase): 23 | def test_getattr(self): 24 | class MyEvents(Events): 25 | __events__ = ('on_eventOne', ) 26 | 27 | try: 28 | MyEvents().on_eventNotOne += self.callback1 29 | except EventsException: 30 | pass 31 | else: 32 | self.fail("'EventsException' expected and not raised.") 33 | 34 | try: 35 | self.events.on_eventNotOne += self.callback1 36 | except: 37 | self.fail("Exception raised but not expected.") 38 | 39 | def test_len(self): 40 | # We want __events__ to be set to verify that it is not counted as 41 | # part of __len__. 42 | self.events = Events(events=("on_change", "on_get")) 43 | self.events.on_change += self.callback1 44 | self.events.on_get += self.callback2 45 | self.assertEqual(len(self.events), 2) 46 | 47 | def test_iter(self): 48 | self.events.on_change += self.callback1 49 | self.events.on_change += self.callback2 50 | self.events.on_edit += self.callback1 51 | i = 0 52 | for event in self.events: 53 | i += 1 54 | self.assertTrue(isinstance(event, events.events._EventSlot)) 55 | self.assertEqual(i, 2) 56 | 57 | def test_iter_custom_event_slot_cls(self): 58 | class CustomEventSlot(events.events._EventSlot): 59 | pass 60 | self.events = Events(event_slot_cls=CustomEventSlot) 61 | self.events.on_change += self.callback1 62 | self.events.on_change += self.callback2 63 | self.events.on_edit += self.callback1 64 | i = 0 65 | for event in self.events: 66 | i += 1 67 | self.assertTrue(isinstance(event, CustomEventSlot)) 68 | self.assertEqual(i, 2) 69 | 70 | def test_event_slot_cls_default(self): 71 | self.assertEqual( 72 | events.events._EventSlot, self.events.__event_slot_cls__) 73 | 74 | def test_event_slot_cls_custom(self): 75 | class CustomEventSlot(events.events._EventSlot): 76 | pass 77 | 78 | custom = Events(event_slot_cls=CustomEventSlot) 79 | self.assertEqual(CustomEventSlot, custom.__event_slot_cls__) 80 | 81 | 82 | class TestEventSlot(TestBase): 83 | def setUp(self): 84 | super(TestEventSlot, self).setUp() 85 | self.events.on_change += self.callback1 86 | self.events.on_change += self.callback2 87 | self.events.on_change += self.callback3 88 | self.events.on_edit += self.callback3 89 | 90 | def test_type(self): 91 | ev = self.events.on_change 92 | self.assertTrue(isinstance(ev, events.events._EventSlot)) 93 | self.assertEqual(ev.__name__, 'on_change') 94 | 95 | def test_len(self): 96 | self.assertEqual(len(self.events.on_change), 3) 97 | self.assertEqual(len(self.events.on_edit), 1) 98 | 99 | def test_repr(self): 100 | ev = self.events.on_change 101 | self.assertEqual(ev.__repr__(), "event 'on_change'") 102 | 103 | def test_iter(self): 104 | ev = self.events.on_change 105 | self.assertEqual(len(ev), 3) 106 | i = 0 107 | for target in ev: 108 | i += 1 109 | self.assertEqual(target.__name__, 'callback%d' % i) 110 | 111 | def test_getitem(self): 112 | ev = self.events.on_edit 113 | self.assertEqual(len(ev), 1) 114 | self.assertTrue(ev[0].__name__, 'callback3') 115 | try: 116 | ev[1] 117 | except IndexError: 118 | pass 119 | else: 120 | self.fail("IndexError expected.") 121 | self.assertIs(ev, self.events["on_edit"]) 122 | self.assertIsNot(ev, self.events["on_change"]) 123 | try: 124 | self.events["on_nonexistent_event"] 125 | except KeyError: 126 | pass 127 | else: 128 | self.fail("KeyError expected.") 129 | 130 | def test_isub(self): 131 | self.events.on_change -= self.callback1 132 | ev = self.events.on_change 133 | self.assertEqual(len(ev), 2) 134 | self.assertEqual(ev[0].__name__, 'callback2') 135 | self.assertEqual(ev[1].__name__, 'callback3') 136 | 137 | 138 | class TestInstanceEvents(TestBase): 139 | 140 | def test_getattr(self): 141 | 142 | MyEvents = Events(('on_eventOne', )) 143 | 144 | try: 145 | MyEvents.on_eventOne += self.callback1 146 | except: 147 | self.fail("Exception raised but not expected.") 148 | 149 | try: 150 | MyEvents.on_eventNotOne += self.callback1 151 | except EventsException: 152 | pass 153 | else: 154 | self.fail("'EventsException' expected and not raised.") 155 | 156 | try: 157 | self.events.on_eventNotOne += self.callback1 158 | except: 159 | self.fail("Exception raised but not expected.") 160 | 161 | def test_instance_restriction(self): 162 | 163 | class MyEvents(Events): 164 | __events__ = ('on_eventOne', 'on_eventTwo') 165 | 166 | MyRestrictedInstance = MyEvents(('on_everyTwo', )) 167 | 168 | try: 169 | MyRestrictedInstance.on_everyTwo += self.callback1 170 | except: 171 | self.fail("Exception raised but not expected.") 172 | 173 | try: 174 | MyRestrictedInstance.on_everyOne += self.callback1 175 | except: 176 | pass 177 | else: 178 | self.fail("'EventsException' expected and not raised.") 179 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | DESCRIPTION = ("Bringing the elegance of C# EventHandler to Python") 6 | VERSION = __import__('events').__version__ 7 | with open("README.rst") as f: 8 | LONG_DESCRIPTION = f.read() 9 | 10 | setup( 11 | name='Events', 12 | version=VERSION, 13 | description=DESCRIPTION, 14 | long_description=LONG_DESCRIPTION, 15 | author='Nicola Iarocci', 16 | author_email='nicola@nicolaiarocci.com', 17 | url='http://github.com/pyeve/events', 18 | license="BSD", 19 | platforms=["any"], 20 | packages=find_packages(), 21 | include_package_data=True, 22 | test_suite="events.tests", 23 | install_requires=[], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | #'Environment :: Web Environment', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.7', 37 | 'Programming Language :: Python :: 3.8', 38 | 'Topic :: Software Development', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py34,py35,py36,py37,py38,flake8,pypy3 3 | 4 | [testenv] 5 | commands=python setup.py test 6 | 7 | [testenv:flake8] 8 | deps=flake8 9 | basepython=python 10 | commands=flake8 --ignore=E731 events {posargs} 11 | --------------------------------------------------------------------------------