10 |
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.landscape.yml:
--------------------------------------------------------------------------------
1 | # doc-warnings: true
2 | test-warnings: false
3 | # strictness: veryhigh
4 | max-line-length: 120
5 | ignore-paths:
6 | - tools
7 | - docs
8 | - examples
9 | python-targets:
10 | - 3
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "3.3"
5 | - "3.4"
6 | - "3.5"
7 | - "3.6"
8 | - "3.7-dev"
9 |
10 | # command to install dependencies
11 | install:
12 | - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install typing; fi
13 | - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install typing enum34 pathlib weakrefmethod; fi
14 | # - if [[ $TRAVIS_PYTHON_VERSION == 'pypy3' ]]; then pip install typing enum34 pathlib weakrefmethod importlib; fi
15 | - pip install pytest-cov
16 | - pip install coveralls
17 | - pip install -e .
18 |
19 | # command to run tests
20 | script:
21 | - cd tests/suite
22 | - py.test --cov=pubsub --cov-report term-missing
23 |
24 | afer_success:
25 | - coveralls
26 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://badge.fury.io/py/Pypubsub.svg
2 | :target: https://badge.fury.io/py/Pypubsub
3 | .. image:: https://img.shields.io/travis/schollii/pypubsub.svg
4 | :target: https://img.shields.io/travis/schollii/pypubsub
5 |
6 | News
7 | ====
8 |
9 | - January 2019: pypubsub 4.0.3 released! (for those -- and only those -- who use Python >= 3).
10 | - March 2019: pypubsub 3.4.2 released (for those -- and only those -- who use Python 2.7.x).
11 |
12 | Overview
13 | ========
14 |
15 | Provides a publish-subscribe API to facilitate event-based or message-based
16 | architecture in a single-process application. It is pure Python and works on
17 | Python 3.3+. It is centered on the notion of a topic; senders publish
18 | messages of a given topic, and listeners subscribe to messages of a given
19 | topic, all inside the same process. The package also supports a variety of
20 | advanced features that facilitate debugging and maintaining topics
21 | and messages in larger desktop- or server-based applications.
22 |
23 | Install most recent stable with "pip install pypubsub".
24 |
25 | Useful links:
26 |
27 | - Project on PyPI: https://pypi.python.org/pypi/PyPubSub
28 | - The documentation for latest stable release is at
29 | http://pypubsub.readthedocs.io.
30 | - The documentation for latest code is at
31 | http://pypubsub.readthedocs.io/en/latest.
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README_WxPython.txt:
--------------------------------------------------------------------------------
1 | # this file gets copied to wx/lib/pubsub folder when release to wxPython
2 |
3 | For wxPython users who are using wx.lib.pubsub:
4 |
5 | Do not use wx.lib.pubsub: this package is an earlier version of PyPubSub that
6 | was copied into wxPython for legacy reasons. If you attempt to use wx.lib.pubsub
7 | in wxPython >= 4.0.4, you will get a deprecation message that you should install
8 | pubsub directly from PyPubSub.
9 |
10 | Note that PyPubSub does not have any external dependencies and can be
11 | used with PyQt, PyGTK, etc.
12 |
13 | There is a wxPython example in PyPubSub source distribution /examples folder,
14 | and wx/lib/pubsub/examples. The WxPython wiki also discusses usage of pubsub in
15 | wxPython, the latest docs are only maintained at pypubsub.readthedocs.org
16 |
17 | Oliver Schoenborn
18 | January 2019
19 |
--------------------------------------------------------------------------------
/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 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 | @echo " coverage to run coverage check of the documentation (if enabled)"
49 |
50 | clean:
51 | rm -rf $(BUILDDIR)/*
52 |
53 | html:
54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
55 | @echo
56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
57 |
58 | dirhtml:
59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
60 | @echo
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
62 |
63 | singlehtml:
64 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
65 | @echo
66 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
67 |
68 | pickle:
69 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
70 | @echo
71 | @echo "Build finished; now you can process the pickle files."
72 |
73 | json:
74 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
75 | @echo
76 | @echo "Build finished; now you can process the JSON files."
77 |
78 | htmlhelp:
79 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
80 | @echo
81 | @echo "Build finished; now you can run HTML Help Workshop with the" \
82 | ".hhp project file in $(BUILDDIR)/htmlhelp."
83 |
84 | qthelp:
85 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
86 | @echo
87 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
88 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
89 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyPubSub.qhcp"
90 | @echo "To view the help file:"
91 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyPubSub.qhc"
92 |
93 | devhelp:
94 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
95 | @echo
96 | @echo "Build finished."
97 | @echo "To view the help file:"
98 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyPubSub"
99 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyPubSub"
100 | @echo "# devhelp"
101 |
102 | epub:
103 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
104 | @echo
105 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
106 |
107 | latex:
108 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
109 | @echo
110 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
111 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
112 | "(use \`make latexpdf' here to do that automatically)."
113 |
114 | latexpdf:
115 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
116 | @echo "Running LaTeX files through pdflatex..."
117 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
118 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
119 |
120 | latexpdfja:
121 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
122 | @echo "Running LaTeX files through platex and dvipdfmx..."
123 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
124 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
125 |
126 | text:
127 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
128 | @echo
129 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
130 |
131 | man:
132 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
133 | @echo
134 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
135 |
136 | texinfo:
137 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
138 | @echo
139 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
140 | @echo "Run \`make' in that directory to run these through makeinfo" \
141 | "(use \`make info' here to do that automatically)."
142 |
143 | info:
144 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
145 | @echo "Running Texinfo files through makeinfo..."
146 | make -C $(BUILDDIR)/texinfo info
147 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
148 |
149 | gettext:
150 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
151 | @echo
152 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
153 |
154 | changes:
155 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
156 | @echo
157 | @echo "The overview file is in $(BUILDDIR)/changes."
158 |
159 | linkcheck:
160 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
161 | @echo
162 | @echo "Link check complete; look for any errors in the above output " \
163 | "or in $(BUILDDIR)/linkcheck/output.txt."
164 |
165 | doctest:
166 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
167 | @echo "Testing of doctests in the sources finished, look at the " \
168 | "results in $(BUILDDIR)/doctest/output.txt."
169 |
170 | coverage:
171 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
172 | @echo "Testing of coverage in the sources finished, look at the " \
173 | "results in $(BUILDDIR)/coverage/python.txt."
174 |
175 | xml:
176 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
177 | @echo
178 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
179 |
180 | pseudoxml:
181 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
182 | @echo
183 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
184 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | ---------
3 |
4 | High-level changelog. For details, consult the SVN logs.
5 |
6 | :4.0.3 (Jan 2019):
7 |
8 | * Cleanup for Python 3.7 (mostly add support for keyword-only args, use
9 | Python 3 inspect signature/Parameter and add tests for
10 |
11 | :4.0.0 (Dec 2016):
12 |
13 | * Verified support Python 3.5 and 3.6
14 | * Distribution via wheel
15 | * Abandon support for Python 2.x and easy_install; now requires Python >= 3.3
16 | * Abandon support for long-ago deprecated arg1 messaging protocol
17 | * Added currying of subscribed listener args
18 | * Significant speed improvement for message delivery
19 | * Use PEP 484 style of annotations throughout
20 | * Use enum instead of constants when practical
21 |
22 |
23 | :3.3.0 (Feb 2014):
24 |
25 | * cleanup low-level API: exception classes, moved some out of pub module that did not
26 | belong there (clutter), move couple modules; specifically:
27 |
28 | * Removed from pub (available on object returned from pub.getDefaultTopicMgr())
29 | * getOrCreateTopic -> pub.getDefaultTopicMgr().getOrCreateTopic
30 | * getTopic -> pub.getDefaultTopicMgr().getTopic
31 | * newTopic -> pub.getDefaultTopicMgr().newTopic
32 | * delTopic -> pub.getDefaultTopicMgr().delTopic
33 | * getAssociatedTopics -> pub.getDefaultTopicMgr().getTopics
34 | * getDefaultTopicTreeRoot -> pub.getDefaultTopicMgr().getRootAllTopics
35 | * Removed from pub (available from pubsub.core):
36 | * ITopicDefnProvider
37 | * Moved from pub into to pubsub.core.TopicDefnProvider class as classmethod:
38 | * registerTopicDefnProviderType
39 | * Renamed:
40 | * TopicNameInvalid -> TopicNameError
41 | * UndefinedTopic(RuntimeError) -> TopicNameError(ValueError)
42 | * UndefinedSubtopic(RuntimeError) -> TopicNameError(ValueError)
43 | * ListenerInadequate(TypeError) -> ListenerMismatchError(ValueError)
44 | * UnrecognizedImportFormat -> UnrecognizedSourceFormatError
45 | * ListenerSpecInvalid -> MessageDataSpecError
46 | * SenderMissingReqdArgs -> SenderMissingReqdMsgDataError
47 | * SenderUnknownOptArgs -> SenderUnknownMsgDataError
48 | * ListenerNotValidatable -> TopicDefnErrorcd
49 | * Changed;
50 | * Topic.isSendable -> hasMDS
51 | * TopicManager.??? -> isTopicInUse + hasTopicDefinition
52 |
53 | * completed the ref docs
54 | * support installation via pip
55 | * cleanup versioning metadata: use pubsub.__version__ instead of pub.PUBSUB_VERSION
56 | * support Python 3
57 | * add getListenersIter() to iterate over listeners without temp copy of listener list
58 | * add deprecation message when import setuparg1
59 | * new wxPubsubMonitor utility class
60 |
61 | :3.2.0 (sep 2013):
62 |
63 | - cleanup of docs
64 | - merged importTopicTree to addTopicDefnProvider
65 | - renamed pub.getDefaultRootAllTopics to pub.getDefaultTopicTreeRoot
66 | - removed pub.importTopicTree, use pub.addTopicDefnProvider(source, format)
67 | - renamed pub.exportTopicTree to pub.exportTopicTreeSpec
68 | - several minor bug fixes
69 | - incorporated some patches contributed by users: one for performance improvement when
70 | high-frequency of subscribers/messages; one for reading topic tree specification from
71 | XML rather than .py module
72 | - v1 and v2 APIs no longer supported
73 |
74 | :3.1.2 (2011):
75 |
76 | - added some docs
77 | - more configurable importTopicTree
78 | - using importTopicTree now allows to use the topic hierarchy as topic names instead of
79 | string, thereby enabling python editors to support PyPubSub-based development via
80 | code completion and sendMessage keyword arguments.
81 |
82 | :3.1.1b (2010):
83 |
84 | - cleanup docs
85 | - couple minor tweaks (for instance added pub.getMsgProtocol())
86 |
87 | :3.1.0b (2009):
88 |
89 | - Import/export of topic tree and its documentation using Python interpreter
90 | - Better support for evolving topic tree during application development,
91 | with "freezing" certain parts of tree
92 | - Helper functions to transition from *arg1* to *kwargs* messaging protocol
93 | - Improved error messages (in exceptions raised)
94 | - PyPubSub can be installed inside other packages and will not interfere with
95 | system-wide pubsub
96 | - pubsubconf module moved inside pubsub package so manual install easier
97 | - Support !**kwargs in listeners
98 | - Support for more than one pubusb notification handler
99 | - Multiple publisher engines in one application (for instance, in separate
100 | threads, or for different "domains" in a large application)
101 | - Upgraded docs
102 | - Bug fixes, cleanup
103 |
104 | :3.0 (2008):
105 |
106 | - Use keyword arguments in sendMessage
107 | - Support any kind of listener, not just those with one unnamed argument
108 | - Validate listeners at subscription time
109 | - Support "inheritance" of keyword arguments by subtopics during
110 | message sending (prevents a common bug which was to send data using
111 | wrong argument names).
112 | - Topic tree can be documented (including topic message arguments)
113 | - Support user-defined notification handling of certain events occuring in
114 | PyPubSub such as "subscribe", "sendMessage".
115 | - Support user-defined exception handling of exceptions raised by
116 | listeners
117 | - Proto-Documentation on own website using Sphinx
118 | - Separate regression testing folder for nose-based automated testing
119 | - Configuration module for choosing which PyPubSub API to use in application,
120 | useful for backwards compatibility
121 |
122 | :2.0 (2007):
123 |
124 | - more Pythonic API (new ``PublisherClass`` API, at module level
125 | so easier to call -- no need to know about singleton)
126 | - Support definition of topic tree via a python class, for increased
127 | rigor and documentability of topics
128 | - Topics are objects
129 |
130 | :1.0 (2005):
131 |
132 | - Given its own "home" as separate package from wxPython's ``wx.lib.pubsub``
133 | - Factored out weakmethod
134 | - Put on Cheese Shop
135 |
136 | :Pre 1.0:
137 |
138 | - Created by Rob Shecter in wxPython's ``wx.lib`` (early 2000?)
139 | - Weakmethod added by Oliver Schoenborn (2004)
140 | - Further development transfered to Schoenborn (2004)
141 |
142 |
--------------------------------------------------------------------------------
/docs/development/dev_index.rst:
--------------------------------------------------------------------------------
1 | Contribute
2 | ==========
3 |
4 | This page is intended for developers of (or contributing to) PyPubSub.
5 |
6 | .. contents:: In this section:
7 | :depth: 1
8 | :local:
9 |
10 |
11 | .. _label-contributing:
12 |
13 | Contributing
14 | ------------
15 |
16 | Contributions are welcome! There are many ways you could contribute:
17 |
18 | - bug fixes
19 | - new features
20 | - test results on different platforms
21 | - documentation
22 | - screencasts! (of applications using PyPubSub with output when user clicks)
23 | - example topic trees (using ``pubsub.utils.printTopicTree()`` in latest
24 | version, or ``print Publisher`` in versions 1)
25 | - other improvements
26 | - money!
27 |
28 | Please contact by posting on the forum pypubsub-dev forum (link in the
29 | :ref:`label-support` section) or via http://github/schollii/pypubsub.
30 |
31 |
32 | System Requirements
33 | -------------------
34 |
35 | In addition to the :ref:`label-install-reqs`, the following are required:
36 |
37 | - To run unit tests:
38 |
39 | - pytest
40 |
41 | - To generate the docs:
42 |
43 | - sphinx >= 1.4.8
44 | - In PyPubSub 3.3, which used an older version of sphinx, sphinx had to be patched as per post on
45 | sphinx-dev, but this no longer seems to be the required:
46 |
47 | .. literalinclude:: sphinx_patch1.txt
48 | .. literalinclude:: sphinx_patch2.txt
49 |
50 | - To change code: PyCharm is recommended (Community Edition is sufficient). Various
51 | build configurations are available via the PyPubSub project when loaded into PyCharm.
52 |
53 |
54 | Scripts Available
55 | -----------------
56 |
57 | *Unit Testing*:
58 | The test suite is most conveniently run from PyCharm via the "py.test in suite"
59 | build configuration. The tests can also be run automatically via pytest suite from
60 | the :file:`tests` folder.
61 |
62 | Once this passes using the project's default interpreter, a Terminal can be
63 | opened in PyCharm (or alternately a command shell from Windows), and from
64 | the PyPubSub root folder, run :command:`tox`. This will attempt to run the
65 | test suite in every 3.x version of Python, x>=3 (ie 3.3, 3.4, etc).
66 |
67 | After changes are committed to github, the Travis CI will automatically
68 | run the tests on a Linux platform, for all versions of Python supported
69 | by PyPubSub. The results will be at https://travis-ci.org/schollii/pypubsub/builds.
70 |
71 | There is also a buildbot maintained by Jerome Laheurte to test on additional
72 | \*nix flavors, including OSX. Test results can be viewed at
73 | https://jeromelaheurte.net/buildbot/pubsub/console.
74 |
75 | *Performance Test*:
76 | A small performance test is available in the :file:`tests` folder.
77 | It can be run from PyCharm via the perf build configuration. This will
78 | generate a new :file:`.pstats` file which can be analysed.
79 | The test can also be run directly from command shell via
80 | :command:`python perf.py 1000`. The test is meant to compare the impact
81 | of changes before/after and is designed to compare on results on the
82 | same system (hardwards, OS).
83 |
84 | *Documentation*:
85 | The documentation can be generated locally on Windows via the Gen Docs build
86 | configuration in PyCharm. Alternatively, it can be generated by running
87 | :command:`make html` from the :file:`docs` folder of source
88 | distribution.
89 |
90 | The documentation is automatically built and available online at
91 | http://pypubsub.readthedocs.io. The latest from master branch is at
92 | http://pypubsub.readthedocs.io/en/master/. The stable (released)
93 | documentation is at http://pypubsub.readthedocs.io/en/stable/.
94 |
95 |
96 | Releases
97 | --------
98 |
99 | PyPubSub uses the latest stable Python packaging and distribution tools:
100 | wheel, twine, and pypi.
101 |
102 | Generating a new release involves the following sequence of steps:
103 |
104 | - Verify that tox, sphinx, wheel, twine, and setuptools are installed.
105 | - Ensure that pytest suite runs 100%, and that the examples run without error in
106 | examples/ folder (one of the examples requires wxPython -- install latest stable)
107 | - Ensure that `tox` (run from pypubsub root folder) runs to completion without errors or
108 | warnings on all versions of Python (3.x)
109 | - Update version number via a search-replace in the `Version Change` scope of PyCharm:
110 |
111 | - src/pubsub/__init__.py: version
112 | - docs/changelog.rst
113 | - src/pubsub/RELEASE_NOTES.txt
114 | - README.rst
115 |
116 | - Add section at top of `docs/changelog.rst` with details of what changed (audience: pypubsub developers)
117 | - Update `src/pubsub/RELEASE_NOTES.txt` (audience: pypubsub end-users) to have high-level summary
118 | of changes for this release, handling incompatibilities, etc
119 | - Update the setup.py classifiers (such as adding a new version of Python supported)
120 | - In docs folder:
121 |
122 | - Update index.rst and docs/installation.rst
123 | - Regenerate HTML docs via `make`, confirm ok (no warnings etc)
124 |
125 | Persist to server:
126 |
127 | - Commit and push to remote master repository
128 | - Confirm that travis CI all pass
129 |
130 | Distribute:
131 |
132 | - Clean out the `dist/` folder
133 | - Generate the source and wheel distributions: `python setup.py bdist_wheel sdist`
134 | - Upload to PyPI: `twine upload dist/\*`
135 | - Verify new release info and links on https://pypi.python.org
136 | - Create new branch (tag) in github
137 | - Verify documentation updated on pypubsub.readthedocs.io
138 | - Confirm installation will work: attempt to install locally via PyPI, then import
139 | from Python shell and verify version
140 |
141 |
142 | Py2Exe and cx_Freeze
143 | --------------------
144 |
145 | For packaging py2exe or cx_Freeze, see (possibly out of date):
146 |
147 | .. toctree::
148 |
149 | py2exe.rst
150 |
--------------------------------------------------------------------------------
/docs/development/py2exe.rst:
--------------------------------------------------------------------------------
1 | Packaging with py2exe and cxFreeze
2 | ==================================
3 |
4 | In this section we will see how to package applications that use
5 | PyPubSub, with `py2exe`_ and `cx_Freeze`_ packaging tools.
6 |
7 | Introduction
8 | ------------
9 |
10 | Packaging tools such as py2exe and cx_Freeze determine the dependencies
11 | that have to be included in a package by recursively finding the modules
12 | from the import statements used. Recursive finding of modules from the
13 | import statements uses straight forward approach i.e., if the python
14 | code dynamically imports certain modules by modifying the ``sys.path`` at
15 | runtime or if the code uses ``__import__`` statements, those modules
16 | are likely to be left out by the packaging tool. This can be a problem
17 | for some packaged applications.
18 |
19 |
20 | Packaging modules that use PyPubSub
21 | -----------------------------------
22 |
23 | PyPubSub supports two different messaging protocols namely ``args1`` and
24 | ``kwargs``; choosing and switching between these protocols is done by
25 | modifying the module path dynamically. This can result in import error
26 | like this at runtime::
27 |
28 | from listenerimpl import Listener, ListenerValidator
29 | ImportError: No module named listenerimpl
30 |
31 | In the following sections we show an example script that uses
32 | PyPubSub and discuss the setup script to package it using *py2exe* or
33 | *cx_Freeze* packaging tools.
34 |
35 | .. _py2exe: http://www.py2exe.org
36 | .. _cx_Freeze: http://cx-freeze.sourceforge.net
37 |
38 | Example
39 | -------
40 |
41 | Consider a sample application which has a single file named say
42 | ``testpubsub.py``
43 |
44 | .. literalinclude:: testpubsub.py
45 |
46 | To package this with *py2exe* and *cx_Freeze* you
47 | write a conventional ``setup.py`` module, but with extra options that
48 | the packaging tool uses to create the final distribution.
49 |
50 | Setup file using py2exe
51 | -----------------------
52 |
53 | The ``setup.py`` for this would look something like this
54 |
55 | .. literalinclude:: setup-py2exe.py
56 |
57 | The line ``'packages': 'encodings, pubsub'`` explicitly
58 | tells *py2exe* to include ``pubsub`` as a package so that the entire pubsub
59 | folder (from the installation location) including its sub packages are
60 | included for packaging. As the package has the entire list of python
61 | modules under pubsub, runtime protocol selection is now possible in
62 | the generated ``exe`` file.
63 |
64 | To build, run::
65 |
66 | python setup.py py2exe
67 |
68 | which will produce a dist folder containing ``testpubsub.exe`` and other
69 | DLLs and files required to run the application. Interestingly, py2exe
70 | command complains about modules that *appear* to be missing:
71 |
72 | The following modules appear to be missing
73 | ['callables', 'core', 'core.notificationmgr', ... , 'topicu', 'validatedefnargs']
74 |
75 | however, the application runs fine.
76 |
77 | Setup file using cx_Freeze
78 | --------------------------
79 |
80 | The ``setup.py`` for this would look something like this
81 |
82 | .. literalinclude:: setup-cxfreeze.py
83 |
84 | To build, run::
85 |
86 | python setup.py build
87 |
88 | We can safely ignore the missing modules warning in the build log::
89 |
90 | Missing modules:
91 | ? core.publisher imported from pubsub.pub
92 | ? listenerimpl imported from pubsub.core.listener
93 | ? publishermixin imported from pubsub.core.topicobj
94 | ? topicargspecimpl imported from pubsub.core.topicargspec
95 | ? topicmgrimpl imported from pubsub.core.topicmgr
96 |
97 |
98 |
--------------------------------------------------------------------------------
/docs/development/setup-cxfreeze.py:
--------------------------------------------------------------------------------
1 | from cx_Freeze import setup, Executable as cxExecutable
2 | import platform
3 |
4 | if platform.system() == 'Windows':
5 | # base must be set on Windows to either console or gui app
6 | # testpubsub is currently a console application
7 | # base = 'Win32GUI'
8 | base = 'Console'
9 | else:
10 | base = None
11 |
12 | opts = { 'compressed' : True,
13 | 'create_shared_zip' : False,
14 | }
15 |
16 | WIN_Target = cxExecutable(
17 | script='testpubsub.py',
18 | base=base,
19 | targetName='testpubsub.exe',
20 | compress=True,
21 | appendScriptToLibrary=False,
22 | appendScriptToExe=True
23 | )
24 |
25 | setup(
26 | name='TestPubSub',
27 | description="Script to test pubsub for packaging with cxfreeze",
28 | version='0.1',
29 |
30 | options={'build_exe' : opts},
31 | executables=[WIN_Target]
32 | )
33 |
34 |
--------------------------------------------------------------------------------
/docs/development/setup-py2exe.py:
--------------------------------------------------------------------------------
1 | """
2 | File based on a contribution from Josh Immanuel. Use via
3 |
4 | python setup-py2exe.py py2exe
5 |
6 | which will create a dist folder containing the .exe, the python DLL, and a
7 | few other DLL deemed by py2exe to be critical to the application execution.
8 |
9 | The contents of the dist folder should then be packaged using a tool such
10 | as NSIS or Inno Setup. The py2exe page has an example for NSIS.
11 | """
12 |
13 | from distutils.core import setup
14 |
15 | import py2exe
16 |
17 | setup (
18 | name='TestPubSub',
19 | description="Script to test pubsub for packaging",
20 | version="0.1",
21 |
22 | console=[{'script': 'testpubsub.py'}],
23 | options={ 'py2exe': {
24 | 'packages': 'encodings, pubsub',
25 | 'includes': None}
26 | },
27 | )
28 |
--------------------------------------------------------------------------------
/docs/development/sphinx_patch1.txt:
--------------------------------------------------------------------------------
1 | --- C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg_orig/sphinx/environment.py Thu Mar 18 09:59:23 2010
2 | +++ C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg/sphinx/environment.py Thu Mar 18 09:57:40 2010
3 | @@ -933,12 +933,12 @@
4 | node['refuri'] = node['anchorname'] or '#'
5 | return toc
6 |
7 | - def get_toctree_for(self, docname, builder, collapse):
8 | + def get_toctree_for(self, docname, builder, **tmplKw):
9 | """Return the global TOC nodetree."""
10 | doctree = self.get_doctree(self.config.master_doc)
11 | for toctreenode in doctree.traverse(addnodes.toctree):
12 | result = self.resolve_toctree(docname, builder, toctreenode,
13 | - prune=True, collapse=collapse)
14 | + prune=True, **tmplKw)
15 | if result is not None:
16 | return result
17 |
18 |
--------------------------------------------------------------------------------
/docs/development/sphinx_patch2.txt:
--------------------------------------------------------------------------------
1 | --- C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg_orig/sphinx/builders/html.py Thu Mar 18 09:59:25 2010
2 | +++ C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg/sphinx/builders/html.py Thu Mar 18 09:55:40 2010
3 | @@ -623,9 +623,9 @@
4 | if self.indexer is not None and title:
5 | self.indexer.feed(pagename, title, doctree)
6 |
7 | - def _get_local_toctree(self, docname, collapse=True):
8 | + def _get_local_toctree(self, docname, **tmplKw):
9 | return self.render_partial(self.env.get_toctree_for(
10 | - docname, self, collapse))['fragment']
11 | + docname, self, **tmplKw))['fragment']
12 |
13 | def get_outfilename(self, pagename):
14 | return path.join(self.outdir, os_path(pagename) + self.out_suffix)
15 |
--------------------------------------------------------------------------------
/docs/development/testpubsub.py:
--------------------------------------------------------------------------------
1 | from pubsub import pub
2 |
3 | def listener1(msg):
4 | print "The listener received the message : %s" % (msg, )
5 |
6 | pub.subscribe(listener1, 'test.pubsub')
7 |
8 | def sender():
9 | pub.sendMessage('test.pubsub', msg="Hola! this is a test message")
10 |
11 | if __name__ == "__main__":
12 | sender()
13 |
14 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. PyPubSub documentation master file, created by
2 | sphinx-quickstart on Mon May 20 21:24:10 2013.
3 |
4 | Welcome to PyPubSub's Home Page!
5 | ================================
6 |
7 | This is the documentation for the PyPubSub project. This Python project defines
8 | a package called 'pypubsub' which provides a publish-subscribe API to facilitate
9 |
10 | 1. event-based programming
11 | 2. decoupling an application's in-memory components
12 |
13 | PyPubSub provides the infrastructure for using the Observer pattern in your
14 | single-process application. It is pure Python and works on Python 3.3+.
15 |
16 | Using the Observer pattern in your single-process application can dramatically
17 | simplify its design and improve testability.
18 | The Observer allows code to observe "messages", without knowing anything about the source of
19 | the message (which Python object or function), and in turn allows code to emit messages
20 | without any regard for which code will receive the message (it may not be received at all),
21 | what the receiving code will do with the message, etc.
22 | Basically the Observer pattern is like a radio broadcast, it is a one-way message sent,
23 | the only contract is in the message content: the receiver/listener must have the ability
24 | to decode the message.
25 |
26 | A classic example where PyPubSub could be useful: a GUI application. How do components like
27 | views and dialogs communicate their changes to one another? Without a publish-subscribe
28 | mechanism, the code can become a real spaghetti.
29 |
30 | PyPubSub makes it easy for your code to emit messages, and other code, in the same process,
31 | to receive those messages. PyPubSub takes care of the plumbing.
32 |
33 | The Publish-Subscribe API provided by PyPubSub has the following characteristics:
34 |
35 | 1. Message Sender: The sender of a PyPubSub message is the ccode that calls pub.sendMessage().
36 | 2. Message Topic:
37 | a. Every message is specific to a "topic", defined as a string name;
38 | b. Topics form a hierarchy. A parent topic is more generic than a child topic.
39 | 3. Message Data: any keyword arguments used by the sender, pub.sendMessage(topic, \**data);
40 |
41 | a. A topic may have no associated message data, or may have any mixture of required
42 | and optional data; this is known as its Message Data Specification (MDS);
43 | b. The MDS of a child topic cannot be more restrictive than that of a parent topic;
44 | c. Once the MDS is set for a topic, it never changes during the runtime of an application.
45 |
46 | 4. Message Listener: All message listeners are callables that get registered with PyPubSub
47 | in order to receive messages of a given topic, and must have a signature that is
48 | compatible with the topic's MDS.
49 | 5. Message Delivery:
50 |
51 | a. Messages sent will be delivered to all registered listeners of a given topic; this
52 | includes listeners of the topic, parent topic, etc. Hence the root of all topics
53 | (called ALL_TOPICS) receives all messages.
54 | b. Sequence of delivery is unspecified and can change at any time. This is fundamental
55 | to the Observer pattern, and your application's listeners must be designed to not depend
56 | on the order in which they receive a given message.
57 | c. Messages are delivered synchronously: a listener must return or throw an exception
58 | before the message is delivered to the next listener.
59 | d. A listener that raises an exception does not prevent remaining listeners from
60 | receiving the message.
61 | e. A message sent will be delivered to all registered listeners of the specified topic
62 | before control is returned to the sender.
63 |
64 | 6. Message Immutability: message contents must be left unchanged by listeners, but PyPubSub
65 | does not verify this.
66 | 7. Message Direction: a message is one-way from sender to set-of-listeners; PyPubSub does not
67 | support "answering" with a response from each listener to the sender. This could, of course,
68 | be achieved by having the sender include a callback as message data, and each listener
69 | calling that callback with agreed-upon data, but this (typically) increases coupling.
70 | 8. Message Source: PyPubSub does not provide any information to the listeners regarding the
71 | origin (aka source, or provenance) of a message. The sender could, of course, include such
72 | information with the message data, but this is *not* recommended as it defeats the purpose
73 | of the Observer pattern.
74 |
75 | Here is a schematic representation of the role of PyPubSub during message sending and delivery:
76 |
77 | .. image:: pubsub_concept.png
78 | :alt: Sketch showing how PyPubSub fits into a Python application
79 | :align: center
80 | :width: 450px
81 |
82 | ..
83 |
84 | PyPybSub was originally written by Robb Shecter as wx.lib.pubsub in wxPython 2.x, sometime
85 | around y2k. Robb and the wxPython author, Robin Dunn, allowed me to take over the package around
86 | 2003, and I moved it out into a standalone package (no dependencies on wxPython) around 2006
87 | with their approval. I hosted the code on SourceForget.net for about 10 years, then moved it to
88 | github sometime in 2016. The code is very mature and stable. See :ref:`label-history` for
89 | details on its history and :ref:`label-roadmap` for possible future work.
90 |
91 | Contents:
92 |
93 | .. toctree::
94 | :maxdepth: 2
95 |
96 | about
97 | installation
98 | usage/index
99 | development/dev_index
100 |
101 |
102 | Indices and tables
103 | ==================
104 |
105 | * :ref:`genindex`
106 | * :ref:`modindex`
107 | * :ref:`search`
108 |
109 |
--------------------------------------------------------------------------------
/docs/index_redirect.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | Install
2 | =======
3 |
4 | .. contents:: In this section:
5 | :depth: 1
6 | :local:
7 |
8 | .. _label-install-reqs:
9 |
10 | System Requirements
11 | -------------------
12 |
13 | Requires Python >= 3.3. It has been tested on Windows 7 and various
14 | flavors of \*nix (such as Fedora, OpenSuse, Ubuntu and OSX). For Python < 3.5, there are
15 | additional requirements:
16 |
17 | - Python 3.4: requires the "typing" package (from PyPI)
18 | - Python 3.3: requires the same as 3.4 + enum34, pathlib and weakrefmethod (all on PyPI)
19 |
20 | Please post on pypubsub forum (see :ref:`label-support` section) if you have
21 | successfully used PyPubSub with other combinations of Python and Platform.
22 |
23 | Many thanks to Jerome Laheurte for providing a buildbot with Linux and OSX VM's
24 | for continuous testing.
25 |
26 | How-to
27 | ------
28 |
29 | With pip installed on your system, do :command:`pip install pypubsub`.
30 |
31 | If you want the developer version, you can try :command:`pip install --pre pypubsub`.
32 |
33 | You can also get a zip/tgz from https://github.com/schollii/pypubsub/releases.
34 |
35 | .. _label-support:
36 |
37 | Support
38 | -------
39 |
40 | The forums are currently hosted on google groups:
41 |
42 | - http://googlegroups.com/group/pypubsub: PyPubSub general help and support (hosted by Google Groups)
43 |
44 | - http://googlegroups.com/group/pypubsub_dev: PyPubSub bug reports, feature suggestions, patches, etc (hosted by Google Groups)
45 |
46 | Also, many PyPubSub users are on the `wxPython-users mailing list `_.
47 |
48 |
49 | Release Notes
50 | -------------
51 |
52 | .. include:: ../src/pubsub/RELEASE_NOTES.txt
53 |
54 |
55 | .. include:: changelog.rst
56 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | sphinx-build -b html -a . html
4 | exit /B
5 |
6 | # The rest is the original make.bat, which has too many options!
7 |
8 | REM Command file for Sphinx documentation
9 |
10 | if "%SPHINXBUILD%" == "" (
11 | set SPHINXBUILD=sphinx-build
12 | )
13 | set BUILDDIR=_build
14 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
15 | set I18NSPHINXOPTS=%SPHINXOPTS% .
16 | if NOT "%PAPER%" == "" (
17 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
18 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
19 | )
20 |
21 | if "%1" == "" goto help
22 |
23 | if "%1" == "help" (
24 | :help
25 | echo.Please use `make ^` where ^ is one of
26 | echo. html to make standalone HTML files
27 | echo. dirhtml to make HTML files named index.html in directories
28 | echo. singlehtml to make a single large HTML file
29 | echo. pickle to make pickle files
30 | echo. json to make JSON files
31 | echo. htmlhelp to make HTML files and a HTML help project
32 | echo. qthelp to make HTML files and a qthelp project
33 | echo. devhelp to make HTML files and a Devhelp project
34 | echo. epub to make an epub
35 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
36 | echo. text to make text files
37 | echo. man to make manual pages
38 | echo. texinfo to make Texinfo files
39 | echo. gettext to make PO message catalogs
40 | echo. changes to make an overview over all changed/added/deprecated items
41 | echo. xml to make Docutils-native XML files
42 | echo. pseudoxml to make pseudoxml-XML files for display purposes
43 | echo. linkcheck to check all external links for integrity
44 | echo. doctest to run all doctests embedded in the documentation if enabled
45 | echo. coverage to run coverage check of the documentation if enabled
46 | goto end
47 | )
48 |
49 | if "%1" == "clean" (
50 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
51 | del /q /s %BUILDDIR%\*
52 | goto end
53 | )
54 |
55 |
56 | REM Check if sphinx-build is available and fallback to Python version if any
57 | %SPHINXBUILD% 2> nul
58 | if errorlevel 9009 goto sphinx_python
59 | goto sphinx_ok
60 |
61 | :sphinx_python
62 |
63 | set SPHINXBUILD=python -m sphinx.__init__
64 | %SPHINXBUILD% 2> nul
65 | if errorlevel 9009 (
66 | echo.
67 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
68 | echo.installed, then set the SPHINXBUILD environment variable to point
69 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
70 | echo.may add the Sphinx directory to PATH.
71 | echo.
72 | echo.If you don't have Sphinx installed, grab it from
73 | echo.http://sphinx-doc.org/
74 | exit /b 1
75 | )
76 |
77 | :sphinx_ok
78 |
79 |
80 | if "%1" == "html" (
81 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
82 | if errorlevel 1 exit /b 1
83 | echo.
84 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
85 | goto end
86 | )
87 |
88 | if "%1" == "dirhtml" (
89 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
90 | if errorlevel 1 exit /b 1
91 | echo.
92 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
93 | goto end
94 | )
95 |
96 | if "%1" == "singlehtml" (
97 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
98 | if errorlevel 1 exit /b 1
99 | echo.
100 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
101 | goto end
102 | )
103 |
104 | if "%1" == "pickle" (
105 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
106 | if errorlevel 1 exit /b 1
107 | echo.
108 | echo.Build finished; now you can process the pickle files.
109 | goto end
110 | )
111 |
112 | if "%1" == "json" (
113 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can process the JSON files.
117 | goto end
118 | )
119 |
120 | if "%1" == "htmlhelp" (
121 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
122 | if errorlevel 1 exit /b 1
123 | echo.
124 | echo.Build finished; now you can run HTML Help Workshop with the ^
125 | .hhp project file in %BUILDDIR%/htmlhelp.
126 | goto end
127 | )
128 |
129 | if "%1" == "qthelp" (
130 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
131 | if errorlevel 1 exit /b 1
132 | echo.
133 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
134 | .qhcp project file in %BUILDDIR%/qthelp, like this:
135 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyPubSub.qhcp
136 | echo.To view the help file:
137 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyPubSub.ghc
138 | goto end
139 | )
140 |
141 | if "%1" == "devhelp" (
142 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
143 | if errorlevel 1 exit /b 1
144 | echo.
145 | echo.Build finished.
146 | goto end
147 | )
148 |
149 | if "%1" == "epub" (
150 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
151 | if errorlevel 1 exit /b 1
152 | echo.
153 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
154 | goto end
155 | )
156 |
157 | if "%1" == "latex" (
158 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
159 | if errorlevel 1 exit /b 1
160 | echo.
161 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
162 | goto end
163 | )
164 |
165 | if "%1" == "latexpdf" (
166 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
167 | cd %BUILDDIR%/latex
168 | make all-pdf
169 | cd %~dp0
170 | echo.
171 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
172 | goto end
173 | )
174 |
175 | if "%1" == "latexpdfja" (
176 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
177 | cd %BUILDDIR%/latex
178 | make all-pdf-ja
179 | cd %~dp0
180 | echo.
181 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
182 | goto end
183 | )
184 |
185 | if "%1" == "text" (
186 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
187 | if errorlevel 1 exit /b 1
188 | echo.
189 | echo.Build finished. The text files are in %BUILDDIR%/text.
190 | goto end
191 | )
192 |
193 | if "%1" == "man" (
194 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
195 | if errorlevel 1 exit /b 1
196 | echo.
197 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
198 | goto end
199 | )
200 |
201 | if "%1" == "texinfo" (
202 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
203 | if errorlevel 1 exit /b 1
204 | echo.
205 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
206 | goto end
207 | )
208 |
209 | if "%1" == "gettext" (
210 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
211 | if errorlevel 1 exit /b 1
212 | echo.
213 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
214 | goto end
215 | )
216 |
217 | if "%1" == "changes" (
218 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.The overview file is in %BUILDDIR%/changes.
222 | goto end
223 | )
224 |
225 | if "%1" == "linkcheck" (
226 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
227 | if errorlevel 1 exit /b 1
228 | echo.
229 | echo.Link check complete; look for any errors in the above output ^
230 | or in %BUILDDIR%/linkcheck/output.txt.
231 | goto end
232 | )
233 |
234 | if "%1" == "doctest" (
235 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Testing of doctests in the sources finished, look at the ^
239 | results in %BUILDDIR%/doctest/output.txt.
240 | goto end
241 | )
242 |
243 | if "%1" == "coverage" (
244 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
245 | if errorlevel 1 exit /b 1
246 | echo.
247 | echo.Testing of coverage in the sources finished, look at the ^
248 | results in %BUILDDIR%/coverage/python.txt.
249 | goto end
250 | )
251 |
252 | if "%1" == "xml" (
253 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
254 | if errorlevel 1 exit /b 1
255 | echo.
256 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
257 | goto end
258 | )
259 |
260 | if "%1" == "pseudoxml" (
261 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
262 | if errorlevel 1 exit /b 1
263 | echo.
264 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
265 | goto end
266 | )
267 |
268 | :end
269 |
--------------------------------------------------------------------------------
/docs/pubsub_concept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schollii/pypubsub/dd5c1e848ff501b192a26a0a0187e618fda13f97/docs/pubsub_concept.png
--------------------------------------------------------------------------------
/docs/usage/core_classes.rst:
--------------------------------------------------------------------------------
1 | Core Classes
2 | ============
3 |
4 | The following classes are useful for advanced use of PyPubSub:
5 |
6 | - pubsub.core.Listener
7 | - pubsub.core.TopicObj
8 | - pubsub.core.TopicManager
9 | - pubsub.core.Publisher
10 |
11 | It is not typically necessary to know about or use these: the pub module instantiates a
12 | default Publisher, which contains a TopicManager, which generates a Topic object
13 | for every topic used. The Publisher instance returns a Listener instance from subscribe,
14 | wrapping the given callable with PyPubSub-relevant meta-data about the callable.
15 |
16 |
17 | Publisher
18 | ---------
19 |
20 | .. autoclass:: pubsub.core.Publisher
21 | :members:
22 | :inherited-members:
23 |
24 | TopicManager
25 | ------------
26 |
27 | .. autoclass:: pubsub.core.TopicManager
28 | :members:
29 | :inherited-members:
30 |
31 | Topic
32 | -----
33 |
34 | .. autoclass:: pubsub.core.Topic
35 | :members:
36 | :inherited-members:
37 |
38 | Listener
39 | --------
40 |
41 | .. autoclass:: pubsub.core.Listener
42 | :members:
43 | :inherited-members:
44 |
45 |
--------------------------------------------------------------------------------
/docs/usage/helloworld.py:
--------------------------------------------------------------------------------
1 | """
2 | One listener is subscribed to a topic called 'rootTopic'.
3 | One 'rootTopic' message gets sent.
4 | """
5 |
6 | from pubsub import pub
7 |
8 |
9 | # ------------ create a listener ------------------
10 |
11 | def listener1(arg1, arg2=None):
12 | print('Function listener1 received:')
13 | print(' arg1 =', arg1)
14 | print(' arg2 =', arg2)
15 |
16 |
17 | # ------------ register listener ------------------
18 |
19 | pub.subscribe(listener1, 'rootTopic')
20 |
21 | # ---------------- send a message ------------------
22 |
23 | print('Publish something via pubsub')
24 | anObj = dict(a=456, b='abc')
25 | pub.sendMessage('rootTopic', arg1=123, arg2=anObj)
26 |
--------------------------------------------------------------------------------
/docs/usage/howtos/index.rst:
--------------------------------------------------------------------------------
1 | How Tos
2 | =======
3 |
4 | This section provides "recipes" for various tasks such as migrating an
5 | application from one PyPubSub version or messaging protocol to another.
6 |
7 |
8 | .. _label-migrations:
9 |
10 | Migrations
11 | ----------
12 |
13 | In the very first version of PyPubSub, v1, message data was transported via a
14 | single instance of a class called Message. This had several important limitations,
15 | most importantly the inability to validate whether the correct data fields
16 | were being included in a message sent.
17 |
18 | In PyPubSub v3, a new protocol was implemented, and was named kwargs. This was
19 | not compatible with v1, but v3 made it possible to configure the imported
20 | pubsub to select either the arg1 protocol or the kwargs protocol, and to configure
21 | PyPubSub to help with transition from one to the other.
22 |
23 | PyPubSub v4 only supports the kwargs protocol. If you want to upgrade your
24 | application from using PyPubSub v1 or v3 to v4 and it uses the arg1 protocol,
25 | you will have to do the transition via PyPubSub 3.3, as explained at length
26 | in the documentation for that version (specifically, see the section
27 | `label-migrations`)
28 |
29 | Please post on the list for any additional support needed on this topic.
30 |
--------------------------------------------------------------------------------
/docs/usage/index.rst:
--------------------------------------------------------------------------------
1 | Use
2 | =======
3 |
4 | If you are new to PyPubSub, you will likely want to start with :ref:`label-usage-basic`.
5 | If you want a quick start, jump the :ref:`label-quick-start` section.
6 |
7 | If you develop an application that uses PyPubSub, whether it is prototype or production
8 | quality code, if it is more than just an experiment you will find that it can become
9 | tedious to debug. This is because the decouping can make the link between
10 | causes and effects challenging to identify. Also, you need better control over error
11 | handling, and more maintainability via documentation of message structure. This is when
12 | you turn to the :ref:`label-usage-advanced` section.
13 |
14 | .. contents::
15 | :depth: 1
16 | :local:
17 |
18 | .. toctree::
19 | :maxdepth: 2
20 |
21 | usage_basic
22 | usage_advanced
23 | howtos/index
24 | reference
25 |
--------------------------------------------------------------------------------
/docs/usage/module_pub.rst:
--------------------------------------------------------------------------------
1 | Pub Module
2 | ==========
3 |
4 | .. automodule:: pubsub.pub
5 |
6 | .. autodata:: VERSION_API
7 |
8 | The PyPubSub API version. This is deprecated. The only valid value currently is the integer 4.
9 | Previously, versions 1, 2 and 3 API could also be activated in PyPubSub before importing pub, in
10 | which case pub.VERSION_API had the corresponding value.
11 |
12 |
13 | Sending Messages
14 | ----------------
15 |
16 | Sending messages is achieved via the following function:
17 |
18 | .. autofunction:: sendMessage(topicName, **kwargs)
19 |
20 | The following exception may be raised when sending a message, if the message
21 | data does not comply with the Message Data Specification for the topic:
22 |
23 | .. autoexception:: SenderMissingReqdMsgDataError
24 | :show-inheritance:
25 | .. autoexception:: SenderUnknownMsgDataError
26 | :show-inheritance:
27 |
28 | **Advanced use:**
29 |
30 | The following would typically only be useful in special circumstances, such as
31 | if PyPubSub's default Publisher must be accessed, on or more separate instances of
32 | Publisher is required, and so forth.
33 |
34 | .. autofunction:: getDefaultPublisher
35 | .. autoclass:: pubsub.core.Publisher
36 | :noindex:
37 |
38 | See :py:class:`pubsub.core.Publisher` for details.
39 |
40 |
41 | Receiving Messages
42 | ------------------
43 |
44 | The following functions are available for controlling what callable objects
45 | (functions, methods, or class instances with a __call__ method) will get
46 | called when messages are generated:
47 |
48 | .. autofunction:: subscribe(listener, topicName)
49 | .. autofunction:: unsubscribe(listener, topicName)
50 | .. autofunction:: unsubAll(topicName=None, listenerFilter=None, topicFilter=None)
51 | .. autofunction:: isSubscribed
52 |
53 | The following exceptions are relevant:
54 |
55 | .. autoexception:: ListenerMismatchError
56 | :show-inheritance:
57 |
58 | .. autoexception:: MessageDataSpecError
59 | :show-inheritance:
60 |
61 | .. py:data:: AUTO_TOPIC
62 |
63 | Use this as default parameter in a listener's signature: the listener
64 | will be given the Topic object of the message.
65 |
66 | The following additional functions may be useful during debugging:
67 |
68 | .. autofunction:: isValid
69 | .. autofunction:: validate
70 | .. autoexception:: TopicDefnError
71 | :show-inheritance:
72 |
73 | **Advanced use:**
74 |
75 | The following are not typically required but can be useful in certain circumstances,
76 | especially during debugging:
77 |
78 | .. autoclass:: pubsub.core.Listener
79 | :noindex:
80 |
81 | See :py:class:`pubsub.core.Listener` for details.
82 |
83 | .. autofunction:: pubsub.core.getListenerID
84 |
85 |
86 | Topics
87 | ------
88 |
89 | In most cases, topics are used by name in dotted string format. The following
90 | may be useful for basic PyPubSub use:
91 |
92 | .. autoexception:: TopicNameError
93 | :show-inheritance:
94 |
95 | **Advanced use:**
96 |
97 | Some advanced uses of PyPubSub, especially (but not only) for debugging a PyPubSub-based
98 | application, could require access to the associated Topic instance, topic tree
99 | manager, special topic-related constants, or other helper functions and classes.
100 |
101 | .. autoclass:: TopicTreeTraverser
102 |
103 | .. py:data:: ALL_TOPICS
104 |
105 | Name of topic that is root of topic tree. Subscribe a listener
106 | to this topic to get all PyPubSub messages. Use \**kwargs to receive
107 | all message data, regardless of topic.
108 |
109 | .. py:data:: topicTreeRoot
110 |
111 | The topic object that is parent of all root topics. The name of this
112 | topic is pub.ALL_TOPICS.
113 |
114 | .. py:data:: topicsMap
115 |
116 | The dictionary that maps topic names to Topic objects.
117 |
118 | **Advanced use:**
119 |
120 | The following are not typically required but can be useful in certain circumstances,
121 | such as during debugging:
122 |
123 | .. autofunction:: getDefaultTopicMgr
124 | .. autoclass:: pubsub.core.TopicManager
125 | :noindex:
126 |
127 | See :py:class:`pubsub.core.TopicManager` for details.
128 |
129 | .. autoclass:: pubsub.core.Topic
130 | :noindex:
131 |
132 | See :py:class:`pubsub.core.Topic` for details.
133 |
134 | Listener Exception Handling
135 | ---------------------------
136 |
137 | Listeners that leak exceptions are typically burried deep into the stacktrace, and
138 | can cause an application to abort. The following may simplify the task of providing
139 | useful error messages from misbehaved listeners, without interrupting the application
140 | or even the PyPubSub send-message:
141 |
142 | .. autofunction:: getListenerExcHandler()
143 | .. autofunction:: setListenerExcHandler(handler)
144 | .. autoclass:: IListenerExcHandler
145 | .. autoexception:: ExcHandlerError
146 | :show-inheritance:
147 |
148 | See :mod:`pubsub.utils.exchandling` for ready-made exception handlers which may fit your
149 | requirements.
150 |
151 |
152 | PyPubSub Tracing (aka Notification)
153 | -----------------------------------
154 |
155 | While debugging an application it may be useful to trap some of PyPubSub's activity:
156 |
157 | .. autoclass:: INotificationHandler
158 | .. autofunction:: addNotificationHandler(handler)
159 | .. autofunction:: clearNotificationHandlers()
160 | .. autofunction:: setNotificationFlags(**kwargs)
161 | .. autofunction:: getNotificationFlags()
162 |
163 | See :mod:`pubsub.utils` for some ready-made notification handlers
164 | which may fit your requirements.
165 |
166 |
167 | Topic Specification
168 | -------------------
169 |
170 | Topic definition, documentation, and message data specification (MDS):
171 |
172 | .. autoexception:: TopicDefnError
173 | :show-inheritance:
174 |
175 | .. autofunction:: exportTopicTreeSpec
176 |
177 | .. autofunction:: setTopicUnspecifiedFatal(newVal=True, checkExisting=True)
178 |
179 | .. autofunction:: addTopicDefnProvider(providerOrSource, format=None)
180 | .. autofunction:: getNumTopicDefnProviders()
181 | .. autofunction:: clearTopicDefnProviders()
182 | .. autofunction:: instantiateAllDefinedTopics(provider)
183 | .. autoexception:: UnrecognizedSourceFormatError
184 | :show-inheritance:
185 |
186 | .. autodata:: TOPIC_TREE_FROM_MODULE
187 |
188 | Provide to pub.addTopicDefnProvider() as value for format parameter when the
189 | source is a module which has been imported. The module can contain any
190 | number of classes, the names of which correspond to root topics.
191 |
192 | .. autodata:: TOPIC_TREE_FROM_CLASS
193 |
194 | Provide to pub.addTopicDefnProvider() as value for format parameter when the
195 | source is a class. The class contains, as nested classes, the root topics (and
196 | those contain nested classes for subtopics, etc).
197 |
198 | .. autodata:: TOPIC_TREE_FROM_STRING
199 |
200 | Provide to pub.addTopicDefnProvider() as value for format parameter when the
201 | source is a string. The string contains Python code that defines one class
202 | for each root topic (and those contain nested classes for subtopics, etc).
203 |
204 |
205 | **Developer**:
206 |
207 | The following are useful to extend the capabilities of PyPubSub to support more
208 | topic definition providers or serialization formats for the builtin provider:
209 |
210 | .. autoclass:: pubsub.core.ITopicDefnProvider
211 | .. autoclass:: pubsub.core.ITopicDefnDeserializer
212 | .. autoclass:: pubsub.core.TopicDefnProvider
213 | :show-inheritance:
214 |
215 |
--------------------------------------------------------------------------------
/docs/usage/module_utils.rst:
--------------------------------------------------------------------------------
1 | Utils module
2 | ============
3 |
4 |
5 | .. automodule:: pubsub.utils
6 | :members:
7 | :show-inheritance:
8 | :inherited-members:
--------------------------------------------------------------------------------
/docs/usage/reference.rst:
--------------------------------------------------------------------------------
1 |
2 | Reference
3 | =========
4 |
5 | The pubsub package contains the following:
6 |
7 | .. toctree::
8 |
9 | module_pub
10 | module_utils
11 | core_classes
12 |
--------------------------------------------------------------------------------
/docs/usage/types_of_errors.rst:
--------------------------------------------------------------------------------
1 | Types of Errors
2 | ---------------
3 |
4 | While developing an application that uses PyPubSub, calls to PyPubSub
5 | functions and methods may raise an exception, in the following
6 | circumstances:
7 |
8 | * the listener given to pub.subscribe() is not valid:
9 |
10 | - :py:class:`pub.ListenerMismatchError`
11 | - ``ValueError``
12 |
13 | * the data sent via pub.sendMessage() does not satisfy the topic's MDS:
14 |
15 | - :py:class:`pub.SenderMissingReqdMsgDataError`
16 | - :py:class:`pub.SenderUnknownMsgDataError`
17 | - :py:class:`pub.SenderTooManyKwargs`
18 | - :py:class:`pub.SenderWrongKwargName`
19 |
20 | * there is a problem with a topic name:
21 |
22 | - :py:class:`pub.TopicNameError`
23 | - :py:class:`pub.TopicDefnError`
24 | - ``ValueError``
25 |
26 | * a callback registered via pub.setListenerExcHandler() raises an exception
27 | while handling an exception raised by a listener:
28 |
29 | - :py:class:`pub.ExcHandlerError`
30 |
31 | * a subclass derived from a pubsub core or utils base class is missing
32 | some implementation:
33 |
34 | - ``NotImplementedError``
35 |
36 | * a topic's MDS, defined explicitly via TopicDefnProvider, is not valid:
37 |
38 | - :py:class:`pub.MessageDataSpecError`
39 | - :py:class:`pub.UnrecognizedSourceFormatError`
40 |
41 | For basic PyPubSub usage, the most common ones are ``ListenerMismatchError``
42 | and the ``Sender...`` exceptions. All others are relevant to usage of
43 | more advanced PyPubSub features such as topic tree specification,
44 | listener exception trapping, and PyPubSub notification trapping.
45 |
46 |
47 | Listener Mismatch Errors
48 | ^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | The most common type of error results from attempting to subscribe an invalid
51 | listener: one that does not have a signature (call protocol) compatible with the
52 | topic's MDS. When this happens, PyPubSub raises a
53 | :py:class:`pub.ListenerMismatchError` exception.
54 |
55 | By default, PyPubSub infers topic MDSs. In that case, the error typically happens
56 | when more than one listener is registered for a given topic, and introspection of
57 | the listener identifies that it does not satisfy the topic's MDS. For example,
58 | consider ::
59 |
60 | def listener0(arg1, arg2=default0): pass
61 | def listener1(arg1=val, arg2=default3): pass
62 | def listener2(arg1): pass
63 | def listener3(arg1, arg2): pass
64 |
65 | pub.subscribe(listener0, "topic") // OK: infers MDS
66 | pub.subscribe(listener1, "topic") // OK: satisfies MDS
67 | pub.subscribe(listener2, "topic") // FAIL: violates MDS
68 |
69 | PyPubSub will raise a ListenerMismatchError exception on the last line since arg2 was
70 | inferred in the first subscription, from listener0, as being part of the MDS, yet
71 | listener2 does not accept this data.
72 |
73 | Similarly, if the last line had been ::
74 |
75 | pub.subscribe(listener3, "topic")
76 |
77 | a ``pub.ListenerMismatchError`` exception would get raised because listener3
78 | *requires* arg2,
79 | yet the MDS inferred from listener0 has it as optional, indicating the sender
80 | may not provide it. PyPubSub is flagging the fact that listener3 is "more demanding"
81 | than the MDS can guarantee.
82 |
83 | Sender Exceptions
84 | ^^^^^^^^^^^^^^^^^
85 |
86 | The sender exceptions are very useful as they indicate clearly what message
87 | data is wrong:
88 |
89 | - :py:class:`pub.SenderMissingReqdMsgDataError`: some required data is missing
90 | - :py:class:`pub.SenderUnknownMsgDataError`: one of the keyword arguments is
91 | not part of MDS
92 |
93 | For example, given the previous code involving a topic "topic" MDS inferred
94 | from listener0, the following code would raise a
95 | ``pub.SenderUnknownMsgDataError`` ::
96 |
97 | pub.sendMessage("topic", arg1=1, arg3=3)
98 |
99 | because arg3 is not part of the MDS.
100 |
101 | Topic Name Errors
102 | ^^^^^^^^^^^^^^^^^
103 |
104 | A topic name must satisfy the following:
105 |
106 | - is not empty: '' or None
107 | - is not a reserved name: the only one currently is the value of :py:data:`pub.ALL_TOPICS`
108 | - starts with any of '-', 0-9, a-z, A-Z (so UNDERSCORE '_' not allowed; it is reserved)
109 |
110 | This applies to all levels of a topic path, i.e. the items between '.'. For example
111 | the following are not allowed: 'a.', '.a', '.', 'a..b', etc.
112 |
113 | If a topic name does not satisfy the above, PyPubSub raises ``pub.TopicNameError``.
114 |
115 | Some functions in PyPubSub raise an exception if the topic doesn't exist:
116 |
117 | - :py:func:`pub.isValid(listener, topicName)`
118 | - :py:func:`pub.validate(listener, topicName)`
119 | - :py:func:`pub.isSubscribed(listener, topicName)`
120 | - :py:func:`pub.unsubscribe(listener, topicName)`
121 | - :py:func:`pub.unsubAll(topicName)`
122 |
123 | since the operation does not make sense: it does not make sense, for example,
124 | to test if given listener is valid if topic does not exist!
125 |
126 | By default,
127 |
128 | - PyPubSub does *not* complain about topic names that have never
129 | been subscribed to.
130 | - subscribing a listener to a topic never used before 'creates'
131 | the topic.
132 |
133 | Hence there is, by default, no way of trapping
134 | the following mistakes::
135 |
136 | pub.subscribe(listener1, 'topic') # creates 'topic' topic
137 | # next line has typo in topic name:
138 | pub.subscribe(listener2, 'tpic') # creates 'tpic' topic
139 | pub.sendMessage('topic') # only listener1 will receive
140 | # next line has typo in topic name:
141 | pub.sendMessage('topc') # creates 'topc' topic; no listener will receive
142 |
143 | These can lead to hard-to-isolate bugs as some listeners never get the messages.
144 | To trap such typos, use :py:func:`pub.setTopicUnspecifiedFatal(true)`, and
145 | specify all allowed topics at application startup by registering a Topic
146 | Definition Provider via :py:func:`pub.addTopidDefnProvider()`. Both above
147 | typos will then lead to PyPubSub
148 | raising :py:class:`TopicDefnError`. Note: a provider can easily be created via the
149 | :py:func:`pub.exportTopicTreeSpec()`.
150 |
151 |
--------------------------------------------------------------------------------
/docs/usage/usage_advanced.rst:
--------------------------------------------------------------------------------
1 |
2 | .. _label-usage-advanced:
3 |
4 | Advanced Usage
5 | ==============
6 |
7 | .. toctree::
8 | :maxdepth: 2
9 |
10 | usage_advanced_debug
11 | usage_advanced_maintain
12 | usage_advanced_other
13 |
14 |
--------------------------------------------------------------------------------
/docs/usage/usage_advanced_debug.rst:
--------------------------------------------------------------------------------
1 | Debugging an application
2 | ========================
3 |
4 | .. contents:: In this section:
5 | :depth: 2
6 | :local:
7 |
8 |
9 | Types of Errors
10 | ---------------
11 |
12 | While developing an application that uses PyPubSub, calls to PyPubSub
13 | functions and methods may raise an exception. These are discussed in:
14 |
15 | .. toctree::
16 |
17 | types_of_errors
18 |
19 |
20 | Notification: Tracking PyPubSub activity
21 | ----------------------------------------
22 |
23 | PyPubSub can call a specified handler every time it performs a certain task:
24 |
25 | - *subscribe*: whenever a listener subscribes to a topic
26 | - *unsubscribe*: whenever a listener unsubscribes from a topic
27 | - *deadListener*: whenever PyPubSub finds out that a listener has died
28 | - *send*: whenever the user calls sendMessage()
29 | - *newTopic*: whenever the user defines a new topic
30 | - *delTopic*: whenever the user undefines a topic
31 |
32 | A notification handler must adhere to the pub.INotificationHandler::
33 |
34 | import pubsub.utils
35 | class MyNotifHandler(INotificationHandler):
36 | def onSendMessage(...):
37 | ...
38 |
39 | pub.addNotificationHandler( MyNotifHandler() )
40 |
41 |
42 | A simple handler class is available already in ``pubsub.utils``:
43 | ``notification.NotifyByPubsubMessage``.
44 | This handler takes each notification received and generates a PyPubSub
45 | message of a "pubsub." topic named after the operation, such as "pubsub.subscribe".
46 | To use notification via this notifier, you must register one or more
47 | listeners for the "pubsub.*" topics of interest.
48 |
49 | A utility function is available from pubsub.utils for the most common case::
50 |
51 | from pubsub.utils import notification
52 | notification.useNotifyByPubsubMessage()
53 |
54 | .. _label-exchandling:
55 |
56 | Naughty Listeners: Trap Exceptions
57 | ----------------------------------
58 |
59 | A sender has no way of knowing what can go wrong during message handling by the
60 | subscribed listeners. As a result, a listener must not raise any exceptions (or
61 | rather, must not let any exceptions escape): if an exception does escape a
62 | listener, it interrupts the ``pub.sendMessage()`` call such that some listeners may
63 | not be sent the message. Putting a try/except clause around every sendMessage is
64 | typically not practical.
65 |
66 | Since exceptions are common during application development (bugs due to
67 | invalid arguments, failed assertions, etc.), PyPubSub provdes a hook to register
68 | a 'listener exception' handler: whenever a listener raises an exception,
69 | PyPubSub then sends it to the handler, and continues with the send operation
70 | until all listeners have received the message. The handler might print it to
71 | a log file, output a message in a status bar, show an error box, etc. The
72 | handling itself is very application-specific, hence this strategy.
73 |
74 | The handler must adhere to the ``pub.IListenerExcHandler`` protocol. An instance
75 | of the handler can be given to ``pub.setListenerExcHandler()``.
76 |
77 |
78 | Listen for messages from all topics
79 | -----------------------------------
80 |
81 | PyPubSub defines a special topic named pub.ALL_TOPICS. A listener that subscribes to
82 | this topic will receives all messages of every topic. By default, the listener
83 | will not receive any data since pub.ALL_TOPICS is the parent of all root topics:
84 | its MDS must be empty.
85 |
86 | However, any listener that is a callable
87 | with a "catch-all" \**kwargs parameter will be given all message data. Moreover,
88 | PyPubSub sends the topic object automatically with the message data if it
89 | finds that listener accepts a keyword argument with a default value of pub.AUTO_TOPIC.
90 | Together, these can be used to obtain complete information about all messages::
91 |
92 | >>> def snoop(topicObj=pub.AUTO_TOPIC, **mesgData):
93 | >>> print 'topic "%s": %s' % (topicObj.getName(), mesgData)
94 | >>>
95 | >>> pub.subscribe(snoop, pub.ALL_TOPICS)
96 | (, True)
97 | >>> pub.sendMessage('some.topic.name', a=1, b=2)
98 | topic "some.topic.name": {'a': 1, 'b': 2}
99 |
100 |
101 | Using the pub.Listener class
102 | ----------------------------
103 |
104 | Every callable that is subscribed via pub.subscribe() is wrapped in a
105 | pub.Listener instance returned by this function. This class has several
106 | useful functions such as name(), typeName(), module(), and isDead().
107 | For example::
108 |
109 | >>> def snoop(topicObj=pub.AUTO_TOPIC, **mesgData):
110 | >>> pass
111 | >>>
112 | >>> pubListener, first = pub.subscribe(snoop, pub.ALL_TOPICS)
113 | >>> assert first == true # since first time subscribed
114 | >>> assert pubListener.isDead() == false
115 | >>> assert pubListener.wantsTopicObjOnCall() == true
116 | >>> assert pubListener.wantsAllMessageData() == true
117 | >>> print pubListener.name()
118 | snoop_2752
119 | >>> print pubListener.name()
120 | snoop
121 |
122 |
123 | Doing something with every topic
124 | --------------------------------
125 |
126 | Derive from pub.ITopicTreeVisitor and give instance to an instance of
127 | pub.TopicTreeTraverser, then call traverse() method. For example, assume
128 | a callable 'listener' has been subscribed to several topics. An easy way
129 | to verify all topics subscribed to use this:
130 |
131 | >>> class MyVisitor(pub.ITopicTreeVisitor):
132 | >>> def __init__(self, listener):
133 | >>> self.subscribed = []
134 | >>> self.listener = listener
135 | >>> def _onTopic(self, topicObj):
136 | >>> if topicObj.hasListener(self.listener):
137 | >>> self.subscribed.append(topicObj.getName())
138 | >>>
139 | >>> tester = new MyVisitor(listener)
140 | >>> traverser = pub.TopicTreeTraverser( tester )
141 | >>> traverser.traverse(pub.getDefaultTopicTreeRoot())
142 | >>> print tester.subscribed
143 | ['topic-name', 'topic-name2', ...]
144 |
145 |
146 | Printing Topic Tree
147 | -------------------
148 |
149 | See pubsub.utils.printTreeDocs().
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/usage/usage_advanced_maintain.rst:
--------------------------------------------------------------------------------
1 | Maintainabiity
2 | ==============
3 |
4 | .. contents:: In this section:
5 | :depth: 2
6 | :local:
7 |
8 |
9 | .. _label-topic_tree_def:
10 |
11 | Specify topic tree def
12 | ----------------------
13 |
14 | *Topic Specification* can be used to have better control over your topic hierarchy.
15 | If you don't specify your application's topics, PyPubSub infers them from the first
16 | subscribed listener of each topic. E.g.::
17 |
18 | def listener1(arg1, arg2=None): pass
19 | def listener2(arg1=None, arg2=None): pass
20 |
21 | pub.subscribe(listener1, 'topic.sub')
22 | pub.subscribe(listener2, 'topic.sub')
23 |
24 | Because listener1 is the first to be subscribed to 'topic.sub' topic, PyPubSub uses it to
25 | infer the specification of 'topic.sub': the specification is "messages of that topic
26 | *must* provide data for arg1, and *may* provide data for arg2". The second listener
27 | subscribed, listener2, is allowed to subscribe because it is compatible with the
28 | topic's specification created at the previous call. What if your intent was that arg1 is
29 | optional as well, i.e. the signature of listener1 is wrong (it should provide a default
30 | value for arg1)? Or what if per chance listener2 gets subscribed first (could happen if
31 | both are subscribed in different modules whose load order changes)?
32 |
33 | The only way to not depend on the order of subscription of listeners is to use
34 | *Topic definition providers* (TDP). This is described below.
35 |
36 |
37 | Topic Definition Providers
38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
39 |
40 | The easiest way to understand a topic tree definition is to get PyPubSub to output one
41 | for your application via ``pub.exportTopicTreeSpec()``. Here is an example, taken from
42 | the file examples/advanced/kwargs_topics.py generated by that function, assuming
43 | two root topics 'topic_1' and 'topic_2' and the call
44 | ``pub.exportTopicTreeSpec('kwargs_topics')``:
45 |
46 | .. literalinclude:: ../../examples/advanced/kwargs_topics.py
47 |
48 | This shows how the topic definition tree is defined using a Python module with
49 | a nested class tree that represents the topics, and msgDataSpec() functions
50 | that represent the listener signatures for the given topics. This also shows
51 | how it is possible to document each topic and message datum.
52 |
53 | An application uses the above module via the following::
54 |
55 | import kwargs_topics
56 | pub.addTopicDefnProvider( kwargs_topics, pub.TOPIC_TREE_FROM_CLASS )
57 |
58 | The format type is ``pub.TOPIC_TREE_FROM_CLASS`` because once imported, the
59 | kwargs_topics object is a module containing topic definitions as classes; based
60 | on that setting, PyPubSub will look for all classes in the kwargs_topics object,
61 | and instantiate one topic definition for each one.
62 |
63 | See examples/advanced/main_kwargs.py for an example of using a topic tree
64 | definition in an application.
65 |
66 | It is possible to support other formats for topic tree definition. For example,
67 | ``pubsub.utils.XmlTopicDefnProvider`` was contributed to PyPubSub by one of its
68 | devoted users. A new type of provider need only adhere to
69 | the ``pub.ITopicTreeDefnProvider`` interface; ``pub.addTopicDefnProvider()``
70 | accepts any instance that implements from that interface::
71 |
72 | xmlString = open('xml_topics.xml', 'r').read()
73 | provider = XmlTopicDefnProvider(xmlString)
74 | pub.addTopicDefnProvider( provider )
75 |
76 | It is typically useful to combine topic tree definition with the following
77 | call, placed once at the beginning of an application::
78 |
79 | pub.setTopicUnspecifiedFatal(True)
80 |
81 | Then any attempt to use a topic that is not defined in the topic tree definition
82 | will raise an ``pub.TopicUnspecifiedError``.
83 |
84 | Note that any topic that does not have a docstring is not considered
85 | to be defined. This may allow for some temporary "undefining" of topics.
86 |
87 |
--------------------------------------------------------------------------------
/docs/usage/usage_advanced_other.rst:
--------------------------------------------------------------------------------
1 |
2 | Other
3 | =====
4 |
5 | .. contents:: In this section:
6 | :depth: 2
7 | :local:
8 |
9 |
10 | Dev app (process)
11 | -----------------
12 |
13 | Suggestions while developing application that uses PyPubSub:
14 |
15 | - Design your application into independent modules or subpackages
16 | that don't import one another
17 | - Define basic topics exist in the application: 'user' (events from
18 | user interface), 'filesystem' (events from local filesystem), etc.
19 | These are your messaging topics. You may find it useful
20 | to use ``printTreeDocs`` from ``pubsub.utils``.
21 | - Use Topic Definition Providers as eary as possible. Use
22 | pub.exportTopicTreeSpec() if already have partial implementation,
23 | and pub.addTopicDefnProvider() and pub.setTopicUnspecifiedFatal().
24 | - Start all listener functions and methods with *pubOn*, for
25 | instance ``def psOnCloseDocument()``
26 | - Define some data for each message type, and which data are optional/required
27 | - Implement your modules
28 |
29 | - Subscribe listeners with appropriate signature (according to
30 | data for each topic/event type)
31 | - Send messages with appropriate data
32 | - Handle messages in listeners, without making any assumptions
33 | about sender or order of receipt
34 |
35 | - Testing: import your control modules and generate messages to exercise them.
36 |
37 | You can see a very informative view of an application before and after
38 | incorporatng PyPubSub, at `Steven Sproat's dev site`_ (click "expand all"
39 | and "show diffs side-by-side"). Steven says:
40 |
41 | *You can see how I removed some GUI logic from the Canvas class (a
42 | child of the GUI) and placed "controller" functions into my GUI that
43 | subscribed to PyPubSub topics and delegates to the appropriate classes.*
44 |
45 | .. _Steven Sproat's dev site: http://bazaar.launchpad.net/~sproaty/whyteboard/development/revision/286
46 |
47 |
48 | .. _label-msg_protocols:
49 |
50 | Messaging Protocol
51 | ------------------
52 |
53 | The very first version of PyPubSub supported a messaging protocol that became
54 | known as 'arg1'. This protocol made it difficult to specify (i.e. define) what
55 | data was allowed in a topic. For larger applications, the developer had to put in
56 | verification code in the listener, had to deal with exceptions resulting
57 | from mismatches in field names in the message object, etc. It worked but
58 | made debugging the use of topics and PyPubSub messages complicated.
59 |
60 | The kwargs protocol was then designed: it allows the sender to name each datum,
61 | and the recipient (listener) to be checked via introspection at subscription time
62 | for its capability to receive the data. It also makes it easier to document the
63 | message data, and to specify it. The protocol was implemented in PyPubSub version 3.
64 |
65 | PyPubSub v4 supports only one way of transmitting data to listeners, namely via the
66 | 'kwargs' protocol. Since this is the only protocol supported, there is no code left
67 | that handles protocol name or selection.
68 |
69 |
70 | .. _label-pubsub_versions:
71 |
72 | API Versions
73 | ------------
74 |
75 | As PyPubSub matured, its API went through changes:
76 |
77 | - API version 1 (PyPubSub v1): the version that was part of wxPython and supported only the arg1 protocol.
78 | - API version 2 (PyPubSub v2): also part of wxPython, it made various improvements on v1 but was short
79 | lived as it did not properly address some inherent limitations of version 1.
80 | - API version 3 (PyPubSub v3): PyPubSub was moved out of wxPython to be a standalone project and
81 | supported 2 messaging protocols: the original arg1 for backwards compatibility, and the new
82 | kwargs. Since then, wxPython's wx.lib.pubsub is a verbatim copy of the standalone PyPubSub. The
83 | arg1 protocol was deprecated.
84 | - API version 4 (PyPubSub v4): Support for arg1 was dropped; only kwargs is now supported, which
85 | simplifies the code base considerably.
86 |
87 |
88 | Receiving all data of a message
89 | -------------------------------
90 |
91 | If a Listener uses \**kwargs then it will be given all data of a message,
92 | not just the portion specific to the topic it is subscribed to. For example, ::
93 |
94 | >>> def listener0(arg1, arg2): print('listener0: ', arg1, arg2)
95 | >>> def listener1(**kwargs): print('listener1: ', kwargs)
96 | >>> pub.subscribe(listener0, 'topic')
97 | >>> pub.subscribe(listener1, 'topic')
98 | >>> pub.sendMessage('topic', arg1=1, arg2=2)
99 |
100 | Then listener1 will receive arg1 and arg2.
101 |
102 | Note: as explained in :ref:`label-topic_tree_def`, PyPubSub infers a topic's *Message Data Specification*
103 | based on the first listener subscribed, unless there is a *Topic Definition Provider* for the topic. In the above
104 | example, PyPubSub would infer that *topic* has 2 required data: arg1 and arg2. However, if listener1
105 | were subscribed first, PyPubSub would infer that *topic* had no required data (because there are
106 | no positional parameters in the listener1 signature), and no optional data (because there are no
107 | parameters with default values in the the listener1 signature). Thus the subscription of listener0
108 | to *topic* would raise an exception (because listener0 requires arg1 and arg2). In real-world code,
109 | it can be difficult
110 | to guarantee the order of registration of listeners. Such issue is one of the intended use cases
111 | for a *Topic Definition Provider*, as explained in :ref:`label-topic_tree_def`.
112 |
--------------------------------------------------------------------------------
/docs/usage/usage_basic.rst:
--------------------------------------------------------------------------------
1 |
2 | .. _label-usage-basic:
3 |
4 | Basic Usage
5 | ===========
6 |
7 | Basic usage of PyPubSub involves subscribing listeners, sending messages, and responding to
8 | messages. The :ref:`label-quick-start` subsection below provides examples. For details,
9 | navigate to the :ref:`label-basic-tasks` subsection:
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | usage_basic_tasks
15 |
16 | .. _label-quick-start:
17 |
18 | Quick Start
19 | -----------
20 |
21 | Simplest example of use:
22 |
23 | .. literalinclude:: helloworld.py
24 |
25 |
26 | Running the above as a script (available in the docs/usage folder of the source
27 | distribution as helloworld.py) will produce the result::
28 |
29 | Publish something via pubsub
30 | Function listener1 received:
31 | arg1 = 123
32 | arg2 = {'a': 456, 'b': 'abc'}
33 |
34 |
35 | Other Examples
36 | ^^^^^^^^^^^^^^
37 |
38 | There are several examples that can be found in the source distribution
39 | in the ``examples`` folder. Some focus on the basics, others on more
40 | advanced aspects of PyPubSub usage. Some examples are GUI-based and may
41 | require other packages (such as wxPython).
42 |
43 | The examples/basic_kwargs folder contains examples of basic usage of PyPubSub
44 | "out of the box", i.e. using the default ("kwargs") messaging protocol.
45 | The README.txt file in `examples_basic_kwargs`_ explains:
46 |
47 | .. include:: ../../examples/basic_kwargs/README.txt
48 |
49 |
50 | .. _examples_basic_kwargs: http://svn.code.sf.net/p/pubsub/code/trunk/examples/basic_kwargs
51 |
52 |
53 |
--------------------------------------------------------------------------------
/examples/advanced/advanced_main.py:
--------------------------------------------------------------------------------
1 | """
2 | Advanced example that shows other capabilities of pubsub such as pubsub notification, listener
3 | exception handling, and topic definition providers.
4 |
5 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
6 | :license: BSD, see LICENSE.txt for details.
7 | """
8 |
9 | from pubsub import pub
10 |
11 | import notifhandle
12 | import exchandle
13 |
14 | import kwargs_topics
15 |
16 | # ***** actual application **********
17 |
18 | print('Using "kwargs" messaging protocol of pubsub v3')
19 |
20 | try:
21 | print('------- init ----------')
22 |
23 | pub.addTopicDefnProvider(kwargs_topics, pub.TOPIC_TREE_FROM_CLASS)
24 | pub.setTopicUnspecifiedFatal()
25 |
26 | import kwargs_listeners
27 | import kwargs_senders as senders
28 |
29 | print('-----------------------')
30 | senders.doSomething1()
31 | senders.doSomething2()
32 |
33 | print('------- done ----------')
34 |
35 | print('Exporting topic tree to', kwargs_topics.__name__)
36 | pub.exportTopicTreeSpec('kwargs_topics_out')
37 |
38 | except Exception:
39 | import traceback
40 |
41 | traceback.print_exc()
42 | print(pub.exportTopicTreeSpec())
43 |
44 | print('------ exiting --------')
45 |
--------------------------------------------------------------------------------
/examples/advanced/exchandle.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 | """
6 |
7 | import traceback
8 |
9 | from pubsub import pub
10 |
11 |
12 | # create one special notification handler that ignores all except
13 | # one type of notification
14 | class MyPubsubExcHandler(pub.IListenerExcHandler):
15 | def __call__(self, listenerID: str, topicObj: pub.Topic):
16 | print('Exception raised in listener %s during sendMessage()' % listenerID)
17 | traceback.print_exc()
18 |
19 |
20 | pub.setListenerExcHandler(MyPubsubExcHandler())
21 |
--------------------------------------------------------------------------------
/examples/advanced/kwargs_listeners.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 | """
6 |
7 | from pubsub import pub
8 |
9 |
10 | # ------------ create some listeners --------------
11 |
12 | class Listener:
13 | def onTopic11(self, msg, msg2, extra=None):
14 | print('Method Listener.onTopic11 received: ', repr(msg), repr(msg2), repr(extra))
15 |
16 | def onTopic1(self, msg, topic=pub.AUTO_TOPIC):
17 | info = 'Method Listener.onTopic1 received "%s" message: %s'
18 | print(info % (topic.getName(), repr(msg)))
19 |
20 | def __call__(self, **kwargs):
21 | print('Listener instance received: ', kwargs)
22 |
23 |
24 | listenerObj = Listener()
25 |
26 |
27 | def listenerFn(msg, msg2, extra=None):
28 | print('Function listenerFn received: ', repr(msg), repr(msg2), repr(extra))
29 |
30 |
31 | # ------------ subscribe listeners ------------------
32 |
33 | pub.subscribe(listenerObj, pub.ALL_TOPICS) # via its __call__
34 |
35 | pub.subscribe(listenerFn, 'topic_1.subtopic_11')
36 | pub.subscribe(listenerObj.onTopic11, 'topic_1.subtopic_11')
37 |
38 | pub.subscribe(listenerObj.onTopic1, 'topic_1')
39 |
--------------------------------------------------------------------------------
/examples/advanced/kwargs_senders.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | from pubsub import pub
9 |
10 |
11 | def doSomething1():
12 | pub.sendMessage('topic_1.subtopic_11',
13 | msg='message for subtopic 11', msg2='other message', extra=123)
14 |
15 |
16 | def doSomething2():
17 | pub.sendMessage('topic_1', msg='message for topic 1')
18 | pub.sendMessage('topic_2.subtopic_21', msg='message for subtopic 2')
19 |
--------------------------------------------------------------------------------
/examples/advanced/kwargs_topics.py:
--------------------------------------------------------------------------------
1 | # Automatically generated by TopicTreeSpecPrinter(**kwargs).
2 | # The kwargs were:
3 | # - fileObj: file
4 | # - width: 70
5 | # - treeDoc: None
6 | # - indentStep: 4
7 | # - footer: '# End of topic tree definition. Note that application may l...'
8 |
9 |
10 | class topic_1:
11 | """
12 | Explain when topic_1 should be used
13 | """
14 |
15 | def msgDataSpec(msg):
16 | """
17 | - msg: a text string message for recipient
18 | """
19 |
20 | class subtopic_11:
21 | """
22 | Explain when subtopic_11 should be used
23 | """
24 |
25 | def msgDataSpec(msg, msg2, extra=None):
26 | """
27 | - extra: something optional
28 | - msg2: a text string message #2 for recipient
29 | """
30 |
31 |
32 | class topic_2:
33 | """
34 | Some something useful about topic2
35 | """
36 |
37 | def msgDataSpec(msg=None):
38 | """
39 | - msg: a text string
40 | """
41 |
42 | class subtopic_21:
43 | """
44 | description for subtopic 21
45 | """
46 |
47 | def msgDataSpec(msg, arg1=None):
48 | """
49 | - arg1: UNDOCUMENTED
50 | """
51 |
52 | # End of topic tree definition. Note that application may load
53 | # more than one definitions provider.
54 |
--------------------------------------------------------------------------------
/examples/advanced/kwargs_topics_out.py:
--------------------------------------------------------------------------------
1 | # Automatically generated by TopicTreeSpecPrinter(**kwargs).
2 | # The kwargs were:
3 | # - fileObj: TextIOWrapper
4 | # - footer: '# End of topic tree definition. Note that application may l...'
5 | # - indentStep: 4
6 | # - treeDoc: None
7 | # - width: 70
8 |
9 |
10 | class topic_1:
11 | """
12 | Explain when topic_1 should be used
13 | """
14 |
15 | def msgDataSpec(msg):
16 | """
17 | - msg: a text string message for recipient
18 | """
19 |
20 | class subtopic_11:
21 | """
22 | Explain when subtopic_11 should be used
23 | """
24 |
25 | def msgDataSpec(msg, msg2, extra=None):
26 | """
27 | - extra: something optional
28 | - msg2: a text string message #2 for recipient
29 | """
30 |
31 | class topic_2:
32 | """
33 | Some something useful about topic2
34 | """
35 |
36 | def msgDataSpec(msg=None):
37 | """
38 | - msg: a text string
39 | """
40 |
41 | class subtopic_21:
42 | """
43 | description for subtopic 21
44 | """
45 |
46 | def msgDataSpec(msg, arg1=None):
47 | """
48 | - arg1: UNDOCUMENTED
49 | """
50 |
51 |
52 | # End of topic tree definition. Note that application may load
53 | # more than one definitions provider.
54 |
--------------------------------------------------------------------------------
/examples/advanced/notifhandle.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import sys
9 |
10 | from pubsub import pub
11 | from pubsub.utils.notification import useNotifyByWriteFile, IgnoreNotificationsMixin
12 |
13 |
14 | # create one special notification handler that ignores all except
15 | # one type of notification
16 | class MyPubsubNotifHandler(IgnoreNotificationsMixin):
17 | def notifySubscribe(self, pubListener, topicObj, newSub):
18 | newSubMsg = ''
19 | if not newSub:
20 | newSubMsg = ' was already'
21 | msg = 'MyPubsubNotifHandler: listener %s%s subscribed to %s'
22 | print(msg % (pubListener.name(), newSubMsg, topicObj.getName()))
23 |
24 |
25 | pub.addNotificationHandler(MyPubsubNotifHandler())
26 |
27 | # print(all notifications to stdout)
28 |
29 | useNotifyByWriteFile(sys.stdout, prefix='NotifyByWriteFile:')
30 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/README.txt:
--------------------------------------------------------------------------------
1 | These two examples demonstrate a simple use of pubsub. There are two examples that can be run from
2 | this folder:
3 |
4 | **console_main.py**: basic console based, uses the console_senders.py and console_listeners.py modules.
5 |
6 | **wx_main.py**: wxPython GUI application with two windows (win1 and win2) that exchange data without
7 | any reference to the other. This example looks for pubsub on your system path so default install ok.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/console_listeners.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | from pubsub import pub
9 |
10 |
11 | # ------------ create some listeners --------------
12 |
13 | class Listener:
14 | def onTopic11(self, msg, extra=None):
15 | print('Method Listener.onTopic11 received: ', repr(msg), repr(extra))
16 |
17 | def onTopic1(self, msg, topic=pub.AUTO_TOPIC):
18 | info = 'Method Listener.onTopic1 received "%s" message: %s'
19 | print(info % (topic.getName(), repr(msg)))
20 |
21 | def __call__(self, **kwargs):
22 | print('Listener instance received: ', kwargs)
23 |
24 |
25 | listenerObj = Listener()
26 |
27 |
28 | def listenerFn(msg, extra=None):
29 | print('Function listenerFn received: ', repr(msg), repr(extra))
30 |
31 |
32 | # ------------ subscribe listeners ------------------
33 |
34 | pub.subscribe(listenerObj, pub.ALL_TOPICS) # via its __call__
35 |
36 | pub.subscribe(listenerFn, 'topic1.subtopic11')
37 | pub.subscribe(listenerObj.onTopic11, 'topic1.subtopic11')
38 |
39 | pub.subscribe(listenerObj.onTopic1, 'topic1')
40 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/console_main.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import console_listeners
9 | import console_senders as senders
10 |
11 |
12 | def run():
13 | print('Using "kwargs" messaging protocol of pubsub v3')
14 |
15 | senders.doSomething1()
16 | senders.doSomething2()
17 |
18 |
19 | if __name__ == '__main__':
20 | run()
21 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/console_senders.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | from pubsub import pub
9 |
10 |
11 | def doSomething1():
12 | print('--- SENDING topic1.subtopic11 message ---')
13 | pub.sendMessage('topic1.subtopic11', msg='message for 11', extra=123)
14 | print('---- SENT topic1.subtopic11 message ----')
15 |
16 |
17 | def doSomething2():
18 | print('--- SENDING topic1 message ---')
19 | pub.sendMessage('topic1', msg='message for 1')
20 | print('---- SENT topic1 message ----')
21 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/wx_main.py:
--------------------------------------------------------------------------------
1 | """
2 | Adapted from wxPython website at http://wiki.wxpython.org/ModelViewController/.
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE.txt for details.
6 | """
7 |
8 | import wx
9 |
10 | from pubsub import pub
11 |
12 | print('pubsub API version', pub.VERSION_API)
13 |
14 | # notification
15 | from pubsub.utils.notification import useNotifyByWriteFile
16 | import sys
17 |
18 | useNotifyByWriteFile(sys.stdout)
19 |
20 | # the following two modules don't know about each other yet will
21 | # exchange data via pubsub:
22 | from wx_win1 import View
23 | from wx_win2 import ChangerWidget
24 |
25 |
26 | class Model:
27 | def __init__(self):
28 | self.myMoney = 0
29 |
30 | def addMoney(self, value):
31 | self.myMoney += value
32 | # now tell anyone who cares that the value has been changed
33 | pub.sendMessage("money_changed", money=self.myMoney)
34 |
35 | def removeMoney(self, value):
36 | self.myMoney -= value
37 | # now tell anyone who cares that the value has been changed
38 | pub.sendMessage("money_changed", money=self.myMoney)
39 |
40 |
41 | class Controller:
42 | def __init__(self):
43 | self.model = Model()
44 |
45 | # set up the first frame which displays the current Model value
46 | self.view1 = View()
47 | self.view1.setMoney(self.model.myMoney)
48 |
49 | # set up the second frame which allows the user to modify the Model's value
50 | self.view2 = ChangerWidget()
51 |
52 | self.view1.Show()
53 | self.view2.Show()
54 |
55 | pub.subscribe(self.changeMoney, 'money_changing')
56 |
57 | def changeMoney(self, amount):
58 | if amount >= 0:
59 | self.model.addMoney(amount)
60 | else:
61 | self.model.removeMoney(-amount)
62 |
63 |
64 | if __name__ == "__main__":
65 | app = wx.App()
66 | c = Controller()
67 | sys.stdout = sys.__stdout__
68 |
69 | print('---- Starting main event loop ----')
70 | app.MainLoop()
71 | print('---- Exited main event loop ----')
72 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/wx_win1.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import wx
9 | from pubsub import pub
10 |
11 |
12 | class View(wx.Frame):
13 | def __init__(self, parent=None):
14 | wx.Frame.__init__(self, parent, -1, "Main View")
15 |
16 | sizer = wx.BoxSizer(wx.VERTICAL)
17 | text = wx.StaticText(self, -1, "My Money")
18 | ctrl = wx.TextCtrl(self, -1, "")
19 | sizer.Add(text, 0, wx.EXPAND | wx.ALL)
20 | sizer.Add(ctrl, 0, wx.EXPAND | wx.ALL)
21 |
22 | self.moneyCtrl = ctrl
23 | ctrl.SetEditable(False)
24 | self.SetSizer(sizer)
25 |
26 | # subscribe to all "MONEY CHANGED" messages from the Model
27 | # to subscribe to ALL messages (topics), omit the second argument below
28 | pub.subscribe(self.setMoney, "money_changed")
29 |
30 | def setMoney(self, money):
31 | self.moneyCtrl.SetValue(str(money))
32 |
--------------------------------------------------------------------------------
/examples/basic_kwargs/wx_win2.py:
--------------------------------------------------------------------------------
1 | """
2 | Widget from which money can be added or removed from account.
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE.txt for details.
6 | """
7 |
8 | import wx
9 | from pubsub import pub
10 |
11 |
12 | class ChangerWidget(wx.Frame):
13 | CHANGE = 10 # by how much money changes every time click
14 |
15 | def __init__(self, parent=None):
16 | wx.Frame.__init__(self, parent, -1, "Changer View")
17 |
18 | sizer = wx.BoxSizer(wx.VERTICAL)
19 | self.add = wx.Button(self, -1, "Add Money")
20 | self.remove = wx.Button(self, -1, "Remove Money")
21 | sizer.Add(self.add, 0, wx.EXPAND | wx.ALL)
22 | sizer.Add(self.remove, 0, wx.EXPAND | wx.ALL)
23 | self.SetSizer(sizer)
24 |
25 | self.add.Bind(wx.EVT_BUTTON, self.onAdd)
26 | self.remove.Bind(wx.EVT_BUTTON, self.onRemove)
27 |
28 | def onAdd(self, evt):
29 | print('-----')
30 | pub.sendMessage("money_changing", amount=self.CHANGE)
31 |
32 | def onRemove(self, evt):
33 | print('-----')
34 | pub.sendMessage("money_changing", amount=- self.CHANGE)
35 |
--------------------------------------------------------------------------------
/examples/multithreadloop.py:
--------------------------------------------------------------------------------
1 | """
2 | This test gives an example of how some computation results from an
3 | auxiliary thread could be 'published' via pubsub in a thread-safe
4 | manner, in a 'gui'-like application, ie an application where the
5 | main thread is in an infinite event loop and supports the callback
6 | of user-defined functions when the gui is idle.
7 |
8 | The worker thread 'work' is to increment a counter
9 | as fast as interpreter can handle. Every so often (every resultStep counts),
10 | the thread stores the count in a synchronized queue, for later retrieval
11 | by the main thread. In parallel to this, the main thread loops forever (or
12 | until user interrupts via keyboard), doing some hypothetical work
13 | (represented by the sleep(1) call) and calling all registered 'idle'
14 | callbacks. The transfer is done by extracting items from the queue and
15 | publishing them via pubsub.
16 |
17 | Oliver Schoenborn
18 | May 2009
19 |
20 | :copyright: Copyright 2008-2009 by Oliver Schoenborn, all rights reserved.
21 | :license: BSD, see LICENSE.txt for details.
22 |
23 | """
24 |
25 | from queue import Queue
26 | import time
27 | import threading
28 | import sys
29 |
30 | from pubsub import pub
31 |
32 | __author__ = "schoenb"
33 | __date__ = "$31-May-2009 9:11:41 PM$"
34 |
35 | resultStep = 1000000 # how many counts for thread "result" to be available
36 |
37 |
38 | def threadObserver(transfers, threadObj, count):
39 | """Listener that listens for data from testTopic. This function
40 | doesn't know where the data comes from (or in what thread it was
41 | generated... but threadObj is the thread in which this
42 | threadObserver is called and should indicate Main thread)."""
43 |
44 | print(transfers, threadObj, count / resultStep)
45 |
46 |
47 | pub.subscribe(threadObserver, 'testTopic')
48 |
49 |
50 | def onIdle():
51 | """This should be registered with 'gui' to be called when gui is idle
52 | so we get a chance to transfer data from aux thread without blocking
53 | the gui. Ie this function must spend as little time as possible so
54 | 'gui' remains reponsive."""
55 | thread.transferData()
56 |
57 |
58 | class ParaFunction(threading.Thread):
59 | """
60 | Represent a function running in a parallel thread. The thread
61 | just increments a counter and puts the counter value on a synchronized
62 | queue every resultStep counts. The content of the queue can be published by
63 | calling transferData().
64 | """
65 |
66 | def __init__(self):
67 | threading.Thread.__init__(self)
68 | self.running = False # set to True when thread should stop
69 | self.count = 0 # our workload: keep counting!
70 | self.queue = Queue() # to transfer data to main thread
71 | self.transfer = 0 # count how many transfers occurred
72 |
73 | def run(self):
74 | print('aux thread started')
75 | self.running = True
76 | while self.running:
77 | self.count += 1
78 | if self.count % resultStep == 0:
79 | self.queue.put(self.count)
80 |
81 | print('aux thread done')
82 |
83 | def stop(self):
84 | self.running = False
85 |
86 | def transferData(self):
87 | """Send data from aux thread to main thread. The data was put in
88 | self.queue by the aux thread, and this queue is a Queue.Queue which
89 | is a synchronized queue for inter-thread communication.
90 | Note: This method must be called from main thread."""
91 | self.transfer += 1
92 | while not self.queue.empty():
93 | pub.sendMessage('testTopic',
94 | transfers=self.transfer,
95 | threadObj=threading.currentThread(),
96 | count=self.queue.get())
97 |
98 |
99 | thread = ParaFunction()
100 |
101 |
102 | def main():
103 | idleFns = [] # list of functions to call when 'gui' idle
104 | idleFns.append(onIdle)
105 |
106 | try:
107 | thread.start()
108 |
109 | print('starting event loop')
110 | eventLoop = True
111 | while eventLoop:
112 | time.sleep(1) # pretend that main thread does other stuff
113 | for idleFn in idleFns:
114 | idleFn()
115 |
116 | except KeyboardInterrupt:
117 | print('Main interrupted, stopping aux thread')
118 | thread.stop()
119 |
120 | except Exception as exc:
121 | exc = sys.exc_info()[1]
122 | print(exc)
123 | print('Exception, stopping aux thread')
124 | thread.stop()
125 |
126 |
127 | main()
128 |
--------------------------------------------------------------------------------
/examples/runall.bat:
--------------------------------------------------------------------------------
1 | echo off
2 |
3 | rem This script runs all examples. This should be mostly for using the
4 | rem examples as regression tests (after all tests have passed in tests
5 | rem folder).
6 | rem One command line argument is required, the python version number to
7 | rem use, no dots: 24 for 2.4, 30 for 3.0, etc.
8 | rem
9 | rem (C) Oliver Schoenborn 2009
10 |
11 | set PY_VER=%1
12 | IF "%1" EQU "" (
13 | SET PY_VER=26
14 | echo Will use Python 2.6. To use other, put version ID as command line arg
15 | echo Example: for Python 2.7 put 27, for 3.0 put 30, etc.
16 | )
17 |
18 | set PYTHON_EXE=python
19 | echo python exe is %PYTHON_EXE%
20 |
21 | echo.
22 | echo.
23 | echo ######################## basic - kwargs - console #########################
24 | echo.
25 |
26 | pushd basic_kwargs
27 | %PYTHON_EXE% console_main.py
28 | popd
29 | pause
30 |
31 |
32 | echo.
33 | echo.
34 | echo ######################## advanced - kwargs - console #########################
35 | echo.
36 |
37 | pushd advanced
38 | %PYTHON_EXE% main_kwargs.py
39 | popd
40 | pause
41 |
42 |
43 | echo.
44 | echo.
45 | echo ######################## basic - kwargs - wx #########################
46 | echo.
47 |
48 | pushd basic_kwargs
49 | %PYTHON_EXE% wx_main.py
50 | popd
51 | pause
52 |
53 |
54 |
--------------------------------------------------------------------------------
/release.bat:
--------------------------------------------------------------------------------
1 | echo off
2 |
3 | REM Run this script: it indicates specific steps to follow,
4 | REM and generates the distributions.
5 | REM
6 | REM Oliver, Dec 2016
7 |
8 | echo ==============================================================
9 | echo Before continuing, consult the instructions in Release
10 | echo section of Dev docs.
11 | echo ==============================================================
12 | echo Creating source distribution:
13 | python setup.py sdist
14 |
15 | echo ==============================================================
16 | echo Creating wheel distribution:
17 | python setup.py bdist_wheel
18 |
19 | echo ==============================================================
20 | echo To UPLOAD the dist/* distributions to PyPi, press ENTER,
21 | echo OTHERWISE, press ctrl-c:
22 | pause
23 |
24 | twine upload dist/*
25 |
26 | echo ==============================================================
27 | echo Upload completed.
28 | echo Follow remaining instructions in Release section of Dev docs.
29 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 |
2 | from setuptools import setup, find_packages
3 |
4 |
5 | def getPackagesToDistribute():
6 | packages = find_packages('src')
7 | import sys
8 | setupCmd = sys.argv[1]
9 | if setupCmd in ['sdist', 'bdist', 'bdist_egg', 'bdist_wininst']:
10 | print( '*'*40 )
11 | print( 'Packaging:', packages)
12 | print( '*'*40)
13 | return packages
14 |
15 |
16 | def getPubsubVersion():
17 | import sys
18 | sys.path.insert(0, 'src')
19 | import pubsub
20 | return pubsub.__version__
21 |
22 |
23 | def getInstallRequires():
24 | import sys
25 | if sys.version_info < (3,5):
26 | return ['typing']
27 | return []
28 |
29 |
30 | setup(
31 | name = 'Pypubsub',
32 | version = getPubsubVersion(),
33 | description = 'Python Publish-Subscribe Package',
34 | keywords = "publish subscribe observer pattern signal signals event events message messages messaging dispatch dispatching",
35 | author = 'Oliver Schoenborn (aka "schollii")',
36 | author_email = 'oliver.schoenborn@gmail.com',
37 | url = 'https://github.com/schollii/pypubsub',
38 | license = "BSD License",
39 | zip_safe = False,
40 |
41 | packages = getPackagesToDistribute(),
42 | package_dir = {'': 'src'},
43 | package_data = {'pubsub': ['LICENSE_BSD_Simple.txt', 'RELEASE_NOTES.txt']},
44 | install_requires=getInstallRequires(),
45 |
46 | classifiers = [
47 | 'Development Status :: 5 - Production/Stable',
48 | 'Intended Audience :: Developers',
49 | 'License :: OSI Approved :: BSD License',
50 | 'Natural Language :: English',
51 | 'Programming Language :: Python :: 3 :: Only',
52 | 'Programming Language :: Python :: 3.3',
53 | 'Programming Language :: Python :: 3.4',
54 | 'Programming Language :: Python :: 3.5',
55 | 'Programming Language :: Python :: 3.6',
56 | 'Programming Language :: Python :: 3.7',
57 | 'Topic :: Software Development :: Libraries :: Application Frameworks',
58 | 'Topic :: Software Development :: Libraries :: Python Modules',
59 | ],
60 | python_requires = ">=3.3, <4",
61 |
62 | # use the module docs as the long description:
63 | long_description = open('README.rst', 'r').read()
64 | )
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/contrib/netpubsub.py:
--------------------------------------------------------------------------------
1 | """
2 | Skeleton code (will not load... must be completed):
3 | UDP-based net-centric Pubsub extension so that pubsub can be used over a
4 | network of pubsub-powered applications. Extending to use TCP wouldn't be
5 | too difficult.
6 |
7 | Usage: each of your applications that uses pubsub would create a
8 | suitable AppListener, then call its sendMessage when wanting to
9 | send a message, or call processMessages whenever it is ready to
10 | process queued messages from other applications. In addition, one
11 | of your applications must be the "pubsub server", or the server
12 | could live in its own python program. For it, you would
13 | create a PubsubAppServer then call its processMessages()
14 | in an infinite loop.
15 |
16 | Oliver Schoenborn
17 | """
18 |
19 | from socket import socket, timeout
20 | from pubsub import pub
21 |
22 |
23 | class Marshaller:
24 | ...
25 | depends
26 | on
27 | your
28 | strategy / method...
29 | ...
30 | for instance, could use unpickle...
31 | ... or XML, SOAP, your
32 | own
33 | format, etc...
34 |
35 | def pack(self, topic, **args):
36 | ...
37 | pack
38 | topic and args
39 | into
40 | a
41 | data
42 | string...
43 | return data
44 |
45 | def unpack(self, data):
46 | ...
47 | unpack
48 | data
49 | into
50 | a
51 | topic and args
52 | return topic, msgData
53 |
54 | def getTopic(self, data):
55 | ...
56 | get
57 | topic
58 | from data...
59 | return topic
60 |
61 |
62 | class AppListener:
63 | def __init__(self, server, readPort, maxWait=0.01):
64 | self.__server = server
65 | self.__marshaller = Marshaller()
66 | self.__udpSocket = socket(...
67 | server...)
68 | self.__udpSocket.bind(...
69 | readPort...)
70 | self.__udpSocket.settimeout(maxWait) # want limited blocking
71 |
72 | def subscribe(self, topic):
73 | self.__udpSocket.write('subscribe %s' % topic)
74 |
75 | def processMessages(self):
76 | bufsize = 4096
77 | more = True
78 | while more:
79 | try:
80 | data, sender = self.__udpSocket.recvfrom(bufsize)
81 | if sender == self.__server:
82 | self.__publish(data)
83 | except timeout:
84 | more = False
85 |
86 | def __publish(self, data):
87 | topic, argsValues = self.__marshaller.unpack(data)
88 | pub.sendMessage(topic, **argsValues)
89 |
90 | def sendMessage(topic, **msgData):
91 | packedData = self.__marshaller.pack(msgData)
92 | self.server.write(packedData)
93 |
94 |
95 | class PubsubAppServer:
96 | SERVER_PORT = 8743 # pick to suit you
97 |
98 | def __init__(self):
99 | self.__listenerApps = {} # will be list of sets by topic
100 | self.__marshaller = Marshaller()
101 | self.__udpSocket = socket(...)
102 | self.__udpSocket.bind(...
103 | SERVER_PORT...)
104 |
105 | def processMessages(self):
106 | bufsize = 4096
107 | more = True
108 | while more:
109 | try:
110 | data, sender = self.__udpSocket.recv(bufsize)
111 | if data.startswith('subscribe'):
112 | self.__register(sender, data)
113 | else:
114 | self.__dispatch(data)
115 | except timeout:
116 | more = False
117 |
118 | def __register(self, sender, data):
119 | topic = data.split(' ')[1] # second item
120 | self.__listenerApps.setdefault(topic, set()).add(sender)
121 |
122 | def __dispatch(self, data):
123 | topic = self.__marshaller.getTopic(data)
124 | # send to all registered apps for that topic;
125 | # if no such topic then do nothing:
126 | for listenerApp in self.__listenerApps.get(topic, []):
127 | self.__udpSocket.write(data)
128 |
--------------------------------------------------------------------------------
/src/pubsub/LICENSE_BSD_Simple.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) since 2006, Oliver Schoenborn
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
24 |
--------------------------------------------------------------------------------
/src/pubsub/RELEASE_NOTES.txt:
--------------------------------------------------------------------------------
1 | Main changes in v4.0.3 (compared to 3.3)
2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 |
4 | * Support Python 3.5 and 3.6
5 | * Distribution via wheel
6 | * Abandon support for Python 2.x and easy_install
7 | * Abandon support for (long-ago deprecated) arg1 messaging protocol
8 | * Added currying of subscribed listener args
9 | * Significant speed improvement for message delivery
10 | * Use PEP 484 style of annotations throughout
11 | * Support listeners with keyword-only arguments
12 |
13 | Consequences of these changes:
14 |
15 | - If your app runs in Python 2.x, you cannot upgrade to pypubsub v4.
16 | - If your Python 3.x application uses setupkwargs.py, simply remove the import statement
17 | from your app. Your app should run without further changes. If your app won't run
18 | after doing this, please post on https://groups.google.com/forum/#!forum/pypubsub.
19 | - If your Python 3.x application uses setuparg1.py, this means you are using the
20 | long-ago deprecated arg1 API for messaging. Before upgrading to v4 pypubsub, you
21 | will have to migrate your app/package to use pypubsub's kwargs API. This is most
22 | easily done via pypubsub 3.3, which has functions and docs
23 | (http://pypubsub.readthedocs.io/en/stable/usage/howtos/index.html)
24 | to help with this task. Once you have completed this migration, you should be able
25 | to upgrade to pypubsub v4 without further changes.
26 | - The delivery order of sendMessage() has been changed. However as described in the
27 | documentation since the very early days of Pypubsub 3, you should design your
28 | application to not depend on the order of message delivery to listeners. If you
29 | did not follow this very important architectural principle, you should fix your
30 | application before upgrading. Then the upgrade will be trivial.
31 |
32 |
33 | Oliver Schoenborn
34 | January 2019
35 |
36 |
--------------------------------------------------------------------------------
/src/pubsub/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Pubsub package initialization.
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
6 | """
7 |
8 | __version__ = "4.0.3"
9 |
10 | __all__ = [
11 | 'pub',
12 | 'utils',
13 | '__version__'
14 | ]
15 |
--------------------------------------------------------------------------------
/src/pubsub/core/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Core package of pubsub, holding the publisher, listener, and topic
3 | object modules.
4 |
5 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
6 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
7 |
8 | """
9 |
10 | from .publisher import Publisher
11 |
12 | from .callables import (
13 | AUTO_TOPIC,
14 | )
15 |
16 | from .listener import (
17 | getID as getListenerID,
18 | ListenerMismatchError,
19 | IListenerExcHandler,
20 | Listener,
21 | )
22 | from .topicobj import (
23 | Topic,
24 | SenderUnknownMsgDataError,
25 | SenderMissingReqdMsgDataError,
26 | MessageDataSpecError,
27 | TopicDefnError,
28 | ExcHandlerError,
29 | )
30 |
31 | from .topicmgr import (
32 | TopicManager,
33 | TopicDefnError,
34 | TopicNameError,
35 | ALL_TOPICS,
36 | )
37 |
38 | from .topicdefnprovider import (
39 | ITopicDefnProvider,
40 | TopicDefnProvider,
41 | ITopicDefnDeserializer,
42 | UnrecognizedSourceFormatError,
43 |
44 | exportTopicTreeSpec,
45 | TOPIC_TREE_FROM_MODULE,
46 | TOPIC_TREE_FROM_STRING,
47 | TOPIC_TREE_FROM_CLASS,
48 | )
49 |
50 | from .topictreetraverser import (
51 | TopicTreeTraverser,
52 | TreeTraversal,
53 | )
54 |
55 | from .notificationmgr import (
56 | INotificationHandler,
57 | )
58 |
--------------------------------------------------------------------------------
/src/pubsub/core/annotations.py:
--------------------------------------------------------------------------------
1 | """
2 | Miscellaneous utility items
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
6 | """
7 |
8 | from typing import Any
9 |
10 |
11 | def annotationType(sig_obj: Any):
12 | """
13 | Use to forward declare an annotation type, i.e. a type that will be fully defined later but is not available
14 | at the annotation location in the source code. The following example shows a MyType class with a method that
15 | accepts a MyType instance; annotating this cannot be done in Python::
16 |
17 | class MyType:
18 | def copy_from(other: MyType): # interpreter will croak
19 | ...
20 |
21 | The recommended approach is to use a string annotation, but this is rather unnatural in Python code::
22 |
23 | class MyType:
24 | def copy_from(other: 'MyType'):
25 | ...
26 |
27 | The annotationType function allows a more pythonic syntax:
28 |
29 | @annotationType
30 | class MyType:
31 | pass
32 |
33 | class MyType:
34 | def copy_from(other: MyType):
35 | ...
36 |
37 | This decorator doesn't actually do anything to its argument.
38 | """
39 | return sig_obj
40 |
--------------------------------------------------------------------------------
/src/pubsub/core/callables.py:
--------------------------------------------------------------------------------
1 | """
2 | Low level functions and classes related to callables.
3 |
4 | The AUTO_TOPIC
5 | is the "marker" to use in callables to indicate that when a message
6 | is sent to those callables, the topic object for that message should be
7 | added to the data sent via the call arguments. See the docs in
8 | CallArgsInfo regarding its autoTopicArgName data member.
9 |
10 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
11 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
12 |
13 | """
14 |
15 | from inspect import ismethod, isfunction, signature, Parameter
16 | import sys
17 | from types import ModuleType
18 | from typing import Tuple, List, Sequence, Callable, Any
19 |
20 | # Opaque constant used to mark a kwarg of a listener as one to which pubsub should assign the topic of the
21 | # message being sent to the listener. This constant should be used by reference; its value is "unique" such that
22 | # pubsub can find such kwarg.
23 | class AUTO_TOPIC: pass
24 |
25 | # In the user domain, a listener is any callable, regardless of signature. The return value is ignored,
26 | # i.e. the listener will be treated as though it is a Callable[..., None]. Also, the args, "...", must be
27 | # consistent with the MDS of the topic to which listener is being subscribed.
28 | UserListener = Callable[..., Any]
29 |
30 |
31 | def getModule(obj: Any) -> ModuleType:
32 | """
33 | Get the module in which an object was defined.
34 | :param obj: the object for which to get module
35 | :return: the module object, or the string '__main__' if no module defined for obj
36 | (which usually indicates either a builtin, or a definition within main script).
37 | """
38 | if hasattr(obj, '__module__'):
39 | module = obj.__module__
40 | else:
41 | module = '__main__'
42 | return module
43 |
44 |
45 | def getID(callable_obj: UserListener) -> Tuple[str, ModuleType]:
46 | """
47 | Get "ID" of a callable, in the form of its name and module in which it is defined
48 | E.g. getID(Foo.bar) returns ('Foo.bar', 'a.b') if Foo.bar was defined in module a.b.
49 | :param callable_obj: a callable, ie function, bound method or callable instance
50 | """
51 | sc = callable_obj
52 | if ismethod(sc):
53 | module = getModule(sc.__self__)
54 | obj_name = '%s.%s' % (sc.__self__.__class__.__name__, sc.__func__.__name__)
55 | elif isfunction(sc):
56 | module = getModule(sc)
57 | obj_name = sc.__name__
58 | else: # must be a functor (instance of a class that has __call__ method)
59 | module = getModule(sc)
60 | obj_name = sc.__class__.__name__
61 |
62 | return obj_name, module
63 |
64 |
65 | def getRawFunction(callable_obj: UserListener) -> Tuple[Callable]:
66 | """
67 | Get raw function information about a callable.
68 | :param callable_obj: any object that can be called
69 | :return: function corresponding to callable, and offset is 0 or 1 to
70 | indicate whether the function's first argument is 'self' (1) or not (0)
71 | :raise ValueError: if callable_obj is not of a recognized type (function, method or object with __call__ method).
72 | """
73 | firstArg = 0
74 | if isfunction(callable_obj):
75 | # print 'Function', getID(callable_obj)
76 | func = callable_obj
77 | elif ismethod(callable_obj):
78 | # print 'Method', getID(callable_obj)
79 | func = callable_obj
80 | elif hasattr(callable_obj, '__call__'):
81 | # print 'Functor', getID(callable_obj)
82 | func = callable_obj.__call__
83 | else:
84 | msg = 'type "%s" not supported' % type(callable_obj).__name__
85 | raise ValueError(msg)
86 |
87 | return func
88 |
89 |
90 | class ListenerMismatchError(ValueError):
91 | """
92 | Raised when an attempt is made to subscribe a listener to
93 | a topic, but listener does not satisfy the topic's message data
94 | specification (MDS). This specification is inferred from the first
95 | listener subscribed to a topic, or from an imported topic tree
96 | specification (see pub.addTopicDefnProvider()).
97 | """
98 |
99 | def __init__(self, msg: str, listener: UserListener, *args):
100 | idStr, module = getID(listener)
101 | msg = 'Listener "%s" (from module "%s") inadequate: %s' % (idStr, module, msg)
102 | ValueError.__init__(self, msg)
103 | self.args = args
104 | self.msg = msg
105 | self.module = module
106 | self.idStr = idStr
107 |
108 | def __str__(self):
109 | return self.msg
110 |
111 |
112 | class CallArgsInfo:
113 | """
114 | Represent the "signature" of a listener of topic messages: which arguments are
115 | required vs optional.
116 | """
117 |
118 | def __init__(self, func: UserListener, ignoreArgs: Sequence[str] = ()):
119 | """
120 | :param func: the callable for which to get paramaters info
121 | :param ignoreArgs: do not include the given names in the get*Args() return values
122 |
123 | After construction,
124 | - self.acceptsAllKwargs = True if the listener has a **kwargs arg
125 | - self.autoTopicArgName will be the name of argument in which to put the Topic
126 | object for which pubsub message is sent, or None if auto off. This is identified
127 | by a parameter that has a default value of AUTO_TOPIC.
128 |
129 | For instance,
130 | - listener(self, arg1, arg2=AUTO_TOPIC, arg3=None) will have self.allParams = (arg1, arg2, arg3),
131 | self.numRequired=1, and self.autoTopicArgName = 'arg2', whereas
132 | - listener(self, arg1, arg3=None) will have self.allParams = (arg1, arg3), self.numRequired=1, and
133 | self.autoTopicArgName = None.
134 | """
135 |
136 | requiredArgs = []
137 | optionalArgs = []
138 | self.autoTopicArgName = None
139 | self.acceptsAllKwargs = False
140 | for argName, param in signature(func).parameters.items():
141 | if argName in ignoreArgs or param.kind == Parameter.VAR_POSITIONAL:
142 | continue
143 |
144 | if param.kind == Parameter.VAR_KEYWORD:
145 | self.acceptsAllKwargs = True
146 | continue
147 |
148 | if param.default == Parameter.empty:
149 | requiredArgs.append(argName)
150 | else:
151 | if param.default == AUTO_TOPIC:
152 | self.autoTopicArgName = argName
153 | else:
154 | optionalArgs.append(argName)
155 |
156 | self.requiredArgs = tuple(requiredArgs)
157 | self.optionalArgs = tuple(optionalArgs)
158 | self.allParams = self.requiredArgs + self.optionalArgs
159 |
160 | def getAllArgs(self) -> Tuple[str]:
161 | """
162 | Return a tuple of names indicating the complete set of message data
163 | (keyword args) that can be given to this listener
164 | """
165 | return self.optionalArgs
166 |
167 | def getOptionalArgs(self) -> Tuple[str]:
168 | """
169 | Return a tuple of names indicating which message data (keyword args)
170 | are optional when this listener is called.
171 | """
172 | return self.optionalArgs
173 |
174 | def getRequiredArgs(self) -> Tuple[str]:
175 | """
176 | Return a tuple of names indicating which message data (keyword args)
177 | are required when this listener is called.
178 | """
179 | return self.requiredArgs
180 |
181 |
182 | def getArgs(callable_obj: UserListener, ignoreArgs: Sequence[str] = ()) -> CallArgsInfo:
183 | """
184 | Get the call parameters of a callable to be used as listener.
185 | :param callable_obj: the callable for which to get call parameters
186 | :param ignoreArgs: optional list of names of parameters of callable_obj that should not be in the returned object
187 | :return: an instance of CallArgsInfo for the given callable_obj
188 | :raise ListenerMismatchError: if callable_obj is not a callable, or ignoreArgs has an item that is not a call
189 | param of callable
190 | """
191 | # figure out what is the actual function object to inspect:
192 | try:
193 | func = getRawFunction(callable_obj)
194 | except ValueError:
195 | exc = sys.exc_info()[1]
196 | raise ListenerMismatchError(str(exc), callable_obj)
197 |
198 | return CallArgsInfo(func, ignoreArgs=ignoreArgs)
199 |
--------------------------------------------------------------------------------
/src/pubsub/core/topicexc.py:
--------------------------------------------------------------------------------
1 | """
2 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
3 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
4 | """
5 |
6 | from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO
7 |
8 | from .annotations import annotationType
9 |
10 |
11 | @annotationType
12 | class Topic:
13 | pass
14 |
15 |
16 | class TopicNameError(ValueError):
17 | """
18 | Raised when the topic name is not properly formatted or
19 | no corresponding Topic object found.
20 | """
21 |
22 | def __init__(self, name: str, msg: str):
23 | ValueError.__init__(self, 'Topic name "%s": %s' % (name, msg))
24 |
25 |
26 | class TopicDefnError(RuntimeError):
27 | """
28 | Raised when an operation requires a topic have an MDS, but it doesn't.
29 | See also pub.setTopicUnspecifiedFatal().
30 | """
31 |
32 | def __init__(self, topicNameTuple: Sequence[str]):
33 | msg = "No topic specification for topic '%s'." % '.'.join(topicNameTuple)
34 | RuntimeError.__init__(self, msg +
35 | " See pub.addTopicDefnProvider() and/or pub.setTopicUnspecifiedFatal()")
36 |
37 |
38 | class MessageDataSpecError(RuntimeError):
39 | """
40 | Raised when an attempt is made to define a topic's Message Data
41 | Specification (MDS) to something that is not valid.
42 |
43 | The keyword names for invalid data go in the 'args' list,
44 | and the msg should state the problem and contain "%s" for the
45 | args, such as MessageDataSpecError('duplicate args %s', ('arg1', 'arg2')).
46 | """
47 |
48 | def __init__(self, msg: str, args: Sequence[str]):
49 | argsMsg = msg % ','.join(args)
50 | RuntimeError.__init__(self, 'Invalid message data spec: ' + argsMsg)
51 |
52 |
53 | class ExcHandlerError(RuntimeError):
54 | """
55 | Raised when a listener exception handler (see pub.setListenerExcHandler())
56 | raises an exception. The original exception is contained.
57 | """
58 |
59 | def __init__(self, badExcListenerID: str, topicObj: Topic, origExc: Exception = None):
60 | """
61 | The badExcListenerID is the name of the listener that raised
62 | the original exception that handler was attempting to handle.
63 | The topicObj is the Topic object for the topic of the
64 | sendMessage that had an exception raised.
65 | The origExc is the exception that was raised.
66 | """
67 | self.badExcListenerID = badExcListenerID
68 | import traceback
69 | self.exc = traceback.format_exc()
70 | msg = 'The exception handler registered with pubsub raised an ' \
71 | + 'exception, *while* handling an exception raised by listener ' \
72 | + ' "%s" of topic "%s"):\n%s' \
73 | % (self.badExcListenerID, topicObj.getName(), self.exc)
74 | RuntimeError.__init__(self, msg)
75 |
76 |
77 | class UnrecognizedSourceFormatError(ValueError):
78 | """
79 | Raised when a topic definition provider doesn't recognize the format
80 | of source input it was given.
81 | """
82 |
83 | def __init__(self):
84 | ValueError.__init__(self, 'Source format not recognized')
85 |
--------------------------------------------------------------------------------
/src/pubsub/core/topictreetraverser.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
5 |
6 | """
7 |
8 | from enum import IntEnum
9 | from .topicobj import Topic
10 |
11 |
12 | class ITopicTreeVisitor:
13 | """
14 | Derive from ITopicTreeVisitor and override one or more of the
15 | self._*() methods. Give an instance to an instance of
16 | TopicTreeTraverser.
17 | """
18 |
19 | def _accept(self, topicObj: Topic):
20 | """
21 | Override this to filter nodes of topic tree. Must return
22 | True (accept node) of False (reject node). Note that rejected
23 | nodes cause traversal to move to next branch (no children
24 | traversed).
25 | """
26 | return True
27 |
28 | def _startTraversal(self):
29 | """Override this to define what to do when traversal() starts."""
30 | pass
31 |
32 | def _onTopic(self, topicObj):
33 | """Override this to define what to do for each node."""
34 | pass
35 |
36 | def _startChildren(self):
37 | """
38 | Override this to take special action whenever a
39 | new level of the topic hierarchy is started (e.g., indent
40 | some output).
41 | """
42 | pass
43 |
44 | def _endChildren(self):
45 | """
46 | Override this to take special action whenever a
47 | level of the topic hierarchy is completed (e.g., dedent
48 | some output).
49 | """
50 | pass
51 |
52 | def _doneTraversal(self):
53 | """Override this to take special action when traversal done."""
54 | pass
55 |
56 |
57 | class TreeTraversal(IntEnum):
58 | """MAP is sequential through topic manager\'s topics map; the other two, through topic tree."""
59 | DEPTH, BREADTH, MAP = range(3)
60 |
61 |
62 | class TopicTreeTraverser:
63 | """
64 | Supports taking action on every topic in the topic tree. The traverse() method
65 | traverses a topic tree and calls visitor._onTopic() for each topic in the tree
66 | that satisfies visitor._accept(). Additionally it calls visitor._startChildren()
67 | whenever it starts traversing the subtopics of a topic, and
68 | visitor._endChildren() when it is done with the subtopics. Finally, it calls
69 | visitor._doneTraversal() when traversal has been completed. The visitor must
70 | therefore adhere to the ITopicTreeVisitor interface.
71 | """
72 |
73 | def __init__(self, visitor: ITopicTreeVisitor = None):
74 | """The visitor, if given, must adhere to API of ITopicTreeVisitor."""
75 | self.__handler = visitor
76 |
77 | def setVisitor(self, visitor: ITopicTreeVisitor):
78 | """The visitor must adhere to API of ITopicTreeVisitor."""
79 | self.__handler = visitor
80 |
81 | def traverse(self, topicObj: Topic, how: TreeTraversal = TreeTraversal.DEPTH, onlyFiltered: bool = True):
82 | """
83 | Start traversing tree at topicObj. Note that topicObj is a
84 | Topic object, not a topic name. The how defines if tree should
85 | be traversed breadth or depth first. If onlyFiltered is
86 | False, then all nodes are accepted (_accept(node) not called).
87 |
88 | This method can be called multiple times.
89 | """
90 | if how == TreeTraversal.MAP:
91 | raise NotImplementedError('not yet available')
92 |
93 | self.__handler._startTraversal()
94 |
95 | if how == TreeTraversal.BREADTH:
96 | self.__traverseBreadth(topicObj, onlyFiltered)
97 | else:
98 | assert how == TreeTraversal.DEPTH
99 | self.__traverseDepth(topicObj, onlyFiltered)
100 |
101 | self.__handler._doneTraversal()
102 |
103 | def __traverseBreadth(self, topicObj: Topic, onlyFiltered: bool):
104 | visitor = self.__handler
105 |
106 | def extendQueue(subtopics):
107 | topics.append(visitor._startChildren)
108 | topics.extend(subtopics)
109 | topics.append(visitor._endChildren)
110 |
111 | topics = [topicObj]
112 | while topics:
113 | topicObj = topics.pop(0)
114 |
115 | if topicObj in (visitor._startChildren, visitor._endChildren):
116 | topicObj()
117 | continue
118 |
119 | if onlyFiltered:
120 | if visitor._accept(topicObj):
121 | extendQueue(topicObj.getSubtopics())
122 | visitor._onTopic(topicObj)
123 | else:
124 | extendQueue(topicObj.getSubtopics())
125 | visitor._onTopic(topicObj)
126 |
127 | def __traverseDepth(self, topicObj: Topic, onlyFiltered: bool):
128 | visitor = self.__handler
129 |
130 | def extendStack(topicTreeStack, subtopics):
131 | topicTreeStack.insert(0, visitor._endChildren) # marker functor
132 | # put subtopics in list in alphabetical order
133 | subtopicsTmp = list(subtopics)
134 | subtopicsTmp.sort(reverse=True, key=topicObj.__class__.getName)
135 | for sub in subtopicsTmp:
136 | topicTreeStack.insert(0, sub) # this puts them in reverse order
137 | topicTreeStack.insert(0, visitor._startChildren) # marker functor
138 |
139 | topics = [topicObj]
140 | while topics:
141 | topicObj = topics.pop(0)
142 |
143 | if topicObj in (visitor._startChildren, visitor._endChildren):
144 | topicObj()
145 | continue
146 |
147 | if onlyFiltered:
148 | if visitor._accept(topicObj):
149 | extendStack(topics, topicObj.getSubtopics())
150 | visitor._onTopic(topicObj)
151 | else:
152 | extendStack(topics, topicObj.getSubtopics())
153 | visitor._onTopic(topicObj)
154 |
--------------------------------------------------------------------------------
/src/pubsub/core/topicutils.py:
--------------------------------------------------------------------------------
1 | """
2 | Various utilities used by topic-related modules.
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
6 |
7 | """
8 |
9 | from textwrap import TextWrapper, dedent
10 | import sys
11 | from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO
12 | from .topicexc import TopicNameError
13 |
14 | __all__ = []
15 |
16 | UNDERSCORE = '_' # topic name can't start with this
17 | # just want something unlikely to clash with user's topic names
18 | ALL_TOPICS = 'ALL_TOPICS'
19 |
20 |
21 | class WeakNone:
22 | """
23 | Pretend to be a weak reference to nothing. Used by ArgsInfos to
24 | refer to parent when None so no if-else blocks needed.
25 | """
26 |
27 | def __call__(self):
28 | return None
29 |
30 |
31 | def smartDedent(paragraph: str) -> str:
32 | """
33 | Dedent paragraph using textwrap.dedent(), but properly dedents
34 | even if the first line of paragraph does not contain blanks.
35 | This handles the case where a user types a documentation string as
36 | '''A long string spanning
37 | several lines.'''
38 | """
39 | if paragraph.startswith(' '):
40 | para = dedent(paragraph)
41 | else:
42 | lines = paragraph.split('\n')
43 | exceptFirst = dedent('\n'.join(lines[1:]))
44 | para = lines[0] + exceptFirst
45 |
46 | return para
47 |
48 |
49 | import re
50 |
51 | _validNameRE = re.compile(r'[-0-9a-zA-Z]\w*')
52 |
53 |
54 | def validateName(topicName: str):
55 | """Raise TopicNameError if nameTuple not valid as topic name."""
56 | topicNameTuple = tupleize(topicName)
57 | if not topicNameTuple:
58 | reason = 'name tuple must have at least one item!'
59 | raise TopicNameError(None, reason)
60 |
61 | class topic:
62 | pass
63 |
64 | for subname in topicNameTuple:
65 | if not subname:
66 | reason = 'can\'t contain empty string or None'
67 | raise TopicNameError(topicNameTuple, reason)
68 |
69 | if subname.startswith(UNDERSCORE):
70 | reason = 'must not start with "%s"' % UNDERSCORE
71 | raise TopicNameError(topicNameTuple, reason)
72 |
73 | if subname == ALL_TOPICS:
74 | reason = 'string "%s" is reserved for root topic' % ALL_TOPICS
75 | raise TopicNameError(topicNameTuple, reason)
76 |
77 | if _validNameRE.match(subname) is None:
78 | reason = 'element #%s ("%s") has invalid characters' % \
79 | (1 + list(topicNameTuple).index(subname), subname)
80 | raise TopicNameError(topicNameTuple, reason)
81 |
82 |
83 | def stringize(topicName: Sequence[str]) -> str:
84 | """
85 | If topicName is a string, just return it
86 | as is. If it is a topic definition object (ie an object that has
87 | 'msgDataSpec' as data member), return the dotted name of corresponding
88 | topic. Otherwise, assume topicName is a tuple and convert it to to a
89 | dotted name i.e. ('a','b','c') => 'a.b.c'. Empty name is not allowed
90 | (ValueError). The reverse operation is tupleize(topicName).
91 | """
92 | if isinstance(topicName, str):
93 | return topicName
94 |
95 | if hasattr(topicName, "_topicNameStr"):
96 | return topicName._topicNameStr
97 |
98 | try:
99 | name = '.'.join(topicName)
100 | except Exception:
101 | exc = sys.exc_info()[1]
102 | raise TopicNameError(topicName, str(exc))
103 |
104 | return name
105 |
106 |
107 | def tupleize(topicName: str) -> Tuple[str, ...]:
108 | """
109 | If topicName is a tuple of strings, just return it as is. Otherwise,
110 | convert it to tuple, assuming dotted notation used for topicName. I.e.
111 | 'a.b.c' => ('a','b','c'). Empty topicName is not allowed (ValueError).
112 | The reverse operation is stringize(topicNameTuple).
113 | """
114 | # assume name is most often str; if more often tuple,
115 | # then better use isinstance(name, tuple)
116 | if hasattr(topicName, "msgDataSpec"):
117 | topicName = topicName._topicNameStr
118 | if isinstance(topicName, str):
119 | topicTuple = tuple(topicName.split('.'))
120 | else:
121 | topicTuple = tuple(topicName) # assume already tuple of strings
122 |
123 | if not topicTuple:
124 | raise TopicNameError(topicTuple, "Topic name can't be empty!")
125 |
126 | return topicTuple
127 |
--------------------------------------------------------------------------------
/src/pubsub/core/weakmethod.py:
--------------------------------------------------------------------------------
1 | """
2 | This module hides the source of implementation of weak ref to a method: for Python 3.4, it is
3 | Python's weakref module; for earlier Python, it is the weakrefmethod module from PyPI.
4 | Prior to pypubsub 4.0, WeakMethod was a custom class that adhered to the WeakRef API.
5 |
6 | Use the getWeakRef(object) module function to create the
7 | proper type of weak reference (weakref.WeakRef or WeakMethod) for given object.
8 |
9 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
10 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
11 | """
12 |
13 | from inspect import ismethod
14 | from weakref import ref as WeakRef
15 |
16 | # for weakly bound methods:
17 | try:
18 | from weakref import WeakMethod
19 | except:
20 | from weakrefmethod import WeakMethod
21 |
22 | # type hinting:
23 | from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO
24 |
25 |
26 | WeakObjOrMethod = Union[WeakMethod, WeakRef]
27 |
28 | DeadRefObserver = Callable[[WeakObjOrMethod], None]
29 |
30 |
31 | def getWeakRef(obj, notifyDead: DeadRefObserver = None):
32 | """
33 | Get a weak reference to obj. If obj is a bound method, a WeakMethod
34 | object, that behaves like a WeakRef, is returned; if it is
35 | anything else a WeakRef is returned. If obj is an unbound method,
36 | a ValueError will be raised.
37 | """
38 | if ismethod(obj):
39 | createRef = WeakMethod
40 | else:
41 | createRef = WeakRef
42 |
43 | return createRef(obj, notifyDead)
44 |
--------------------------------------------------------------------------------
/src/pubsub/pub.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the main entry-point to pubsub's core functionality. The :mod:`~pubsub.pub`
3 | module supports:
4 |
5 | * messaging: publishing and receiving messages of a given topic
6 | * tracing: tracing pubsub activity in an application
7 | * trapping exceptions: dealing with "badly behaved" listeners (ie that leak exceptions)
8 | * specificatio of topic tree: defining (or just documenting) the topic tree of an
9 | application; message data specification (MDS)
10 |
11 | The recommended usage is ::
12 |
13 | from pubsub import pub
14 |
15 | // use pub functions:
16 | pub.sendMessage(...)
17 |
18 | Note that this module creates a "default" instance of
19 | pubsub.core.Publisher and binds several local functions to some of its methods
20 | and those of the pubsub.core.TopicManager instance that it contains. However, an
21 | application may create as many independent instances of Publisher as
22 | required (for instance, one in each thread; with a custom queue to mediate
23 | message transfer between threads).
24 | """
25 |
26 | """
27 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
28 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
29 | """
30 |
31 | VERSION_API = 4 #: major API version
32 |
33 | VERSION_SVN = "$Rev: 243 $".split()[1] # DO NOT CHANGE: automatically updated by VCS
34 |
35 | from typing import List
36 |
37 | from .core import (
38 | Publisher,
39 |
40 | AUTO_TOPIC,
41 |
42 | ListenerMismatchError,
43 | TopicDefnError,
44 |
45 | IListenerExcHandler,
46 | ExcHandlerError,
47 |
48 | TopicDefnError,
49 | TopicNameError,
50 | UnrecognizedSourceFormatError,
51 | SenderUnknownMsgDataError,
52 | SenderMissingReqdMsgDataError,
53 |
54 | TopicManager,
55 | ALL_TOPICS,
56 | Topic,
57 |
58 | MessageDataSpecError,
59 | exportTopicTreeSpec,
60 | TOPIC_TREE_FROM_MODULE,
61 | TOPIC_TREE_FROM_STRING,
62 | TOPIC_TREE_FROM_CLASS,
63 |
64 | TopicTreeTraverser,
65 |
66 | INotificationHandler,
67 | )
68 |
69 | __all__ = [
70 | # listener stuff:
71 | 'subscribe',
72 | 'unsubscribe',
73 | 'unsubAll',
74 | 'isSubscribed',
75 |
76 | 'isValid',
77 | 'validate',
78 | 'ListenerMismatchError',
79 | 'AUTO_TOPIC',
80 |
81 | 'IListenerExcHandler',
82 | 'getListenerExcHandler',
83 | 'setListenerExcHandler',
84 | 'ExcHandlerError',
85 |
86 | # topic stuff:
87 |
88 | 'ALL_TOPICS',
89 | 'Topic',
90 | 'topicTreeRoot',
91 | 'topicsMap',
92 | 'TopicManager',
93 | 'getDefaultTopicMgr',
94 | 'getDefaultTopicTreeRoot',
95 |
96 | # topioc defn provider stuff
97 |
98 | 'addTopicDefnProvider',
99 | 'clearTopicDefnProviders',
100 | 'getNumTopicDefnProviders',
101 | 'TOPIC_TREE_FROM_MODULE',
102 | 'TOPIC_TREE_FROM_CLASS',
103 | 'TOPIC_TREE_FROM_STRING',
104 | 'exportTopicTreeSpec',
105 | 'instantiateAllDefinedTopics',
106 |
107 | 'TopicDefnError',
108 | 'TopicNameError',
109 |
110 | 'setTopicUnspecifiedFatal',
111 |
112 | # publisher stuff:
113 |
114 | 'sendMessage',
115 |
116 | # misc:
117 |
118 | 'addNotificationHandler',
119 | 'setNotificationFlags',
120 | 'getNotificationFlags',
121 | 'clearNotificationHandlers',
122 |
123 | 'TopicTreeTraverser',
124 |
125 | ]
126 |
127 | # --------- Publisher singleton and bound methods ------------------------------------
128 |
129 | _publisher = Publisher()
130 |
131 | subscribe = _publisher.subscribe
132 | unsubscribe = _publisher.unsubscribe
133 | unsubAll = _publisher.unsubAll
134 | sendMessage = _publisher.sendMessage
135 |
136 | getListenerExcHandler = _publisher.getListenerExcHandler
137 | setListenerExcHandler = _publisher.setListenerExcHandler
138 |
139 | addNotificationHandler = _publisher.addNotificationHandler
140 | clearNotificationHandlers = _publisher.clearNotificationHandlers
141 | setNotificationFlags = _publisher.setNotificationFlags
142 | getNotificationFlags = _publisher.getNotificationFlags
143 |
144 | setTopicUnspecifiedFatal = _publisher.setTopicUnspecifiedFatal
145 |
146 |
147 | def getDefaultPublisher() -> Publisher:
148 | """
149 | Get the Publisher instance created by default when this module
150 | is imported. See the module doc for details about this instance.
151 | """
152 | return _publisher
153 |
154 |
155 | # ---------- default TopicManager instance and bound methods ------------------------
156 |
157 | _topicMgr = _publisher.getTopicMgr()
158 |
159 | topicTreeRoot = _topicMgr.getRootAllTopics()
160 | topicsMap = _topicMgr._topicsMap
161 |
162 |
163 | def isValid(listener, topicName, curriedArgNames=None) -> bool:
164 | """
165 | Return true only if listener can subscribe to messages of given topic.
166 | If curriedArgNames can be a list of parameters of the given listener, that
167 | should be assumed curried (i.e. actual listener signature is signature of
168 | given listener minus curried args).
169 | """
170 | return _topicMgr.getTopic(topicName).isValid(listener, curriedArgNames=curriedArgNames)
171 |
172 |
173 | def validate(listener, topicName, curriedArgNames=None):
174 | """
175 | Checks if listener can subscribe to topicName. If not, raises
176 | ListenerMismatchError, otherwise just returns. The curriedArgNames is
177 | same as for isValid().
178 | """
179 | _topicMgr.getTopic(topicName).validate(listener, curriedArgNames=curriedArgNames)
180 |
181 |
182 | def isSubscribed(listener, topicName) -> bool:
183 | """
184 | Returns true if listener has subscribed to topicName, false otherwise.
185 | WARNING: a false return is not a guarantee that listener won't get
186 | messages of topicName: it could receive messages of a subtopic of
187 | topicName.
188 | """
189 | return _topicMgr.getTopic(topicName).hasListener(listener)
190 |
191 |
192 | def getDefaultTopicMgr() -> TopicManager:
193 | """
194 | Get the TopicManager instance created by default when this
195 | module is imported. This function is a shortcut for
196 | ``pub.getDefaultPublisher().getTopicMgr()``.
197 | """
198 | return _topicMgr
199 |
200 |
201 | def getDefaultTopicTreeRoot() -> Topic:
202 | """Get the root of topic tree."""
203 | return _topicMgr.getRootAllTopics()
204 |
205 |
206 | addTopicDefnProvider = _topicMgr.addDefnProvider
207 | clearTopicDefnProviders = _topicMgr.clearDefnProviders
208 | getNumTopicDefnProviders = _topicMgr.getNumDefnProviders
209 |
210 |
211 | def instantiateAllDefinedTopics(provider) -> List[Topic]:
212 | """
213 | Loop over all topics of given provider and "instantiate" each topic, thus
214 | forcing a parse of the topics documentation, message data specification (MDS),
215 | comparison with parent MDS, and MDS documentation. Without this function call,
216 | an error among any of those characteristics will manifest only if the a
217 | listener is registered on it.
218 | """
219 | all_topics = []
220 | for topic_name in provider:
221 | _topicMgr.getOrCreateTopic(topic_name)
222 | all_topics.append(topic_name)
223 |
224 | return all_topics
225 |
226 | # ---------------------------------------------------------------------------
227 |
--------------------------------------------------------------------------------
/src/pubsub/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Provides utility functions and classes that are not required for using
3 | pubsub but are likely to be very useful.
4 |
5 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
6 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
7 | """
8 |
9 | from .topictreeprinter import printTreeDocs
10 |
11 | from .notification import (
12 | useNotifyByPubsubMessage,
13 | useNotifyByWriteFile,
14 | IgnoreNotificationsMixin,
15 | )
16 |
17 | from .exchandling import ExcPublisher
18 |
19 | __all__ = [
20 | 'printTreeDocs',
21 | 'useNotifyByPubsubMessage',
22 | 'useNotifyByWriteFile',
23 | 'IgnoreNotificationsMixin',
24 | 'ExcPublisher'
25 | ]
26 |
--------------------------------------------------------------------------------
/src/pubsub/utils/exchandling.py:
--------------------------------------------------------------------------------
1 | """
2 | Some utility classes for exception handling of exceptions raised
3 | within listeners:
4 |
5 | - TracebackInfo: convenient way of getting stack trace of latest
6 | exception raised. The handler can create the instance to retrieve
7 | the stack trace and then log it, present it to user, etc.
8 | - ExcPublisher: example handler that publishes a message containing
9 | traceback info
10 |
11 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
12 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
13 |
14 | """
15 |
16 | import sys, traceback
17 | from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO
18 |
19 | from ..core.listener import IListenerExcHandler
20 | from ..core.topicmgr import TopicManager
21 |
22 |
23 | class TracebackInfo:
24 | """
25 | Represent the traceback information for when an exception is
26 | raised -- but not caught -- in a listener. The complete
27 | traceback cannot be stored since this leads to circular
28 | references (see docs for sys.exc_info()) which keeps
29 | listeners alive even after the application is no longer
30 | referring to them.
31 |
32 | Instances of this object are given to listeners of the
33 | 'uncaughtExcInListener' topic as the excTraceback kwarg.
34 | The instance calls sys.exc_info() to get the traceback
35 | info but keeps only the following info:
36 |
37 | * self.ExcClass: the class of exception that was raised and not caught
38 | * self.excArg: the argument given to exception when raised
39 | * self.traceback: list of quadruples as returned by traceback.extract_tb()
40 |
41 | Normally you just need to call one of the two getFormatted() methods.
42 | """
43 |
44 | def __init__(self):
45 | tmpInfo = sys.exc_info()
46 | self.ExcClass = tmpInfo[0]
47 | self.excArg = tmpInfo[1]
48 | # for the traceback, skip the first 3 entries, since they relate to
49 | # implementation details for pubsub.
50 | tb_list = traceback.extract_tb(tmpInfo[2])
51 | IGNORE_FRAMES = 2
52 | import re
53 | assert re.search(r'pubsub.core.listener\.py', tb_list[IGNORE_FRAMES - 1][0])
54 | self.traceback = tb_list[IGNORE_FRAMES:]
55 | # help avoid circular refs
56 | del tmpInfo
57 |
58 | def getFormattedList(self) -> List[str]:
59 | """
60 | Get a list of strings as returned by the traceback module's
61 | format_list() and format_exception_only() functions.
62 | """
63 | tmp = traceback.format_list(self.traceback)
64 | tmp.extend(traceback.format_exception_only(self.ExcClass, self.excArg))
65 | return tmp
66 |
67 | def getFormattedString(self) -> str:
68 | """
69 | Get a string similar to the stack trace that gets printed
70 | to stdout by Python interpreter when an exception is not caught.
71 | """
72 | return ''.join(self.getFormattedList())
73 |
74 | def __str__(self):
75 | return self.getFormattedString()
76 |
77 |
78 | class ExcPublisher(IListenerExcHandler):
79 | """
80 | Example exception handler that simply publishes the exception traceback.
81 | The messages will have topic name given by topicUncaughtExc.
82 | """
83 |
84 | # name of the topic
85 | topicUncaughtExc = 'uncaughtExcInListener'
86 |
87 | def __init__(self, topicMgr: TopicManager = None):
88 | """
89 | If topic manager is specified, will automatically call init().
90 | Otherwise, caller must call init() after pubsub imported. See
91 | pub.setListenerExcHandler().
92 | """
93 | if topicMgr is not None:
94 | self.init(topicMgr)
95 |
96 | def init(self, topicMgr: TopicManager):
97 | """
98 | Must be called only after pubsub has been imported since this
99 | handler creates a pubsub topic.
100 | """
101 | obj = topicMgr.getOrCreateTopic(self.topicUncaughtExc)
102 | obj.setDescription('generated when a listener raises an exception')
103 | obj.setMsgArgSpec(dict(
104 | listenerStr='string representation of listener',
105 | excTraceback='instance of TracebackInfo containing exception info'))
106 | self.__topicObj = obj
107 |
108 | def __call__(self, listenerID: str, topicObj):
109 | """
110 | Handle the exception raised by given listener. Send the
111 | Traceback to all subscribers of topic self.topicUncaughtExc.
112 | """
113 | tbInfo = TracebackInfo()
114 | self.__topicObj.publish(listenerStr=listenerID, excTraceback=tbInfo)
115 |
--------------------------------------------------------------------------------
/src/pubsub/utils/misc.py:
--------------------------------------------------------------------------------
1 | """
2 | Provides useful functions and classes. Most useful are probably
3 | printTreeDocs and printTreeSpec.
4 |
5 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
6 | :license: BSD, see LICENSE_BSD_Simple.txt for details.
7 | """
8 |
9 | import sys
10 |
11 | __all__ = ('printImported', 'Callback')
12 |
13 |
14 | def printImported():
15 | """Output a list of pubsub modules imported so far"""
16 | ll = [mod for mod in sys.modules.keys() if mod.find('pubsub') >= 0] # iter keys ok
17 | ll.sort()
18 | print('\n'.join(ll))
19 |
20 |
21 | class Callback:
22 | """
23 | This can be used to wrap functions that are referenced by class
24 | data if the data should be called as a function. E.g. given
25 | >>> def func(): pass
26 | >>> class A:
27 | ....def __init__(self): self.a = func
28 | then doing
29 | >>> boo=A(); boo.a()
30 | will fail since Python will try to call a() as a method of boo,
31 | whereas a() is a free function. But if you have instead
32 | "self.a = Callback(func)", then "boo.a()" works as expected.
33 | """
34 |
35 | def __init__(self, callable_obj):
36 | self.__callable = callable_obj
37 |
38 | def __call__(self, *args, **kwargs):
39 | return self.__callable(*args, **kwargs)
40 |
--------------------------------------------------------------------------------
/src/pubsub/utils/topictreeprinter.py:
--------------------------------------------------------------------------------
1 | """
Output various aspects of topic tree to string or file.
:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
:license: BSD, see LICENSE_BSD_Simple.txt for details.
"""
from typing import TextIO
from textwrap import TextWrapper
from ..core.topictreetraverser import (ITopicTreeVisitor, TopicTreeTraverser)
class TopicTreePrinter(ITopicTreeVisitor):
"""
Example topic tree visitor that prints a prettified representation
of topic tree by doing a depth-first traversal of topic tree and
print information at each (topic) node of tree. Extra info to be
printed is specified via the 'extra' kwarg. Its value must be a
list of characters, the order determines output order:
- D: print description of topic
- a: print kwarg names only
- A: print topic kwargs and their description
- L: print listeners currently subscribed to topic
E.g. TopicTreePrinter(extra='LaDA') would print, for each topic,
the list of subscribed listeners, the topic's list of kwargs, the
topic description, and the description for each kwarg,
>>> Topic "delTopic"
>> Listeners:
> listener1_2880 (from yourModule)
> listener2_3450 (from yourModule)
>> Names of Message arguments:
> arg1
> arg2
>> Description: whenever a topic is deleted
>> Descriptions of Message arguments:
> arg1: (required) its description
> arg2: some other description
"""
allowedExtras = frozenset('DAaL') # must NOT change
ALL_TOPICS_NAME = 'ALL_TOPICS' # output for name of 'all topics' topic
def __init__(self, extra=None, width: int = 70, indentStep: int = 4,
bulletTopic: str = '\\--', bulletTopicItem: str = '|==', bulletTopicArg: str = '-',
fileObj: TextIO = None):
"""
Topic tree printer will print listeners for each topic only
if printListeners is True. The width will be used to limit
the width of text output, while indentStep is the number of
spaces added each time the text is indented further. The
three bullet parameters define the strings used for each
item (topic, topic items, and kwargs).
"""
self.__contentMeth = dict(
D=self.__printTopicDescription,
A=self.__printTopicArgsAll,
a=self.__printTopicArgNames,
L=self.__printTopicListeners)
assert self.allowedExtras == set(self.__contentMeth.keys())
import sys
self.__destination = fileObj or sys.stdout
self.__output = []
self.__content = extra or ''
unknownSel = set(self.__content) - self.allowedExtras
if unknownSel:
msg = 'These extra chars not known: %s' % ','.join(unknownSel)
raise ValueError(msg)
self.__width = width
self.__wrapper = TextWrapper(width)
self.__indent = 0
self.__indentStep = indentStep
self.__topicsBullet = bulletTopic
self.__topicItemsBullet = bulletTopicItem
self.__topicArgsBullet = bulletTopicArg
def getOutput(self):
return '\n'.join(self.__output)
def _doneTraversal(self):
if self.__destination is not None:
self.__destination.write(self.getOutput())
def _onTopic(self, topicObj):
"""This gets called for each topic. Print as per specified content."""
# topic name
self.__wrapper.width = self.__width
indent = self.__indent
if topicObj.isAll():
topicName = self.ALL_TOPICS_NAME
else:
topicName = topicObj.getNodeName()
head = '%s Topic "%s"' % (self.__topicsBullet, topicName)
self.__output.append(self.__formatDefn(indent, head))
indent += self.__indentStep
# each extra content (assume constructor verified that chars are valid)
for item in self.__content:
function = self.__contentMeth[item]
function(indent, topicObj)
def _startChildren(self):
"""Increase the indent"""
self.__indent += self.__indentStep
def _endChildren(self):
"""Decrease the indent"""
self.__indent -= self.__indentStep
def __formatDefn(self, indent, item, defn='', sep=': '):
"""
Print a definition: a block of text at a certain indent,
has item name, and an optional definition separated from
item by sep.
"""
if defn:
prefix = '%s%s%s' % (' ' * indent, item, sep)
self.__wrapper.initial_indent = prefix
self.__wrapper.subsequent_indent = ' ' * (indent + self.__indentStep)
return self.__wrapper.fill(defn)
else:
return '%s%s' % (' ' * indent, item)
def __printTopicDescription(self, indent, topicObj):
# topic description
defn = '%s Description' % self.__topicItemsBullet
self.__output.append(
self.__formatDefn(indent, defn, topicObj.getDescription()))
def __printTopicArgsAll(self, indent, topicObj, desc=True):
# topic kwargs
args = topicObj.getArgDescriptions()
if args:
# required, optional, complete = topicObj.getArgs()
headName = 'Names of Message arguments:'
if desc:
headName = 'Descriptions of message arguments:'
head = '%s %s' % (self.__topicItemsBullet, headName)
self.__output.append(self.__formatDefn(indent, head))
tmpIndent = indent + self.__indentStep
required = topicObj.getArgs()[0]
for key, arg in args.items(): # iter in 3, list in 2 ok
if not desc:
arg = ''
elif key in required:
arg = '(required) %s' % arg
msg = '%s %s' % (self.__topicArgsBullet, key)
self.__output.append(self.__formatDefn(tmpIndent, msg, arg))
def __printTopicArgNames(self, indent, topicObj):
self.__printTopicArgsAll(indent, topicObj, False)
def __printTopicListeners(self, indent, topicObj):
if topicObj.hasListeners():
item = '%s Listeners:' % self.__topicItemsBullet
self.__output.append(self.__formatDefn(indent, item))
tmpIndent = indent + self.__indentStep
for listener in topicObj.getListenersIter():
item = '%s %s (from %s)' % (self.__topicArgsBullet, listener.name(), listener.module())
self.__output.append(self.__formatDefn(tmpIndent, item))
def printTreeDocs(rootTopic=None, topicMgr=None, **kwargs):
"""
Print out the topic tree to a file (or file-like object like a
StringIO), starting at rootTopic. If root topic should be root of
whole tree, get it from pub.getDefaultTopicTreeRoot().
The treeVisitor is an instance of pub.TopicTreeTraverser.
Printing the tree docs would normally involve this::
from pubsub import pub
from pubsub.utils.topictreeprinter import TopicTreePrinter
traverser = pub.TopicTreeTraverser( TopicTreePrinter(**kwargs) )
traverser.traverse( pub.getDefaultTopicTreeRoot() )
With printTreeDocs, it looks like this::
from pubsub import pub
from pubsub.utils import printTreeDocs
printTreeDocs()
The kwargs are the same as for TopicTreePrinter constructor:
extra(None), width(70), indentStep(4), bulletTopic, bulletTopicItem,
bulletTopicArg, fileObj(stdout). If fileObj not given, stdout is used.
"""
if rootTopic is None:
if topicMgr is None:
from .. import pub
topicMgr = pub.getDefaultTopicMgr()
rootTopic = topicMgr.getRootAllTopics()
printer = TopicTreePrinter(**kwargs)
traverser = TopicTreeTraverser(printer)
traverser.traverse(rootTopic)
--------------------------------------------------------------------------------
/tests/perf.py:
--------------------------------------------------------------------------------
1 | """
2 | Measure performance of pubsub, so that impact of proposed performance enhancing
3 | algorithms can be proven. Measure with
4 |
5 | python -m timeit -n1 "import perf; perf.runTest()"
6 |
7 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
8 | :license: BSD, see LICENSE.txt for details.
9 | """
10 |
11 | from pathlib import Path
12 | from time import perf_counter
13 | from typing import Tuple
14 |
15 | from pubsub import pub
16 | from pubsub.core import Listener
17 |
18 |
19 | topicMgr = pub.getDefaultTopicMgr()
20 |
21 |
22 | def loop_subscribe(topic_names, listeners):
23 | topicMgr = pub.getDefaultTopicMgr()
24 | topicMgr.clearTree()
25 |
26 | num_samples = 1000
27 | start = perf_counter()
28 | for count in range(num_samples):
29 | for listener in listeners:
30 | for topic_name in topic_names:
31 | pub.subscribe(listener, topic_name)
32 |
33 | tot_time = (perf_counter() - start)
34 |
35 | return round(tot_time, 2)
36 |
37 |
38 | def perf_subscribe():
39 | """
40 | Subscription involves inspecting listener to (upon first subscription to a topic) create the
41 | topic and its MDS or (upon subsequent subscriptions to same topic) verify its signature is
42 | compatible with topic MDS, then adding listener to topic listeners, and adding topic to
43 | topics mapping (of name to topic object). This suggests that complexity of signature and
44 | complexity of topic name are the two axes of performance for subscriptions, as the
45 | creation of a topic is not likely to be nearly as common as subscriptions to existing topics.
46 |
47 | Results indicate that deeper topics and listeners have only marginal (ie insignificant)
48 | overhead over shallow topics and listeners:
49 | - root topic and no-args listeners: 6.77
50 | - deep topics and no-args listeners: 6.83
51 | - root topics and many-args listeners: 6.8
52 | - deep topics and many-args listeners: 6.83
53 | """
54 | print("-"*40)
55 | print("Performance measurement for subscribing:")
56 |
57 | num_topics = 100
58 | num_listeners = 10
59 |
60 | # root topic and no-args listeners:
61 | topic_names = ['some_topic_' + str(topic_index) for topic_index in range(num_topics)]
62 | listeners = [lambda: n for n in range(num_listeners)]
63 | print('root topic and no-args listeners:', loop_subscribe(topic_names, listeners))
64 |
65 | # deep topics and no-args listeners:
66 | topic_names = ['rt.st.sst.ssst.leaf_topic_' + str(topic_index) for topic_index in range(num_topics)]
67 | print('deep topics and no-args listeners:', loop_subscribe(topic_names, listeners))
68 |
69 | # root topics and many-args listeners:
70 | topic_names = ['some_topic_' + str(topic_index) for topic_index in range(num_topics)]
71 | listeners = [lambda x, y, z, a, b, c: n for n in range(num_listeners)]
72 | print('root topics and many-args listeners:', loop_subscribe(topic_names, listeners))
73 |
74 | # deep topics and many-args listeners:
75 | topic_names = ['rt.st.sst.ssst.leaf_topic_' + str(topic_index) for topic_index in range(num_topics)]
76 | print('deep topics and many-args listeners:', loop_subscribe(topic_names, listeners))
77 |
78 |
79 | def loop_send(subscriptions: Tuple[Listener, str], messages):
80 | topicMgr = pub.getDefaultTopicMgr()
81 | topicMgr.clearTree()
82 |
83 | for listener, topic_name in subscriptions:
84 | pub.subscribe(listener, topic_name)
85 |
86 | num_samples = 1000
87 | start = perf_counter()
88 | for count in range(num_samples):
89 | for topic_name, kwargs in messages:
90 | pub.sendMessage(topic_name, **kwargs)
91 |
92 | tot_time = (perf_counter() - start)
93 |
94 | return round(tot_time, 2)
95 |
96 |
97 | def perf_send():
98 | """
99 | Sending message involves calling each listener with data, and going up the tree to root actor
100 | each level has fewer data so data must be filtered out.
101 |
102 | ----------------------------------------
103 | Performance measurement for sending:
104 | listeners ['obs1', 'obs2', 'obs3', 'obs4', 'obs5', 'obs6', 'obs7', 'obs8']
105 | topic names ['t1', 't1.t2', ..., 't1.t2.t3.t4.t5.t6.t7.t8']
106 | with depth 8: with depth 8: % less X faster
107 | 8 data 9.31 8 data 4.76 49 2.0
108 | 4 data 8.48 4 data 4.48 47 1.9
109 | 2 data 8.23 2 data 4.5 45 1.8
110 | 1 data 8.26 1 data 4.31 48 1.9
111 | no data 8.05 no data 4.27 47 1.8
112 | with depth 4: with depth 4:
113 | 4 data 4.4 4 data 2.63 40 1.7
114 | 2 data 3.96 2 data 2.56 35 1.5
115 | 1 data 3.99 1 data 2.51 37 1.6
116 | no data 4.1 no data 2.43 40 1.7
117 | with depth 2: with depth 2:
118 | 2 data 2.28 2 data 1.61 29 1.4
119 | 1 data 1.84 1 data 1.57 15 1.2
120 | no data 2.18 no data 1.52 30 1.4
121 |
122 | with depth 1: with depth 1:
123 | 1 data 1.2 1 data 1.1 8 1.1
124 | no data 1.24 no data 1.05 15 1.2
125 | """
126 | print("-"*40)
127 | print("Performance measurement for sending:")
128 |
129 | # root topic and no-args listeners:
130 | def obs1(arg1=None): pass
131 | def obs2(arg1=None, arg2=None): pass
132 | def obs3(arg1=None, arg2=None, arg3=None): pass
133 | def obs4(arg1=None, arg2=None, arg3=None, arg4=None): pass
134 | def obs5(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None): pass
135 | def obs6(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None): pass
136 | def obs7(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None, arg7=None): pass
137 | def obs8(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None, arg7=None, arg8=None): pass
138 |
139 | local_objs = locals().copy()
140 | listeners = [local_objs['obs' + str(n)] for n in range(1, 9)]
141 | print('listeners', [cb.__name__ for cb in listeners])
142 |
143 | topic_names = ['t1']
144 | for index in range(2, 9):
145 | topic_names.append(topic_names[-1] + ".t" + str(index))
146 | print('topic names', topic_names)
147 |
148 | num_messages = 100
149 |
150 | def sub_test(topic_names):
151 | subscriptions = [(obs, name) for obs, name in zip(listeners, topic_names)]
152 | num_topics = len(topic_names)
153 | num_subs = len(subscriptions)
154 |
155 | print('with depth {}:'.format(num_topics))
156 |
157 | if len(topic_names) >= 8:
158 | msg_data = dict(arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6, arg7=7, arg8=8)
159 | messages = [(topic_names[-1], msg_data)] * num_messages
160 | print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages))
161 |
162 | if len(topic_names) >= 4:
163 | msg_data = dict(arg1=1, arg2=2, arg3=3, arg4=4)
164 | messages = [(topic_names[-1], msg_data)] * num_messages
165 | print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages))
166 |
167 | if len(topic_names) >= 2:
168 | msg_data = dict(arg1=1, arg2=2)
169 | messages = [(topic_names[-1], msg_data)] * num_messages
170 | print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages))
171 |
172 | if len(topic_names) >= 1:
173 | msg_data = dict(arg1=1)
174 | messages = [(topic_names[-1], msg_data)] * num_messages
175 | print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages))
176 |
177 | messages = [(topic_names[-1], {})] * num_messages
178 | print(' {} data'.format('no'), loop_send(subscriptions, messages))
179 |
180 | sub_test(topic_names)
181 | sub_test(topic_names[:4])
182 | sub_test(topic_names[:2])
183 | sub_test(topic_names[:1])
184 |
185 |
186 | if __name__ == '__main__':
187 | perf_subscribe()
188 | perf_send()
--------------------------------------------------------------------------------
/tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | python_files = test*.py
3 |
--------------------------------------------------------------------------------
/tests/suite/my_import_topics.py:
--------------------------------------------------------------------------------
1 | class root_topic_1:
2 | """root topic 1"""
3 |
4 | class subtopic_2:
5 | "subtopic 2"
6 |
7 | class subsubtopic_21:
8 | """Sub sub topic 1 of subtopic 2."""
9 | def msgDataSpec(arg1, arg2=None, arg4=None):
10 | """
11 | - arg4: doc for arg4
12 | """
13 | pass
14 |
15 |
16 | class root_topic_2:
17 | 'docs for root_topic_2'
18 |
19 | class subtopic_21:
20 | 'docs for subtopic_21'
21 |
--------------------------------------------------------------------------------
/tests/suite/raisinglistener.py:
--------------------------------------------------------------------------------
1 | def getRaisingListener():
2 | def raisingListener():
3 | def nested():
4 | raise RuntimeError2('test')
5 | nested()
6 |
7 | return raisingListener
--------------------------------------------------------------------------------
/tests/suite/test2_spec.py:
--------------------------------------------------------------------------------
1 | """
2 | Test topicargspec.
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE.txt for details.
6 | """
7 |
8 | import pytest
9 |
10 | from pubsub.core.topicargspec import (
11 | ArgsInfo,
12 | ArgSpecGiven,
13 | SenderMissingReqdMsgDataError
14 | )
15 | from pubsub.core import SenderUnknownMsgDataError
16 |
17 |
18 | #class Test2_specTestCase:
19 | #def setUp(self):
20 | # self.foo = Test2_spec()
21 | #
22 |
23 | #def tearDown(self):
24 | # self.foo.dispose()
25 | # self.foo = None
26 |
27 | def test_create():
28 | # root
29 | td1 = ArgSpecGiven( dict() )
30 | ai1 = ArgsInfo(('t1',), td1, None)
31 | assert ai1.isComplete()
32 | assert ai1.numArgs() == 0
33 | assert ai1.getArgs() == ()
34 | assert ai1.getCompleteAI() is ai1
35 |
36 | # sub, complete
37 | td2 = ArgSpecGiven(
38 | argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'),
39 | reqdArgs = ('arg2',))
40 | ai2 = ArgsInfo(('t1','st1'), td2, ai1)
41 | assert ai2.isComplete()
42 | assert ai2.numArgs() == 2
43 | assert ai2.getArgs() == ('arg1', 'arg2')
44 | assert ai2.getCompleteAI() is ai2
45 |
46 | # sub, missing
47 | td2.argsSpecType = ArgSpecGiven.SPEC_GIVEN_NONE
48 | ai4 = ArgsInfo(('t1','st3'), td2, ai1)
49 | assert not ai4.isComplete()
50 | assert ai4.numArgs() == 0
51 | assert ai4.getArgs() == ()
52 | assert ai4.getCompleteAI() is ai1
53 |
54 | # sub, of incomplete spec, given ALL args
55 | td3 = ArgSpecGiven(
56 | argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'),
57 | reqdArgs = ('arg2',))
58 | ai5 = ArgsInfo(('t1','st3','sst1'), td3, ai4)
59 | assert ai5.isComplete()
60 | assert ai5.numArgs() == 2
61 | assert ai5.hasSameArgs('arg1', 'arg2')
62 | assert ai5.getCompleteAI() is ai5
63 |
64 | def test_update():
65 | td1 = ArgSpecGiven( dict() )
66 | td2 = ArgSpecGiven()
67 | td4 = ArgSpecGiven()
68 | td5 = ArgSpecGiven(
69 | argsDocs = dict(
70 | arg1='doc for arg1', arg2='doc for arg2',
71 | arg3='doc for arg3', arg4='doc for arg4'),
72 | reqdArgs = ('arg4','arg2'))
73 |
74 | ai1 = ArgsInfo(('t1',), td1, None) # root, complete
75 | ai2 = ArgsInfo(('t1','st1'), td2, ai1) # sub 1, empty
76 | ai4 = ArgsInfo(('t1','st1','sst2'), td4, ai2) # empty sub of sub 1
77 | ai5 = ArgsInfo(('t1','st1','sst3'), td5, ai2) # completed sub of sub 1
78 |
79 | # check assumptions before we start:
80 | assert not ai2.isComplete()
81 | assert not ai4.isComplete()
82 | assert ai5.isComplete()
83 | assert ai2.numArgs() == 0
84 | assert ai4.numArgs() == 0
85 | assert ai5.numArgs() == 4
86 |
87 | # pretend we have an update for ai2: all args now available
88 | ai2.updateAllArgsFinal( ArgSpecGiven(
89 | dict(arg1='doc for arg1', arg2='doc for arg2'),
90 | ('arg2',)) )
91 | assert ai2.isComplete()
92 | assert ai2.numArgs() == 2
93 | assert ai2.hasSameArgs('arg1', 'arg2')
94 | assert ai2.getCompleteAI() is ai2
95 |
96 | assert not ai4.isComplete()
97 |
98 | assert ai2.numArgs() == 2
99 | assert ai4.numArgs() == 0
100 | assert ai5.numArgs() == 4
101 |
102 | assert ai4.getCompleteAI() is ai2
103 |
104 | assert ai2.hasSameArgs('arg1', 'arg2')
105 | assert ai5.hasSameArgs('arg1', 'arg2', 'arg3', 'arg4')
106 |
107 | def test_filter():
108 | td = ArgSpecGiven(
109 | argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'),
110 | reqdArgs = ('arg2',))
111 | ai = ArgsInfo(('t1',), td, None)
112 |
113 | # check:
114 | argsMissingReqd = {}
115 | pytest.raises(SenderMissingReqdMsgDataError, ai.check, argsMissingReqd)
116 |
117 | argsExtraOpt = dict(arg2=2, arg5=5)
118 | pytest.raises(SenderUnknownMsgDataError, ai.check, argsExtraOpt)
119 |
120 | args = dict(arg1=1, arg2=2)
121 | ai.check(args)
122 |
123 | # filter:
124 | msgArgs = dict(arg1=1, arg2=2)
125 | argsOK = msgArgs.copy()
126 | assert ai.filterArgs( msgArgs ) == argsOK
127 | msgArgs.update(arg3=3, arg4=4)
128 | assert ai.filterArgs( msgArgs ) == argsOK
129 |
130 |
--------------------------------------------------------------------------------
/tests/suite/test2a_topic.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import pytest
9 |
10 | from pubsub.core.topicobj import Topic
11 | from pubsub.core.topicmgr import TreeConfig
12 | from pubsub.core.topicutils import ALL_TOPICS
13 | from pubsub.core.topicargspec import ArgsInfo, ArgSpecGiven
14 | from pubsub.core.listener import ListenerMismatchError
15 | from pubsub.core.topicexc import MessageDataSpecError
16 |
17 |
18 |
19 | rootTopic = None
20 | treeConfig = TreeConfig()
21 |
22 |
23 | def test_CreateRoot():
24 | #
25 | # Test create and then modify state of a topic object
26 | #
27 |
28 | nameTuple = ('root',)
29 | description = 'root description'
30 | msgArgsInfo = None
31 |
32 | # when parent is None, only nameTuple=ALL_TOPICS is allowed, thereby
33 | # guaranteeing that only one tree root can be created
34 | pytest.raises(ValueError, Topic, treeConfig, nameTuple, description, msgArgsInfo)
35 |
36 | # create the ALL TOPICS topic; it has no message args
37 | nameTuple = (ALL_TOPICS,)
38 | argSpec = ArgSpecGiven( dict() )
39 | msgArgsInfo = ArgsInfo(nameTuple, argSpec, None)
40 | obj = Topic(treeConfig, nameTuple, description, msgArgsInfo)
41 |
42 | # verify its state is as expected after creation:
43 | assert obj.getListeners() == []
44 | assert obj.getNumListeners() == 0
45 | assert obj.hasListeners() == False
46 |
47 | def listener1(): pass
48 | def listener2(): pass
49 | def badListener1(arg1): pass # extra required arg
50 | def listener3(arg1=None): pass # extra is optional
51 | assert obj.isValid(listener1)
52 | assert not obj.isValid(badListener1)
53 | assert obj.isValid(listener3)
54 |
55 | global rootTopic
56 | rootTopic = obj
57 |
58 |
59 | def test_SubUnsub():
60 | #
61 | # Test subscription and unsubscription of listeners
62 | #
63 |
64 | def listener1(): pass
65 | def listener2(): pass
66 | obj = rootTopic
67 |
68 | # now modify its state by subscribing listeners
69 | obj.subscribe(listener1)
70 | obj.subscribe(listener2)
71 |
72 | obj.hasListener(listener1)
73 | obj.hasListener(listener2)
74 | assert obj.hasListeners() == True
75 | assert set(obj.getListenersIter()) == set([listener1, listener2])
76 | assert obj.getNumListeners() == 2
77 |
78 | # try to subscribe an invalid listener
79 | def badListener(arg1): pass # extra required arg
80 | pytest.raises(ListenerMismatchError, obj.subscribe, badListener)
81 |
82 | # try unsubscribe
83 | obj.unsubscribe(listener1)
84 | assert obj.hasListeners()
85 | assert obj.getListeners() == [listener2]
86 | assert obj.getNumListeners() == 1
87 |
88 | # try unsubscribe all, with filtering
89 | obj.subscribe(listener1)
90 | def listener3(): pass
91 | obj.subscribe(listener3)
92 | assert obj.getNumListeners() == 3
93 | def ff(listener):
94 | # use != since it is defined in terms of ==; also, put listener
95 | # on RHS to verify works even when Listener used on RHS
96 | return listener2 != listener
97 | obj.unsubscribeAllListeners(filter=ff)
98 | assert obj.getNumListeners() == 1
99 | assert obj.getListeners() == [listener2]
100 | obj.subscribe(listener1)
101 | obj.subscribe(listener3)
102 | assert obj.getNumListeners() == 3
103 | obj.unsubscribeAllListeners()
104 | assert obj.getNumListeners() == 0
105 |
106 |
107 | def test_CreateChild():
108 | #
109 | # Test creation of a child topic, subscription of listeners
110 | #
111 |
112 | nameTuple = ('childOfAll',)
113 | description = 'child description'
114 | argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc')
115 | reqdArgs = ('arg2',)
116 | argSpec = ArgSpecGiven(argsDocs=argsDocs, reqdArgs = reqdArgs)
117 | msgArgsInfo = ArgsInfo(nameTuple, argSpec, rootTopic._getListenerSpec())
118 | parent = Topic(treeConfig, nameTuple, description, msgArgsInfo, parent=rootTopic)
119 | assert parent.getParent() is rootTopic
120 |
121 | # now create a child of child with wrong arguments so we can test exceptions
122 | nameTuple = ('childOfAll', 'grandChild')
123 | description = 'grandchild description'
124 |
125 | def tryCreate(ad, r):
126 | argSpec = ArgSpecGiven(argsDocs=ad, reqdArgs = r)
127 | msgArgsInfo = ArgsInfo(nameTuple, argSpec, parent._getListenerSpec())
128 | obj = Topic(treeConfig, nameTuple, description, msgArgsInfo, parent=parent)
129 |
130 | # test when all OK
131 | argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc')
132 | reqdArgs = ('arg2',)
133 | tryCreate(argsDocs, reqdArgs)
134 | # test when requiredArg wrong
135 | reqdArgs = ('arg3',)
136 | pytest.raises(MessageDataSpecError, tryCreate, argsDocs, reqdArgs)
137 | reqdArgs = ()
138 | pytest.raises(MessageDataSpecError, tryCreate, argsDocs, reqdArgs)
139 | # test when missing opt arg
140 | argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc')
141 | reqdArgs = ('arg2',)
142 |
143 |
--------------------------------------------------------------------------------
/tests/suite/test2c_notify.py:
--------------------------------------------------------------------------------
1 | """
2 | This one tests NotifyByWriteFile and custom notification handler
3 |
4 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
5 | :license: BSD, see LICENSE.txt for details.
6 |
7 | """
8 |
9 | import io
10 | import gc
11 | from difflib import unified_diff
12 |
13 | # setup notification and logging
14 | from pubsub import pub
15 | from pubsub.utils.notification import useNotifyByWriteFile
16 | from pubsub.core import INotificationHandler
17 |
18 | topicMgr = pub.getDefaultTopicMgr()
19 |
20 | def captureStdout():
21 | capture = io.StringIO()
22 | useNotifyByWriteFile( fileObj = capture )
23 | return capture
24 |
25 |
26 | def testNotifyByPrint():
27 | capture = captureStdout()
28 |
29 | def listener1(arg1):
30 | pass
31 | pub.subscribe(listener1, 'baz')
32 | pub.sendMessage('baz', arg1=123)
33 | pub.unsubscribe(listener1, 'baz')
34 |
35 | def doa():
36 | def listener2():
37 | pass
38 | pub.subscribe(listener2, 'bar')
39 | doa() # listener2 should be gc'd
40 | gc.collect() # for pypy: the gc doesn't work the same as cpython's
41 |
42 | topicMgr.delTopic('baz')
43 |
44 | expect = """\
45 | PUBSUB: New topic "baz" created
46 | PUBSUB: Subscribed listener "listener1" to topic "baz"
47 | PUBSUB: Start sending message of topic "baz"
48 | PUBSUB: Sending message of topic "baz" to listener listener1
49 | PUBSUB: Done sending message of topic "baz"
50 | PUBSUB: Unsubscribed listener "listener1" from topic "baz"
51 | PUBSUB: New topic "bar" created
52 | PUBSUB: Subscribed listener "listener2" to topic "bar"
53 | PUBSUB: Listener "listener2" of Topic "bar" has died
54 | PUBSUB: Topic "baz" destroyed
55 | """
56 | captured = capture.getvalue()
57 | #print captured
58 | #print repr(expect)
59 | assert captured == expect, \
60 | '\n'.join( unified_diff(expect.splitlines(), captured.splitlines(), n=0) )
61 |
62 |
63 | def testFlagChanges():
64 | savedFlags = pub.getNotificationFlags()
65 |
66 | pub.setNotificationFlags(all=True, sendMessage=False, deadListener=False)
67 | flags = pub.getNotificationFlags()
68 | assert not flags['sendMessage']
69 | assert not flags['deadListener']
70 | assert flags['newTopic']
71 | assert flags['delTopic']
72 | assert flags['subscribe']
73 | assert flags['unsubscribe']
74 |
75 | pub.setNotificationFlags(subscribe=False, deadListener=True)
76 | flags = pub.getNotificationFlags()
77 | assert not flags['sendMessage']
78 | assert not flags['subscribe']
79 | assert flags['newTopic']
80 | assert flags['delTopic']
81 | assert flags['deadListener']
82 | assert flags['unsubscribe']
83 |
84 | pub.setNotificationFlags(all=False, subscribe=True, unsubscribe=True)
85 | flags = pub.getNotificationFlags()
86 | assert not flags['sendMessage']
87 | assert not flags['deadListener']
88 | assert not flags['newTopic']
89 | assert not flags['delTopic']
90 | assert flags['subscribe']
91 | assert flags['unsubscribe']
92 |
93 | pub.setNotificationFlags(** savedFlags)
94 |
95 |
96 | def testNotifications():
97 | class Handler(INotificationHandler):
98 | def __init__(self):
99 | self.resetCounts()
100 | def resetCounts(self):
101 | self.counts = dict(send=0, sub=0, unsub=0, delt=0, newt=0, dead=0, all=0)
102 | def notifySubscribe(self, pubListener, topicObj, newSub):
103 | self.counts['sub'] += 1
104 | def notifyUnsubscribe(self, pubListener, topicObj):
105 | self.counts['unsub'] += 1
106 | def notifyDeadListener(self, pubListener, topicObj):
107 | self.counts['dead'] += 1
108 | def notifySend(self, stage, topicObj, pubListener=None):
109 | if stage == 'pre': self.counts['send'] += 1
110 | def notifyNewTopic(self, topicObj, description, required, argsDocs):
111 | self.counts['newt'] += 1
112 | def notifyDelTopic(self, topicName):
113 | self.counts['delt'] += 1
114 |
115 | notifiee = Handler()
116 | pub.addNotificationHandler(notifiee)
117 | pub.setNotificationFlags(all=True)
118 |
119 | def verify(**ref):
120 | gc.collect() # for pypy: the gc doesn't work the same as cpython's
121 | for key, val in notifiee.counts.items():
122 | if key in ref:
123 | assert val == ref[key], "\n%s\n%s" % (notifiee.counts, ref)
124 | else:
125 | assert val == 0, "%s = %s, expected 0" % (key, val)
126 | notifiee.resetCounts()
127 |
128 | verify()
129 | def testListener(): pass
130 | def testListener2(): pass
131 | def testListener3(): pass
132 | class TestListener:
133 | def __call__(self): pass
134 | def __del__(self): pass
135 | testListener = TestListener()
136 |
137 | topicMgr = pub.getDefaultTopicMgr()
138 | topicMgr.getOrCreateTopic('newTopic')
139 | verify(newt=1)
140 |
141 | pub.subscribe(testListener, 'newTopic')
142 | pub.subscribe(testListener2, 'newTopic')
143 | pub.subscribe(testListener3, 'newTopic')
144 | verify(sub=3)
145 |
146 | pub.sendMessage('newTopic')
147 | verify(send=1)
148 |
149 | verify(dead=0)
150 | del testListener
151 | del testListener3
152 | verify(dead=2)
153 |
154 | pub.unsubscribe(testListener2,'newTopic')
155 | verify(unsub=1)
156 |
157 | topicMgr.delTopic('newTopic')
158 | verify(delt=1)
159 |
--------------------------------------------------------------------------------
/tests/suite/test2c_notify2.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | from pubsub import pub
9 | from pubsub.utils.notification import useNotifyByPubsubMessage
10 | topicMgr = pub.getDefaultTopicMgr()
11 |
12 |
13 | def test_NotificationTopics():
14 | assert not topicMgr.getTopic('pubsub', okIfNone=True)
15 | useNotifyByPubsubMessage()
16 | assert topicMgr.getTopic('pubsub')
17 |
18 | assert topicMgr.getTopic('pubsub').hasSubtopic()
19 |
20 | pubsubTopicNames = [obj.getName() for obj in topicMgr.getTopic('pubsub').getSubtopics()]
21 | assert (
22 | set( pubsubTopicNames ) ==
23 | set(['pubsub.sendMessage', 'pubsub.deadListener',
24 | 'pubsub.subscribe', 'pubsub.unsubscribe',
25 | 'pubsub.newTopic', 'pubsub.delTopic']))
26 |
27 |
28 | def test_SubscribeNotify():
29 | class MyListener:
30 | countSub = 0
31 | countUnsub = 0
32 | def listenerSub(self, msgTopic=pub.AUTO_TOPIC, listener=None, topic=None, newSub=None):
33 | assert msgTopic.getName() == 'pubsub.subscribe'
34 | assert topic.getName() in ('pubsub.unsubscribe', 'testSubscribeNotify')
35 | if newSub:
36 | self.countSub += 1
37 | def listenerUnsub(self, msgTopic=pub.AUTO_TOPIC, topic=None, listener=None, listenerRaw=None):
38 | assert topic.getName() in ('testSubscribeNotify', 'pubsub.subscribe' )
39 | assert msgTopic.getName() == 'pubsub.unsubscribe'
40 | if listener is not None:
41 | self.countUnsub += 1
42 | def listenerTest(self):
43 | raise NotImplementedError # should never get here
44 |
45 | pub.setNotificationFlags(subscribe=True, unsubscribe=True)
46 | topicMgr.getOrCreateTopic('testSubscribeNotify')
47 | tmp = MyListener()
48 |
49 | pub.subscribe(tmp.listenerSub, 'pubsub.subscribe')
50 | assert tmp.countSub == 0 # don't notify of self subscription
51 | assert tmp.countUnsub == 0
52 | sl, ok = pub.subscribe(tmp.listenerUnsub, 'pubsub.unsubscribe')
53 | assert ok
54 | assert tmp.countSub == 1
55 | assert tmp.countUnsub == 0
56 |
57 | pub.subscribe(tmp.listenerTest, 'testSubscribeNotify')
58 | #assert_equal(tmp.countSub, 2)
59 | assert tmp.countUnsub == 0
60 | pub.unsubscribe(tmp.listenerTest, 'testSubscribeNotify')
61 | #assert_equal(tmp.countSub, 2)
62 | assert tmp.countUnsub == 1
63 |
64 | pub.unsubscribe(tmp.listenerSub, 'pubsub.subscribe')
65 | assert tmp.countSub == 2
66 | assert tmp.countUnsub == 2
67 | pub.unsubscribe(tmp.listenerUnsub, 'pubsub.unsubscribe')
68 | assert tmp.countSub == 2
69 | assert tmp.countUnsub == 2 # don't notify of self unsubscription
70 |
71 |
72 | def test_SendNotify():
73 | # trap the pubsub.sendMessage topic:
74 | class SendHandler:
75 | def __init__(self):
76 | self.pre = self.post = 0
77 | def __call__(self, topic=None, stage=None, listener=None, msgTopic=pub.AUTO_TOPIC):
78 | if stage == 'pre':
79 | self.pre += 1
80 | else:
81 | self.post += 1
82 | assert msgTopic.getName() == 'pubsub.sendMessage'
83 | assert topic.getName() == 'testSendNotify'
84 | sh = SendHandler()
85 | pub.subscribe(sh, 'pubsub.sendMessage')
86 | pub.setNotificationFlags(sendMessage=True)
87 |
88 | # generate a message that will cause pubsub.sendMessage to be generated too
89 | assert sh.pre == 0
90 | assert sh.post == 0
91 | topicMgr.getOrCreateTopic('testSendNotify')
92 | pub.sendMessage('testSendNotify')
93 | assert sh.pre == 1
94 | assert sh.post == 1
95 |
96 |
97 |
--------------------------------------------------------------------------------
/tests/suite/test2d_except.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import gc
9 |
10 | import pytest
11 |
12 | from pubsub import pub
13 |
14 | topicMgr = pub.getDefaultTopicMgr()
15 |
16 |
17 | def throws():
18 | raise RuntimeError('test')
19 |
20 |
21 | def testHandleExcept1a():
22 | from pubsub.utils.exchandling import ExcPublisher
23 | excPublisher = ExcPublisher( pub.getDefaultTopicMgr() )
24 | pub.setListenerExcHandler(excPublisher)
25 |
26 | # create a listener that raises an exception:
27 | from raisinglistener import getRaisingListener
28 | raisingListener = getRaisingListener()
29 |
30 | pub.setNotificationFlags(all=False)
31 | pub.subscribe(raisingListener, 'testHandleExcept1a')
32 |
33 | # first test when a listener raises an exception and exception listener also raises!
34 | class BadUncaughtExcListener:
35 | def __call__(self, listenerStr=None, excTraceback=None):
36 | raise RuntimeError('bad exception listener!')
37 | handler = BadUncaughtExcListener()
38 | pub.subscribe(handler, ExcPublisher.topicUncaughtExc)
39 | pytest.raises(pub.ExcHandlerError, pub.sendMessage, 'testHandleExcept1a')
40 | pub.unsubscribe(handler, ExcPublisher.topicUncaughtExc)
41 |
42 |
43 | def testHandleExcept1b():
44 | # create a listener that raises an exception:
45 | from raisinglistener import getRaisingListener
46 | raisingListener = getRaisingListener()
47 | pub.subscribe(raisingListener, 'testHandleExcept1b')
48 |
49 | # subscribe a good exception listener and validate
50 | # create the listener for uncaught exceptions in listeners:
51 | class UncaughtExcListener:
52 | def __call__(self, listenerStr=None, excTraceback=None):
53 | # verify that information received; first the listenerStr
54 | assert listenerStr.startswith('raisingListener')
55 | # next the traceback:
56 | tb = excTraceback.traceback
57 | assert len(tb) == 2
58 | def validateTB(tbItem, eFN, eLine, eFnN):
59 | assert tbItem[0].endswith(eFN), '%s !~ %s' % (tbItem[0], eFN)
60 | assert tbItem[1] == eLine
61 | assert tbItem[2] == eFnN
62 | validateTB(tb[0], 'raisinglistener.py', 5, 'raisingListener')
63 | validateTB(tb[1], 'raisinglistener.py', 4, 'nested')
64 | # next the formatted traceback:
65 | assert len( excTraceback.getFormattedList() ) == len(tb)+1
66 | # finally the string for formatted traceback:
67 | msg = excTraceback.getFormattedString()
68 | #print 'Msg "%s"' % msg
69 | assert msg.startswith(' File')
70 | assert msg.endswith("name 'RuntimeError2' is not defined\n")
71 |
72 | from pubsub.utils.exchandling import ExcPublisher
73 | topic = topicMgr.getTopic( ExcPublisher.topicUncaughtExc )
74 | assert not topic.hasListeners()
75 | handler = UncaughtExcListener()
76 | pub.subscribe(handler, ExcPublisher.topicUncaughtExc)
77 | pub.sendMessage('testHandleExcept1b')
78 |
79 | # verify that listener isn't stuck in a cyclic reference by sys.exc_info()
80 | del raisingListener
81 | gc.collect() # for pypy: the gc doesn't work the same as cpython's
82 | assert not topicMgr.getTopic('testHandleExcept1b').hasListeners()
83 | pub.unsubscribe(handler, ExcPublisher.topicUncaughtExc)
84 |
85 |
86 | def testHandleExcept2():
87 | #Test sendMessage when one handler, then change handler and verify changed
88 | testTopic = 'testTopics.testHandleExcept2'
89 | pub.subscribe(throws, testTopic)
90 | pub.setListenerExcHandler(None)
91 | #pubsub.utils.notification.useNotifyByWriteFile()
92 | #assert_equal( topicMgr.getTopic(testTopic).getNumListeners(), 1 )
93 |
94 | expect = None
95 |
96 | def validate(className):
97 | global expect
98 | assert expect == className
99 | expect = None
100 |
101 | class MyExcHandler:
102 | def __call__(self, listener, topicObj):
103 | validate(self.__class__.__name__)
104 |
105 | class MyExcHandler2:
106 | def __call__(self, listener, topicObj):
107 | validate(self.__class__.__name__)
108 |
109 | def doHandling(HandlerClass):
110 | global expect
111 | expect = HandlerClass.__name__ #'MyExcHandler'
112 | excHandler = HandlerClass()
113 | pub.setListenerExcHandler(excHandler)
114 | pub.sendMessage(testTopic)
115 | assert expect is None
116 |
117 | doHandling(MyExcHandler)
118 | doHandling(MyExcHandler2)
119 |
120 | # restore to no handling and verify:
121 | pub.setListenerExcHandler(None)
122 | pytest.raises( RuntimeError, pub.sendMessage, testTopic)
123 |
124 |
125 | def testNoExceptionHandling1():
126 | pub.setListenerExcHandler(None)
127 |
128 | def raises():
129 | raise RuntimeError('test')
130 | topicMgr.getOrCreateTopic('testNoExceptionTrapping')
131 | pub.subscribe(raises, 'testNoExceptionTrapping')
132 | pytest.raises( RuntimeError, pub.sendMessage, 'testNoExceptionTrapping')
133 |
134 |
135 | def testNoExceptionHandling2():
136 | testTopic = 'testTopics.testNoExceptionHandling'
137 | pub.subscribe(throws, testTopic)
138 | assert pub.getListenerExcHandler() is None
139 | pytest.raises( RuntimeError, pub.sendMessage, testTopic)
140 |
141 |
142 |
--------------------------------------------------------------------------------
/tests/suite/test3b_sel_default.py:
--------------------------------------------------------------------------------
1 | def test_import():
2 | from pubsub import pub
3 | assert pub.VERSION_API == 4
4 |
--------------------------------------------------------------------------------
/tests/suite/test3d_defaultlog.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
4 | :license: BSD, see LICENSE.txt for details.
5 |
6 | """
7 |
8 | import io
9 | from pubsub.utils import notification
10 |
11 | def testNotifications():
12 | capture = io.StringIO()
13 | logger = notification.useNotifyByWriteFile(capture)
14 | from pubsub import pub
15 | def block():
16 | def listener(): pass
17 | pub.subscribe(listener, 'testNotifications')
18 | block()
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/suite/test4_prov_module_expect.py:
--------------------------------------------------------------------------------
1 | # Automatically generated by TopicTreeSpecPrinter(**kwargs).
2 | # The kwargs were:
3 | # - fileObj: file
4 | # - footer: '# End of topic tree definition. Note that application may l...'
5 | # - indentStep: 4
6 | # - treeDoc: 'Tree docs, can be anything you want....'
7 | # - width: 70
8 |
9 |
10 | """
11 | Tree docs, can be anything you want.
12 | """
13 |
14 |
15 | class root_topic_1b:
16 | """
17 | Root topic 1.
18 | """
19 |
20 | class subtopic_1:
21 | """
22 | Sub topic 1 of root topic. Docs rely on one blank line for
23 | topic doc, and indentation for each argument doc.
24 | """
25 |
26 | def msgDataSpec(arg1, arg2=None):
27 | """
28 | - arg1: some multiline doc
29 | for arg1
30 | - arg2: some multiline doc
31 | for arg2
32 | """
33 |
34 |
35 | # End of topic tree definition. Note that application may load
36 | # more than one definitions provider.
37 |
--------------------------------------------------------------------------------
/tests/suite/test5_xmlprovider.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from pubsub import pub
4 | from pubsub.utils.xmltopicdefnprovider import (
5 | XmlTopicDefnProvider,
6 | TOPIC_TREE_FROM_FILE,
7 | exportTopicTreeSpecXml
8 | )
9 |
10 | topicMgr = pub.getDefaultTopicMgr()
11 |
12 | def test_xml_from_file():
13 | pub.clearTopicDefnProviders()
14 |
15 | provider = XmlTopicDefnProvider('xmlprovider_topics.xml', TOPIC_TREE_FROM_FILE)
16 |
17 | assert topicMgr.getTopic('parent', True) is None
18 | assert topicMgr.getTopic('parent.child', True) is None
19 | assert topicMgr.getOrCreateTopic('parent') is not None
20 | assert topicMgr.getOrCreateTopic('parent.child') is not None
21 |
22 | def test_xml_import():
23 | pub.clearTopicDefnProviders()
24 | topicMgr.delTopic('parent')
25 | # verify pre:
26 | assert topicMgr.getTopic('parent', True) is None
27 | assert topicMgr.getTopic('parent.child', True) is None
28 |
29 | provider = XmlTopicDefnProvider('xmlprovider_topics.xml', TOPIC_TREE_FROM_FILE)
30 | pub.addTopicDefnProvider( provider )
31 | # force instantiation of two topic definitions that were defined in xml:
32 | pub.sendMessage('parent', lastname='')
33 | pub.sendMessage('parent.child', lastname='', nick='')
34 |
35 | # verify post:
36 | assert topicMgr.getTopic('parent') is not None
37 | assert topicMgr.getTopic('parent.child') is not None
38 |
39 | def test_xml_string_import():
40 | xml="""
41 | Test Topics showing hierarchy and topic inheritance
42 |
43 | Parent with a parameter and subtopics
44 |
45 | surname
46 | given name
47 |
48 |
49 | This is the first child
50 |
51 | A nickname
52 |
53 |
54 |
55 | """
56 |
57 | topicMgr.delTopic('parent')
58 | pub.clearTopicDefnProviders()
59 | assert topicMgr.getTopic('parent', True) is None
60 | assert topicMgr.getTopic('parent.child', True) is None
61 |
62 | provider = XmlTopicDefnProvider(xml)
63 | pub.addTopicDefnProvider( provider )
64 | # to force instantiation of two topic definitions that were defined in xml,
65 | # this time we just instantiate all of them:
66 | pub.instantiateAllDefinedTopics(provider)
67 |
68 | assert topicMgr.getTopic('parent') is not None
69 | assert topicMgr.getTopic('parent.child') is not None
70 |
71 | def test_xml_topics():
72 | # validate that topic specs were properly parsed
73 | def isValid(topicName, listener):
74 | topic = topicMgr.getTopic(topicName)
75 | assert topic.getDescription() is not None
76 | assert topic.hasMDS()
77 | return topic.isValid(listener)
78 |
79 | def hello(lastname, name=None): pass
80 | def friend(lastname, nick, name=None): pass
81 |
82 | assert isValid('parent', hello)
83 | assert isValid('parent.child', friend)
84 |
85 |
--------------------------------------------------------------------------------
/tests/suite/xmlprovider_topics.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test Topics showing hierarchy and topic inheritance
4 |
5 | Parent with a parameter and subtopics
6 |
7 | surname
8 | given name
9 |
10 |
11 | This is the first child
12 |
13 | A nickname
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tools/compare_timings.py:
--------------------------------------------------------------------------------
1 | '''
2 | Timing helper function to time several Python statements. It makes
3 | using the timeit module much more convenient. The only important
4 | function is times(), to which you provide the number of iterations
5 | to time, an initialization statement (typically, to initialize some
6 | locals), and the list of statements to time. E.g.
7 |
8 | times(init='l=range(1,1000)',
9 | s1='if l!=[]: pass',
10 | s2='if l: pass',
11 | s3='if len(l): pass')
12 |
13 | will print(out)
14 |
15 | s2 => 0.046
16 | s1 => 0.086
17 | s3 => 0.121
18 |
19 | showing that s2 is the fastest (in fact, two to three times faster
20 | than the alternatives).
21 | '''
22 |
23 | from timeit import Timer
24 |
25 | class Timing:
26 | def __init__(self, name, num, init, statement):
27 | self.__timer = Timer(statement, init)
28 | self.__num = num
29 | self.name = name
30 | self.statement = statement
31 | self.__result = None
32 |
33 | def timeit(self):
34 | self.__result = self.__timer.timeit(self.__num)
35 |
36 | def getResult(self):
37 | return self.__result
38 |
39 |
40 | def times(num=1000000, reverse=False, init='', **statements):
41 | '''The num is the number of times that each statement will
42 | be executed. The init statement is executed only once for
43 | statement and is not part of the timing result. The
44 | statements kwarg is a dict of statements to time, where key
45 | is a 'name' for the statement, and value is the statement
46 | (as a string). Prints the timings from smallest to largest
47 | (unless reverse=True, then opposite). '''
48 | # time each statement
49 | timings = []
50 | for n, s in statements.iteritems():
51 | t = Timing(n, num, init, s)
52 | t.timeit()
53 | timings.append(t)
54 |
55 | # print(results)
56 | timings.sort(key=Timing.getResult, reverse=reverse)
57 | for t in timings:
58 | print(" %10s => %.3f s" % (t.name, t.getResult()))
59 |
60 |
61 | if __name__ == '__main__':
62 | inits = ('l=range(1,1000)', 'l=[]')
63 | for ii in inits:
64 | print('\nFor %s:' % ii)
65 | times(init=ii,
66 | boolean = 'if l: pass',
67 | empty = 'if l!=[]: pass',
68 | len_ = 'if len(l): pass')
69 |
70 | print('\nFor list -> dict:')
71 | times(100, init='l=range(1,1000)',
72 | zip = 'dict(zip(l[::2], l[1::2]))',
73 | listcomp = 'dict((l[i],l[i+1]) for i in range(0,len(l)-1,2))')
74 |
75 | print('\nFor hasattr vs except:')
76 | times(10000, init='class Foo: pass\nfoo = Foo()',
77 | hasattr = 'if hasattr(foo, "a"): a = getattr(foo, "a")',
78 | excep = 'try: a = foo.a\nexcept Exception: pass')
79 |
80 | print('\nFor cost of enumerate:')
81 | times(100, init='l=range(1,1000)\ndef f(v): return (v*10-50)/2',
82 | enum = 'for i, v in enumerate(l): f(v)',
83 | noenum = 'for v in l: f(v)',
84 | count = 'ii=0\nfor v in l: \n f(v)\nii += 1')
85 |
86 | print('\nFor slice before/in loop:')
87 | times(100, init='l=range(0,10000)\nl2=range(0,1000)',
88 | before10k = 'l3=l[9000:]\nfor i in l3: pass',
89 | before1k = 'l3=l2[1:]\nfor i in l3: pass',
90 | inloop10k = 'for i in l[9000:]: pass',
91 | inloop1k = 'for i in l2[1:]: pass',
92 | range = 'for i in xrange(9000,10000): l[i]',
93 | nocopy = 'for i in l2: pass')
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://tox.testrun.org/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist = py33, py34, py35, py36, py37
8 | toxworkdir=C:\temp\tox\pypubsub
9 |
10 | [testenv]
11 | usedevelop = True
12 | changedir = tests/suite
13 | commands = py.test
14 | install_command = pip install {opts} {packages} --trusted-host pypi.python.org
15 |
16 | deps =
17 | pytest
18 |
19 | [testenv:py33]
20 | deps =
21 | pytest
22 | typing
23 | enum34
24 | pathlib
25 | weakrefmethod
26 |
27 | [testenv:py34]
28 | deps =
29 | pytest
30 | typing
31 |
--------------------------------------------------------------------------------