├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyleSettings.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── dictionaries │ └── Oliver_Schoenborn.xml ├── encodings.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── pypubsub.iml ├── runConfigurations │ ├── Build_Wheel_and_Source.xml │ ├── advanced_main.xml │ ├── console_main.xml │ ├── perf.xml │ ├── py_test_in_suite.xml │ └── wx_main.xml ├── scopes │ ├── Version_Change.xml │ └── scope_settings.xml ├── vagrant.xml └── vcs.xml ├── .landscape.yml ├── .travis.yml ├── MANIFEST.in ├── README.rst ├── README_WxPython.txt ├── docs ├── Makefile ├── about.rst ├── changelog.rst ├── conf.py ├── development │ ├── dev_index.rst │ ├── py2exe.rst │ ├── setup-cxfreeze.py │ ├── setup-py2exe.py │ ├── sphinx_patch1.txt │ ├── sphinx_patch2.txt │ └── testpubsub.py ├── index.rst ├── index_redirect.php ├── installation.rst ├── make.bat ├── pubsub_concept.png └── usage │ ├── core_classes.rst │ ├── helloworld.py │ ├── howtos │ └── index.rst │ ├── index.rst │ ├── module_pub.rst │ ├── module_utils.rst │ ├── reference.rst │ ├── types_of_errors.rst │ ├── usage_advanced.rst │ ├── usage_advanced_debug.rst │ ├── usage_advanced_maintain.rst │ ├── usage_advanced_other.rst │ ├── usage_basic.rst │ └── usage_basic_tasks.rst ├── examples ├── advanced │ ├── advanced_main.py │ ├── exchandle.py │ ├── kwargs_listeners.py │ ├── kwargs_senders.py │ ├── kwargs_topics.py │ ├── kwargs_topics_out.py │ └── notifhandle.py ├── basic_kwargs │ ├── README.txt │ ├── console_listeners.py │ ├── console_main.py │ ├── console_senders.py │ ├── wx_main.py │ ├── wx_win1.py │ └── wx_win2.py ├── multithreadloop.py ├── runall.bat └── runall_regression.txt ├── release.bat ├── setup.py ├── src ├── contrib │ ├── netpubsub.py │ └── wx_monitor.py └── pubsub │ ├── LICENSE_BSD_Simple.txt │ ├── RELEASE_NOTES.txt │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── annotations.py │ ├── callables.py │ ├── listener.py │ ├── notificationmgr.py │ ├── publisher.py │ ├── topicargspec.py │ ├── topicdefnprovider.py │ ├── topicexc.py │ ├── topicmgr.py │ ├── topicobj.py │ ├── topictreetraverser.py │ ├── topicutils.py │ └── weakmethod.py │ ├── pub.py │ └── utils │ ├── __init__.py │ ├── exchandling.py │ ├── misc.py │ ├── notification.py │ ├── topictreeprinter.py │ └── xmltopicdefnprovider.py ├── tests ├── perf.py ├── pytest.ini └── suite │ ├── my_import_topics.py │ ├── raisinglistener.py │ ├── test1_listener.py │ ├── test2_spec.py │ ├── test2a_topic.py │ ├── test2b_topicmgr.py │ ├── test2c_notify.py │ ├── test2c_notify2.py │ ├── test2d_except.py │ ├── test3b_sel_default.py │ ├── test3c_pubsub3.py │ ├── test3d_defaultlog.py │ ├── test4_prov_module_expect.py │ ├── test4_provider.py │ ├── test5_xmlprovider.py │ └── xmlprovider_topics.xml ├── tools └── compare_timings.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | __pycache__ 3 | 4 | /.vscode 5 | /build 6 | /dist 7 | 8 | /docs/html 9 | /docs/_build 10 | 11 | /src/PyPubSub.egg-info 12 | 13 | /tests/suite/htmlcov/ 14 | /tests/*.pstats 15 | /tests/suite/.coverage 16 | 17 | /pypubsub.code-workspace 18 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | /workspace.xml 2 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | pypubsub -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/Oliver_Schoenborn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pypubsub.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Build_Wheel_and_Source.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations/advanced_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations/console_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations/perf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations/py_test_in_suite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/runConfigurations/wx_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/scopes/Version_Change.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vagrant.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 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 | --------------------------------------------------------------------------------