pax_global_header00006660000000000000000000000064134234470670014524gustar00rootroot0000000000000052 comment=8f0f8ce3ddd4a282aa789f0120ec27e585ee89d6 pypubsub-4.0.3/000077500000000000000000000000001342344706700134015ustar00rootroot00000000000000pypubsub-4.0.3/.gitignore000066400000000000000000000002171342344706700153710ustar00rootroot00000000000000*.bak __pycache__ /build /dist /docs/html /docs/_build /src/PyPubSub.egg-info /tests/suite/htmlcov/ /tests/*.pstats /tests/suite/.coverage pypubsub-4.0.3/.idea/000077500000000000000000000000001342344706700143615ustar00rootroot00000000000000pypubsub-4.0.3/.idea/.gitignore000066400000000000000000000000171342344706700163470ustar00rootroot00000000000000/workspace.xml pypubsub-4.0.3/.idea/.name000066400000000000000000000000101342344706700152710ustar00rootroot00000000000000pypubsubpypubsub-4.0.3/.idea/codeStyleSettings.xml000066400000000000000000000004311342344706700205550ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/compiler.xml000066400000000000000000000013071342344706700167160ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/copyright/000077500000000000000000000000001342344706700163715ustar00rootroot00000000000000pypubsub-4.0.3/.idea/copyright/profiles_settings.xml000066400000000000000000000001571342344706700226610ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/dictionaries/000077500000000000000000000000001342344706700170365ustar00rootroot00000000000000pypubsub-4.0.3/.idea/dictionaries/Oliver_Schoenborn.xml000066400000000000000000000001401342344706700231730ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/encodings.xml000066400000000000000000000003341342344706700170540ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/inspectionProfiles/000077500000000000000000000000001342344706700202405ustar00rootroot00000000000000pypubsub-4.0.3/.idea/inspectionProfiles/Project_Default.xml000066400000000000000000000021171342344706700240350ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/inspectionProfiles/profiles_settings.xml000066400000000000000000000003531342344706700245260ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/misc.xml000066400000000000000000000006411342344706700160370ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/modules.xml000066400000000000000000000004141342344706700165520ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/pypubsub.iml000066400000000000000000000021741342344706700167410ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/000077500000000000000000000000001342344706700201005ustar00rootroot00000000000000pypubsub-4.0.3/.idea/runConfigurations/Build_Wheel_and_Source.xml000066400000000000000000000020251342344706700251460ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/advanced_main.xml000066400000000000000000000020461342344706700233750ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/console_main.xml000066400000000000000000000020541342344706700232710ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/perf.xml000066400000000000000000000017301342344706700215570ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/py_test_in_suite.xml000066400000000000000000000041151342344706700242110ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/runConfigurations/wx_main.xml000066400000000000000000000020421342344706700222620ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/scopes/000077500000000000000000000000001342344706700156555ustar00rootroot00000000000000pypubsub-4.0.3/.idea/scopes/Version_Change.xml000066400000000000000000000003201342344706700212640ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/scopes/scope_settings.xml000066400000000000000000000002131342344706700214240ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/vagrant.xml000066400000000000000000000005661342344706700165540ustar00rootroot00000000000000 pypubsub-4.0.3/.idea/vcs.xml000066400000000000000000000002471342344706700157010ustar00rootroot00000000000000 pypubsub-4.0.3/.landscape.yml000066400000000000000000000002321342344706700161310ustar00rootroot00000000000000# doc-warnings: true test-warnings: false # strictness: veryhigh max-line-length: 120 ignore-paths: - tools - docs - examples python-targets: - 3 pypubsub-4.0.3/.travis.yml000066400000000000000000000011671342344706700155170ustar00rootroot00000000000000language: python python: - "3.3" - "3.4" - "3.5" - "3.6" - "3.7-dev" # command to install dependencies install: - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install typing; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then pip install typing enum34 pathlib weakrefmethod; fi # - if [[ $TRAVIS_PYTHON_VERSION == 'pypy3' ]]; then pip install typing enum34 pathlib weakrefmethod importlib; fi - pip install pytest-cov - pip install coveralls - pip install -e . # command to run tests script: - cd tests/suite - py.test --cov=pubsub --cov-report term-missing afer_success: - coveralls pypubsub-4.0.3/MANIFEST.in000066400000000000000000000000011342344706700151260ustar00rootroot00000000000000 pypubsub-4.0.3/README.rst000066400000000000000000000021141342344706700150660ustar00rootroot00000000000000.. image:: https://badge.fury.io/py/PyPubSub.svg :target: https://badge.fury.io/py/PyPubSub .. image:: https://img.shields.io/travis/schollii/pypubsub.svg :target: https://img.shields.io/travis/schollii/pypubsub News ==== January 2019: pypubsub 4.0.3 released! Overview ======== Provides a publish-subscribe API to facilitate event-based or message-based architecture in a single-process application. It is pure Python and works on Python 3.3+. It is centered on the notion of a topic; senders publish messages of a given topic, and listeners subscribe to messages of a given topic, all inside the same process. The package also supports a variety of advanced features that facilitate debugging and maintaining topics and messages in larger desktop- or server-based applications. Install most recent stable with "pip install pypubsub". Useful links: - Project on PyPI: https://pypi.python.org/pypi/PyPubSub - The documentation for latest stable release is at http://pypubsub.readthedocs.io. - The documentation for latest code is at http://pypubsub.readthedocs.io/en/latest. pypubsub-4.0.3/README_WxPython.txt000066400000000000000000000013661342344706700167650ustar00rootroot00000000000000# this file gets copied to wx/lib/pubsub folder when release to wxPython For wxPython users who are using wx.lib.pubsub: Do not use wx.lib.pubsub: this package is an earlier version of PyPubSub that was copied into wxPython for legacy reasons. If you attempt to use wx.lib.pubsub in wxPython >= 4.0.4, you will get a deprecation message that you should install pubsub directly from PyPubSub. Note that PyPubSub does not have any external dependencies and can be used with PyQt, PyGTK, etc. There is a wxPython example in PyPubSub source distribution /examples folder, and wx/lib/pubsub/examples. The WxPython wiki also discusses usage of pubsub in wxPython, the latest docs are only maintained at pypubsub.readthedocs.org Oliver Schoenborn January 2019 pypubsub-4.0.3/docs/000077500000000000000000000000001342344706700143315ustar00rootroot00000000000000pypubsub-4.0.3/docs/Makefile000066400000000000000000000156201342344706700157750ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyPubSub.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyPubSub.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PyPubSub" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyPubSub" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pypubsub-4.0.3/docs/about.rst000066400000000000000000000373421342344706700162060ustar00rootroot00000000000000About ===== .. contents:: In this section: :depth: 1 :local: License ------- Simplified BSD: .. literalinclude:: ../src/pubsub/LICENSE_BSD_Simple.txt Authors ------- The main developer of the package is Oliver Schoenborn, but Robb Shecter started it all back in early 2000's. The code is now hosted on github at http://github.com/schollii/pypubsub (previous to that, it was hosted on SourceForge.net, and prior to that, it was hosted in wxPython). PyPubSub is on the Python Package Index at http://pypi.python.org/pypi/PyPubSub. As listed in the :ref:`label-contributing` section, contributions of any form are welcome. Even questions help progress the project. :author: Oliver Schoenborn Acknowledgements ---------------- Thanks to SourceForge.net for hosting the project until September 2016. Thanks to Github.com for hosting the project from October 2016. Thanks to Robb Shecter for having given me the chance to take over this project from him many years ago (ca 2004!). Thanks to all those users of PyPubSub who ask questions, make suggestions, point out bugs, etc. PyPubSub Users -------------- The page at https://libraries.io/pypi/PyPubSub has useful statics on which other github projects use/watch/fork pypubsub. In addition, several users have donated a bit of their time to describe how they use/d PyPubSub in their Python projects. *Darin Gordon, for Yosai, since 2015*: Yosai (https://github.com/YosaiProject) is a security framework for python applications, offering authentication, authorization, and session management from a common API. Yosai uses PyPubSub to facilitate event-driven responses to security-related changes. For instance, when a session expires, valuable information is logged and cached authorization info is cleared. *Jerome Laheurte, for Task Coach, since Feb 2012*: Task Coach (https://sourceforge.net/projects/taskcoach/) is a simple open source todo manager to keep track of personal tasks and todo lists. It is designed for composite tasks, and also offers effort tracking, categories, notes and more. Task Coach uses PyPubSub as its Publisher/Listener implementation to cleanly separate model and view layers. *Steven Sproat, for Whyteboard, since Feb 2010*: I've been using PyPubSub for around 2 months in my cross-platform drawing application, Whyteboard (http://launchpad.net/whyteboard). My Shape models (rectangle, polygons etc) use PyPubSub to notify the GUI of any changes to themselves or to request actions be performed on the canvas (e.g capture user's mouse), and the GUI responds by updating various dialogs with this information. This means that my shapes no longer need to maintain references to the canvas in order to perform operations on it, and can instead send a message saying "do something" without caring how it's done. *Josh English, for WMS, since 2008*: I use it in my Writing Management System (http://joshua.r.english .googlepages.com/wms). I'm using it to control interfaces, such as telling the frame to change the status bar, or a notebook to change a panel. PyPubSub enables me to focus on *what* data to pass around my application, rather than *how* to pass it around. This makes it easy to put in the finer details of my application. *Geoff Gilmour-Taylor, since April 2008*: I use wx.lib.pubsub for a suite of in-house batch conversion tools for DAISY talking books, called Garden Tools (in-house software for the CNIB Library, http://www.cnib.ca/en/Services/library/). For MVC, communication in a wxPython app. Loose coupling of business logic and GUI. It allows me to trigger multiple actions on a single message without having to locate and modify all the places where the message is sent. I was able to add a logging module that reads the same status messages that are sent to the GUI without having to modify any of my other code. *Phil Mayes, for Listomax, since 2007*: Listomax (http://www.listomax.com/) uses version 1 of PyPubSub for MVC: multiple View (UI) components may need to change when the Model changes; simpler than direct calls, lower coupling. *Mike Driscoll, for PyTimesheet and Zimbra Alerts, since 2007*: I use wx.lib.pubsub in two internal projects at my employer's business, "PyTimesheet" and the "Zimbra Alerts". I use it to send information between various frames, such as an options menu back to the main application that launched it. The main application I use it for though is a Timesheet program where I use it to tell my program which frame to display when. Basically when one closes, I need another one to open and I found that PyPubSub made this quite trivial. The other program is used in conjunction with our Zimbra web mail and will pop-up an alert when we receive an email and it also has an Outlook-like Reminder dialog for appointments... And thanks for providing such a nice tool for my arsenal! *Anthony Floyd, RAVEN, since 200?*: Our project is called "RAVEN", it's an analytical and finite-element analysis program for simulating and analyzing the processing of composite materials in the aerospace industry. We use PyPubSub as the communications backbone. We essentially have a MVC framework, and use PyPubSub to have the UI respond to things happening in the data. However, we also use it to have data objects respond to changes in other data objects. We're quite enamoured with PyPubSub! It's proven to be an effective way to keep the UI out of the backend, and an effective way to keep the backend modularized. *Sebastian Zurek, OpenSynergy, since 2007*: I'm using wx.lib.pubsub module as part of the OpenSynergy framework (http://www.opensynergy.pl, temporarily offline) that I am developing, and I found it VERY usefull. PyPubSub is used as the communication layer betteen the extensions components and the framework, between the Model and Visual, and between the Visual elements. *Werner F. Bruhin, for the The Wine Cellar Book, since 2006* Have been using PyPubSub for years and since I started work on version 4 of my application [http://thewinecellarbook.com)] over a year ago I switched to the PyPubSub v3 API and defined a topic tree. Having a topic tree is just great as you make sure that you don't mistype the topic names and on top you have nice documentation on what topics you already defined and what parameter(s) need to be passed for each topic. Currently I have topics to keep track of database state, data needs saving, database item/row has changed, was deleted etc which trigger updates to relevant lists to update themselves if needed and to show messages on a wx.InfoBar. *Mike Rooney, for wxBanker, since 2006*: I use PyPubSub as the crucial event handling mechanism for wxBanker (https://launchpad.net/wxbanker). It works well for implementing design patterns such as MVC where you want to eliminate coupling, since it doesn't require that you know specific method names or implementation details of other classes, modules, or libraries. PyPubSub is also great when you want to make an announcement without requiring that anything (or how many things) is listening to or acting upon that announcement. In short, PyPubSub makes intra-process communication a dream come true. *QVI (http://www.qvii.com/) for several applications, since 2006*: Here at QVI we use PyPubSub for most of our wxPython applications (notably SmartTree), to achieve very lightweight, simple, and readable communication between classes and modules. One of the nice aspects of PyPubSub is how easy it is to incorporate into existing code, and how well-suited it is for pluggable/modular designs which want to make announcements about events, but don't require that or care if any other module is listening. It makes handling "events" easy, whatever we define them to be, and removes the need for the handlers to have any specific knowledge of how the announcements are made or where they came from. After discovering we could use PyPubSub independently of wxPython, we also use it in an application or two that doesn't use wxPython at all, but where we still desire a lightweight event handling mechanism (when don't you?). *Oliver Schoenborn (Author of PyPubSub), for several applications, from 2004 to 2010*: I have used PyPubSub on several projects. Applications which, for example, - show tree structures with selectable nodes, and selected node's associated information panel - show objects on information panels with info regarding progress of other components (running on other machines) updated in real time - show dialog boxes with with entry fields for settings - have several panels in a wizard style to configure a task for execution With PyPubSub, one event occurs due to a mouse click on an icon, and and all parts of the code that need updating get called with the new data. This means automatic update of menu items (adding, removing etc), state in various panels, etc. Gone are the long sequences of calls through the code. Last time I had to build or maintain a Python event-based application was 2009, but I'm dedicated to maintaining PyPubSub for other developers. When I make or incorporate improvements based on user feedback, I rely on the high % coverage of the unit regression tests, the useful examples, and the exception messages which give a lot of useful information when the message data fields don't adhere to the topic definition (inferred or specified). I look forward to my next event-based Python application, which might be in the fall of 2013. .. _label-history: History ------- PyPubSub was originally created by Robb Shecter as a module in the wxPython library, named wx.lib.pubsub, sometime around y2k. At that time, pubsub had one responsiblity: to allow for messages to be sent to listeners based on a hierarchy of topics. In the Spring of 2004, I added the ability to automaticaly unregister listeners that were no longer in use outside of PyPubSub (by making the module's Publisher use weak references to listeners). For large PyPubSub-based application, this greatly simplified listener management. I asked Robin Dunn if he would like the changes to be put in wx.lib.pubsub; he forwarded the request to Robb. And the rest is history. Only a few minor tweaks and improvements happened for the next couple years. In 2006 when I used PyPubSub on a couple larger projects, I wished that topic trees and the topic message data could be documented. I also found that a major time waster when using pubsub at that time was debugging incorrect message data, so I started thinking of a way that I could validate message data. I also wished that I could find a design that would allow the use of tools like pylint to point out invalid topic names. So I developed version 2 of wx.lib.pubsub in the Fall of 2006. I also created an entry in the Python Package Index as PyPubSub (http://pypi.python.org/pypi/PyPubSub) and used PyPI to hold a snapshot of my files so that even developers not using wxPython could benefit from it. In May 2007 I decided it was time to create a project on SourceForge.net for it. It was http://sourceforge.net/projects/pubsub, so the web site was at http://pubsub.sourceforge.net. The wx.lib.pubsub was then a verbatim copy of the src folder from sf.net/projects/pubsub, as it was before PyPubSub version 2. In 2008 someone created, unbeknownst to me, an unrelated Python project on sourceforge and named it PyPubSub. The author did not realize that mine already existed with that name in PyPI and that therefore he would have to rename his so as not to confuse users. This project came to my attention when I wanted to rename pubsub on SF.net to pypubsub to make it clear that it was python based, and to match the already one-year old entry on PyPI. In the end, the author renamed his project and sf.net/projects/pypubsub was available for my taking. After using PyPubSub version 2 for a bit I wasn't really happy with it, so I went back to the drawing board to support topic and message data documentation, definition and validation started. Version 3.0.0, completed some time in 2008, achieved this via keyword-based message data and topic definition providers. Version 3 also added support for tracking PyPubSub activity such as listener subscription, topic creation, and sending messages, very useful in large applications. Version 3 of PyPubSub was not compatible with v2 or v1, so I couldn't directly upgrade wx.lib.pubsub to it without at least supporting a deprecated v1 for a while. This led to version 3.1.0 in early 2010, which supported the v1 API via a setupv1.py configuration module that could be imported before the first import of pubsub. This was quite a challenge as there was a fair bit of commonality between PyPubSub v1 and v3, but also some significant differences. In retrospect I should not have done that because it made the code rather complex. I did a good job of it so it was easy to make fixes, but it could be a pain to troubleshoot. If I had to walk the same mile again, I would just have two separate implementations, with an easy way to import one or the other (e.g. pubsub vs pubsub3 package package folders). Not much happened between early 2010 and first half of 2013, except for a minor release 3.1.2 in November 2011: the code was stable and did its job nicely so no major changes needed. Also in that period I didn't develop or maintain any event-based Python application so I didn't have any reason to update PyPubSub. I did accumulate about a dozen tickets on SF.net involving minor bugs or patches contributed by users in that period. The overhaul of wxPython 'Phoenix' in 2013 was the perfect opportunity to make pubsub version 3 API the default, and to make version 1 API accessible only on demand (via the setuparg1.py configuration module). I also removed all the code that was there just to support the old version 1 API, leaving just a version 3 API with two message protocols available. I took the opportunity to address the dozen tickets during the summer of 2013, and to improve the docs. In early 2016 I started work to remove the deprecated code and support only the original messaging protocol that I had designed in 3.0. With two busy kids, it is not easy to find the time to do this, so it took me till October 2016 for me to get my act together and finally release v4: a nice simple design with no import magic needed, no configuration, no complicated docs to explain the mulitple APIs, use of wheels instead of eggs, use of annotations, etc. .. _label-roadmap: Roadmap ------- List of things I would like to add to PyPubSub: - complete implementation of multi-threading helper class, no change required to PyPubSub, rather just utility class to help user (pseudo-code already in src/contrib) - figure out a good way to prevent wrapped listener subscriptions from being DOA (PyPubSub only keeps weak reference to listener, so if listener subscribe like ``pub.subscribe( wrapper(yourListener) )`` then listener will be unsubscribed as soon as subscribe returns; you need ``refListener = wrapper(yourListener); pub.subscribe(refListener)``) - finish the src/contrib/monitor implementation to monitor PyPubSub messages, or some way of monitoring message sending If anyone is interested in helping, please post on the dev forum. The following is no longer on list of things to do: - support pubsub over UDP and TCP sockets: mqtt does this! PyPubSub and mqtt are complementary: PyPubSub for messaging between application components within one Python interpreter; mqtt for messaging between compoonents on a network. pypubsub-4.0.3/docs/changelog.rst000066400000000000000000000126731342344706700170230ustar00rootroot00000000000000Changelog --------- High-level changelog. For details, consult the SVN logs. :4.0.3 (Jan 2019): * Cleanup for Python 3.7 (mostly add support for keyword-only args, use Python 3 inspect signature/Parameter and add tests for :4.0.0 (Dec 2016): * Verified support Python 3.5 and 3.6 * Distribution via wheel * Abandon support for Python 2.x and easy_install; now requires Python >= 3.3 * Abandon support for long-ago deprecated arg1 messaging protocol * Added currying of subscribed listener args * Significant speed improvement for message delivery * Use PEP 484 style of annotations throughout * Use enum instead of constants when practical :3.3.0 (Feb 2014): * cleanup low-level API: exception classes, moved some out of pub module that did not belong there (clutter), move couple modules; specifically: * Removed from pub (available on object returned from pub.getDefaultTopicMgr()) * getOrCreateTopic -> pub.getDefaultTopicMgr().getOrCreateTopic * getTopic -> pub.getDefaultTopicMgr().getTopic * newTopic -> pub.getDefaultTopicMgr().newTopic * delTopic -> pub.getDefaultTopicMgr().delTopic * getAssociatedTopics -> pub.getDefaultTopicMgr().getTopics * getDefaultTopicTreeRoot -> pub.getDefaultTopicMgr().getRootAllTopics * Removed from pub (available from pubsub.core): * ITopicDefnProvider * Moved from pub into to pubsub.core.TopicDefnProvider class as classmethod: * registerTopicDefnProviderType * Renamed: * TopicNameInvalid -> TopicNameError * UndefinedTopic(RuntimeError) -> TopicNameError(ValueError) * UndefinedSubtopic(RuntimeError) -> TopicNameError(ValueError) * ListenerInadequate(TypeError) -> ListenerMismatchError(ValueError) * UnrecognizedImportFormat -> UnrecognizedSourceFormatError * ListenerSpecInvalid -> MessageDataSpecError * SenderMissingReqdArgs -> SenderMissingReqdMsgDataError * SenderUnknownOptArgs -> SenderUnknownMsgDataError * ListenerNotValidatable -> TopicDefnErrorcd * Changed; * Topic.isSendable -> hasMDS * TopicManager.??? -> isTopicInUse + hasTopicDefinition * completed the ref docs * support installation via pip * cleanup versioning metadata: use pubsub.__version__ instead of pub.PUBSUB_VERSION * support Python 3 * add getListenersIter() to iterate over listeners without temp copy of listener list * add deprecation message when import setuparg1 * new wxPubsubMonitor utility class :3.2.0 (sep 2013): - cleanup of docs - merged importTopicTree to addTopicDefnProvider - renamed pub.getDefaultRootAllTopics to pub.getDefaultTopicTreeRoot - removed pub.importTopicTree, use pub.addTopicDefnProvider(source, format) - renamed pub.exportTopicTree to pub.exportTopicTreeSpec - several minor bug fixes - incorporated some patches contributed by users: one for performance improvement when high-frequency of subscribers/messages; one for reading topic tree specification from XML rather than .py module - v1 and v2 APIs no longer supported :3.1.2 (2011): - added some docs - more configurable importTopicTree - using importTopicTree now allows to use the topic hierarchy as topic names instead of string, thereby enabling python editors to support PyPubSub-based development via code completion and sendMessage keyword arguments. :3.1.1b (2010): - cleanup docs - couple minor tweaks (for instance added pub.getMsgProtocol()) :3.1.0b (2009): - Import/export of topic tree and its documentation using Python interpreter - Better support for evolving topic tree during application development, with "freezing" certain parts of tree - Helper functions to transition from *arg1* to *kwargs* messaging protocol - Improved error messages (in exceptions raised) - PyPubSub can be installed inside other packages and will not interfere with system-wide pubsub - pubsubconf module moved inside pubsub package so manual install easier - Support !**kwargs in listeners - Support for more than one pubusb notification handler - Multiple publisher engines in one application (for instance, in separate threads, or for different "domains" in a large application) - Upgraded docs - Bug fixes, cleanup :3.0 (2008): - Use keyword arguments in sendMessage - Support any kind of listener, not just those with one unnamed argument - Validate listeners at subscription time - Support "inheritance" of keyword arguments by subtopics during message sending (prevents a common bug which was to send data using wrong argument names). - Topic tree can be documented (including topic message arguments) - Support user-defined notification handling of certain events occuring in PyPubSub such as "subscribe", "sendMessage". - Support user-defined exception handling of exceptions raised by listeners - Proto-Documentation on own website using Sphinx - Separate regression testing folder for nose-based automated testing - Configuration module for choosing which PyPubSub API to use in application, useful for backwards compatibility :2.0 (2007): - more Pythonic API (new ``PublisherClass`` API, at module level so easier to call -- no need to know about singleton) - Support definition of topic tree via a python class, for increased rigor and documentability of topics - Topics are objects :1.0 (2005): - Given its own "home" as separate package from wxPython's ``wx.lib.pubsub`` - Factored out weakmethod - Put on Cheese Shop :Pre 1.0: - Created by Rob Shecter in wxPython's ``wx.lib`` (early 2000?) - Weakmethod added by Oliver Schoenborn (2004) - Further development transfered to Schoenborn (2004) pypubsub-4.0.3/docs/conf.py000066400000000000000000000224511342344706700156340ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Pypubsub documentation build configuration file, created by # sphinx-quickstart on Sat Feb 21 17:20:55 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..\\src')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Pypubsub' copyright = 'Oliver Schoenborn (since 2006)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # import pubsub # The full version, including alpha/beta/rc tags. release = pubsub.__version__ # The short X.Y version. import re version = re.search("[0-9]\.[0-9]", release).group(0) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'changelog.rst', 'apidocs'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'bizstyle' # html_theme = 'default' # html_theme = 'sphinxdoc' # html_theme = 'pyramid' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "Pypubsub v" + release + " Documentation" # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "Pypubsub v" + release # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = { # '**': ['globaltoc.html', 'localtoc.html', 'searchbox.html'], # } # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. html_split_index = True # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'PyPubSubdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', # Latex figure (float) alignment # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Pypubsub.tex', 'Pypubsub Documentation', 'Oliver Schoenborn', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pypubsub', 'Pypubsub Documentation', ['Oliver Schoenborn'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Pypubsub', 'Pypubsub Documentation', 'Oliver Schoenborn', 'Pypubsub', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False pypubsub-4.0.3/docs/development/000077500000000000000000000000001342344706700166535ustar00rootroot00000000000000pypubsub-4.0.3/docs/development/dev_index.rst000066400000000000000000000121441342344706700213540ustar00rootroot00000000000000Contribute ========== This page is intended for developers of (or contributing to) PyPubSub. .. contents:: In this section: :depth: 1 :local: .. _label-contributing: Contributing ------------ Contributions are welcome! There are many ways you could contribute: - bug fixes - new features - test results on different platforms - documentation - screencasts! (of applications using PyPubSub with output when user clicks) - example topic trees (using ``pubsub.utils.printTopicTree()`` in latest version, or ``print Publisher`` in versions 1) - other improvements - money! Please contact by posting on the forum pypubsub-dev forum (link in the :ref:`label-support` section) or via http://github/schollii/pypubsub. System Requirements ------------------- In addition to the :ref:`label-install-reqs`, the following are required: - To run unit tests: - pytest - To generate the docs: - sphinx >= 1.4.8 - In PyPubSub 3.3, which used an older version of sphinx, sphinx had to be patched as per post on sphinx-dev, but this no longer seems to be the required: .. literalinclude:: sphinx_patch1.txt .. literalinclude:: sphinx_patch2.txt - To change code: PyCharm is recommended (Community Edition is sufficient). Various build configurations are available via the PyPubSub project when loaded into PyCharm. Scripts Available ----------------- *Unit Testing*: The test suite is most conveniently run from PyCharm via the "py.test in suite" build configuration. The tests can also be run automatically via pytest suite from the :file:`tests` folder. Once this passes using the project's default interpreter, a Terminal can be opened in PyCharm (or alternately a command shell from Windows), and from the PyPubSub root folder, run :command:`tox`. This will attempt to run the test suite in every 3.x version of Python, x>=3 (ie 3.3, 3.4, etc). After changes are committed to github, the Travis CI will automatically run the tests on a Linux platform, for all versions of Python supported by PyPubSub. The results will be at https://travis-ci.org/schollii/pypubsub/builds. There is also a buildbot maintained by Jerome Laheurte to test on additional \*nix flavors, including OSX. Test results can be viewed at https://jeromelaheurte.net/buildbot/pubsub/console. *Performance Test*: A small performance test is available in the :file:`tests` folder. It can be run from PyCharm via the perf build configuration. This will generate a new :file:`.pstats` file which can be analysed. The test can also be run directly from command shell via :command:`python perf.py 1000`. The test is meant to compare the impact of changes before/after and is designed to compare on results on the same system (hardwards, OS). *Documentation*: The documentation can be generated locally on Windows via the Gen Docs build configuration in PyCharm. Alternatively, it can be generated by running :command:`make html` from the :file:`docs` folder of source distribution. The documentation is automatically built and available online at http://pypubsub.readthedocs.io. The latest from master branch is at http://pypubsub.readthedocs.io/en/master/. The stable (released) documentation is at http://pypubsub.readthedocs.io/en/stable/. Releases -------- PyPubSub uses the latest stable Python packaging and distribution tools: wheel, twine, and pypi. Generating a new release involves the following sequence of steps: - Verify that tox, sphinx, wheel, twine, and setuptools are installed. - Ensure that pytest suite runs 100%, and that the examples run without error in examples/ folder (one of the examples requires wxPython -- install latest stable) - Ensure that `tox` (run from pypubsub root folder) runs to completion without errors or warnings on all versions of Python (3.x) - Update version number via a search-replace in the `Version Change` scope of PyCharm: - src/pubsub/__init__.py: version - docs/changelog.rst - src/pubsub/RELEASE_NOTES.txt - README.rst - Add section at top of `docs/changelog.rst` with details of what changed (audience: pypubsub developers) - Update `src/pubsub/RELEASE_NOTES.txt` (audience: pypubsub end-users) to have high-level summary of changes for this release, handling incompatibilities, etc - Update the setup.py classifiers (such as adding a new version of Python supported) - In docs folder: - Update index.rst and docs/installation.rst - Regenerate HTML docs via `make`, confirm ok (no warnings etc) Persist to server: - Commit and push to remote master repository - Confirm that travis CI all pass Distribute: - Clean out the `dist/` folder - Generate the source and wheel distributions: `python setup.py bdist_wheel sdist` - Upload to PyPI: twine upload dist/\*: - Verify new release info and links on pypi.python.org - Create new branch (tag) in remote master repository - Confirm installation will work: attempt to install locally via PyPI, then import from Python shell Py2Exe and cx_Freeze -------------------- For packaging py2exe or cx_Freeze, see (possibly out of date): .. toctree:: py2exe.rst pypubsub-4.0.3/docs/development/py2exe.rst000066400000000000000000000062311342344706700206230ustar00rootroot00000000000000Packaging with py2exe and cxFreeze ================================== In this section we will see how to package applications that use PyPubSub, with `py2exe`_ and `cx_Freeze`_ packaging tools. Introduction ------------ Packaging tools such as py2exe and cx_Freeze determine the dependencies that have to be included in a package by recursively finding the modules from the import statements used. Recursive finding of modules from the import statements uses straight forward approach i.e., if the python code dynamically imports certain modules by modifying the ``sys.path`` at runtime or if the code uses ``__import__`` statements, those modules are likely to be left out by the packaging tool. This can be a problem for some packaged applications. Packaging modules that use PyPubSub ----------------------------------- PyPubSub supports two different messaging protocols namely ``args1`` and ``kwargs``; choosing and switching between these protocols is done by modifying the module path dynamically. This can result in import error like this at runtime:: from listenerimpl import Listener, ListenerValidator ImportError: No module named listenerimpl In the following sections we show an example script that uses PyPubSub and discuss the setup script to package it using *py2exe* or *cx_Freeze* packaging tools. .. _py2exe: http://www.py2exe.org .. _cx_Freeze: http://cx-freeze.sourceforge.net Example ------- Consider a sample application which has a single file named say ``testpubsub.py`` .. literalinclude:: testpubsub.py To package this with *py2exe* and *cx_Freeze* you write a conventional ``setup.py`` module, but with extra options that the packaging tool uses to create the final distribution. Setup file using py2exe ----------------------- The ``setup.py`` for this would look something like this .. literalinclude:: setup-py2exe.py The line ``'packages': 'encodings, pubsub'`` explicitly tells *py2exe* to include ``pubsub`` as a package so that the entire pubsub folder (from the installation location) including its sub packages are included for packaging. As the package has the entire list of python modules under pubsub, runtime protocol selection is now possible in the generated ``exe`` file. To build, run:: python setup.py py2exe which will produce a dist folder containing ``testpubsub.exe`` and other DLLs and files required to run the application. Interestingly, py2exe command complains about modules that *appear* to be missing: The following modules appear to be missing ['callables', 'core', 'core.notificationmgr', ... , 'topicu', 'validatedefnargs'] however, the application runs fine. Setup file using cx_Freeze -------------------------- The ``setup.py`` for this would look something like this .. literalinclude:: setup-cxfreeze.py To build, run:: python setup.py build We can safely ignore the missing modules warning in the build log:: Missing modules: ? core.publisher imported from pubsub.pub ? listenerimpl imported from pubsub.core.listener ? publishermixin imported from pubsub.core.topicobj ? topicargspecimpl imported from pubsub.core.topicargspec ? topicmgrimpl imported from pubsub.core.topicmgr pypubsub-4.0.3/docs/development/setup-cxfreeze.py000066400000000000000000000013611342344706700221770ustar00rootroot00000000000000from cx_Freeze import setup, Executable as cxExecutable import platform if platform.system() == 'Windows': # base must be set on Windows to either console or gui app # testpubsub is currently a console application # base = 'Win32GUI' base = 'Console' else: base = None opts = { 'compressed' : True, 'create_shared_zip' : False, } WIN_Target = cxExecutable( script='testpubsub.py', base=base, targetName='testpubsub.exe', compress=True, appendScriptToLibrary=False, appendScriptToExe=True ) setup( name='TestPubSub', description="Script to test pubsub for packaging with cxfreeze", version='0.1', options={'build_exe' : opts}, executables=[WIN_Target] ) pypubsub-4.0.3/docs/development/setup-py2exe.py000066400000000000000000000013251342344706700216000ustar00rootroot00000000000000""" File based on a contribution from Josh Immanuel. Use via python setup-py2exe.py py2exe which will create a dist folder containing the .exe, the python DLL, and a few other DLL deemed by py2exe to be critical to the application execution. The contents of the dist folder should then be packaged using a tool such as NSIS or Inno Setup. The py2exe page has an example for NSIS. """ from distutils.core import setup import py2exe setup ( name='TestPubSub', description="Script to test pubsub for packaging", version="0.1", console=[{'script': 'testpubsub.py'}], options={ 'py2exe': { 'packages': 'encodings, pubsub', 'includes': None} }, ) pypubsub-4.0.3/docs/development/sphinx_patch1.txt000066400000000000000000000015661342344706700221750ustar00rootroot00000000000000--- C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg_orig/sphinx/environment.py Thu Mar 18 09:59:23 2010 +++ C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg/sphinx/environment.py Thu Mar 18 09:57:40 2010 @@ -933,12 +933,12 @@ node['refuri'] = node['anchorname'] or '#' return toc - def get_toctree_for(self, docname, builder, collapse): + def get_toctree_for(self, docname, builder, **tmplKw): """Return the global TOC nodetree.""" doctree = self.get_doctree(self.config.master_doc) for toctreenode in doctree.traverse(addnodes.toctree): result = self.resolve_toctree(docname, builder, toctreenode, - prune=True, collapse=collapse) + prune=True, **tmplKw) if result is not None: return result pypubsub-4.0.3/docs/development/sphinx_patch2.txt000066400000000000000000000013451342344706700221710ustar00rootroot00000000000000--- C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg_orig/sphinx/builders/html.py Thu Mar 18 09:59:25 2010 +++ C:/Python24/Lib/site-packages/Sphinx-0.6.5-py2.4.egg/sphinx/builders/html.py Thu Mar 18 09:55:40 2010 @@ -623,9 +623,9 @@ if self.indexer is not None and title: self.indexer.feed(pagename, title, doctree) - def _get_local_toctree(self, docname, collapse=True): + def _get_local_toctree(self, docname, **tmplKw): return self.render_partial(self.env.get_toctree_for( - docname, self, collapse))['fragment'] + docname, self, **tmplKw))['fragment'] def get_outfilename(self, pagename): return path.join(self.outdir, os_path(pagename) + self.out_suffix) pypubsub-4.0.3/docs/development/testpubsub.py000066400000000000000000000004221342344706700214230ustar00rootroot00000000000000from pubsub import pub def listener1(msg): print "The listener received the message : %s" % (msg, ) pub.subscribe(listener1, 'test.pubsub') def sender(): pub.sendMessage('test.pubsub', msg="Hola! this is a test message") if __name__ == "__main__": sender() pypubsub-4.0.3/docs/index.rst000066400000000000000000000122251342344706700161740ustar00rootroot00000000000000.. PyPubSub documentation master file, created by sphinx-quickstart on Mon May 20 21:24:10 2013. Welcome to PyPubSub's Home Page! ================================ This is the documentation for the PyPubSub project. This Python project defines a package called 'pypubsub' which provides a publish-subscribe API to facilitate 1. event-based programming 2. decoupling an application's in-memory components PyPubSub provides the infrastructure for using the Observer pattern in your single-process application. It is pure Python and works on Python 3.3+. Using the Observer pattern in your single-process application can dramatically simplify its design and improve testability. The Observer allows code to observe "messages", without knowing anything about the source of the message (which Python object or function), and in turn allows code to emit messages without any regard for which code will receive the message (it may not be received at all), what the receiving code will do with the message, etc. Basically the Observer pattern is like a radio broadcast, it is a one-way message sent, the only contract is in the message content: the receiver/listener must have the ability to decode the message. A classic example where PyPubSub could be useful: a GUI application. How do components like views and dialogs communicate their changes to one another? Without a publish-subscribe mechanism, the code can become a real spaghetti. PyPubSub makes it easy for your code to emit messages, and other code, in the same process, to receive those messages. PyPubSub takes care of the plumbing. The Publish-Subscribe API provided by PyPubSub has the following characteristics: 1. Message Sender: The sender of a PyPubSub message is the ccode that calls pub.sendMessage(). 2. Message Topic: a. Every message is specific to a "topic", defined as a string name; b. Topics form a hierarchy. A parent topic is more generic than a child topic. 3. Message Data: any keyword arguments used by the sender, pub.sendMessage(topic, \**data); a. A topic may have no associated message data, or may have any mixture of required and optional data; this is known as its Message Data Specification (MDS); b. The MDS of a child topic cannot be more restrictive than that of a parent topic; c. Once the MDS is set for a topic, it never changes during the runtime of an application. 4. Message Listener: All message listeners are callables that get registered with PyPubSub in order to receive messages of a given topic, and must have a signature that is compatible with the topic's MDS. 5. Message Delivery: a. Messages sent will be delivered to all registered listeners of a given topic; this includes listeners of the topic, parent topic, etc. Hence the root of all topics (called ALL_TOPICS) receives all messages. b. Sequence of delivery is unspecified and can change at any time. This is fundamental to the Observer pattern, and your application's listeners must be designed to not depend on the order in which they receive a given message. c. Messages are delivered synchronously: a listener must return or throw an exception before the message is delivered to the next listener. d. A listener that raises an exception does not prevent remaining listeners from receiving the message. e. A message sent will be delivered to all registered listeners of the specified topic before control is returned to the sender. 6. Message Immutability: message contents must be left unchanged by listeners, but PyPubSub does not verify this. 7. Message Direction: a message is one-way from sender to set-of-listeners; PyPubSub does not support "answering" with a response from each listener to the sender. This could, of course, be achieved by having the sender include a callback as message data, and each listener calling that callback with agreed-upon data, but this (typically) increases coupling. 8. Message Source: PyPubSub does not provide any information to the listeners regarding the origin (aka source, or provenance) of a message. The sender could, of course, include such information with the message data, but this is *not* recommended as it defeats the purpose of the Observer pattern. Here is a schematic representation of the role of PyPubSub during message sending and delivery: .. image:: pubsub_concept.png :alt: Sketch showing how PyPubSub fits into a Python application :align: center :width: 450px .. PyPybSub was originally written by Robb Shecter as wx.lib.pubsub in wxPython 2.x, sometime around y2k. Robb and the wxPython author, Robin Dunn, allowed me to take over the package around 2003, and I moved it out into a standalone package (no dependencies on wxPython) around 2006 with their approval. I hosted the code on SourceForget.net for about 10 years, then moved it to github sometime in 2016. The code is very mature and stable. See :ref:`label-history` for details on its history and :ref:`label-roadmap` for possible future work. Contents: .. toctree:: :maxdepth: 2 about installation usage/index development/dev_index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pypubsub-4.0.3/docs/index_redirect.php000066400000000000000000000003561342344706700200360ustar00rootroot00000000000000pypubsub-4.0.3/docs/installation.rst000066400000000000000000000027551342344706700175750ustar00rootroot00000000000000Install ======= .. contents:: In this section: :depth: 1 :local: .. _label-install-reqs: System Requirements ------------------- Requires Python >= 3.3. It has been tested on Windows 7 and various flavors of \*nix (such as Fedora, OpenSuse, Ubuntu and OSX). For Python < 3.5, there are additional requirements: - Python 3.4: requires the "typing" package (from PyPI) - Python 3.3: requires the same as 3.4 + enum34, pathlib and weakrefmethod (all on PyPI) Please post on pypubsub forum (see :ref:`label-support` section) if you have successfully used PyPubSub with other combinations of Python and Platform. Many thanks to Jerome Laheurte for providing a buildbot with Linux and OSX VM's for continuous testing. How-to ------ With pip installed on your system, do :command:`pip install pypubsub`. If you want the developer version, you can try :command:`pip install --pre pypubsub`. You can also get a zip/tgz from https://github.com/schollii/pypubsub/releases. .. _label-support: Support ------- The forums are currently hosted on google groups: - http://googlegroups.com/group/pypubsub: PyPubSub general help and support (hosted by Google Groups) - http://googlegroups.com/group/pypubsub_dev: PyPubSub bug reports, feature suggestions, patches, etc (hosted by Google Groups) Also, many PyPubSub users are on the `wxPython-users mailing list `_. Release Notes ------------- .. include:: ../src/pubsub/RELEASE_NOTES.txt .. include:: changelog.rst pypubsub-4.0.3/docs/make.bat000066400000000000000000000156631342344706700157510ustar00rootroot00000000000000@ECHO OFF sphinx-build -b html -a . html exit /B # The rest is the original make.bat, which has too many options! REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyPubSub.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyPubSub.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end pypubsub-4.0.3/docs/pubsub_concept.png000066400000000000000000001355671342344706700200730ustar00rootroot00000000000000‰PNG  IHDRB”Ù(M¯sBIT|dˆ pHYs : :ðd’JtEXtSoftwarewww.inkscape.org›î< IDATxœìÝu|eúðgfvÖ}7›Õ¸[“¦îN[J[(~èázØ¡wØzpŠ_)P¡B)ÕÔÓ¦»g“Ý$ën3óû£$¿P¤–f“ôý~>|ØÌ¼3ï3Ód÷Ùw^Á` ‚ ‚\€X •R3ÕÑŽAAdH4Ô˜ÁÚã>‘¥fªáž'/ŠvL‚ ‚ Câ¿ÿÖ7àÑAA$ZP"„ ‚ È %B‚ ‚\°P"„ ‚ È %Bƒ¤®²K´umiÜÀmß{<¾®²K4”q5*‹¶Õ !€‚ rN™­|mçøû¯]õܺϋS~mÿ3÷®¹þÁ>yÒÒíf~xgï/w¯¾ùþkW=÷õªƒéCQßê•ñÞ¾/û~¦(ûòýý_®ùèàÂóQß'ÿÞ“ÿk ÏçÿÝû§/þ»ïÙóQ'‚ ‚Œ6§L„.¿iR©ßJØòÍñ§69pßû¯î˜ÐÒÐs§\)lPÆŠBç/Ì3SZÜ*ko¶Ül·zçØYy4b œ‘);TIÇù8ÿŽM•ol[_¶èäíÊXq…B%êDAÑæ”‰TÎ/Z‘ÿ|0NøçëoéÛÞRß#8´»þ ¡ˆ{üÉW.ýæü†yfÖqd!F«õÒ¯mÏœŽ+/q¼õåÍÏÜñçy‡†²Îg^_±æïÿ½fåPÖ‰ ‚ #ët ]výĆc[?nkêýãÿ>Ø_tÍmS«ß~që}E‹®¾uÊ‹3}eK4+>ûÏÞ»ÜÍ0¤@È©]´¢à½‹¯(hí+óäÿ»=¡¸/xÝ[ëyø¦ÏáñÙÖÞ½jÀ›Ïm™]SÞµâŽGæ>óé;Ew¹þ1")¯üÏn|á÷â5¶Ú.–Èøû—\1vÍÊ×w^ñå{ûæ>öÒ²MË-[óiJFìžß‹sõÊYÁ@81#O»eæÂ,“PÄ-miè¹øWêÊ0uØ?ÜzORZìæ1ãã_£)š¿ö³ÃïíØT©ëÕáO²Y<Ó¶m({Nkí/œœø I޽?ÖüëÃ7vþVEc^w ÀåðÅômÛøUI⪷vîó“Ó²4ŸOœ‘ò¼F/Ûmíq§õ•iiè's›sÇÅý;§Ðð:—GvU—{þ¡oûñ%¥©¶`Ð|§!)Mµ%)Mµ%%SÝÚ¯Ûè?Ÿ¥ÛÍ~é± ÙzÝÓ“Òc¿ÊŸ˜ø€mþúØ{«ÞÚ]0à:5—¿ðÍç¶¼EEh^zŽæC’MXªMzó¹-³ïž#‚ ÈHuZ-B!‡Zz͸çÖ|tðãâ½/Š$¼ÃO¾|é†e>ù÷žkiŠÞúà¬[¦ÍËè0ž¼ã˯·m(¿cÙµã=› ÅR~ù¿>¾þÕÓ){d_ÓÅ ·ßt߬C‰iª-%íOý°¾ÌpÑò1?ë¯ EÔWß:õú¾Öª¶¦ÞC½ïëo6}UrÓÜ%9/ö•£"´lÜŒ”Çï}já^€p(²÷î+?ü¨¸¨áŽ[˜}ûé^Ç–oŽÿ‰ pç ï^}[¬Vüióe^þðotض{®úß\ßóøàÑ¿/ÝrÃÂw•Êùõþ}é–ß«óÝ—¶-…"ºK¯Óe×OløÃE÷^ýáçwÕß{óý³úw2 °4ÙöçÞ¾ò³Ÿ®sËmËßßÜPm^»N÷:Ad¤8£áóK®Û«•¬|éÕ…oœ¼ßnñÅÜã}I€F/ (T¢]—?ÿlƒLËÖì;r–n7ÛÒãž§R‹· „ àšÛ¦îÂp,°ûûª_´ ñìÚìâ“c|òa‘Óá+X'pÏ]/èd³h]¼üŸ7”ÕÝåäœNl›ôºc5q²M’ _µumiÜßÿ¼nù“wþïÖÇoÿò.†<¦bÎfd^¯Ù™Ïå‘}I—GÒ½l›ßÊ89þió2v÷½&Ù,Z$á "ª3­AAF‚ÓnêÃæ°Ü±Z‰çä}¡`D#òÑ9˜/äôÒ4#h©ï$¦©¼gZçâË ÊN§Ü'ÿÞ3‹¦h!‡K:ß~a댾í\.Ùbît. øÃïsy$Ý·Ãaõž|ŸÝÛkvÅRõõ}"IÂ2°€XÊï¬ìH›jÁ²¼SŽ ;\Ô f $R¾é÷Ê=u×ê[Û›-7’$aáñÙÍ\>if†ÌØfœéè¼€?¬e³Y–“· ÅÜ^€ñc8<ùZH6áôûB gR'‚ ‚ŒgœýîÉHÜQ¿˜@0ˆˆ0 Ç%+}†Q4Mÿ¬%‚¢h,¢”ÀƒÎ“Ix‘Ó©¿±Ö¼ µ±÷öÖÆ_ä8ðñÛ»'ÜùèüþD-¦Å¿ˆ5,Ü90ñ¡"¿¼&Ÿ7(HL±ŸNl¹ZëOÇI«Ì‘}MŠöfËMj½ôëW>¼î;íÏ=øí• Õ¦q§SÏÉH6Ëù•øþ° >Yé8›ó"‚ Èh0¨3K DÜ+0¦³ÍƸÝÚëžÈá’-}ÉÏ6û}á´åV¯<Sgý¦h[Úã ŒKLUý÷³î:ð¿Öß1ÅÂmU¥Æ%ñyƒ™g~¦(³[½ãø|vãÀr‘­XÿÅ‘ŸM(i6:&$Ñ“š¥ùEËØ¯‰OŽñq¸d«Ùè˜þ[eªËŒzÀÒ³µ?ë7ÔÝå˜prYœÀ|Á@ä7“ª>b ¯Ñï §?Ô"ÿù9Y,šž£uŸNü‚ 2 j"4iÞçE‹^z|Ã3EÛjԥŭ²Goùüþ€?œ2f|ÜG}å’ÒUÅ ÃÿzfÓý[זƽòÔÆ…»¿¯zÇ1ßÙÖÝ7¹àÜ%9ÛOÞÇå‘´2V¼ÃióM«*5JìbÞ~aëS%šMµÝÂ'ïüßÝÁ@8aìä¤/áX`Ë7Çßþ]…Þdtp_xxí §Ý7-)=öó3‰1Bü½žà˜‡oúìÏ;7WjMFwÝçÅ)/?ùÝb€œƒà rìPËÕ?¬/3ÔUv‰žº{õ=î`îÉçâñÙ 6‹gÒ›Ïm™ýåûû²K4+~­Î7Nü 0 ßudzÛ6”ªJ’§ïùê&·Ó?!5Kýѯƒ ‚ ŠA}4vñ­]í¶‡îªâƒíø€ pwnaÜ }#®î|tþ¡Æó·Ý]Î_¼·oŽc¾Ü¸Wë«Mg5 4EÑXW‡ãb¾S1p8ÿ@…S’¶mþúØk?=¼ ;_ÿ5€D.ØÏã“=o>÷ý· ð0 fåëÿyëCs~633‡C¶'¤Älúì?EŸÐ4ÃÁ0 õ ŠÏûÇÒugç½O-܉lyº¬¸õUoí^þÓfZ¥‘l€-…S’¬YùúWjÊ:üü?{W-–òeäjÿ]]j||à¹,Ë{{뺲GŽly’¦~Sm÷…S’¾>¹ÎñÓ’­ /sÿÎÍ•O}önÑjÇü©Yê·{iÙ†“Ë#‚ È…€)“f¦Â=O^4¨'>¸«^åó†ÈY‹²ºNîhܧ¦¼SÜ\×#Ÿ»$§}`'æ¡pÛò÷>á 8-o~qÓß,Ýnöá¢FíŒÆ“û#Ýíªçý¾°þƒõ·ßìõ‰ÝßWÆMM2jäשÜU¯êîrŠ3ÇèºO~<ÕÙfã–lÖLL4þs©g ã‡ZäÝ]NáÜ%9F’ÍÒû ‚ ÃÉ;ÿíiÜ¡&ÏNë9U™Ì<+3Oç:_1œ.e¬(4pýo9Ôé”;?ÝŸ_½Gºxy@/oŒz*˜”hÛ`ŸAAFªAí#„ ‚ 2’œ·¡ánÜ´äw…"î)ç4š0#e×äEL‚ ‚ ­ 6ºã‘y‡O§ÜuwN¯8ß± ‚ èÑ‚ ‚ ,”!‚ rÁB‰‚ ‚ ,”!‚ rÁº`;K#‚ £C(Áþ0NS4Æá‘4‡KÒ8ŽýêD¾r2”!‚ ÃEјµÇöõº9N‡Ÿ „Y‘Sa Dh"¡p*BãEEãT„&(ŠÆàÄ* ?ƒã8M°pš pš 0Š`á4‹ h‚Ä)‹ Y,œf‘%sC2¥0¨Œy|6šqÿ„!AdHù¼!ÂÒíâØ-^ŽËáãxÜAŽÏäüa6Ã0¿HjpI›Í—’$€Å&€Í&€$YÀ" Àq Âa "! Âa ‡#x8DA$LA(H/„Hä·s6›á ØIxA©\Tă2¥0ŒýjTÈh€!A伉Dh¬­©—ßÕfÙm^¾ßæ„ÑŸ}öà8B1*H¤<Èø –ò€ËcÉ&€$ À‰ÁéÒÊ0 „Ã4DÂ#àvÀåðÓá—ÝÇr:üB§Ý'„û€øpšÇ'ƒB7«“zRbÅ\Àñ¡irÁ0 Ø?µ$ñÊ øY™€/t"1røÁåðËáÇ6¯Ûääu›œ²ò£mÀpÊX‘['ó$$ÇxY$ú$P(BAΉ×$Zz„f£Ch·xEÁ`˜8ÑÒ+µ^ ½ *!`#à—Ï.Ÿ ±ZÉ϶{=A0í`6:ÀltpÛ›-ÜöfKLqQ-‘ñ¼*µÄcHR¸Õ:i0J¡#g%B‚ È£"4VSÞ)n®ïQ¸>>üÔaY"ãCbš 4)¨4 I"Ê‘)jHÉPÃ8¬0u8 ËhÇ{Í.‘ÝêÕUui82¬“ÙrÆì?í¸‘ß‡!Aä´Ù,²ºÔ(ïl³ËÃá‹ pHH‰­Aj½ øv´C S A¦BV¨ =&'˜Œho¶- =±­=*yŒÈ•š©¶%g¨=# 1ì‚„!Aäw1 @CµIÔXc–Û,`" ò²ã 9=ØôQB°pÐd 1È `Rtu8 ¾² ëj·Kõ4HÊŠ[ƒ†$¥-»À`9T´ãEþúíEA~•×$*KÚå-Vy fcú¤gk@­—jáøu†.Nº8x\¨¯6Ac™S_eÒ4Ö˜Õ1j±##Wg5$*üÑŽA‰‚ r†aàØÁECµ96¡.drÇ %K!'Úá(B1ÆNJ„1ãã¡­±êªLXw—SÖÝ唩ÔbǤYi&4?ºP"„ ‚ôko¶ðJ4뼞 O à0g¤@|ŠÃq´4å¹ ’Òc!)=¬½¨8ÚÆ6›tó×ÇÄ)™jsá”$Z$:P"„ ‚€×$ínP›ŒvŽcXvò ã0‚5t Pkc¯xçæÊìŽk_ÀñŒŸ–T1kQvë`ÿ­ç¿_ U7Ü=£x°Îy61B˜µ(Œ­V8º¿¯«ìÒ[­ò±“;ã“c|ÑŒíB„!A Ô·Êj+»4‘0E¨uR˜0=ÄRþÆáõY/<¼ö©ô;ìCeÉ©zs×M_}t0 ªÔ˜ºö³â›¬=n…F/ëüøíÝ÷U•_¯:”÷þ«;îÒ'(::Z¬ñ­½ã£qm§C(æÂìÅÙ0ka°9,¼ôpkÜ‘}MÑû¹€ !A LG‹•·G]"CÓÄŒ`HŒþçmnaœåª?Nyg×–Êy/=Þ¸\¦´,Z‘¿qÁò1_|p²R%j½ýá¹ÛøBN`ÏÖê9Wýqr)€TÎï|ä…K6[­ñû~¬ÍÎÎו·N™“¶ùº»¦€£w_±2#Š—xZô Lj°›* ®²K „‰ió2º£×h†!䜅‚<àã4EcIs¸$F? ÈðÔXkÙÛa6{q¨õÒh‡ÔoÑŠüúE+òëvûÃ×wÎûöÓâç-Í{ÆÒ펵öºþrÏW÷õ•e³‰@ßkµNÚÚ÷Z"ãÛvŸ ÀÒíJξaâ–¾}òQû]Ê9á Ø°`ùØõ}´6öª‚kÎÅÙ#a¶‘%BH¿€?Œ÷š]»ÕËñºd$L‘G‰ÐD$BáT„Æ)Š&(ŠÆ©MÐ1?­14Žã4Aà4ÁÂh‚À)‚…Ó,‚  §X,‚f±pšE‡KRR9?¨T‰‚R…­Éƒ çQu©QRz¸Õ@² ˜³$SÄ£Ò¯’Èø¡+ÿ8¹è©»V/6w:ø2…Àêvúͯ®ºþßgre¬¸©ºÔh;9±Àfñ4éˆH†ØÌ[’ EÛj ³Ý&ÿa]1ï’Ü´ÊýàC‰ÐÈaõ’–7ÇaóqÜN?ÇãpüÞ'Š¿u ŽcÀ" †M²0—’$€Å&€M@²YÀ" Àq Âa "! ÂáSx8DA8LA(a|¡ ¡èߌ‹ pšËgBNP(æ%R~P# ÆÄŠ‚èAÎÍñC-òªR£–ÏçÀÜKr0‰lhG…ÊÇoï™ØÑb‰OÏÕÖу—lž¬‹—ÓdÞËoš´ÿ¯÷­™ýâ#k¯(˜”Xæqx³[q÷ vþÞ9ó'$”ìÚR¹œä°"-u݉AXˆ¥|Hy –ñA(âÉ&NüG€8À¯´üœ àÄŒµá0 ‘pÂ! ü¾¸~pÙ}àtøp§ÝÏë59y=¦Ÿ –`¸…JŠvL£J„F *Bcµâ¶&‹Ôi÷ ûu±9$Ä'+A£—Æ u‹#bŠ!(b„]`*BC·É ¦;˜ŒVßB†eGÚ@(âøU©+»@o“Èø()B.H5f¡Íâ'$Ç@bš*Úá çˆÃ%aêÜtìÇïʱ’ͱ –é8õQÈ© DhqX½dåñyg›]GX8ŽJ-^ jƒ äJ\HÃ+ Zƒ ´|!0Ðu"1â5×wóZzT •Е–¥±&¥Çz£2‚ ©Êã±8ŽAÁäÄh‡‚ ’X­ô 0¶Z%f££Wºœ3” s ÐTk6T›Ö^·0‘˜yãâ 9#ØôO؇ËgCbš ÓTÀ0 tu8 ¡ª ël³Kt×KŽn Æ'+­ÙϦ¢/‚œOu•]"+ÀÏÈÑŽºÖá ݘññ`lµb¥Å­ª…úü1Àp†>E‡)¯'HTïµ7[刃aèž­µ^PÃÏYÁ0 tq2ÐÅÉÀë@}µšjÍìÚŠ.m}•Y«•82ò´V]œ}›BF¥Ö†^d5D;dÉÐÅÉ¡«Ã&öyC_€¾Ø ” 3>O8\Ôk2:d4Mã\. 9c š¥AßêÎ’@Ä…‚‰ 0f|ÖÞd…ºª.Üd´ËMF»\$æùÆLˆ7%¤ÄŒ¨!µò{(ŠÆìV¯P+v£B‘ÁaHR@g» kª5‹Ð²sƒ¡a‚aÊŽ´Êê*ºÔá0ÅRÆŠ!#G qÉ Àq´6î`ÀqRc !5V/ÔW› ©¶›¿o{mrCµÙ>ifŠY$ᡎÕȈ×ÚØËD(BŸ ?uáóì¡?ýÓø©É{î®ŸÏæ°¼7Ý7kõu™%SÔziÓ-š½E­—úN,’z`gÝ\ Ç肉 ®ºeJ ›Ã¢÷l­Žÿa]Ù|‡Ý§!IÂÃ=3>/œ’dþøíݓʶO øB‘”×ýÏþðžÇ _z|ýuv«W+rmyããŽ\wçô£á0…¯þ`áÑÍ3BŽ]Ÿ hfsX¡[œ³Ÿ¦ì½W~œYSÖ9/à8§ÎM/ºäêšèÞ½ßfHPÀa¬ºÚíb” úL˜žíX.X]ívî®-UñÆV«‚Ã#ñɳÒ`Ü”$*.¬ÎÏC‰Ëgƒ.^Ii*ð¸‚`2ÚyMµÝò` LkôR?ºïÈHVWÑ%³[½Âü ÀrKòïí»‘aï=O\ôEs}bëÚÒË…nï]Î_shwC¾±Í&?-¹uǦÊÄ_•\wÕ-“?_|yÁ_[è°ù"9c æ×þºéöY ³¶?øìÅkScj¥rAÀnõrÿ÷Á›ýûÒ_uË”í<>»'>9ÆS8‡CÚî|tþ&}¢¢yí§Å×&¤ªªTj±ÿ«Œ=º¿yÆ]Î_%s­;6U^-r,Sç¦×}ðÚÎéuå]…·>4瓼qq5«?^Q«ÖI‡ek1‹E@G‹¼î ž•¯·F;ž‘èÈÞ&0¶Ù55D‘ßÂw_¥Ù¹¥2Åãð3ót°ôêqŸ<ô+A;¬^ÎÎÍ•‰%íç¥òM_•dìÙZ>Î}.".ÌZ”³f‡Gâ5åÚ _Iio¶ ¯õä üa€P4<§_sÛÔ­qIJ÷œÅ9Å¡PDxçŸçÿ ‹—{ &%–´5õ¦ìÛ^;)F-j¶özDG÷7Ç«µ’öÒâÖq$Ih»kKUBFžÎ—¤t EœP$Lq¿[]2¡®ªK6}Af;€@ȉèâåÎÏÿ»w¦Õ%ÓI’ìÚR•PuÜ8f줄}y:ÛÅWŒ­‹ÕŠëúb¬<Ö>I/ohªíŽmªíŽ•Ç;vl®ûuºB„Ãôdç¡%åGÛ¥5ešp8ŠQ‹aâô*CG(ÁŸºkõ]N»O+–ñ»¼î€R(âZ^ùèºw³žÒâ¶\©B`›¹0«m0Ï;Xô ÐèeXűv¨.5òжÕ$kô2Û¤™)fˆ‹:"ž†` Œ[zÜì€/Ì #x(!Â! ‡#x$4p_Š`€ŸÞ¥Y,‚b± š$ Šd4›Í¢ÙÅæ°(¹R¢É1ÏXÀ&q/úýƒ0 è´l@"ç8\Ò%sÃB'Q\¿7$ŽP4«±ÆÜ?Ö?>9¦àÉ—/}ÿ«N^ûYñUß|rˆu×c ÞËk°<øìæyjŸ IDATÅÿܸºdÆÛò°B%jùÇ{׬lk²ˆ^ýËÆ?NIú!cŒ®ÞãHúV£÷z‚R™Bèî;?_Àqõ½ú#b—Ó/k¬1€DÊ·iôRóÜ¢³Æã³¢Üï á<>­?v–P"4ÄB¡¾ý» ƒÍâs¸$L™–‰i±Q¶æ£ƒcý¾øýu·?Õ·­±Æ,X†¦¬±Æ,5$*Ü<>»ÿCÉã B17ì°z98 §ÅÞÏV§"4ÖÒÐ#NÉTÿlá°áŠ`á?!’ÒTP¼· LF»|Óšcâ©sÓ[õ ´ãÀnõÖÇi÷r\ÎÇ{‹ö„€˜)üt°XÅ㳃щÅx¥²‹ñ*T¢Aàh½¹_AQ4Îb †Óú€CJ¥•´w¶ÙRyá’M'ïS¨D»_° v½ððÚ+wlª›3Ö°-g¬Á’3Ö°–¦™uw_±òo‡÷4hËŽ´%k ²ª;þÛókõW,òÄj¡`„@‰ÐÙC‰ÐòûBÄÊã]N¿ >Y g¤F} ÞnW ‡KzhšÁpc&._¼·¯pǦŠky<¶Ýë *gÌÏ\óÇføÃÄ]W¬|;o\ÜææºžÂH„âdåë‹üÛÅßìÙZÿé»E÷p8,Í.–òÌR…À­ë<b)æ]’ - =phwkïµI¦§´%gÄz¢ÛP£iëh±ò:ÛlBK·Kèqy4}Ò¢½lH¤<IN,ØËå’@’°~Z°—dÀbýôòÄk  "4„ÃD„C„Çi‡"'¶…)ð¸à´û—ÝÏ3w:øÐùÿõb _È Êc„­AæŽO‰ñ±ÙhUn‡vؼ¡`Œ¼ÖÎgî[“wÿVý9>9¦Úã ˆ9Vàñ._wß5«OVVbÆt´XsïyrÁ[›¾*É(ú±v–.NÖìrø¥,’ŽŸžbJxÁמÙtÉËO~ç1w:hŠé“]|EAñ»ÿØvãC7|ú€ßól;ŽcÀµ·O]ÿÎ?¶Ý÷ÈÍŸé´qò&[¯[ªª¿õÁ9û£wW~_À`P«é¹A‰Ðq9ü¬›*½ž 7-Gã§&‹ŽÐ_1öпžÙ4åμ¨O—ÏXyhÖ¢ìV€†j³ôÇïʯ¿çñ‹þ9~z²©òX‡òÕ§7>=gIN¥Z'õàNÿç›[ÿÖÚØ+~öož øÃÛ¸<’Z³êàuc''n½ç‰‹v–·Å¼ö×MKR6DõbÏPbª „".ìÚR….jHÂíYùzשÙ,Ýnv[S¯°Çä9í>A$B$I€Z'°h/$RpÏrx6ÁÂO|HóN¯!)¦ÀåðƒËî—û³t»¸-VnG‹Uyd_#’ð¼1±b>AîÑÅ+üÃàO,*8<2àõA,åE5–kn›úfßk]¼Ü}õ-S>ìû9bb§HÂû@ª_ÿôÆ—·m(O5w:é9ÚÆÙ‹³›nºoæÇÕ¥F_È ÜòÀìíR… ˜‘«³rxd¸³Í“[×0󢬯qc²óõÖçß¹êÙ][ª2¦ÏÏ8ªP‰|ˆœëü×Ç׿Uz¸U¥“»_z|ýí •È 7.¾÷å•xa×–ªT‡Í+3>¾vÖ¬Ö!½YgÈç I²(Ô2znP"4¬½ö®-U‰ˆ7>ò ã¢R¿´lý?_ßúÌw«fVmÏýä¢û·o¬¨xáÝ«WíßQ—Îf³<‡÷6fÞÛ˜@’„¯ôp«aáeùµËÿ0~@BJŒ‹Ë#ÅEºô\­Ååðë—]3®`Ì„ø^EŒ¨1zWyöbÔbX°,Û¾©‚9v¨%.wLJ-[gÂÒífW•å½&§$³~ZÛM%­^ ƒ *àxô2 ’$úךÈaó‚ÉèS‡ëér vŸ°±Ö $IDä1BwZ¶ÖŸ¬–#Ρˆ°[½QO„^–ßÿH äDf/>ñE @¥ûUq;Á™E+òëO>Gá”$sᔤŸõ×asXôü¥yMÐtry­AæýÃÓJNÞÞT×-Yûiñ mœ¬ký—G2ÜÎ@ì%W=Þ·_"㇖ÿa|ÕÙ\çPcV/Ãá²Ð«ç%Bç™Ùèàm«I ‡(ÖÄ)š¥‰vH¿€ã³üÚñÕ˯_]~´mç«ÙôlS]÷Z¿/Ä#<°û?D&ÏNû.5[Óÿ†”ÓßBBx8 “w€ÄqŒËøÁ¾}l.kÄ~IXxi>¶}cT•uÁ`„˜43µ7Úq+†a ®Ò$nª5ËíV¯@ äBj–´)Äj¥Qt{:¤rHåÈÌÓMÑÐcrÉh‡®{ÿB¼Çrü )1¶ìƒƒÍýÏÓTîê2#t¶Û¢2 u¸ŠÕH|1jQ¯Éh×hô2ãmÍ]¯ŒÈÙå-Ýn#˜Æ »àÙ¶áÿ.7‚u´XyûwÔ%Ò4ML_qIÃï ©­É"ŠOVö¢ )#Xx@"å³óõ͇v×_>ï’Üʸ¤ÿ/ð‡‰ß:grz¬“$ ßÎÍ•©Ë¯_ð‡‰ž.g†>Añ‹on#…PÌ……—Ž›+¡±Æ¬#Äô™ÃzDÉoq;ý¬Êc2c«U FH à .Q iÙˆÕIGôò-8ƒZ/µ^ “ÁaóB}• Zê{¸U¥F]]¥I­ÖIì™cô¶X­$xê3ŽL2… Ìp]í6à ‹§ðÂPÌ ßt߬CÑŽc0ÛN4L'¤ÄŒúÇõçJ„ί'HÚÝøœÅ9 ÖK£Ò¯úìÝ¢E&£=5V+iŠDh²»Ó‘ž¯ß©Œf\$n?¸«~û³úæC’ò8‡Ã t¶ÛÒýûÒ«4’ßmᙾ óÛ _½­üHÛas§3•/äŒø.Ÿ ó—æÁÎÍ•ÐÖl‰‘ïðgFĈ8€õíiˆ5µÛå4Ã`|‡“§ƒ”Lõ¨]†A*À„é)0vR"ÖÒÐ Õ&ÂØfSÛlJ¹Rèš8#ŤP‰F壅X­ÄÕÒУêê°ƒ.NípADS4´Ö÷0A#ZÏšYú<Ù±±"Îí ð'ÍJÅ ‰Ã¯%¨ÏŒ‹2«¤rA+0ŽKRvÜúМus—äöO+?m^F­.NV¡hJ¡Ú–]3nGr†Ú‰8ðøìúômÿŒ¦B·-o\œ™/äDò'$tªµÒJŠf¨‹/»}ÜÔ¤cé9ÚN…J4"›¡û,tñrh®ë“Ñ!ÒÆÉ#aÁÃò#mÒý;ê6¯P¡a¦§À„)X¬V $ù›{£Nà ˆBj–´9„Ã0w:8Íu= ·3½Ô‡#ctÕé’ÊÁ†j³Âaõb©Yša×Ògévq·m(OÇ0,¢Œü÷…ê2£¼±Æ,7$*ܧ.=²ÔW™ µ©KÍRwkãä(:K}3Kc0eÒÌT¸çÉ‹¢Ó¨Qr Y^SÞ©KLUÁÔ¹éÑ9º:ì°ss%E\ß²kÇ5 ×g&£ƒ{d_£Öåð 8S0)KÎP»Åh0P¼¯\?pyìà˜ññ]©YêQÕߢh[º½Ù3m^$¤ÄD;œ~%šÕýÞ¤´Ø£Ód2'ÝØ·ïÇïÊ“ë*M ÷>yÑŽs©ãã·÷L4uØuO¼¼|íésß5«óûBrÇ(IøuñòšÇÿ±ì[‚5|FeE¬ÿòÃйôºñu,’6±4ïüý8´§=lo¨6«ùjeÅ´dæé ¦¼“_QÒ!Í7¼=ŒDhlÿŽZµ±Õª`ÀR3Õ?)ãpNk¨úA­—Â’+ÇBMi'T”´s5$6Õ™Sf§w‰¥£cñÝÂɉ½m6EÉfL£—bîÐÿû[{Ü\Iпÿäõ¹Ié±GûDz '—·˜ÝS‡ýCkCÁÞkvñtñrïoÕ3°Åù¦ûf>¹Œßbõ˜\•h†€?Œeçë{P48P"4ÈŽjQF"1nZìÑÿÈáB–;.šj»™ú*SlvÞ9\æòÂø¶ åñN»O(„é) T‰¢Ö°„ã8d5@Bš Žîk‚ŽV«dÛú2Þ¬EÙ-ÊØ‘ßwH âRÙ†Îò£m†;ëaÖ¢ì!k tÚ}ìç^{›ÓæÕÓ4C¨4’ú¿¾qùª¯W[v¤m.†sß5«2~~Éú:üvw9ù»¨^Qüû®Yõ˜>A^ûØ?–mø÷‹[ç–h¾„ËgÛìÖ‡æü·pJ’yÕ[»'Wï(ôûB†f‰\Ðuóý³¾MËÖØW¾¶sZW‡]ÿÌë+V‡‚üÅGÖ^ÛÞb-”ÊùíáÅû÷ê?þý÷â×Å˽\ér;ýC¿öÑoho²@C¤r'âè›Æ#ZP"4ˆü¾ÑÞdQŠ%¿éép0B<}ïš>}·hÊíÏÝkîtªq£~~ÉæÇÄj%¾Ye­¯<Þ1æÅÿ\½ àÈÞ&ÍÑÍ—<õÊeKÍR;^}zã’/ßßweᔤ·zÍ®ŒÇ^ZöLÖ½íŸOlX¶îóâ'·4}ðÚŽY»OýʇxB+x=ÁßüìÛµ¥jñÁ] Ó]ŸZ®v¬¸~â/æ#Š—Ã‡ö40$IP3dv ?ëÁ3ºzFYMY§$¡ˆœBúð¹@däjÍf1mM½òhÇb·zÉÖ—%yÜ^^aL˜ž‚~ÏPJ¦f,Èš¢X{~¨Nniè6­çbÆE™]|'PVÜ eÅC³îqg›-5#O{\ äD¤ A0%3öx{Sï÷8^Üš¢ˆ5§f© –å¶öx’ûö+bDYcô6€9‹sŠÛš,y'Ÿ£¥¾'+#Ww´oÎ ó›>3Çè‹—];nÓìÅÙë<î€ü»¯JÆœĩÍfñÀ¶ eL8LÁØÉI£åÑíp¡Ad6:Ä8Ž!aøŽCI OP`^Og·z£Ö§×ìbÿø]y²ß⌟šyãã£ʈgHTÀì‹sÃ1ìஆ„†j“ðÔG o.I/¼tL³PÌõUk‡â½§½èí°‡Aÿ…`^ÿ¢ØïìH­“X¦ÎMï¸ê–)¥ùö–ìož9až-s§¶m(gBAŠ?-¹m´uèP"4H‚0î°û„±:)êt1$* ¹®[úàà®z}8D‘Sç¤Cz®.aŒ*jæ/ÍÃHÇŽl1ø<ÁÿGÍr¨E—å·HåO}• ö祥>É.^ÞP_ið‡ ‡ÕËi¬é.ˆKŽ9åR;Ú8™Õë öO|T0!¡ÑÖëNjmìlÛP>Q¡öOÎjíq§6Ö˜%»·V‹OV–Ÿ|΄TUMmEW¡Ãêåœèx}ª8V/§©¶;S­“Fm"ØŽ ìÚ\ À=eNZKZ¶fÔM0 >BƒÄØjãÓ4éã¢þ„>}·hB\¢¢§¢¤#Õfñ(ÇOK.¹hù˜†•¯ïœÞkv©ç.ÉÙ?yvZÿú>wÕëvo­žô‡yÙ†Š+nžTpbÆw«K Zz’1§'ÎH9ºhE~½¥Ûub{co Aà‘ɳӊ,Ëkj¬1K¾ûßÑ©v›WiHP´,¾¼à¸>AÑÿíeýG²K‹[ó%2¾mÂô”ʺŠ.Ø}àD'É/ßß7ÓnñÆÄhÄ]·>8g7ÏÍ¿Zƒ 0 ƒÞn—¬§<`•·Ê\N¿ 3W‰iª¡®~ÔRÄaüôlßöZÖÁÝ ê¹Kr:O}ÔðÆá’ôÂËò[·o,7´6öJÂ! f,ÈÛÎ0€ÝöМÿö•WÆŠë^ûëæûq£„"®õÌ^sò9o}pÎî^kxðÆO_’)­¡`Dð[¥7­9vã÷ß–†q kô²ÚknŸºýÜîÄÙi¨6CqQ#°H"2cAF‹Æ0òû« Wh¡ARVÜ&«8Ö®Ÿ½8'곸>~û—·9m>]z®ö _ÀöÚÓ¸L/+—Èø6¹RhÝ·½öòg^¿üÙ„”×öI«W¸=o\܉Œï:´»aÑ„)?Ü|ÿ¬ƒ/?ùÝR§Ý§˜6/c¿×à2 À7O*ÿÇcë/óy‚¢©sÓ¸N`ÌŠ&Vl][šÚÞbUk ²žÆsb屎Ùï}{Ûã g¾ùäpîÖµ¥×M˜‘²)à ñjÊ:§ð…ë¿>¾þm—ÓOþùæÏŸÑ'È+Òr´õ%š'áƼôÁµïEõFžµŸ3 ÷-»f\óPÖëqX›ÖKcsXø%WbÂäˆCmÇæJ0uØaÚ¼Œ¦„”˜»fÞ@ ÃÀÎÍU:“Ñ.Q‹aæ‚Làž§ÙÅ»:ì6› ÎuM/¯'È2w:Éé±ý³¹¯zk÷䶦ޤ¿½yÅm6Áo ­ïã°z9æN‡ 9Sí IbX®9Ç0 Të€ò#mÀå’¡Y‹³[”£töóhCó 2Ÿ÷Ä(¾`x,U«“Ö?ð×Å?ÔUš ¨0Í~äùK64T›³÷l­ÎL¸wæáÖ—-Ì-4ì¼ÿéE?p¸dèÀκ7ß?ë ¥Û­Ó'È›æ.Éi¸P¥µÇ­MHUÕ̽$·ià›ÉO«L7´Ô÷ˆy|v¨©¶{ÌÎ-•Ió—æ5ÜU?;g¬açíÏÝ ðÂÃk¥v«7à«LäòHçÓ¯­X0oINíC7~úбÕ*Ø¢4œñø$æu‡¼ÐÁÝõêH„"¦ÎM¿ fˆŽ†‰ÓS`ãW%pì`‹Î hN“ë- Ã`΢m5T{³%fÃê&|<––£ôöZƒìw““Ó%r"“ “* *A©B0lט³özàðž°Y< ps/ÉmA£Ï?” ’@ LðxÃcºødeKßk¡˜kSiÄ]}?‹%<›¥Ç­pÙýšjG§úž+?œÀ`‡VÜ0aÚU‡VÜyùÊñÉÊ’Ë®Ÿðcnaœå²ë'¬ÿæ“Ã+î¼ìƒ…ñ)Ê’7Nú1;_o=¸«^·zåk ‹$\K$LñªÌñó—æ5y=AyZ¶¦?¦¸dek_"ÔÝéÔxÜÕ=W~ø—¾ý\éìl³‰GJ"Äå±Áaó éßSW»ÛÝå”éãýý”Á's!·0J‹[¹ÇÚ¥ùìÑŽi°ÌXi®-ïôUëÐÙßD6ÖvÃÄÉ ŒJw·36}~Fuv¾~h†ÁGÁ`J·Acµ&.Ii™8#¥‡Ã%‡e«Õhƒ¡AB’ SÀv0€a}ÒÏ¿ú-–Ãc¹2rt%w?±`çÉû&ÎLíš83õí–úñçÿÝ»ðã·÷\û¯¯kÊœtã”9éo6ÕuK>ÿÏÞÅ¿½ûªW>¼îÝ_•\T0)±¨oF×{¯þèIša0>Ÿíh®ë6@=€±ÕÚ?™‰PÌuKdüÎ×>¹áÍA¼C*ŠAàCú¦ÕÕa¤d©‡²Ú Rj–J‹[ÁÒíæÀ¨I„2òt®¤ôXÏá¢Æ˜ŽfKÌÖueXJ† &%@4f¢>)™j'dˆYøød ÐTg†ã[™`0ŒI¤|ïøéÉjtضZFhÔØ áñÙa¿wd=Ê;)qoÙ‘¶Ùë>/Î øÃD]E—lÓW%kVc6:øúD…G¥‘˜ |õáüî.'ß pÇÄŠ»û¶N™:ì:›ÅÃYùúΩ.‡¿øRÁ¤„ý%í³ÿ÷Áþ‚ÞØ5¥½Ù’×7œuá¥cŠíOâ{¯lŸa6:ø–n÷ì/ˆÆý8[_H6Ê:V/@®ñ£»‡=—¾€.‡ŸíXÎ6‡EOŸŸÑ=oinƒDÆ÷4ÖšaÃÿŽ2 Õf-£ì‡»Õ ÛÖ—Á¡Ý @Ó •7.¾ã’« ›Q4ôP‹Ð é› Ëçþï°R%2É•WßÏŠa·"FØÿ-V#´p¸dà¦ûfØý}õ¢ï¿-½šÇg; §&4Ö˜Sv_½@¥7_{ûÔoªÍi{¶V/ cTIÓ`Å ¿_³êà’§îZý˜F/«;9ñ;…òD½×ß=ãH$B³Žj$•ó{2ruzºœ€Œ<í–æükÃ꣗üõþ¯ç³XxP'«€ãCrÃßbDbî>Ëw9ý<=lú¥vŠ![­œP0‚ì37š¨4’à%W¶TëT—µ‡‹Xu•]ž£ÄT°P?´sfît@}¥ :Z­À0 £—['ÎLíæñÙ£òwj$@£Æ‰µÇÍþ~mizj–&Î@‹­ž,à Ã`<>;b³x8Ïþé›û’3bËïzѶhÇv®llùæ8Ä%){g,È4E¡Pÿú£ƒYÚ896{qöPTyÁ«(i‡²#m£jôØï øÃø‘½ªÎv»<¡’$˜äôX,5[Ù¨l;oÂ! šë»¡¾ÒNlj_™BàΟ˜`ÖÅÉѰø(A£Æ™B% ñœ€±ÕÊ=±´A´#^:Z¬¢—ŸÜð_À±9>½J#©¹ñÞ™»£×`èh>1uP\’ÒuŠ¢ƒÆåð³L â U•<¡èDï?·ÓÏ€QŸqy$=}A¦9¢º«JÒÖ†Eme¯¶² bµRHÏÑ€>A8ŽÞì~‹Ãê…º*´ÔwC$B‹EP†D¥-»@oCC⇔ "•FìjmìåÚ,PÄ ~¥f©¬¿ã‰hÇq>´·X€$‰H|²rÈ>1ÂAà´Óævýü¶m(OÞòÍñe‹Wä·`ù˜F€£ûšÔŸ¿·ïÚ©sÒ·÷MØÙÚØ+~ãÙ-·æ‹;øÇ?Í>ݨOÍn;1:û\çÃiH6ÁäOˆ·çOˆ·w´Xyµ]Š^³KÚÝåÀx|6“š¥ÁRc@,áE;Ôa!¢ «ÍõU&è1ŸèÇ-s}‰©*[Ö½ƒd¨×Õ0ƒ¡A””ëlmìUÕ”aÚüŒh‡ƒ Îv;8í>ÐdΡ\àÃ09~k¯GÀ0̰Z\Õaó ­=î4»Í×ÿmÀåôó¬=î4›Ås´o›ÏdY{Üi¶^O}t"=3¶^F«u’ *ȨðF¯'hª<Ö!ëh±(ʶ±Ë¶@È­A ƒ Ô:)°9ÆÇ Ã0`évƒÉ耮;X{ÜÀ0 à8N«uRGF®Î¦Oû£'òÛ.ŒßÔ!¢5Èr¥ÐÕÚÔ+Î.0€L9*®F~GùÑ6ÀqŒÉŸ˜Ð;ÔuKd|¿Ëé¸~K‡OŸ©sÒ…"îkyãâúç®3>ÞtÍmS_KËÖt÷m‹OŽq]sÛÔ×âSb†|Y’³aëõ0×?œ’Îh9ÔÄ)– ÓS,5&aG‹UbëõjÌì†3`Š!h 2Ðêe ˆªGhWLF;tuØÁÜé„pèÄ8 ‚Ài™Bà‰ÕJÜ™côN¾€ME9Tä4 Dh™ß½kK•¸´¸†['ÖÚòNùw«K¦uw9ã~~ÉGg|õûB¬p˜ÂÄÞ9÷Ûæh ÒžK¯›Py61Ýßœ´âƉ¥Ã}­1c«¬=nÐÇË­r¥pH‡ÎÈ”BG«,=îa•éâå^]¼üg­< •(°øò‚Ÿm9‘“· W.‡B¡«“ oö`@j–Æ“š¥ñXzÜìö&‹°Ûä:l>¡¥ÇMT”´IL¬NŠiõ2*ø ‘ñ‡ýE}(Š·Ã»zLN0u8Àíêÿ5`Db®_/÷èâdC’ÒG#æñ J„™.NˆQ‹í6is]7$¥ÇF;¤~Ÿýgï2…Jd¾öö©_+U¢Ÿ½¡úNÑc«5ñùw®úä\ëŠ9sVV¯<°¨¹¾g ÁÂW^sÛÔa;|>CqQC83vrÒ·Ä'+=UÇ;èÊ’,!9Éa×]hÔ(?zbòb]¼­þý;”*QH©ÙÀFÓ ÖÑbåu¶Ù„–n—°³ÍÆ7¶Zû›…Øl#–ñ1‰”b)$2ˆ¤<‰yQi= øBàtøÁåðËî§Ã.‡ŸñºƒÿRŽºÔ IDATŸÛpydHkyÔ:©;!UåE­>#J„΃isÓ»6s\P¼·‰¥T‹±¡îD¸umijÉ)Ë.0´NI2lùæxš¹Ó‘•œ[gíõ΃â°z9Ý]N­×”®ÿâH¶TÎ÷ÎZ”Ý °G¡±Æ¬KRvÏ^|bÀ¦¯J2¦ÌMoÙûcm²ßâÌ_š[£P‰¹:£T!èO´\N?¹sseªÝâ•$¤Ä˜žg §ÝÇno±Œ75iCÉæ©Ã9:¸«|¾ –?!¡#ZkIdüHRzlwCµISyÜyãâ¢ƨgê°Ckc/(U"gJ†zD,ù2à8ÆÄ'+}? "è øÃxG‹…ï°ú¸.§Ÿãó9.»—cévýìsÃ0‰¹ –ñÇ#$YÀb@² IX$$›$‰I²NlgŸØŽã„C„ÃD„C‡iˆ„#ýÛûÿŽ@8HÛé§ÃÏ„Cì¤øiŸTªEA¡ˆ”ÈøAµ^êG£½F”—?5¹cÿκÄ}?ÖÂü¥y@²‡f"²Çoûò»Í«×褵»¾¯¾fÒÌÔuwüy^Q]eW2EÑìök‚ßâ.X–×ÔwŒËég;í^UÀ×–w¦)bE½³e·¾úôÆ%5¥3ã’•ÇöþX{ÙÞk?óúŠÕß~V|ÇöM•"1×êrøcöüP½ôÅÿ\ý’\) nþæø}‚¼5-[³¯¾Ê$û×3›$I—¤¬ª.3dæé>V륿aµîó#ŠQó wÏØóàŸ.l¬1KR2ÕÃnúüêãF0¶Ù@£“ÚrÆ¢߸©IÖ®v›¬êX;7!%ÄR4rg0Q5AàôäÙi¦hÇ3’qy$ýÓ#´Ÿ%“~_ˆè5»8v‹—ãrú9W€ãó9m66óÓ=ç›Í óì @-Š%Ü äÿØ»ëø¸Êtàϱq—¸kÓ¦®i)T(´¥-²Àb˲vwÙ½«¬°wå®°Æ]]´PÜ jÐR§.iÒx&6IÆÝŽÝ?JJhK-39“äù~>ý@&ç¼ç™TòË{Þó¼FuÜœ¡‰Í—ƒ „R¤¸"#Ü×ãëomìÏÚüî1XtM5(R¼!ëÛköU;úåÿ~ë2 %l|ûèÞ—ŸÚõ­Û¿:oÏwyÍú¯\÷ï7}yÖ¦‰Ó \ƒÏ+(±+&äé¶¹‹ü‡ëß8¹VçØþÎå?}ô†ŸVNÌñÚ»¼ïþø—Ý¿£uÛÌù¥½ÅåÖãßùÅòMݵú{/ý{çeßúéÒýÂãÛ¯µdhm¿}ü¶gÎWÿ‘}¶y³æ—n3˜Õñ¼"Ó‘÷^;TóÐ#Ë7$ï+4tµ‡ºàè>¨ÔòØü«ª$ÿÆHQ¤8}nIÏöM'Jömo!_;{X%ѱƒ Æ`ÜÄœ>½Q5ìëÀÆ¥JÆ”X"%Ÿo?!Š"D#,±d<Æ’‰8G%â™Hp$›à)6Á“Ë“,Ë“ÇS+¢(4M 4Mñ4C C ´Œâ%Èd´ “Q‚LÎð2-ÈŒ T2<Íàãìc¡ªYXá µ¡?kãÚ£â•×N$ÔšÔ5À³µ8ó2²uM C W,ßþò“»ˆ†£=ÖY——ÙÏwþ`uGºsU™³rbŽ 'ßÖ”] µ=yAhÚœâ†ãó MM}=þüÓÇqõ‹®¸züºó]¯ö`§Åç L™]ÔÞÞäП”w|ÛÆúë -‚(ÜÝ µ= Ö*¢K®hK—m J,‘œ|£×Þå5íù¸ j”§Õãô#Uñ¨;Ü­":}nɈx²m8Ĉ, Z™VlNiQ‚ @¥–ñ¸¥¡«YPá”˾þhwÎÆ·ŽŠ‹¯­&RÕžža(6çNÝ ¢Œ ˆŒR-»è{Úr9Ír þs÷Yد’)˜S?üÑSŸEY%M“güÄLÓd"ˆž·À†·ŽÔPÉþ߯×gИ†Mk– 4哊 ˆ°gk´7;@oT…®ºnR‡\Á¤EpùUUöÍëj©¶Æ~}"ÎÁü%ã€ÂÅÓ—ìð^Ôî•Z[¸|‚ ƒågÚ©»o™×Lbt lZý=@èRà¿”Ã`ZM±gê좮X4›Ö]ŽÔÛ;ùÅû?“+?Ï ²[îóÔÀ­²sY¼¢ºeÛ†úà×nzò×– ­íÿ}ûS׬šöì»/¸Ã[Gƒ±h°xEõ ¥•™§32:þ_·?óH<Êês LGo`ÞÁÓÇýê÷¯Üøû­Í|øë¯ü^oTvÆ£œî·ßúÛÁÛ¼÷ú¡*€Óg~,¿ÿ_Øô#+ôªÉ¢‰í«sñâ1>^_Îþdç=‹®™Ð“Î3$Iˆ‹¯Ø³cÓ ¾£ÍeÝüî1¸béxHåíØÑ„Mð°kK#tÛÜ`4«ƒKVNêL—۟颋ºå.!VÌ?ûºÔµ ”,¸ûü0ëhu©ölm*â8ž*—Sç%½±X,ÊR]ínmé¸L?ICZÈsÑÚØo(®ÈðT÷®xü¯üíæGŒfu,ˆ1¹…¦ð¹Æñ¹Ãr{·W]6.Ë—îß\D ½©îiã1–(,µ:ç/7,»Ê'˾-–¦ºÞlš¦Ä‰Óó‰ªÉ¹@’8üEÚ›phO›$ˆŒl½oñ5ÕÝñó32lÔ—_Õˆ­Up­•º„† wŸ—Ha©%¢RËZ?ÙÖœÓÒЧélwÁÔ9ÅP6.+iOû(” _>>Ë—Œ±(š+&d{¿èóz£*¡7ªÎ»É`VÇ fõ°Ïê\,Ÿ' û¶·‚£Ï Cñ“¦ôNšY˜”¯åpš5¿Ìe²hbG÷ÙrïµÉ[ûaÖee•gº´´â÷F`ߎVè·û€f(¡zj~ïäYEÞ4žø“Œ\q œ³„{ú=©kA(™(ÈÏ+2ìùeR×2f¨5r¾²:ÇG’dÜÙPw¶¹ÈÞ./˜­PªdR—wA¾¨wzMIWºÏî\(–åáè¾ؽµ ¡˜˜“oô.¾¦º#¯ØWÃ-I@[ÓÉßëP0¢Ùª ”OÈv—Vfžsq?úÌ1æwRLføu©kA(™p±t"€)³‹¼ã&åúëw:Z]æµ=е=•k€ÊêÈ+2a×àKðE ©®ÚûÅD‚#hšâó ÍުɹžÌ}Ú/âN¶¬\C<+×`ÇØ¾ºÃ݆ާ¹þX·¢þX7¨5 ÈÎ3@Vž²ó Iª1UxN€þ^?ôvy¡¯Ç^÷ɬ#“ÑlIE†gÂÔ|öº8ÔmS9Ð̲ Û¿!u-¥ ¡4¤P2Âô¹%žésK<¶f‡º©®×ä°ûõ}=>B¥–Aùøl(¯ÊÅYX-A¡Û憦º^èë9ùà—F+•OÈvO˜’ç-kœ†B®`„i5Åži5Å[‹Sekqê=Ž ¶¥¡OÞÒÐ-ÈÎ3@vž¬Yº´¹]+Š"x\aèëòBo·œ}à…“¿¥2ÍfåB¹…¦@Euv¢ðQøKá%§}†àž|þõ£RׂPª`JsEåá¢òŒpÐí=~¸ËØÝî1ÝßÁÔì„üb3äš!;Ï€¡èS‚ €£7ö./´79 II’bfŽÞWYã9}cGô™¢2k¤¨Ì€^Ÿ;ÌØZš~»_ãs‡5Wˆ®;Ò  ÖÊAoTÞ Qyê¿©š9bY¾(¼ø"à÷F!à‹@ÀA8™oH’ôFeØš¥ ”X‚Y¹†17Ë—líÔ}óyPŽÏÖß+u-¥¡B«Wr5 *œâ¢³éx¯¶¥¡ÏÜÑêÒt´ºƒI 9yFÈÎ7@F¶>m~j~oì]^èëöA¿ÝwrV@¡`åã³ÝÕÓò½jœ0µGÌžbn y³šbV{À+Š"tÛ<Êî·Æçލ"á¸ÜÞå•õtz>wV.gDQI( Ð ŒŒ†¡€–Ñ cH e40ÌÉׂ–åe9à°,,Ë—à!1ðß8TŒ„ã§ß å šÕTqAË)0†ŠJ­aÜE×ÑÔë¦ëuE‘‘­ƒì<#dçÁ`R'­Yc:ˆÇXèíöAïÉ["§¾Q’!jôʈ5SÌ+2‡òŠLÑÁë©*>t^a²…ÑxUæÍÞå6‰DŸG䛣ùƒz*ñ¼@xœ!™Û’û½YЕGBq¹Ï‘sO‰¢8¤?q€HÑ” W2‰Œl}\£UÄõFeÜhÑÄ-™Ú„L†·5S©~àj˜‚\a󥮡Tà 4‚LjvÊ,µ Ð+Šö.¢»Ý­uö5ýv¿º·ÛGÀ'í P2bV®‘0šNÞÂÐT Õ)€KG‰“·A|Qø¢àèõŸzÚ @©’%ò ÍÁì|C¨¨Ì>׆¨³{L‘pá^ÏÞ‚Ü Ë¥(Š­Yº¸5KwÖ[P'ñh‚ŒÅ8*cÉxœ#ÙG² žJ$xRE`d” “Ñ#£™œæårF+hA®d :ÒaAÇ„ˆ²ûå¢{UØÞ-u=¥¡Q‚ r L±ÜS œl‚':Zj{—Wãv5¶‡Òö¹ã Ðhä 3ª@g8Žt%è Êa_oÄs|Qð <Ÿ®9–ÿÜÌÃÐ\F–.”‘c•YB“š½Ðëĵt"d•­Ñ8â;"/y Užä¿@Ó¤Hk¼Z ¼Ôµ ‹ÓNß´)Gx÷Y©kAh8`¥%–Ue…ʪ²BñK:ûr¯;,÷{#òp0.‡âòÞn¯¬§ÓC~þ\´Z90ò“k:h††!ah O­û8ù_FöÙçHŠ6ÁË À±° þä/–?s=H‚Žå!N@4?£W­\A³­"®ÖÊã:½2®7©â«6n0_xð9›æ…ÖµS_ë¹³pŸ÷o¡êïC ¡Ñ&Fd)ÂDáÝ ±ÿM£pØ%u=  Bc„\ÁyEæh^Ñç÷ÎE¯+ĸ!¹Ï–1y8“‡Ãq9ï‘<Ç“âÉÆ›IEQ¤@Q¤ÀÈ(Öš¥‹ktŠ¸Î¨Š›Ìš¸%+uk@>zAëˆÝØyg…úŒúò­„,Ÿí©kAh¸`ãâäö &«†€ÐÙŽa<±T<Α‰K&™ˆó'×|°<Å&x’ãxRD‚¡)fH‘Ñügk@hA® O®Q0‚\ÁR.Þn^h}gêë=wîõÞé-TýŸt• ”>‚D¹.Jä~I)ö¼¤›R׃ÐpÁ „΋‘Q"#£8Ô…$É©Y¡þø7L¶È‹ž"|œ¡.ê–»ÄDÿü«RׂÐpJÿdžJæÖwE’ìóÞ!u-IÍKN³ÆˆÌ›Ô¢í9…Ø“º„†!4&œ’?¯ °×›l³Ôõ $%;¹ò^8O1÷Ì»RׂÐpà „Ƭ¦E–wE’à¬Ëœäùq´B#¶<Í@`HOe"4aBcVBM³A«üe€½ÁÔŽ³Bhlê#—<@ÛYÂ=¹QêZ’!4¦5Ÿœòì÷Þ)u- 7;uMKèé…º“Àb7o4&aBcÚ Y¡ëqV5.ò²)ˆÖóÏ캄¤‚AyÍ‹,ï $á+Üï½KêZ.]Ô-S8ÐÌ1 ‡žº„¤„Ay 5͆¬òö:s[Ø"u= 9ãk4„ö𯒺„¤„A!hZl]'„¯à€g…Ш×NÝ3eµEسAhÌà „°*Š eÈŸWØ•8+„F3ÒOV?ȈþsøuR׃Ô0!ô©¦EÖ÷pVvmôW– /Î>úÔµ ”0!ô©Og…V+ìu–Ö°UêzJ64tˆ(ÿŠLt!l픺„Ò!„iZd}_ o>Î ¡Q¨¾¥´5Gxÿi©kA(]`BhS³BAv%Î ¡Ñ$Fd(ÂDñ= Ññ–IØïº„Ò!„Nóé¬g…Ðhb£î^@(óø7VK] BéƒB§9õY]ii gH]BC"JµQ"ïN…Øû²NlðK]BéƒBgÑt¥õ=$Üù±Û4ù:©ÛîùB~Í+RׂPºÁ „ÐY° Š eÊŸW¹kqVd>rŠ9Fd®R‰«UbwDêzJ7„úM‹­ï:+ôe©kAèRõ×ÝGï/æŸyKêZJG„ú¬‚â‚™òÕŠ ·ÂÚÂY!4â8Éù¹q¼B#¶>-}¬Ôõ ”Ž0!tÍ‹­$áÊ;èÃY!4âô‘W=@ÛSÊ=ùÔµ ”®0!t8+„Fª^rYK®Ô 'þCB\º„Ò!„Σy±uý§³BwK] BÊIÍ‚Xc ÿÔÇRׂP:à „Ðy šºÆÚÊ”º„Χ‹Z5‰í\ƒpä ©kA(ÝaBè šµB(íyÈ_§!|¨q¿Ôµ ”î0!tXųäÏ)B8+„Ò›º{.ªIfá“Ç¥®¡‘ƒB¨iq®BiMŠð‘d °=—_[/u=„º@œœäƒYŠçA¦P–Ôõ tº6ú+W /±òÿGêZ)0!tš[× áÈ;„³B(½p ¡ƒDå2Ñ»1KØÜ.u=„º³Bò ·g…P:i£ï_!‘-¬ZêZI0!t‘š[74ѳB(]Ä ‹00+ÔÊ–º„Ú©{WPê<þÍ礮¡‘ƒB— y‘u£@ýy‡qVI+Lk¢DÞ ±÷½X畺„F B]‚A³BË2‚8+„$ÓIÝ~'ˆüË/K] B#!„.QÓ•Ö Môåñß#u-hlò‘“ÌQ"k•JìzA-ÚBR׃ÐH„A¡KÄ3¤€³BHJ=ä ÷À‹øçÞ”º„F*ZêÒÏ „ ˆR—ÒXÝåæM5¯öÜ“sØow‰êwnGP¶åƒº²U÷ÌÁ®¾(¥<ôe9q¼RÃ7ý…Œ÷'XBêšPr$4Mâ7Ÿa‚Aè6½s´Àí 餮¥74~xYHóåÃOÜ»úãÚerãg[QJ•Üþ{y]P¨´R—„’¤»Ã-u c¡ó I€Ë–Œ“º ”†z:<ê?ýlÝWýÞHþ,¯:õ ‚0⟔JuôW®ã!Ô=-wëAÈÅ?k£ÉÛköá-±a†Ó÷]¢ÜBSø›_ýŒ\Éø¿ ÇRÕ„F?'yyiœÈ˜dåw¼ â7M„†ƒBCPV•åÿÙŸnü“Á¤² ¼°f KB£­Åyj­b?yåõ2ÑÛ’-¬?.eM„¢ÜBSø—­ú›%CÛÀr¼ÜÑPJ]=þù»wÖí6õ’Ë&$cy¦ðÑÛRׄÐhA¡$0ghc¿þç­¯Ê5wt×õáÎô(i¼®pÁ_yÿ‡Žð·ÊEG­U؆O!”$¸X¡$Ñèìÿqó~õßoÞcëõ©§I]B±z ^ûÁJ0fš¹¼<õJs†ÆuÿC‹vK]B#!„’ˆV1Ü/ÿyëÓ-'ú RׂF‡†c=fÔ,ÑÛïÎI„CÔò›§þ]²50¡1Kà‚ã…”tã-*·ú .ín=ÀÈhAê:Ð…³µº>×”Ê`RÛ~ô»•ÿÈ+2ãÞb%!4fµ7;umMýcª[&EQ¢k&´K]ºpý}‘SëÍ2sôÇþ—›þ£7ªRÖ„Ðh‚AyÙzÉGÿ_g_x'ƒFšŸ3  Jª üôËž‘ÉqF¡dýÿú#tEeVÐUç?p„ ãôG¥.cÄimè×G£ Fªë÷5î‡q³¦¹îƼµMõ½¦á¸¦Á¤ŽåšðÖ0!„Ð98zýšP0¦êú¥•Ö#•Õj[o—W?\×x‘À „Æ B!t #ξ¢<% ëÏ犫ÇÛ†ëZ‰8{>n®Ë!”0I¤£Õ¥uõÔ›3´‘¢2kàRÇûõ÷Þ¼5;ÏØó•ï-Ú™œ ÏíØëËOî¾îÆ;g½?s~iïp\!©1&Ö‘!4áßl‰¬y|ûÒ†ZûâÁ¯Q471÷ã‡Y¾N¡dø‹¯«Ý=‘ç†%¹!u·Í=ÃÑçß„BH„$öÐ#Ë©PÉØ¦ãöì½Û[æÖîZúßnà¾ÿ›kß“º6„Bh´Ã $±ò Ùn^ÉN˜’ç^|íÄÆï|éÙñ­ ý3à½p(NoÛP_2iF}pó4·#¨Ø»½¥`ÖüÒNK¦.6x¼€?Êl[__fïòf”Ïê\tMõ=cöïhÍ>v°£8ez£*0s^ikåÄ/@c­ÝØÞì°,½qJóàsŽè°:û‚šÅ+ί¹¾Ï°w[sÇñôÜE•²½Iû!„B)„A(èôJV£Sö…C1+@W›K÷ò“»¾ôGŸ¸õþ¹GŽ;v 3ëå'w}O&§ÿxåµÛ^Ç9åþü}6Á© ‚àw~عcsöŸÿå¦WH’~÷£µ7Öé¾J&§*µÜ$Œ­«•?ûþ7Øðö‘™‡ö´¯\zã”o ®mÝ+ØZ\S¯¨~xðë Çìeo¬Þû J-sG#¬ñã õ̵·Nê¦/Ï®Må×j´Ë~¿g ˆ¤gqöNVEaó<„J Bi¤îH·Ùç ÍêKêüÛmsϘ1¯ä­ïübù&€§þºeÞ¶ õw½úôî†Û˜w¸£Õ¥­?Ò}Õô¹%kzdù†óv}Ô˜©5?ܵø«ÿ½øÏ5 +z¢‘ýóo½öàû¯ºçŠ««~zúlÕH’u"Tu]ÓŸˆsç:.[® ¹+óÖ9*5ɼ~ Ù3Þ‹ÆË2÷aB¡ÔI»½Æš'þ°yùŸ~¶nÅO|éG~÷‹VT_Òú •ZæúÖÃK7|ü•ï.Ú¥7ª:÷íhY0ðA‚J- >oÞâÊ®K,*«s¶×,¬èPªdÜ]_›ÿ›àÕo¿¸Æ¥Ž™.h‚ dAüZ×Ú¥ü ­[9ø5 ˆ”à‹žqMƒQeÿÙ×_ùQA©õ˜Ï¶ötx¦Oœ^ðÁäY…ÎÓ­([ ]k—ò.Ÿîp—jì"A@|nöÎ%Çs²\¡¨|ð7¯«Šs#Þ+2÷ |ìŸuণ…Sêœ>zœ+¡fÊ?ׇ‰JT?,¿¡¬0Y˜»u Dõtôô羉¢ôÖÃÎ)krT,ê8¼ ûêé¬ò‰‰üŠåî©Æµï0àò­ZåîwòJ }]½&û‰2Цùâ©÷E¦dl{[#¶xN®žßÙ•¨ÊÉ7tNÓ¾ð.KhdG½×.µw¸síõûžc‰¬ŠI‰üÊ«=³ Ͼ*å{G(a’È„iùurU(ν•Æ·¾lósßh®ïÀȨøÊÛ¦ÿgҌ®w^Úï,(±œú†9yVá–̃³¨ÔâÜòAÝl¯;”1iFá¦{¿½`«Þ¨JÔ,¨hwõ^íjs»¡FFŧÌ.Úø¥/Û90£C’„øËÇnþë³ÿ÷ñ·3”‘­ï¼úúÉϵ·8¬Ý6OöÀõòŠÌ¾É3 ß[vóÔ=ÇuåŸ8Ú=‘Qñ·Lû\Ï£±€sEÍžx@F|nÁsØÊøòµj®/¥J|ì©}åT2šüH<« Y…ŒbÛü!õT«==©= ݉@ˆ.Ñi„@ö™³rhäðÀ¤ì½GK›wþ“žwçwƒ4%ļöíßÿ ûÁoškyþ%€x$DìyáŠy÷þ"[.—GÆÍœÙM0†÷þø3ÿý¿Ý|Yf×3ÄyŽå¨íO>¢¼ùç:µnð€û–UëûyδëîM\r‡ž‹Å KèŒ_\Bc!‰\ÿ¥™õP!ÇÞó_W쀽ƒ_;} Žoþäê-ÿ?ã²ÒwÎ6NF¶.zÿC‹vÀîs]Ï’©‹ýà·+× ~íÓžSõ–Ïò Ô0aJžÆTøŒÐÊBjš’ƒÏÝ¢’y¥;'KõZ6¦ebJ/§ày"xx¢Œ€x‘ÎT+¹„šŒž>~LGG²TJÞKÅ>V1˃ÃóÎP²5¸§.ÚùìÊÛ~ûL÷LãÓ/¢~suFô¦¯ß\¿ýCKÅ 3ò-Ä.€¾{A.'#—å®]Í€?ž rñÛ¸{çê?hsþëîiåÊöŸ>¾Ÿ_~tÓÛ–i×ÝZ0Ýþ†AÜâ89Cé#¦fÃ7]B¸X¡!bò5½3³Ì¬'QɃœràuùAçäí]ýŠ µ’üˆûŽn‡ÜtÔ_:ð±ñ¸¿xgO¿¼ÚbàÃ…ïôñ*:^bÐð´u)Õ{SNÿ<•Îú!J?AOY<}Q¢ÄÔ°kàv©^8îÈ(. ŸØò†¼?”]4p¬Ê” zU´sàv™LôÅ ÚD·Ïn£Áø¹&š(=Ñaåj­àh­câ«RAg`às!—ÓrÈÈè©'»â!DY¹nðqN¡á1 )ñ¬Û®ÐD< ¢¬¨Òíó¡3aBèDuL8÷Ê’M½êŒ=Ô…jGäÞòÕwÕe–²Á|JC‡ø2½­¯X}Æíˆ¨ ðô/-8‘YÊ;£d‘®£’¾yðcóŠù9*ÄÏ®%8n(Ø\4+ãÐøæ@QµŠj*U¦fG‘ª?•ï%Ú`fÛlÕw^ýËIFÝ‘>€a‡‚1mîĹ¼Yãï8ÖÓÙ˜!ôŸïëó¹ ´LÐq¶ñMš`wfÙ¤Ò`L¬E#cÄЩÀăœ¢ Ž]È: !„.«¢ÝÓ ç:†“Slï4cL3žó8€ˆYŒ\öÅ·³º§žýZþ\…ÛŸ«pŸís(ýå;äUÏ^tâ`s¹0íVJ§9{ƒY“¶þëÛš¥ßû‹/z±qàØÜ‰—»ßeÜ/¿÷–,]߉Þ`VÕž—Ó]vïO‚eºÝŸÀYæ‹å[λçÇÓ7ýõ{:ÙCÞ•¡²—ÉŘ3h.!T¹æ™ŠGŸÎ÷‹ÐH€A!„’¬tÞug}½H¶¥vÞu·æ5m.ÙòòkÕ$Eƒ(°ÂÒï?毶î}î³½éH ò&̈6n{7瀣§0àè„ Kn Uúv*ÄÞS'L»òÔø$ĸI–]k‰‡½öÀÛÏêyA¸œ¢ ™0ã†ûpÞ@{0!4L2vý„b¡¢Ã}RƒR¦Ò||[áwVjUbƒ÷l«Oѽº¾øŠ‰Ž™+JÂq™.Cì²Â›mO‡ fæ?ùäêÅj__ð²ÍGr”uÍJ-M‡²ï©éT‰õ§®e êó2ÚV—}mq‰3 Ëy0¨C}9ÌóM©{×\„&Á …/xoùËR×RË(èó¬ÄÑ µ½¢ÖqÞQ-âN›E¶“Ÿ>Î —Nœñ„ 1.[ü°)[ ~:ì#„B¡1 ƒB¥£*Ô¹ôÛÿcW3^ïùF ÞC¡43QÿîÇR×€ÐXA¡a’µºå¶~wH_¨U‹÷—=)’6BD!‰aBh¨< ͱ.—ñÝ¥‘1TâÖZk‰s²¡UêºBh¬Ã5B åÎþ¯œhÓüdÊxþ¨Ó#9gJ]B!œBhX$º¥Ez ç/T¼¡¼pb+`Ð8eLGG¥® I/HV˜y¯½ÝßÛtjÓ]Š¢D¹RÅ™ ‰ŽqšM;œu1„ÐÐàŒB)fi æoj·«WUG:ZwOϱDÖÔ·itŸ8¦H]J[_¶½›4£ hFA„<jÏ+ÿ0¼ú¿¿™zÐwË*ñ,þ"„†ƒB)&î퟽½»O®·¨œŒäÈR}=ߨ’º6”^rÆMO,,ÝøÌU¥¯?qÍœæÕ7>ô­ƒjSß~h¯! ÅF©ëCh4 „P 1Q^ærÍWåÆÄÖýÙÖcwV•„>éîW™:"™R׈ғBtFòé‡Ç-¸1†žÐȤ® ¡ÑƒB)dØëšøâ‰6ÍʼRØÓ7Ïü\ó­š7ÛWNÑéoµt*©=ý³¥®¥— Yiõ“Õ}° ¼>|íŠ#ï=§*š¶ÕñÇR׆Ðh„‹¥J!¡É7ñ¸ÛG>z¤^ùƒ?ç;BY+Pèpß]ëž„¨ ¶&gÝËÊóJ»'ݲåÉßš³¾ûåñ%² ‡¤®¡Ñ×!”$/’jW¼tav¦HkÎvŒ8ݺÿÁñå¬dMjwB;Ü5¢ô§Z½³9Üuèc²Ïo'u=F8#„P Ðqæfg~¸jv&8+4¶³ãªÒuN»~ü¢"`“tŽÐÊÂÁ°ŠKD"yl¨ˆP `B(**Ñ=ÍpÖ™ "b÷ÔsƒÆŽeéîëoD˃~¿rûS?U/øÚo#ùÚ–ÃRׇÐh„A!„Ò€±`ubËk²¶ý[òH’LyEñEßü]8ÏØw0GönÒ‹P `B!‰)„¾àœ™¥ç̼ýÔk„È p´â[.JŒr–‡Ð¨†A!„$Æ@ ‘ͯÇÛ¤IŸC¡OÅ «JêBà g„ИwpO›HŒ½yN ( öŒ­ÌI]^$ÆULj¬‰”óTs?ÿ“Ôu!„†!4f)U2Î`TG¤®c8Q4)H]ƒÔÄx«‡œY&Šª„©B’¦!Ø¥Û÷„£µRׇ^„И•S` çÃR×R‹í$//UÕQ"§šU&|L.:O˜…O^¶»Ž«ÄÎÀ¹Æà8lÍ£ÏSžó9A„B£N˜(6¸È¹Õa¢¸:NXªD ä4„{UbÏ1PWkv¶’¿ànY–'šOô¥²d„D0!4Lˆ¬º@iß]«HÀî$®·ÇÌq é˜/¼mW°ß;)˜©èó(Gÿ4Å&]ä¼b9qb”È­æ@“G€ÀÊDwƒQ8ô–YØ{\+6º/eìêéžãÇÔâ*™œ¹àˆÐH‡A¡a’ux·µ%¼Ò[ Ül«1íbTZ®u9½©!  lH.:ê­Bí³VaG½BìOI›(¢™þ¯U!¢t¡A8úv ÿÔ¦‹9Ÿ„D !„F0 BçÁ "¬yb‡Ôe ¬œ›jÈŒÊMbàßï<Ñv¡§5ýÊí‘oµ/ZóQÿꋼd¸û"Ǩ¸/ úYíž7ñß”4„F§º†1ƒÐ9˜¬Ú0I’#~’Öµ~õ"ŽƒÐA3ì´º ~,¹=È¿_—Ý;Ùl|ÎNóçløw6.ÿs}¼€M¼|eD¹l«¼j¥¼ª‡á7¨Š®ïdxßÅŽy©™–VUÜ2ŽÉœ9•ÖN!u\”ôÕ&:·<m]{„uÕúNþ£dIñ–_Œi¼N7ÿ÷Œ:3RûŸßÍ/7Y3u=¬G®5+¬™: ÊDc”Z+K]ÃX‚Aèf_^æ’º4ò?ÛQ“ÐÑÜ8©ýbÎãÜ+ðzÏ­wЦÇnÈyn%töó;âÿ,Ùå¾!ßÃÞz_@³4¦¥?°OÒ¿Ø_¥ý§؆ÂKN³ºÈysbDÖ\43E •$$l2Ñû±F8²'‡ï-q9$LNEgp‹ íÔŠ_ŸË¯½ßZUß U—víã´Ü.ºì«o˜<æÚ 4Z`B(…Šöx&’œ˜ï*Q=r±çÆtt<ª§ßUyÙ˜¿†UPÜPj eÈCÇnÈyAà^+ÝæZ¡qÆo/Ùé^™wØÿa•fM÷TCëPÆ@NÚ©k«ƒDEMœ0Í@^ ÆiT‹mÿ2 ûö˜…=½C¹ÆPuQ·Lq‘ó~OBÜVÌ=ó#Ø0¤™â^Ôã“UBhøaB(…Œ‘e«.¸ÛU¢~Á6×tìBÇ ãôjáœ(äÔp„n¶¤ŽÖΈ¾=jÑöxŽðÞ!™èI$£î¡j¥¿v•Ÿÿ0#vTòùu2ê¢ æ6&£>„40!”"ò '“‡¸Åa«ü¹KÃY¦qìõnÓõÅn€¤¡œœäO,ÍÜHâ¦ò­®yúîè]ÙuÇ­-¡£ÞÕó- ,Ÿœ~ŽÑK-¯ãçÆ s Ê*‘§!rT%v®6 ‡÷d[ÎÛƒg¸5Ð?¼;Bä? û^®â~ÿ/ø¤¬ý£!âÒ À$°#ªMBè$ B¥HñnÏå„Êî)úCÇS¬~=«.ðxá>oUÇ,ã‰dÕ7@$ ±i±u'ì,ÙéžjjÜemýÙd‹´²/½rÚÞÙÒ"¿†%ôsD Ìp.FôïÑŠ/f ÷«Äî êÆ<ÜxPQ'èþ0A˜—kÄ–?Up­Mæø´ôdˆ(×éÄúa[|ŽJ B¥ˆÆ_Î*©½ÞB•g(ã´Ï5³6…--áU³Œ¿JV}gÓv™ùpÛeæÃ¤mÜ6–}¯Ë\þK»|<@ŠD딢ý ½x|O6ÿAs*ëH†Q n¡¿þTMÂþñÏïIö5d¢Ç{òZ¹F BL„Js[ØÂÄø™ž"Õ/’1^0Sþº¡'ú#“-òOO‘ê’vQ?—‘¡è#—M EsYÂP#”ÓYð~u¬×ìƒÏ)'Ô~4Ie‡Í2®m¾¹'l–%»„¤ò32:©Ûþ$eÈ6}#‡?%{°)¡× ÌF¸¨§BéƒB)s,°T$‰pû\ÓÎdŒ×z¹ùÃi¯ô|3ÿïzO‘êédŒé"çåxÈYscDF êiCA¬I.:6hņÝ9ü{'HŠ“:µ KþÁØmjWü¾‰kíwG ²7;f_óå+Ón¤—Z^ÞG.ù| é+&a¿#U×Ò M> @dA‡ ¦¡0!”*obY\CmN¨é¤lk‘PÓlÔÀ¬UzÙë™ÿ<«ºøí2XÐ1vjÅä0Q27Ak „iî׊M¶ ;÷„#gmr—¨]îõ?t}±ÕE»=7«¼ìªªý·Åt̺î©ú—œåšþ¡¿Ë¡³Q_žã%§ÿ†‚èñ2î_?U‰)Ùšcž!Èj BP„J²Â½Þñ$'yŠÔ¿Mæ¸ÝSõk+>rÞU¶ÝµèÄÒÌ Z€}ަ†;5bËɦ†ºàþD,EðØ9Ï*ýìË¥Û\+Õ®Äíe»nÈ?àÛÔ7A»Æ>Io»ä78DÍô·¯åÿ-=ëÇqþñbÞ×PÀyyP`Bh„ „P’™láeMØ:fë“9®»DíJ|âÙªíßg BÃÕÔ0ªgbÇWf¿ÆÄø·Ê¶º®ÒõÅî,Üë]“SØá,Ó¼ì÷~."PÄ úÇ_‹Yw¨Äî§Çqxv¸® p2 3 ç5BɃA¡$’…9F◄Ͳ5©ßUª~-çXàÉ¢O<Õ¶9¦ã_ØÔ°—}»SÝÔUP܉e™‚¸¾|‹ërCOô®œcþ'3ƒ‡<…ªç[¯°ìOÅu$ÓH}ÿç,¡»\'ÖÿºŒ{|H­ .)²^áŒB#!„’¨x·ç2BÕöIº ©¿c¶©>³!\”Üß@?xlPSCAʦ†"IˆMWZ·À¶Òí®¦ŽÈ]M¡¿™Û#þ\ÅóÍ‹¬ÛŠHjÃÁ Q¡o£ïÿ½Š‹°û»ü+‡“9þ…"õr .“âÚ¡¡Ã „PiûãËYµß]¢Nꆽ¢@ÝG^5+Bä×»GUÀÑ )²e´ØnM [/·h8P¸Ï[eiÝe²E~=óùÎî`¦âÅæE– CÝ3 ÀIÎÏí¡nø3Ð9ü»f Ù†^ù¥!!îA3BP„JcGÄÄDùÙÞURšö“WùÈ)sã„u.ʉIA´NAô¿rÛ­Ò°ëŽÞ”óŸd\+:fOtÌ2>œS(Ì:¸CßýÁ´—ºï[d¯´Î·¼52±K·›º±ÚI^þGX{ÿüõBíVQ¯!„F( B%IÞÿÕ"Ñö¹¦í—rþM ádSCF îÕCûo3ø>ÑŠÍ5g¥½YæžMÖ#ú©bŸ¨ë°OÔý¯µ%ôTî!ÿíGü«“ß²ß12oØjLo² «úÊB9é ÷–sýR!:.)L%-†½"Aj„‘‰¾´þ½@ ƒBI¢ô&–Å5ôGq-}Á “O65œY#2çžÙÔ°qO¿®þl›yvM7¼3n³ãîÒíî%'–e~Üw’Î2ÃY¦yLo=W´Ç³JåeW¿ïŽ˜žYÛ=ÍðŠ«Tí<×ùôß&о)oTq¿ÿ¿tÙ䔿 D”Mâ”5oD¥!„’ ÿ€·’bÅRG¥êÑs7¨©aM‚0ν˜¦†ƒy Už¸šþPÛ_#" ðç(üGoÊyJåI¼XºÃ}½Ú¸­|«ó¦‚ýÞ½Õº5½Õº®ÁÇ 'OÐ?þnœ°\¯Ûÿ^ÉýåU©j?¹èòĈ,#`Bh„­¤4 IDATÁ „PXÚ"Ëšè²Õ˜jOÿœ—œjq‘—ÕĈ¬šO›ª†ÒÔp€«Lýzîÿ3ŻܓÛç™&ç ŸˆI­½.ûe&¿Q¾Õ¹TëˆßQ´Ç³<ç¨ÿcG¥fM× ccŒÈR4ÑßþšáÈÏJø§·I]÷é”b· F\'„ЄA¡!bb<-rWELÌ«Ÿoj˜ L5<ÈË?mjxH-¶=ž¬¦†3™õÁZ“-²j$¡¬Šbë¯ÉZGòâûå[œ ô=±»òûŸQw)¿uý·,<¨4™Â–oåòk‡­IãÅЈmA‘ã-!„F B Qñ.ÏܸL«Ýxõ·".ºê—§75Ô‹¶'RÕÔ0­xÍÔùkK(ÃY¦Ñ·eŠ—dl€-Êú¢ë—ß÷"’]ÿÎOšAë17/´"IˆR×y:x‘Þσ ƒB#!„.ÑK-«æÚfó·z ¤HÀ·>mjø¼Q8¼{8š¶^aÙfx±Ë•{Ä“³Lóxª¯7:¨/ÍðLžý bõóêûœ.d¿™qò¿3vF:ÙŠšZ7sr’—ºÎÁà½<È1!4aBè}ÚÔpf„ÈŸËú9"PfRd=…}«óûw¿/ë|r¸›rr’™·UövE€{&¦£ãÃyýdk¡¿±,@Œû±Lôo©äý_÷ä8ëžœ¿?÷ˆ¿8óDðNCWôáé/v=¶Ê_j¹Âò^º¼_8¯€A¡ ƒBçðEM •¢ýM½x|÷’·ŸžªqÄ8º*÷ß”xi ‡ªs¦ñª ý÷–ìp]UMÖ:)jH†ôï¹÷)EûêqÜŸ$€?u¬gо½gŠþ×M¡'sû¿¤í}sÊë=÷ELÌ«ísÍo3å!)k?¹ßƒA¡ƒBƒÄ ‹¼—¼fz˜(ªa ÃÜs55Py؇zKT/Mðå)ýq ½I㌯€„8ÐÐ ôœ WiÅÆß•sÿxuThúš¿;£ÏìóÞ¢r'®÷ΘYÛ9Ãøª§HuÞÖ©@BÜ˃¢PŠk#„†ƒó.µ©aÞ!_Å ÎrõcRÔ=˜£BózþAßê’îimó͇¤®çB…‰bM+ýàïxPVš…}ß/ä×ì»ó¼J¯·@ùo3¾¦x—çzµ;qk凎[bZú}û$ý‹ýUZ{ªkŒ„¸Wg„0¡1ç´¦†5È /¥©¡¥5¼L »­Æ$ù£ëÝÓ -ÙLJM‘Um02‚›œ“ÕE­ú³¤*‹_ÿlaCËÅŽ²Êõ×g¿(r¯—ms-×8âw”ìt¯Ì;ìû¨œvM÷4ÃEy)>ÝoÌ0×B%!4&œ£©á.Øò§‹mjHÇJஊ™·Óå‘î@ŽâuS{ä7¡lG¥fÈ}ŠRÉN­×O.ú# œ§á«FáÐ9·×8Ÿ¸–NÔ­ÈZK±Â»å[\‹u½±;óúVgÕw»KÔ/´Ï5KVígC‹!/„,B¨Ubg8•×B%!4*¥º©añ.÷Býãµë“Y÷P´\aÙ1£³Ë‘sÌ“£Ró©ëù"íÔ=ó|ä´_Q>RÎýýgJÑMÖØ5J·€û‹4ÑÝ"J’‹îuã¸?þ™‚HÊú5/´î€ÝÅ»=“Ìmá/[ZÂ2vD[ÙŠ5Í‹,ñ ™´M[b¿ NX ЬqB©‡AXƒ›Æ s Ê*QHuSC]oL'‹òó|¹Ê?&{ì¡ê˜c\7þ½¾ûKv¹—Ö­ÈZ+u=`ÈúGߌ™·ªÄÎ'Æq®®k·Ï5kŸkú~Þ!_YfCðNcgä3Öt=²Ê_j^hy?¡¦Ù¡^C-¶zX0à‚i„F BhD9[SC8#ú?ÑŠ/f ÷§º©aþ߀ï˜mÜšÊë\Š@–"×Ð?}”>-‚Pœøÿöî;*ª;íøsïôÎz—&  ¨¨X°÷Þ¢FMÓäMϦ77&»‰‰É¦®11‰‰%c7ö®("J‘ÞûÀÀ0…aú}ÿpq‰k¬0fžÏ99‡ïÌ|g/·™š‡ÅõÆ—â~«y¼UÎÞ^:B¾§Uξïï.Õ`$€2Z !„z,B¨Û»ÝPC1uí¢í`¡#óšÍ“ÍæƒŒÝiç¶t¦†Þ¢—Õ3CΪ•$*.Ó™EKD¹•1]c¶Ÿ»=éÛŽ.=iùn(#EµÊHѧ²rÆ€4õC•鑾{k—µ¹±wUÄKoñçµÜÏó`UÛ€‡E¡‹êvþ<ÔP2Ô,ï?5ÐV„ÈQþµŒŸåkÛû”‡ýL]Yn¥9ˆßÔÄÿV¤4m NnšÃo¶<yT¹Ð(b¨é/ÙÚ.¬¿—ç»^„p™ „z,B¨[ø‹¡†Eªñèí†:šG‘~ŠA(K‡Éºõ¬o‡¼´uµg®ÎW%ªqôëW1æÇªÈá“`ª ¶nx]LåÓR\ï†Î“£Ïší³‰«µn=«š&h4-9«šå—Þr¬>J´¥6VR~7ÏC€Um6!„z,Bˆ=ÔPŸv/C ‰a±“\­ub›„u »Ìú+Å£ÉÒ C½wŽvž2JäÐÉ×%Œ§ÆiÈèwY ½aýü}6Õlväëß/£˜iʙi²ï ;Ý8^Tg\˜ªÞâ“­=ß&ØT1X–{»Ç“”Ym'¸ÞŽÊ‹êX„ÃÜf¨aòý 5t´à Íñ„R(#»Ïì ¿bgöV9{§°Ñô¿Ù¼ÞQç3å3_[j žâRÊß"­«×v\8µ§°rH[Þ$Ï#„:vZ5Ì­ºm™O–ö}†:¿¹8QqËe@`V[Aå輡ƒEu™ëC §E눈„ÿjXöÜ~)ùA†:š¤¶mŠ•MfÕG‰zÄœ˜ò¡²ƒ}ö×­èu¡yJÎt¯]ýü³•d±™vðyÌ×_5òiBªøópëW»;ûõ" ªp¬ûy8ßë|SY™a™{¡þ Y™¡@ãËÝ\4Æý¬AÜ8\K‚QMázcõ8X„P§Ò½%ŒQƒ à›p‹¡†ß{ÛetÖPCG)MB¶Á6BãÍý‚î,wKïÁÑ›D¬C•i>a§v§¥”ÉÎÉ{ùƒiÇô¹7®=k4XxO½6.Å@øñ‹™ÏþÓ‚X©=ýÍ`Û/:#wR:\~¥t¸üŠš:£@¿DVnø`ЦÊ'gKÑ(÷£>Ú†Ä,²;œÏ†º;X„Ðé0Ôp¨‰'Ü<ÔÐÍ~õ¢§ýd9Ý9TÀeõ8 *㥴ÌÀ¹_õQ¢A)ÍsjÊšñÝÁ”ÙîžâäÎxÞä“Ï‰Ýø×Ôd\Icñg0¥žöãÏøØþ(èŒçﮪJ ªJWz]Óúûdk–Ô_ð[õr½œ½­nªªJÃRO„‰ÅTî}]‚r<,Bèžuj8ÔBˆ‡RÀ”`mbQš‹Žjèhü&ód3ŸqNïÎéëH¥œ-r?{8w°Åjc{6Ùš/Ô5¼f£(Âb±e>èsñÞÁ±†VsT›Á‘ZÖwœg(ÑhÛºBfOUvFöž >Z\U-þXQÒú“_FËBa£iňƒ{ì¿ÏKR+`B¨‡À"„îʇ*†Z󟡆¹<ªn—˜ºvÑÛv¤¨'ž{7¼s´þL³½Ocð'º³Ü­!‰a;I WÖjævÜEc±Øò¼-ÍVNFÕÿPÅØýúdá„9ñïö{¨—Ë” ŽT!‚FUˆàqq£4Ƕ÷>Ÿ÷•BW³§:Îí7U¨ îŒ¡ÛÃ"„néöC sijèhùú)ƒh,.O£;˽øäLJ¿xõÑÍân5L!ô_X„Ð *2Á»™ŒOèîC ‰´Q$Wk™Ô&féx…POÀ`Ôêõ‹W¿öÄ^KSëh°ÙE÷û|9Jqe¹î©Ž÷ñ…œ¬à0÷ƒ‹V ;åª%¨ 6캢¨!yì€ó«ÃN©&Š”Æ%A›·údjÎ6† 7W’:õ9TõDX„œX]u 7;½Ò}Â̘[þ5j!³–1=VO„$tjh`‚þrwjèhÁš6Ê£1¢ûϺ.eÿhÝÂUï<¹M n1Ä (Ò÷~Ÿkëæ’WÍm­_¨S¸³÷Žž}pâ¬XÜÓÑÁõe6¸R —aÍây´Q‡ÃN7ŽÔ—ù^ÕlðÌ×]n l*!ïÖ“Ér%X„œÔþßÒzíß–þz¯€¿¬®56ÄHx'ôÄ¡†ŽæVmœbc“¹µ}Åtg¹_" Ïú÷¯ç¿±ö¥=Û4F3÷~žãòù¹¦¦Ð:jî¤>¼4ð(—ÇêQ{Çåú2ÿ]oÌÎ ìã<ÎÀÙsªA² ÃÏ|Ý7ŠÒÖÜ?Þ¦¢ÑŠóÝ}J9B΋úèõ=3ó³k_¤ìÇM.i­fÌí«#‡š yÂÍC eöÔd…=¹Ç 5t$A“™Ï2XG꼸ÿ¦;˃RxŠÌ4uÉš·÷¯¼ŸÇÒ4hxÈËÙ”Em'X·ªX2Rq¹àr`ª:Ò½H¿T^ÚºZZi¨Ðzq·q?få6GçEar*%ùJáWzCÝÔ:¦ý>Sà‹ëÈQ|gjèh—šÇŒªn'èÎÒüƒåmo~<ËL"Á¢¶‚ ìvÛTÄKó*â¥oûdk½r´KܪÛÞðkÕòVwÎÖâDÅ£˜‰ÿo"ä@X„œÄÎÝ-#o?õ¶¡¥õO'æÿöÏúÄaÌ•Î0ÔÐÑ*ó3‘¤õâêèÎÒY\ý„æ®F‚IMÄín¶­í+®¨í+þнXÿƒ_†f±Pi|¦ßŽšÇ 2Öïe ò]:O޾«ó"„Hº ÷—wÍÛÿÃÖZšþ犠ÚüÌà?>{ÇŽ\=™g®Î—i²Çj}¸‡è΂z´©)`ÜÓzc¡Â†+ |¿ÌŸä9§Íµ‡ßlYýGÝž~;jž••ä]•!tîr+?Ÿ»³º²õPEq­¨¡V-jVµJôÚ6q«Þ$6¶YÄåÅ}TJ]¦ÂSd¦;kOá•§›L‘DSépù%º³ žƒIµª)‚š 7›j±ÜËc[üxš?Þüfó¯!IM³Mæ…'æ…̃µ1’­Ê(QMWåFÈ•õø"Ôf0“û·¥EtGq ú ®ö¾qÈÆ/@`ð 3€KNøíL„"¸Ë$£˜yÔÆ"»ôꨒ¥ í|I€k}ÿvDAïßúØAjº“thÔz"T*£Òîkª´AÆnËžé½e°í ;Ý8YÔ`z¸×…¦~W[N){‹6Wǹwnj„\[/B„Åbcòøl»ñèŽÓåÚZ- ÕÀfµ»êoÎ.œÜÜ´QÞaÂ.Ÿd³ÚI‹ÅÆKxÀ°»úåº³É ê¦V°:Ñ÷2‡R©Œ„—hy ŸaÉ굟´QÂN6Ž’Ô—ú§·lôÊÕ]lê%ØT– Ëê”й¸_„ÚùÉaðÈPºct¹Ò%$Ÿ.¤;†Ss«j›jc‘5ý$¥ŽzÍÈX_‹òvÔËu ÊZ ßï\¿ËùT¥À Ò{:Oèvì Â^0Á㜠=£"­4,ñº¦]§(Ògµð6vOî¬×BÈ9MB¨3ðÔ.»Õ:JïÉùŽî,¨çPåzÊj%DV„:*¥H€” ”æ>ŠâÖeŠâÖ5ÒŠ¶­7wKÑÅÉ®>”‹3«Æê (¥y4À¬ =NwÔó`£°µØ€ß%E¨]ùYNÚÿ×+I—ÙØd‰´Ò°ràæªíÑêgqtV×:ÆŠÐÂ"„PÂFÓ ‘¬ñájè΂z&lj[‡e6ºRM?Iiúb¿JF*2 )âzã ý¯ÙÙwoÝÃÂF“Àêé°!ô…z/¦ÉÞ_ë…³ƒÐý#Àò§õÆ¡!BXwå!¿åOðœÛ&a¨LËúì¯Û»³ö)ie›C³ ÔÓà9Bý‡WŽv2Eš²a²º³ žëvëu5uO­ðýNØhÚ|¡y¶ ÉüPïcʇŒ"Öšþ’­ áÂz:r!Ôá!„þƒ§±L6Š˜G-\†•î,¨ç"Á¬¦€Ië^½;§5{–÷–+ |çj½¸ß°[­ !gU¿Çm«^é{ULg6„ºÜ#„'7ÇVÊ·)DÐ峃s#Á¤¶Ó\„Ú™DLóµi^{û¾°SãÄuÆ%—Õ›½s´çU¡‚ÍåCd×èΈݰ!ÒJÃT‹,®àVDwÔ³1ÁpÏëu5‹´çOô6»ëÓ¡›q)¥ÀD¸wë"Ô®:έ8}±ÿ{¥Ãä ÍN;_â•s¥*H"åëGŒï]l6ÛµAn6Yy‡v^ KxmÃÇ÷®(+l§œ- as˜Öñ3còÅž ;½RA0™¤ýJJyÐȉ‘¾²?ÖÉN¯Td¦V¹ÉºÉsú2˜$pél‘§¯›¾±N+Èͬ˜÷ètã²WÞ ¨5€$=¢µSF‰j”Q¢Oee† iê…ÂFÓ1{êi“²v–‘íÄ!£ÈY¹Tª«VKy|¶ ”f“•Ü·5­O~vM¤ÍF1z…»/{61õíéÃ( ÈÏÞýcÀÂå gü‚äúkW«å‡v\IhÕÅ!½= —>3ò2@ME³`צK #'DfÞ}u¸@ÈÑÍ]68¹ã/‘¤cyçO 4›¬œ~ƒƒ2g.˜ q±Ìójjy¯€^ eÊ™¢Qýüòæ,Ï¡åÃqQ>Ùš‰IèJä=fáÊm?\x!¤·çÊÐH/ÍÊg·?¢mióðòu+)ÉWŠ•:i¿A%-Í­>6›•šTCÇ;{4/ÛÐj¾÷å¼mZMëèžÌ%s–Æÿ0mA\À[Om[¾õûó£_ýçô½IñÝ®ï$A}ÿé‰Ä«)åƒ<643;½Rq9©xÖÛkfÿ=¢¯àÙ?½»ýÇä¸%OH°Û(Æ·¿/_E’åØO¡{"Àª¶×îBçÉÑgÍöÙÈÕZ =Û8]Ðh^vºqn@šúH]´ø×º¾âJº3"Ô\ªuT^Ôâ$Ï[´bXÆ~xg ‘—t,ÏÞþೕ¦öŠðH}ãã™{£½«×vò5›Õþ€ÍfgÏ]6xw˜ˆj‹ÅvaŬï?/È®•FôõQŸ<3kpbè'^“ °e]’!ålÑ„eÏ&¦XÌ6á[ŸÌúîæ_d¨ë¥4G“V* ©ÿ}º³ÜŸmõ ^yïùßß÷Ș4»_rl|`㭶ͼTîmµØøW/•Ç^½T `4˜EÊZ §}¿ YV{‘ ö®H»P:àÊ¥ò^L&ø㗔ñŸ³²LiAaî×°ý×õ"äØe6ºŠQÌ4åL÷ÞÉ4Ù÷„jœ ®7. JiþÕ'Ks®1\¸©r´€îŒ=—-B“çö;½y]Ò#OÎ^ŸÐ+ÂãòŒ…Ïö‰óWÝjÛ–fƒos£>ðÙ?E¶ßg³Ù9Åyõn,6£5aLD5‹Å° „œÆ¼¬Ÿàp^kôN;_:!#¹l €ÍN1ób7^5– zH+ SlL¢¬r4Ÿî,÷ë£ïm8u0'8ùTáÀoþyøµÁ£Âö¯xyìù›·³Zí$A€-f`@6“Űýçî«n2þï=—ÕÖþ5“IÚ)ŠbØmv’Á$ÍqCƒ¯Þx¡ÁW}ü¥-í7%RîÍ쀫Úl§(Bí¬Ò–7Ùó0a§Ž„V w«n[æ{U³Á3_w¹9H°¹d„Ï«D=’Ë¡Á‰aµƒÃV'Ÿ*ð;}8wÈ—«¾¾n׊7oµ-‹Å0F Hyú gnþ·Œ‹ežAØþ÷QlÓÎ`æ)óúoŸ¾p@Þ­¶a²ÆyèþptV6Go×*çl¤;Ëý²YíƒIRc¦ö)3µOÙ÷Ÿžh(ºVßλÉùê¦Fwû¶ýÕmþ–´èuF~Ç lV;q§×‰Xvêà5©L!l2*¬æ^ëªHʬ¶\ï;oÙóP$AŽuO€¤^IMq²rÃR|Ý×òÒÖÜ?Þæ¢ÑŠ$ ÷¢Äe‹Pfj…{l|`c˜ˆê€^Š#+Ÿû}X“RÇ ‰ôja0HsVZ…{ÌÀë‡Bz{^ÉN«Yx­.3<Ú[ põR¹G¿ÁAwÔ¸lÚñËÅikW ãñÙÍ‹?"dŸ—Ÿ› nhðÁuŸÆb² ž{gÒgϼ9áø'oí“|øêîÅÜ:«ÕÎIxÊ~ƒƒÖÞéuž{gÒŽ/V|ø­'·~"’ðjÌ&«0 —âjl|àŽ®—诈”¦©.#µ9˜ßDw–;ЫZ(ânn¿½hŰ¯}e­<>Ûúø £×çg×øqylÓS¯;(÷"úú¨¿ßýäÛ)g }í¶ëEz|èÕÁ#CK/+îÕÖjæ –‘8)ª`ÚCqçâ¿;x¢ãü• —'|ß~û¹·'žÌN¯ÌÌJ« ´Ûìäøé}“ãG†ÖÌ[6x¯§¯›ÞAG@‚‘ö…W©r´ rô]ï­¿wŽv‰[MÛq[«W´º³·–ŒTh“°pÏ7ê¶H’Ͼ=‘î,÷ÅÐjfìÞ|)*,Ê ½§Çª”ZnSƒž×~%ÌÛ,ŒÒB¥ÄË×­U¦šîåµ´š6Vuy“( X¡й÷}…Ri’O€¡½*"c}µ÷û<®LVfGœhØÛȯ`‚Ç):³^«¥&  aQNy$å/)k5p|DÆúÕ ÜLwžÎTÂü¿ "ze?Ëˉ$X\nÉ EI±zK¸IDAT«»_zË"®Ö2Âh±v”%Èwê<9X˜Q·±ö££r¶Èu÷(<ÅF…§ø®ÿRáòX¶¨X¿ûú-–ð,÷ûXÔ¹|35) eÃdItgAΉIiÕ@©'ÂÄb*·åÎp.ªA£*Dðµ¸ÎøKÐÅæyüfË‚è?ê6JX{+J»ÕžØšŠf®o  ÷!‡sú%6ºOmžb2O˜Ì7;õ lªY ` |]æðØ­h½¹Ú¬9>2çøÌÑ{p~äè¬ã"N6ìêÿ[õëž¹:ߎÛ~øÚž+JùteE® ‹r)—Õ½V*XÈ?Dwä¼xP«0r—.BíÚ¤,cÎ ïí‹ýæk|xŸ±Ûlý{]húmÀÖª÷ý2ZB-f+Ùª3Æ|ôúÞU•5áúfÈ¡°!—"/5L±3‰Êò!²ktgAÎKd/nÊb,BX¸ kîÏ——<ÜÄ_É0Sþþé-ɯ²×Ùíß 7Å|øêîÕT4séΊ\‡KŸ#„\ Ë`cqôÖqkÝYèræðµ ¬´Ê°¶V³ W„géØé} îõÄtg 0ذim„‹Ð-Ø„½`¼Ç8rV5臟/­jÿ·V½)öŸ¯ìþìï_Î{ÕÛÏ ÏB]÷Ý¥‚ìZé;OÿöøÓó~\µ|Æwkè΃î]¯ä¦ÂN‰jûöÜÙA*éx~|«Î$â Ø†KçŠF¾þį´êMøQ À¦¶‹Ð,Ûs&äB]ãŸÖeÓëŒýÿýê¾›ªZð0êrX„îEQ„_¼tèè°C&“ULwtïDõ¦©V.™îÊ«g¯ü|îïo­™µû…•“­ùiÉZ›aØùKʺs9£ëËl8Çzc]eûOÉ‘j•>L$á¥ry¬"&‹¡jŸÔ_©Ö¯zeוRǦ;'rnø—à]êãÛÜ;Æ÷Lzr©×ñýÙtÇA÷HZÙ&eµÙ†¨xÿ¤;Kw‘V©0¶YÜz)”tgqF$eQÛ ¡Ûx艄¼‡žHøGÇûl6;Qx­N¤Ìmð*oÔr2*å£&G×Ñ•9?,BÈ%ø^i™@`,*?Kwº}óá‘q™©ãMF‹xÂ̘ £§D—Óɑח٧;GOÃ`TdŒ¯62Æ"ÇÀ"„\_m™b2OÅL—?1øá§†'ž}5ålQÄ™#¹ |dª±Óú”ÑËÙ`RSà†{„êæ°!§çŸÞưØCÃ…ÿ¢;Kw SM2…ÐÔ'Î_UY¢ ¹p20¡ÎÇc  ,B¨¶RÍÕ¨ .}žPX”—ŽÉbPtçpVX„SRÖj8ž>€¼¤u²IÔ”%ȲèÎE·V½‰)rn¬®jІ(\þsé LªUM¤ÀL¸±ØT N1¿OyY5òºjµŒîtò ”å‰ÝxÖ;o‰î¡»d6YÉd‡6Ôid‡v^ g²¶ 3cJèΆþׯߟa1Y¹ï®šv„«³N4HY;éÎD7‹ÅF¾°èçÕ>âBƒ´*kµaL&i^´b­ Ï:+hÔz"T*£Ò\öJÅÎ2dT0™ ºc8TYaÔTâ•] ‹Ð]Ò댬cû²¦È=D…ÇöeMc±¦ 3cÖÒ ý/»ÝNæ\©zóƒÿÛ>ñ‹˜ Œ: pýŠƒtÉ]Ì,ÃþÒª)Ÿd¥UšŒöİ‹çô+d±.·:º#p(•ÀHxI‹Ð V›ãZ¿²TJÝ\‚k}W=™Bhúró#ŸÓÝ%  rëšã&ÖÆ ò’$ ‰“¢>xä¹Ä«tÇ£Kߪ¾Ttçp|ªòúzc •œ=’ë8) /G¨›Á"„œEQj³ÙEÑý¾rå„cËwI}‹óêûô Hw›8Ù–“¼#þë}›F$NŠú˜Þt¡›aBN‰º¾G膀^Š ¯}8ã0]y똽$>÷ùEÞ+ÉW>[{<7$øïô&CÝ .±œEQ7ŠÜ]xôÃu ¢3r!ÇÖ+Üóæ…}ífǦÑ!t[X„Sò–€—DP´êë«é΃\ËŠWÆ`0É–öÛ<>»0ºŸŸ†ÎL¡[Ã"„œ’Æn5Èù\õŠ•_p“ñq† r(O‰É/P~cdƒÜC”Jg„Ð_Ã"„œ“ˆU?cù'ÃûúàzEˆÿßð]Löõá•}¼±!ÔMáÉÒÈ)=ÿîd—_\Ñ+2ÆW7ôJÙ•Kqs— Φ;BèÖ°¡»’“Q%©*S¹ÑƒNñ#Bëä"3Ý9PÏ1~û£­…ä‹" .€Pw…EÝÚÀijÔ‹I’âÏW¦;=;EEQ`4Z”tgA=ËÐÑá 6›ýkºs „þ!tO¦.ˆ‰Ÿî••V YitÇ@=Ôðq½±@#Ô9Mª*SZ¥§;F—3ñ(„º•ƒ;2"†ë]&‘òÍ—ÎùˆÜxƨX?\µÝV/B‡Ã4SEè´mtÇq‡I1q¡L§’šTiçKéŽáPзöíÙ£y^ÕåM µŽòÞ±¿uÚ‚¸2ºsÜÝ›R—ûÈ>í78¨áÀïã=}ݪ£býNþÕö%JÉö“ǽýéì]ŽÌ‰º—_„x|¶}þcC èÎÐýà ØV7™À¥—˜Š8ßÍIÙ)‚¢(ÂÓÛ ¶¡vÊš°Û)§ø@^ûhÆVƒ¼mÛnRêÅùõ €EÈ…õø"„POæ$oó ’—ÓÃU \>›îÝÆÎ)tGè4¾<=ÆÓ[Ò¸èÉa•¥*ÑÆŸR[¥Ž ÈÀP÷«o~=¹l¤ÉdùøKóž}kâ^¹‡Èx%¥ÌsÇ/)S{Çøæ¤ž+ÇršÇMï{|Â̘›ÕN¬]}t\ᵺv›ÒÛ+ýùw'fs˜öýÛÒ"s3k"8\¦©(·~`üˆÐc>Ÿx‰ÞO à@E„BN¨©AçÕ¤ÒK¶~~œÝN‘ï|:ûóU_Ï_;(0`òÜþû˜LÒøâÊÉ?¾¸rò¾2ýæoÏ J>]8uü̘?ž{{Ò7m³èË÷= Ó95Íq¥ùʰåócH„gÞîM—–µ¿æçï˜VZØÐwþ£C¶>ò\â•¥ªÈuŸ njçeV3µY¸O¿1~ݤٱ8[ª›À"„BÈ©™ÍVŽÅlå¶4¸ž>Ãä¹ý ¤r ‚Ã=´ÁáZ“¤.Ÿ/Ó§¿ÿ¹AÃCªÜ½Ä†ñ3bNT•5ŵ2¤( Ÿ|mÜÞ~ƒƒ–ÿmÌ9“É*ÎN¯TäfÖŒ1¾÷ñÞ1¾Aa-ý‡'åÖlÏÁ`’¦×>œ±·ï€•—Ÿ›žOÝ !„rjËžM<´e]Ò¤Ïÿ~àm¡˜[?rbÔáyÜzÚ·Ao’çeÕ YýÆÞØöûÜä‚ •RÇàñÙM>þÒV“¤Ø¦¾®Jíæá-1X-6þ¹£y“’ŽçÛÚ˰o,¾+• +ÌÛŸ·„‹B!§ê®}÷_s~7›¬;wü’ÒÿÐŽŒÇ&Ïí÷:›Ã´RÔŸŒpùluh¤×•—Þ›räæçÉϪùË×ðô‘LÒ˜06âøC½z«mH’°Ýê~D/<4†BÈ©mY—407³Zf·S—Ç2³9L›Ã´Ç l$Iºmý…¸ôäR/³ÉJrêÚ•ªÄ_¿??@¥Ôrs3«e›¾=7¯ëwúÜÑÜ©{½­QØéÉ¥^¿ÿ|1öÎDtÂ=B!„œ‚ÜSTÂ^Ç ÷ÕËB5@C½Ö}íGG'Z­vއ—¸dÁãCaýgÛŒEÎH.»”T<êå÷§þðÈs‰—H’°§&>{$w.‡ËÒ„G{gˆ$\“ÜCø§K Q™Øßð·÷§þñÝšã­'äL?´óŠˆ/ä4 œ • ´ Ñ_ïRB´Á"„:MVZ…»Ålc HèU¯Rj¹éÉe~gÇÓ !äÖüøðwí_¿ôÞ”£í_¿üþÔÃpøV™¹h`îÌEs;Þ·ô™‘——>3òòÍÛö¬ì?$xCÇû>\·ðÆm‹aþI'àÄͱh`ÞŒEóîåý ÇÀCc¨ÓÛ—5øð®«#r¯ÖxìÞœúÈóïŒ={$7°ëÓ!„Bÿ ÷¡.1tLxut¿ï´]q¾2ÖM&ЮjŠBÈá°¡.‘“^é¾ã—”©}·hÀO_žJ¸v¥z^kôˆ8ªå/Ý”|ª R£6ø]8Y0--¹tdT?¿ËO¾26éÚÕjù¯ë’f7©ô|[=e^ÿýãg\ŸÜúþK;õŽñÍKO.Òª3Ê{Çø^ZñòØÓ\Ëpîh^ÀþíéÓuš6/‰¿vñ“ÃvõÔðêã[ž6&ülÒñü±Ÿo\ö]ŸB¡î‹ê:‘ÓÔ ÈɨR¤œ)š¾âå±ßDõók¼z©ÜK äXæ.|9;½rhŸþþ)ãgÄdJd|SKS+çËU_í; àÔ/ÙqádAÄÖõž ‹ò^ê®U)ug_ ›8;v§»§X·y]ÒÇ÷eÕL_8 ïÚÕjùÏßœùÛè)ÑÛFŒë¿o[ÚÐukŽ?ÿÕ–GWqy,[Sƒ.ôÌá\Þø™1DÆúÖÓý!„¢ž#„ºœÑ`fRvŠÑP§³9LÛðñ½«‚Bݵ2…ÐD’¤/à´‡{he ¡iß¶´~LÃøÐ „"®eâ¬Ø±¯öÔœ>íÏí:kñ Üac#ª‚B™Yé•ÑÏîî%.˜8+6‡/äX>‘d³Ø8çOäß8iÀ°^ç¦-ˆ+‰ðÔÐñY „ê^pêr‡‡Ô¿R½ëðî«swo¹, ö>ÿôãI¤|óÍÛ65èäf“U¸ú½ÿ×~Av‚$nLc ÷¨lÿZ(áik+šƒ4jƒ¼¥©Õ¿ãc…^½Éhaµßîø§K_Bèfû·¥EîÛ–öXÇûf-ôóô…ðª/'„E9Ä£Ï'^zôùÄK™©î?|~rù®—it2I6›ÍÎhßÎM.P“$iù×ÏK¿¸×Qô"1WÝf07ÜîÜ&‹“]B·e6Û˜!§éÏæ|Ó~ŸT.0Ò™ u<4†º\Ò±¼€#»¯†éµF–PÂ5AI-€¿´$?»&æâéBßÊR•hÆÂW(â“·÷ÍÉL­po¨ÓòölIí“•Vá~§×™2¿ÿùæF}¯µ«Ž))PJªË›„›¿=7H¥Ôr»þ]"„œ A6O‰¡ý?6‡i§;ê¸Gu¹»¨ÉØfÖüy«Íf'íËš²gKª;_Èi ïã6{I|Àì¥ñI{·\¾scÊ܈¾¾Wž|elÒßÞŸúé–uI³Ö}rì9‚ (™»°2*Öo€ÂSTé&åßXµY®ªMF  f``ã/þ×þßÒ§¯ykß“aòð—²ØÌL¹‡¨˜/àXý¹ „zžµ!ð±ië¾`±­Áá/¬œ¼_ ğ΋ê4½0êbû×'°Žš]>jrô-W…Dxj^ùÇ´ƒõkn¿ìþfï}9o[ÇÛ‹ž–Ññöˆ ‘•#&D®½Õc?Û°ä–÷#t'ÊZ ?ór…÷„™×Ç8ØžÞ{䤨±„g¡;ê|}ãü«½ýÜÖôîëÓxêàµðãû³|ýÁaxkͬÝtgC!„Ðd¥UxïÙ’º¤ýö®Í©OV•5‰o÷˜=[Rûlü÷ÙÁ]Ÿu¶ˆ¾>êac#ªä"ãüdždõ‹:U^ÜGw.Ô5pBÝ£Ozøm©\hºÝ6eEþ-Í­ ¸ä X¨‹X­6Ä=]¼z,B!t>ykÿϼ9asp¸‡öØÞÌÐÓ‡sG55è‚Øfë¨ÉÑ<½Åšü¬šav›ýÒÒ/»IùÊU_ÏÿU£6°¿]}lZUyS4I¶˜AIO¾26 àÇ/N 3›¬ìæF½¢¦²9Ò/P–·pŰcí3¯*KU¢¿85«¡NÊá°ô‰“¢ÏYzý\»Oßùcº§X™Ÿ]Û·©AôÖšYŸ…ºkéüŒz²_¿??ÀÃ[ܬh¾x¦0"+­rlX”×Å;?õDX„Bè©t!ƒ™e·SÄž-——ŒšµwÒœ~›j*›…Zu/6>PyádAºVÓ&]ñòØlÓ°ú½$a[ñò˜õõZѮͩì 4ó’ÕܨWäfV1¾÷ö™‹žÞºþüì.Žyó“Y{,ùÉ[û^ð ’çýí½©_f§Wúø=ý±€`ù§‡‡Ô75è| rj‡ ±wØ ½wyúH wzè¯iÔñÅÓ…LF«ˆËciûð?õÔkãOÒ u ,B¨[3›¬$^¶Šº+ÊNÕjãh5mB‹ÙJFÅú5·ÿ›Ë22 fKp¸‡ ¬°A\[©îÿâß'à ÓùÈt™—+R¯\*8ÿ±!YR¹ ü‰—Æ$$ŒŽ8x÷Õy°çø¾¬P£Áâ¶ôéÇXl¦}Ä„Èâô‹eygå 8<ä €·¿[î£ÏJ¡ácp:ϼ9á4œ¦;r ,B¨ÛÉϪ‘ýðùÉ%Mú’$-ÞâÂÉsúMœ…+Ô£n…Á$©…ˇ­?¼ûê”WÛ2ß7P–¹à±¡ÄÆ6Þ¼miaƒ¨Íë’w¼_î.¬iÿÚÝK|cjº‡·Xcl³HêkZävŠb~¶òÀ“Ëb1nü?á$Ç©éÝ,B¨Û1­Œèþþ—g.¸žÉbØ¿ýøØ´Ý[Rç'NŠúŒîlÝlì´>ec§õY[[¥¬ÿôĬí’§ÇÆn`0e§n\™ë,o¡(бôé[$ôº§EÝ=Å-E‘ÿbîZ™âÖ'i“ ÷œ"tðòyÔíÄÆ6>þâè‹r‘Q"å›>‘p¢¹QZYªÑ ¡Ž”µþ–uIê´<‡ic0I‹@ÄÕDÆø–6*µ½NÌ Îͬ–…G{«}¤W¶ýpaÞñýY!µ}îh^ÀÁwz‰³c Nã—«=”r¦È·Y¥çÚy%üÂÉÿ®—97Ü#„º½²cøN£_\OwäšÄ^›ÂCTÞ~[á!*áóÙƒ°_»ZÕÿü‰üéLÃä Ë_öìÈ££§ö)-)h8}tOæDŸ­[õõü_ß^3ûçu›º÷×Ë wþ’ŠyÊ1S£ÈÜ…*›õ¿ëî D\³ÂCTÀæ0ío|<󫟾<=sãÚ³Ë);EºÉµs–Æÿ ÷ÕÊBC?„œ!Ô­]8YàñTáœyùŽ$qŽ¢ÇàİÚÁ‰aÛoÚaJùêïÿp«Ç°X ûÓoŒ?gÚï“Hùæ7?™µöܼýò¿¹ÐñvŸ8ÕšŸ^×~; —B÷þ×ó·Üêµ^ûpúwÿnBá¡1Ôm]N*ñÞðÕéçÇLë³uʼþ…tçA!ä|°¡n)ãb™ç÷Ÿxqä„È]KþoDÝyB9'<4†º‚ìZé·{ÙÛßíš»—X}hç•p€Á#C+å"#ÝùB9,B¨Û©­RK„b®R§1*ŽíËšÖ~¿‡—x«ÜCtO—#„œCyq#0™®uC£n¥;‚KÀ"„ºÑS¢ËGO‰þœîuµü¬@ïßæ;mëêR“ŠéŽ€œ!„¢Áå¤ïtäm7¹ â«-â°Ð¿ãÛäã/ÕуN|ÛFwg†E!„Ìb±‘›¿;·$(ÔýR‹ÚàEwžîÌ'@jô ⹨˸ÖW„êÖ}|l¬§[™_°¼œî,¹:,B!ä@W/•{äeÕ }îí‰ûé΂Â"„Bc·SÄÏßœY6u^ÿß%R¾™î<!,B!ä0¶§÷nÕ™Üs2ª"?~cïìü¬Úþ½IþÉ[ûfÒ !W…'K#„ƒ†¸«úÄùŸn¿Íd‘f‚ ì<>»Î\¹2,B!ä ±ñ±ñGÚoÿðùÉá9U¢VN>Fg.„\!„¢Iï>>• ’´Ó!W†E!„h2bBdåˆ ‘•tç@È•aB÷$ùd0Y ºc8”^‹³ÜBÈYaBw…ÉbØY,¦U«qÍRÀb1€AâE–!äl°¡»2xd¨jðÈPÝ9B¡Î„â"„BÈeaB!„ËÂ"„B!—…E!„B. ‹B!„\!„B¹,,B!„rYX„B!ä²°!„BÈeádi„Ë:¢AwŒnÃl²›Ã¢;B…E!är ‚b2¶F¥[PA“AÚéÎ#aB¹œ"ë žî!úá9B!„rYX„B!ä²°!„BÈeaB!„ËÂ"„B!—…E!„B. ‹B!„\!„B¹,,B!„rYX„B!ä²°!„BÈeaB!„ËÂ"„B!—…E!„B. ‹B!„\!„B¹,,B!„rYX„B!ä²°!„BÈeaB!„ËÂ"„B!—E@‚ÜCa‘^tgA!„rˆ¢¼zhjÐ]/Bt‡A!„¢Ãÿ›K)™¹Dæ’IEND®B`‚pypubsub-4.0.3/docs/usage/000077500000000000000000000000001342344706700154355ustar00rootroot00000000000000pypubsub-4.0.3/docs/usage/core_classes.rst000066400000000000000000000016261342344706700206410ustar00rootroot00000000000000Core Classes ============ The following classes are useful for advanced use of PyPubSub: - pubsub.core.Listener - pubsub.core.TopicObj - pubsub.core.TopicManager - pubsub.core.Publisher It is not typically necessary to know about or use these: the pub module instantiates a default Publisher, which contains a TopicManager, which generates a Topic object for every topic used. The Publisher instance returns a Listener instance from subscribe, wrapping the given callable with PyPubSub-relevant meta-data about the callable. Publisher --------- .. autoclass:: pubsub.core.Publisher :members: :inherited-members: TopicManager ------------ .. autoclass:: pubsub.core.TopicManager :members: :inherited-members: Topic ----- .. autoclass:: pubsub.core.Topic :members: :inherited-members: Listener -------- .. autoclass:: pubsub.core.Listener :members: :inherited-members: pypubsub-4.0.3/docs/usage/helloworld.py000066400000000000000000000011011342344706700201530ustar00rootroot00000000000000""" One listener is subscribed to a topic called 'rootTopic'. One 'rootTopic' message gets sent. """ from pubsub import pub # ------------ create a listener ------------------ def listener1(arg1, arg2=None): print('Function listener1 received:') print(' arg1 =', arg1) print(' arg2 =', arg2) # ------------ register listener ------------------ pub.subscribe(listener1, 'rootTopic') # ---------------- send a message ------------------ print('Publish something via pubsub') anObj = dict(a=456, b='abc') pub.sendMessage('rootTopic', arg1=123, arg2=anObj) pypubsub-4.0.3/docs/usage/howtos/000077500000000000000000000000001342344706700167605ustar00rootroot00000000000000pypubsub-4.0.3/docs/usage/howtos/index.rst000066400000000000000000000022221342344706700206170ustar00rootroot00000000000000How Tos ======= This section provides "recipes" for various tasks such as migrating an application from one PyPubSub version or messaging protocol to another. .. _label-migrations: Migrations ---------- In the very first version of PyPubSub, v1, message data was transported via a single instance of a class called Message. This had several important limitations, most importantly the inability to validate whether the correct data fields were being included in a message sent. In PyPubSub v3, a new protocol was implemented, and was named kwargs. This was not compatible with v1, but v3 made it possible to configure the imported pubsub to select either the arg1 protocol or the kwargs protocol, and to configure PyPubSub to help with transition from one to the other. PyPubSub v4 only supports the kwargs protocol. If you want to upgrade your application from using PyPubSub v1 or v3 to v4 and it uses the arg1 protocol, you will have to do the transition via PyPubSub 3.3, as explained at length in the documentation for that version (specifically, see the section `label-migrations`) Please post on the list for any additional support needed on this topic. pypubsub-4.0.3/docs/usage/index.rst000066400000000000000000000014231342344706700172760ustar00rootroot00000000000000Use ======= If you are new to PyPubSub, you will likely want to start with :ref:`label-usage-basic`. If you want a quick start, jump the :ref:`label-quick-start` section. If you develop an application that uses PyPubSub, whether it is prototype or production quality code, if it is more than just an experiment you will find that it can become tedious to debug. This is because the decouping can make the link between causes and effects challenging to identify. Also, you need better control over error handling, and more maintainability via documentation of message structure. This is when you turn to the :ref:`label-usage-advanced` section. .. contents:: :depth: 1 :local: .. toctree:: :maxdepth: 2 usage_basic usage_advanced howtos/index reference pypubsub-4.0.3/docs/usage/module_pub.rst000066400000000000000000000147371342344706700203360ustar00rootroot00000000000000Pub Module ========== .. automodule:: pubsub.pub .. autodata:: VERSION_API The PyPubSub API version. This is deprecated. The only valid value currently is the integer 4. Previously, versions 1, 2 and 3 API could also be activated in PyPubSub before importing pub, in which case pub.VERSION_API had the corresponding value. Sending Messages ---------------- Sending messages is achieved via the following function: .. autofunction:: sendMessage(topicName, **kwargs) The following exception may be raised when sending a message, if the message data does not comply with the Message Data Specification for the topic: .. autoexception:: SenderMissingReqdMsgDataError :show-inheritance: .. autoexception:: SenderUnknownMsgDataError :show-inheritance: **Advanced use:** The following would typically only be useful in special circumstances, such as if PyPubSub's default Publisher must be accessed, on or more separate instances of Publisher is required, and so forth. .. autofunction:: getDefaultPublisher .. autoclass:: pubsub.core.Publisher :noindex: See :py:class:`pubsub.core.Publisher` for details. Receiving Messages ------------------ The following functions are available for controlling what callable objects (functions, methods, or class instances with a __call__ method) will get called when messages are generated: .. autofunction:: subscribe(listener, topicName) .. autofunction:: unsubscribe(listener, topicName) .. autofunction:: unsubAll(topicName=None, listenerFilter=None, topicFilter=None) .. autofunction:: isSubscribed The following exceptions are relevant: .. autoexception:: ListenerMismatchError :show-inheritance: .. autoexception:: MessageDataSpecError :show-inheritance: .. py:data:: AUTO_TOPIC Use this as default parameter in a listener's signature: the listener will be given the Topic object of the message. The following additional functions may be useful during debugging: .. autofunction:: isValid .. autofunction:: validate .. autoexception:: TopicDefnError :show-inheritance: **Advanced use:** The following are not typically required but can be useful in certain circumstances, especially during debugging: .. autoclass:: pubsub.core.Listener :noindex: See :py:class:`pubsub.core.Listener` for details. .. autofunction:: pubsub.core.getListenerID Topics ------ In most cases, topics are used by name in dotted string format. The following may be useful for basic PyPubSub use: .. autoexception:: TopicNameError :show-inheritance: **Advanced use:** Some advanced uses of PyPubSub, especially (but not only) for debugging a PyPubSub-based application, could require access to the associated Topic instance, topic tree manager, special topic-related constants, or other helper functions and classes. .. autoclass:: TopicTreeTraverser .. py:data:: ALL_TOPICS Name of topic that is root of topic tree. Subscribe a listener to this topic to get all PyPubSub messages. Use \**kwargs to receive all message data, regardless of topic. .. py:data:: topicTreeRoot The topic object that is parent of all root topics. The name of this topic is pub.ALL_TOPICS. .. py:data:: topicsMap The dictionary that maps topic names to Topic objects. **Advanced use:** The following are not typically required but can be useful in certain circumstances, such as during debugging: .. autofunction:: getDefaultTopicMgr .. autoclass:: pubsub.core.TopicManager :noindex: See :py:class:`pubsub.core.TopicManager` for details. .. autoclass:: pubsub.core.Topic :noindex: See :py:class:`pubsub.core.Topic` for details. Listener Exception Handling --------------------------- Listeners that leak exceptions are typically burried deep into the stacktrace, and can cause an application to abort. The following may simplify the task of providing useful error messages from misbehaved listeners, without interrupting the application or even the PyPubSub send-message: .. autofunction:: getListenerExcHandler() .. autofunction:: setListenerExcHandler(handler) .. autoclass:: IListenerExcHandler .. autoexception:: ExcHandlerError :show-inheritance: See :mod:`pubsub.utils.exchandling` for ready-made exception handlers which may fit your requirements. PyPubSub Tracing (aka Notification) ----------------------------------- While debugging an application it may be useful to trap some of PyPubSub's activity: .. autoclass:: INotificationHandler .. autofunction:: addNotificationHandler(handler) .. autofunction:: clearNotificationHandlers() .. autofunction:: setNotificationFlags(**kwargs) .. autofunction:: getNotificationFlags() See :mod:`pubsub.utils` for some ready-made notification handlers which may fit your requirements. Topic Specification ------------------- Topic definition, documentation, and message data specification (MDS): .. autoexception:: TopicDefnError :show-inheritance: .. autofunction:: exportTopicTreeSpec .. autofunction:: setTopicUnspecifiedFatal(newVal=True, checkExisting=True) .. autofunction:: addTopicDefnProvider(providerOrSource, format=None) .. autofunction:: getNumTopicDefnProviders() .. autofunction:: clearTopicDefnProviders() .. autofunction:: instantiateAllDefinedTopics(provider) .. autoexception:: UnrecognizedSourceFormatError :show-inheritance: .. autodata:: TOPIC_TREE_FROM_MODULE Provide to pub.addTopicDefnProvider() as value for format parameter when the source is a module which has been imported. The module can contain any number of classes, the names of which correspond to root topics. .. autodata:: TOPIC_TREE_FROM_CLASS Provide to pub.addTopicDefnProvider() as value for format parameter when the source is a class. The class contains, as nested classes, the root topics (and those contain nested classes for subtopics, etc). .. autodata:: TOPIC_TREE_FROM_STRING Provide to pub.addTopicDefnProvider() as value for format parameter when the source is a string. The string contains Python code that defines one class for each root topic (and those contain nested classes for subtopics, etc). **Developer**: The following are useful to extend the capabilities of PyPubSub to support more topic definition providers or serialization formats for the builtin provider: .. autoclass:: pubsub.core.ITopicDefnProvider .. autoclass:: pubsub.core.ITopicDefnDeserializer .. autoclass:: pubsub.core.TopicDefnProvider :show-inheritance: pypubsub-4.0.3/docs/usage/module_utils.rst000066400000000000000000000001651342344706700206760ustar00rootroot00000000000000Utils module ============ .. automodule:: pubsub.utils :members: :show-inheritance: :inherited-members:pypubsub-4.0.3/docs/usage/reference.rst000066400000000000000000000002011342344706700201160ustar00rootroot00000000000000 Reference ========= The pubsub package contains the following: .. toctree:: module_pub module_utils core_classes pypubsub-4.0.3/docs/usage/types_of_errors.rst000066400000000000000000000127061342344706700214210ustar00rootroot00000000000000Types of Errors --------------- While developing an application that uses PyPubSub, calls to PyPubSub functions and methods may raise an exception, in the following circumstances: * the listener given to pub.subscribe() is not valid: - :py:class:`pub.ListenerMismatchError` - ``ValueError`` * the data sent via pub.sendMessage() does not satisfy the topic's MDS: - :py:class:`pub.SenderMissingReqdMsgDataError` - :py:class:`pub.SenderUnknownMsgDataError` - :py:class:`pub.SenderTooManyKwargs` - :py:class:`pub.SenderWrongKwargName` * there is a problem with a topic name: - :py:class:`pub.TopicNameError` - :py:class:`pub.TopicDefnError` - ``ValueError`` * a callback registered via pub.setListenerExcHandler() raises an exception while handling an exception raised by a listener: - :py:class:`pub.ExcHandlerError` * a subclass derived from a pubsub core or utils base class is missing some implementation: - ``NotImplementedError`` * a topic's MDS, defined explicitly via TopicDefnProvider, is not valid: - :py:class:`pub.MessageDataSpecError` - :py:class:`pub.UnrecognizedSourceFormatError` For basic PyPubSub usage, the most common ones are ``ListenerMismatchError`` and the ``Sender...`` exceptions. All others are relevant to usage of more advanced PyPubSub features such as topic tree specification, listener exception trapping, and PyPubSub notification trapping. Listener Mismatch Errors ^^^^^^^^^^^^^^^^^^^^^^^^ The most common type of error results from attempting to subscribe an invalid listener: one that does not have a signature (call protocol) compatible with the topic's MDS. When this happens, PyPubSub raises a :py:class:`pub.ListenerMismatchError` exception. By default, PyPubSub infers topic MDSs. In that case, the error typically happens when more than one listener is registered for a given topic, and introspection of the listener identifies that it does not satisfy the topic's MDS. For example, consider :: def listener0(arg1, arg2=default0): pass def listener1(arg1=val, arg2=default3): pass def listener2(arg1): pass def listener3(arg1, arg2): pass pub.subscribe(listener0, "topic") // OK: infers MDS pub.subscribe(listener1, "topic") // OK: satisfies MDS pub.subscribe(listener2, "topic") // FAIL: violates MDS PyPubSub will raise a ListenerMismatchError exception on the last line since arg2 was inferred in the first subscription, from listener0, as being part of the MDS, yet listener2 does not accept this data. Similarly, if the last line had been :: pub.subscribe(listener3, "topic") a ``pub.ListenerMismatchError`` exception would get raised because listener3 *requires* arg2, yet the MDS inferred from listener0 has it as optional, indicating the sender may not provide it. PyPubSub is flagging the fact that listener3 is "more demanding" than the MDS can guarantee. Sender Exceptions ^^^^^^^^^^^^^^^^^ The sender exceptions are very useful as they indicate clearly what message data is wrong: - :py:class:`pub.SenderMissingReqdMsgDataError`: some required data is missing - :py:class:`pub.SenderUnknownMsgDataError`: one of the keyword arguments is not part of MDS For example, given the previous code involving a topic "topic" MDS inferred from listener0, the following code would raise a ``pub.SenderUnknownMsgDataError`` :: pub.sendMessage("topic", arg1=1, arg3=3) because arg3 is not part of the MDS. Topic Name Errors ^^^^^^^^^^^^^^^^^ A topic name must satisfy the following: - is not empty: '' or None - is not a reserved name: the only one currently is the value of :py:data:`pub.ALL_TOPICS` - starts with any of '-', 0-9, a-z, A-Z (so UNDERSCORE '_' not allowed; it is reserved) This applies to all levels of a topic path, i.e. the items between '.'. For example the following are not allowed: 'a.', '.a', '.', 'a..b', etc. If a topic name does not satisfy the above, PyPubSub raises ``pub.TopicNameError``. Some functions in PyPubSub raise an exception if the topic doesn't exist: - :py:func:`pub.isValid(listener, topicName)` - :py:func:`pub.validate(listener, topicName)` - :py:func:`pub.isSubscribed(listener, topicName)` - :py:func:`pub.unsubscribe(listener, topicName)` - :py:func:`pub.unsubAll(topicName)` since the operation does not make sense: it does not make sense, for example, to test if given listener is valid if topic does not exist! By default, - PyPubSub does *not* complain about topic names that have never been subscribed to. - subscribing a listener to a topic never used before 'creates' the topic. Hence there is, by default, no way of trapping the following mistakes:: pub.subscribe(listener1, 'topic') # creates 'topic' topic # next line has typo in topic name: pub.subscribe(listener2, 'tpic') # creates 'tpic' topic pub.sendMessage('topic') # only listener1 will receive # next line has typo in topic name: pub.sendMessage('topc') # creates 'topc' topic; no listener will receive These can lead to hard-to-isolate bugs as some listeners never get the messages. To trap such typos, use :py:func:`pub.setTopicUnspecifiedFatal(true)`, and specify all allowed topics at application startup by registering a Topic Definition Provider via :py:func:`pub.addTopidDefnProvider()`. Both above typos will then lead to PyPubSub raising :py:class:`TopicDefnError`. Note: a provider can easily be created via the :py:func:`pub.exportTopicTreeSpec()`. pypubsub-4.0.3/docs/usage/usage_advanced.rst000066400000000000000000000002521342344706700211170ustar00rootroot00000000000000 .. _label-usage-advanced: Advanced Usage ============== .. toctree:: :maxdepth: 2 usage_advanced_debug usage_advanced_maintain usage_advanced_other pypubsub-4.0.3/docs/usage/usage_advanced_debug.rst000066400000000000000000000127751342344706700223020ustar00rootroot00000000000000Debugging an application ======================== .. contents:: In this section: :depth: 2 :local: Types of Errors --------------- While developing an application that uses PyPubSub, calls to PyPubSub functions and methods may raise an exception. These are discussed in: .. toctree:: types_of_errors Notification: Tracking PyPubSub activity ---------------------------------------- PyPubSub can call a specified handler every time it performs a certain task: - *subscribe*: whenever a listener subscribes to a topic - *unsubscribe*: whenever a listener unsubscribes from a topic - *deadListener*: whenever PyPubSub finds out that a listener has died - *send*: whenever the user calls sendMessage() - *newTopic*: whenever the user defines a new topic - *delTopic*: whenever the user undefines a topic A notification handler must adhere to the pub.INotificationHandler:: import pubsub.utils class MyNotifHandler(INotificationHandler): def onSendMessage(...): ... pub.addNotificationHandler( MyNotifHandler() ) A simple handler class is available already in ``pubsub.utils``: ``notification.NotifyByPubsubMessage``. This handler takes each notification received and generates a PyPubSub message of a "pubsub." topic named after the operation, such as "pubsub.subscribe". To use notification via this notifier, you must register one or more listeners for the "pubsub.*" topics of interest. A utility function is available from pubsub.utils for the most common case:: from pubsub.utils import notification notification.useNotifyByPubsubMessage() .. _label-exchandling: Naughty Listeners: Trap Exceptions ---------------------------------- A sender has no way of knowing what can go wrong during message handling by the subscribed listeners. As a result, a listener must not raise any exceptions (or rather, must not let any exceptions escape): if an exception does escape a listener, it interrupts the ``pub.sendMessage()`` call such that some listeners may not be sent the message. Putting a try/except clause around every sendMessage is typically not practical. Since exceptions are common during application development (bugs due to invalid arguments, failed assertions, etc.), PyPubSub provdes a hook to register a 'listener exception' handler: whenever a listener raises an exception, PyPubSub then sends it to the handler, and continues with the send operation until all listeners have received the message. The handler might print it to a log file, output a message in a status bar, show an error box, etc. The handling itself is very application-specific, hence this strategy. The handler must adhere to the ``pub.IListenerExcHandler`` protocol. An instance of the handler can be given to ``pub.setListenerExcHandler()``. Listen for messages from all topics ----------------------------------- PyPubSub defines a special topic named pub.ALL_TOPICS. A listener that subscribes to this topic will receives all messages of every topic. By default, the listener will not receive any data since pub.ALL_TOPICS is the parent of all root topics: its MDS must be empty. However, any listener that is a callable with a "catch-all" \**kwargs parameter will be given all message data. Moreover, PyPubSub sends the topic object automatically with the message data if it finds that listener accepts a keyword argument with a default value of pub.AUTO_TOPIC. Together, these can be used to obtain complete information about all messages:: >>> def snoop(topicObj=pub.AUTO_TOPIC, **mesgData): >>> print 'topic "%s": %s' % (topicObj.getName(), mesgData) >>> >>> pub.subscribe(snoop, pub.ALL_TOPICS) (, True) >>> pub.sendMessage('some.topic.name', a=1, b=2) topic "some.topic.name": {'a': 1, 'b': 2} Using the pub.Listener class ---------------------------- Every callable that is subscribed via pub.subscribe() is wrapped in a pub.Listener instance returned by this function. This class has several useful functions such as name(), typeName(), module(), and isDead(). For example:: >>> def snoop(topicObj=pub.AUTO_TOPIC, **mesgData): >>> pass >>> >>> pubListener, first = pub.subscribe(snoop, pub.ALL_TOPICS) >>> assert first == true # since first time subscribed >>> assert pubListener.isDead() == false >>> assert pubListener.wantsTopicObjOnCall() == true >>> assert pubListener.wantsAllMessageData() == true >>> print pubListener.name() snoop_2752 >>> print pubListener.name() snoop Doing something with every topic -------------------------------- Derive from pub.ITopicTreeVisitor and give instance to an instance of pub.TopicTreeTraverser, then call traverse() method. For example, assume a callable 'listener' has been subscribed to several topics. An easy way to verify all topics subscribed to use this: >>> class MyVisitor(pub.ITopicTreeVisitor): >>> def __init__(self, listener): >>> self.subscribed = [] >>> self.listener = listener >>> def _onTopic(self, topicObj): >>> if topicObj.hasListener(self.listener): >>> self.subscribed.append(topicObj.getName()) >>> >>> tester = new MyVisitor(listener) >>> traverser = pub.TopicTreeTraverser( tester ) >>> traverser.traverse(pub.getDefaultTopicTreeRoot()) >>> print tester.subscribed ['topic-name', 'topic-name2', ...] Printing Topic Tree ------------------- See pubsub.utils.printTreeDocs(). pypubsub-4.0.3/docs/usage/usage_advanced_maintain.rst000066400000000000000000000071041342344706700230020ustar00rootroot00000000000000Maintainabiity ============== .. contents:: In this section: :depth: 2 :local: .. _label-topic_tree_def: Specify topic tree def ---------------------- *Topic Specification* can be used to have better control over your topic hierarchy. If you don't specify your application's topics, PyPubSub infers them from the first subscribed listener of each topic. E.g.:: def listener1(arg1, arg2=None): pass def listener2(arg1=None, arg2=None): pass pub.subscribe(listener1, 'topic.sub') pub.subscribe(listener2, 'topic.sub') Because listener1 is the first to be subscribed to 'topic.sub' topic, PyPubSub uses it to infer the specification of 'topic.sub': the specification is "messages of that topic *must* provide data for arg1, and *may* provide data for arg2". The second listener subscribed, listener2, is allowed to subscribe because it is compatible with the topic's specification created at the previous call. What if your intent was that arg1 is optional as well, i.e. the signature of listener1 is wrong (it should provide a default value for arg1)? Or what if per chance listener2 gets subscribed first (could happen if both are subscribed in different modules whose load order changes)? The only way to not depend on the order of subscription of listeners is to use *Topic definition providers* (TDP). This is described below. Topic Definition Providers ^^^^^^^^^^^^^^^^^^^^^^^^^^ The easiest way to understand a topic tree definition is to get PyPubSub to output one for your application via ``pub.exportTopicTreeSpec()``. Here is an example, taken from the file examples/advanced/kwargs_topics.py generated by that function, assuming two root topics 'topic_1' and 'topic_2' and the call ``pub.exportTopicTreeSpec('kwargs_topics')``: .. literalinclude:: ../../examples/advanced/kwargs_topics.py This shows how the topic definition tree is defined using a Python module with a nested class tree that represents the topics, and msgDataSpec() functions that represent the listener signatures for the given topics. This also shows how it is possible to document each topic and message datum. An application uses the above module via the following:: import kwargs_topics pub.addTopicDefnProvider( kwargs_topics, pub.TOPIC_TREE_FROM_CLASS ) The format type is ``pub.TOPIC_TREE_FROM_CLASS`` because once imported, the kwargs_topics object is a module containing topic definitions as classes; based on that setting, PyPubSub will look for all classes in the kwargs_topics object, and instantiate one topic definition for each one. See examples/advanced/main_kwargs.py for an example of using a topic tree definition in an application. It is possible to support other formats for topic tree definition. For example, ``pubsub.utils.XmlTopicDefnProvider`` was contributed to PyPubSub by one of its devoted users. A new type of provider need only adhere to the ``pub.ITopicTreeDefnProvider`` interface; ``pub.addTopicDefnProvider()`` accepts any instance that implements from that interface:: xmlString = open('xml_topics.xml', 'r').read() provider = XmlTopicDefnProvider(xmlString) pub.addTopicDefnProvider( provider ) It is typically useful to combine topic tree definition with the following call, placed once at the beginning of an application:: pub.setTopicUnspecifiedFatal(True) Then any attempt to use a topic that is not defined in the topic tree definition will raise an ``pub.TopicUnspecifiedError``. Note that any topic that does not have a docstring is not considered to be defined. This may allow for some temporary "undefining" of topics. pypubsub-4.0.3/docs/usage/usage_advanced_other.rst000066400000000000000000000120171342344706700223220ustar00rootroot00000000000000 Other ===== .. contents:: In this section: :depth: 2 :local: Dev app (process) ----------------- Suggestions while developing application that uses PyPubSub: - Design your application into independent modules or subpackages that don't import one another - Define basic topics exist in the application: 'user' (events from user interface), 'filesystem' (events from local filesystem), etc. These are your messaging topics. You may find it useful to use ``printTreeDocs`` from ``pubsub.utils``. - Use Topic Definition Providers as eary as possible. Use pub.exportTopicTreeSpec() if already have partial implementation, and pub.addTopicDefnProvider() and pub.setTopicUnspecifiedFatal(). - Start all listener functions and methods with *pubOn*, for instance ``def psOnCloseDocument()`` - Define some data for each message type, and which data are optional/required - Implement your modules - Subscribe listeners with appropriate signature (according to data for each topic/event type) - Send messages with appropriate data - Handle messages in listeners, without making any assumptions about sender or order of receipt - Testing: import your control modules and generate messages to exercise them. You can see a very informative view of an application before and after incorporatng PyPubSub, at `Steven Sproat's dev site`_ (click "expand all" and "show diffs side-by-side"). Steven says: *You can see how I removed some GUI logic from the Canvas class (a child of the GUI) and placed "controller" functions into my GUI that subscribed to PyPubSub topics and delegates to the appropriate classes.* .. _Steven Sproat's dev site: http://bazaar.launchpad.net/~sproaty/whyteboard/development/revision/286 .. _label-msg_protocols: Messaging Protocol ------------------ The very first version of PyPubSub supported a messaging protocol that became known as 'arg1'. This protocol made it difficult to specify (i.e. define) what data was allowed in a topic. For larger applications, the developer had to put in verification code in the listener, had to deal with exceptions resulting from mismatches in field names in the message object, etc. It worked but made debugging the use of topics and PyPubSub messages complicated. The kwargs protocol was then designed: it allows the sender to name each datum, and the recipient (listener) to be checked via introspection at subscription time for its capability to receive the data. It also makes it easier to document the message data, and to specify it. The protocol was implemented in PyPubSub version 3. PyPubSub v4 supports only one way of transmitting data to listeners, namely via the 'kwargs' protocol. Since this is the only protocol supported, there is no code left that handles protocol name or selection. .. _label-pubsub_versions: API Versions ------------ As PyPubSub matured, its API went through changes: - API version 1 (PyPubSub v1): the version that was part of wxPython and supported only the arg1 protocol. - API version 2 (PyPubSub v2): also part of wxPython, it made various improvements on v1 but was short lived as it did not properly address some inherent limitations of version 1. - API version 3 (PyPubSub v3): PyPubSub was moved out of wxPython to be a standalone project and supported 2 messaging protocols: the original arg1 for backwards compatibility, and the new kwargs. Since then, wxPython's wx.lib.pubsub is a verbatim copy of the standalone PyPubSub. The arg1 protocol was deprecated. - API version 4 (PyPubSub v4): Support for arg1 was dropped; only kwargs is now supported, which simplifies the code base considerably. Receiving all data of a message ------------------------------- If a Listener uses \**kwargs then it will be given all data of a message, not just the portion specific to the topic it is subscribed to. For example, :: >>> def listener0(arg1, arg2): print('listener0: ', arg1, arg2) >>> def listener1(**kwargs): print('listener1: ', kwargs) >>> pub.subscribe(listener0, 'topic') >>> pub.subscribe(listener1, 'topic') >>> pub.sendMessage('topic', arg1=1, arg2=2) Then listener1 will receive arg1 and arg2. Note: as explained in :ref:`label-topic_tree_def`, PyPubSub infers a topic's *Message Data Specification* based on the first listener subscribed, unless there is a *Topic Definition Provider* for the topic. In the above example, PyPubSub would infer that *topic* has 2 required data: arg1 and arg2. However, if listener1 were subscribed first, PyPubSub would infer that *topic* had no required data (because there are no positional parameters in the listener1 signature), and no optional data (because there are no parameters with default values in the the listener1 signature). Thus the subscription of listener0 to *topic* would raise an exception (because listener0 requires arg1 and arg2). In real-world code, it can be difficult to guarantee the order of registration of listeners. Such issue is one of the intended use cases for a *Topic Definition Provider*, as explained in :ref:`label-topic_tree_def`. pypubsub-4.0.3/docs/usage/usage_basic.rst000066400000000000000000000025311342344706700204350ustar00rootroot00000000000000 .. _label-usage-basic: Basic Usage =========== Basic usage of PyPubSub involves subscribing listeners, sending messages, and responding to messages. The :ref:`label-quick-start` subsection below provides examples. For details, navigate to the :ref:`label-basic-tasks` subsection: .. toctree:: :maxdepth: 2 usage_basic_tasks .. _label-quick-start: Quick Start ----------- Simplest example of use: .. literalinclude:: helloworld.py Running the above as a script (available in the docs/usage folder of the source distribution as helloworld.py) will produce the result:: Publish something via pubsub Function listener1 received: arg1 = 123 arg2 = {'a': 456, 'b': 'abc'} Other Examples ^^^^^^^^^^^^^^ There are several examples that can be found in the source distribution in the ``examples`` folder. Some focus on the basics, others on more advanced aspects of PyPubSub usage. Some examples are GUI-based and may require other packages (such as wxPython). The examples/basic_kwargs folder contains examples of basic usage of PyPubSub "out of the box", i.e. using the default ("kwargs") messaging protocol. The README.txt file in `examples_basic_kwargs`_ explains: .. include:: ../../examples/basic_kwargs/README.txt .. _examples_basic_kwargs: http://svn.code.sf.net/p/pubsub/code/trunk/examples/basic_kwargs pypubsub-4.0.3/docs/usage/usage_basic_tasks.rst000066400000000000000000000335441342344706700216520ustar00rootroot00000000000000.. _label-basic-tasks: Basic PyPubSub Tasks ==================== Several essential tasks supported by PyPubSub .. contents:: In this section: :depth: 2 :local: Subscribing to Topics --------------------- Every message that can be sent via PyPubSub is of a specific topic, just as every object in Python is of a specific type. Use ``pub.subscribe(callable, 'topic-path')`` to subscribe callable to all messages of given topic or any of its subtopics. Callable ^^^^^^^^ The callable can be: * any function * any method * any class instance that defines ``__call__`` method Hence given the following definitions:: def function(): pass class Foo: def method(self): pass @staticmethod def staticMeth(): pass @classmethod def classMeth(cls): pass def __call__(self): pass foo = Foo() the following callables could be subscribed to a PyPubSub message topic:: function foo.method foo Foo.staticMeth Foo.classMeth PyPubSub holds listeners by weak reference so that the lifetime of the callable is not affected by PyPubSub: once the application no longer references the callable, it can be garbage collected and PyPubSub can clean up so it is no longer registered (this happens thanks to the weakref module). Without this, it would be imperative to remember to unsubscribe certain listeners, which is error prone; they would end up living until the application exited. A nice example of this is a user control (widget) in a GUI: if a method of the user control is registered as listener in PyPubSub, and the control is discarded, the application need not explicitly unregister the callable: the weak referencing will allow the widget to be garbage collected; otherwise, it would remain visible until explicit unsubscription. .. warning:: One caveat that results from this useful feature is that all callables that subscribe to topics must be referenced from outside PyPubSub. For instance, the following will silently unsubscribe on return from pub.subscribe():: def listener(): pass def wrap(fn): def wrappedListener(): fn() return wrappedListener pub.subscribe(wrap(listener), 'topic') since wrap() returns an object which only PyPubSub references: the wrappedListener gets garbage collected upon return from subscribe(). It is possible to verify that the stored listener is indeed dead:: ll,ok = pub.subscribe(wrap(listener), 'topic') print ll.isDead() # prints True Compare without wrapping:: ll,ok = pub.subscribe(listener, 'topic') print ll.isDead() # prints False Fix by storing a strong reference to wrappedListener:: ww = wrap(listener) # creates strong reference ll,ok = pub.subscribe(ww, 'topic') print ll.isDead() # prints False .. _label-topic-name: Topic Name ^^^^^^^^^^ Every topic has a name and a path. The name can contain any character a-z, A-Z, 0-9 and _&%$#@ and the hyphen. Valid examples are:: 'asdfasdf' 'aS-fds0-123' '_&%$#@-abc-ABC123' Other characters will lead to an exception or undefined behavior. Topics form a hierarchy: * every topic can be child of a "parent" topic * a topic that does not have a parent topic is a "root" topic * every topic can have one or more "children" i.e. sub topics The fully qualified topic name is therefore the path through the topic hierarchy. The path separator is '.'. Hence given the following topic hierarchy:: root-topic-1 sub-topic-2 sub-sub-topic-3 the following subscriptions could be valid:: pub.subscribe(callable, 'root-topic-1') pub.subscribe(callable, 'root-topic-1.sub-topic-2') pub.subscribe(callable, 'root-topic-1.sub-topic-2.sub-sub-topic-3') .. _label-MDS: Message Data ^^^^^^^^^^^^ Messages of a given topic can carry data. Which data is required and which is optional is known as the *Message Data Specification* for the topic, or MDS for short. Unless your application explicitly defines the MDS for every topic in the hierarchy, PyPubSub infers the MDS of each topic based on the first pub.subscribe() or the first pub.sendMessage() for the topic, whichever occurs first during an application run. Once defined, a topic's MDS never changes (during a run). Examples of MDS inferred from a call to pub.subscribe(): ============================================ ================================= Callable signature MDS (inferred) ============================================ ================================= ``callable(arg1)`` - required: arg1 - optional: none ``callable(arg3=1)`` - required: none - optional: arg3 ``callable(arg1, arg2, arg3=1, arg4=None)`` - required: arg1, arg2 - optional: arg3, arg3 ============================================ ================================= All subsequent calls to pub.subscribe() for the same topic or any subtopic must be consistent with the topic's MDS. If a subscription specifies a callable that does not match the given topic's MDS, PyPubSub raises an exception. Therefore, the pub.subscribe() calls above *could* be valid; they *will* be valid if the given callable satisfies the given topic's MDS. Examples of subscriptions: assume MDS of topic 'root' is required=arg1, optional=arg2, then pub.subscribe(callable, 'root') for the following callable signatures are ok: =============================== ==== ================================= Callable OK Why =============================== ==== ================================= callable(arg1, arg3=1) Yes matches MDS callable(arg1=None, arg3=None) Yes signature is less restrictive than MDS, and default value are not part of MDS callable(arg1) No arg2 could be in message, yet callable does not accept it callable(arg1, arg2) No callable requires arg2, but MDS says it won't always be given in message =============================== ==== ================================= A callable subscribed to a topic is a listener. Note that the default value for an optional message data is not part of the MDS. Each listener can therefore decide what default value to use if the data is not provided in the message. Sending messages ---------------- Use ``pub.sendMessage('topic-path-name', **data)`` to send a message with the given data. The topic path name is a dot-separated sequence of topic names from root to topic (see :ref:`label-topic-name`). The message is sent to all registered listeners of given topic, parent topic, and so forth up the "topic tree", by calling each listener, in turn, until all listeners have been sent the message and data. A listener must return before the next listener can be called. The order of listeners (within a topic or up the tree) is not specified. The sender should not make any assumptions about the order in which listeners will be called, or even which ones will be called. If a listener leaks an exception, PyPubSub catches it and interrupts the send operation, unless an exception handler has been defined. This is discussed in :ref:`label-exchandling`. Message Data ^^^^^^^^^^^^ The data must satisfy the topic's MDS, and all arguments must be named. So for a topic 'root' with MDS of arg1, arg2 required and arg3 optional, the send command would have the form:: pub.sendMessage('root', arg1=obj1, arg2=obj2, arg3=obj3) One consequence of this is that the order of arguments does not matter:: pub.sendMessage('root', arg3=obj3, arg2=obj2, arg1=obj1) is equally valid. But :: pub.sendMessage('root', obj1, obj2, arg3=obj3) is not allowed. Only the message data relevant to a topic is sent to the listeners of the topic. For example if topic 'root.sub.subsub' has a MDS involving data arg1, arg2 and arg3, and topic 'root' has only arg1, then listeners of 'root.sub.subsub' topic will get called with arg1, arg2, and arg3, but listeners of 'root' will get called with the arg1 parameter only. The less specific topics have less data. Since messages of a given topic are sent not only to listeners of the topic but also to listeners of topic up the topic tree, PyPubSub requires that subtopic MDS be the same or more restrictive as that of its parent: optional arguments can become required, but required arguments cannot become optional. Indeed if 'root' messages require arg1, then 'root.sub' must also require it; otherwise a message of type 'root.sub' could be sent without an object for arg1, and once the 'root' listeners received the message, they could find the required parameter missing. If 'root' messages have arg2 as optional data, then 'root.sub' can be more restrictive and require it. Examples of subtopic MDS: assume topic 'root' has MDS required arg1 and optional arg2. Then following 'root.sub' MDS would be ==== ================= ==== ========================================= Case MDS extended by OK Why ==== ================= ==== ========================================= 1 + required arg3 Yes Extends MDS of 'root' + optional arg4 2 + optional arg3 No Less restrictive than 'root': arg3 + optional arg4 could be missing from 'root.sub' message ==== ================= ==== ========================================= Topic as Message Data ^^^^^^^^^^^^^^^^^^^^^ If a listener requires to know the topic of the message, a specially named default value ``pub.AUTO_TOPIC`` can be used for one of its call parameters: at call time, PyPubSub will replace the value by the pub.TopicObj object for the topic. It can be queried to find the topic name via Topic.getName():: def listener(topic=pub.AUTO_TOPIC): print "real topic is", topic.getName() pub.subscribe(listener, "some_topic") pub.sendMessage("some_topic") # no data This allows each listener to define whether it needs the topic information (rarely the case). Therefore, it is not part of the MDS. In the above example, the MDS for 'some_topic' is empty. Sending vs Broadcasting ^^^^^^^^^^^^^^^^^^^^^^^ The pub.sendMessage() shares some similarities and differences with "broadcasting". Some similarities: * All callables subscribed to the topic will receive the message; in broadcasting, all receivers tuned in to the emitter frequency will receive the data. Hence the topic is akin to the radio frequency of the broadcast. * The sender has no knowledge of which listeners are subscribed to a topic; in broadcasting, the emitter does not know which receivers are "tuned in" * The order in which listeners receive the broadcast is undefined. In broadcasting, distance to the emitter affects when the receiver will get the message, and the emitter has no knowledge of where receivers are located, so it can't know which receiver will hear the message first. * The listener does not know the source of messages. In broadcasting, the receiver has no way of knowing which emitter is the source of a given message: it will capture all messages from different emitters sa though that had all been generated by the same emitter, as long as they are of the same frequency. * Listeners to not send any data back to the sender as part of the message delivery. In broadcasting, the receiver does not send any data back to the emitter as part of the message. Some differences: * A message sent to a listener must be processed before it can be sent to another listener of same topic. In broadcasting, all receivers can process the message simultaneously. * The listener cannot send data back to the sender: the sender is the line of code that calls pub.sendMessage(), this is not a callable nor is it subscribed to the topic of the message sent. In broadcasting, the receiver can transmit over the same frequency as received message, and the emitter could (if it has reception capability and is tuned to same frequency) read the message. * Listeners of parent topics will get messages for subtopics. In broadcasting, there is no analogy of "sub-frequencies". Handling messages ----------------- A callable subscribed to a topic receives a message by being called. Assuming that the send command is:: pub.sendMessage('topic-path-name', **data) then all listeners subscribed to the named topic will get called with the given \**data dictionary, as well as all listeners of the topic's parent topic, and so forth until the root topic is reached. .. warning:: A listener should not make any assumptions about: * The order of calls of listeners subscribed to same or other topics * Where the message originates Message Data ^^^^^^^^^^^^ Only the portion of data that is relevant to the topic is given to each listener. Assume the following topic branch of the hierarchy:: tt: listeners a and b; MDS is r=arg1, o=arg4 uu: listeners c and d; MDS is r=(arg1, arg2), o=(arg4, arg5) vv: listeners e and f; MDS is r=(arg1, arg2, arg3), o=(arg4, arg5, arg6) then ``pub.sendMessage('root-topic', arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6)`` will call * ``e(arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6)``; same with f; implicitly the topic is tt.uu.vv * ``c(arg1=1, arg2=2, arg4=4, arg5=5)``; same with d; implicitly the topic is tt.uu * ``a(arg1=1, arg4=4)``; same with b; implicitly the topic is tt As stated in the 'Sending Messages' section, the order in which the listeners are called is not specified; your application should not make any assumptions about this order. pypubsub-4.0.3/examples/000077500000000000000000000000001342344706700152175ustar00rootroot00000000000000pypubsub-4.0.3/examples/advanced/000077500000000000000000000000001342344706700167645ustar00rootroot00000000000000pypubsub-4.0.3/examples/advanced/advanced_main.py000066400000000000000000000020321342344706700221040ustar00rootroot00000000000000""" Advanced example that shows other capabilities of pubsub such as pubsub notification, listener exception handling, and topic definition providers. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub import notifhandle import exchandle import kwargs_topics # ***** actual application ********** print('Using "kwargs" messaging protocol of pubsub v3') try: print('------- init ----------') pub.addTopicDefnProvider(kwargs_topics, pub.TOPIC_TREE_FROM_CLASS) pub.setTopicUnspecifiedFatal() import kwargs_listeners import kwargs_senders as senders print('-----------------------') senders.doSomething1() senders.doSomething2() print('------- done ----------') print('Exporting topic tree to', kwargs_topics.__name__) pub.exportTopicTreeSpec('kwargs_topics_out') except Exception: import traceback traceback.print_exc() print(pub.exportTopicTreeSpec()) print('------ exiting --------') pypubsub-4.0.3/examples/advanced/exchandle.py000066400000000000000000000010361342344706700212710ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import traceback from pubsub import pub # create one special notification handler that ignores all except # one type of notification class MyPubsubExcHandler(pub.IListenerExcHandler): def __call__(self, listenerID: str, topicObj: pub.Topic): print('Exception raised in listener %s during sendMessage()' % listenerID) traceback.print_exc() pub.setListenerExcHandler(MyPubsubExcHandler()) pypubsub-4.0.3/examples/advanced/kwargs_listeners.py000066400000000000000000000020301342344706700227170ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub # ------------ create some listeners -------------- class Listener: def onTopic11(self, msg, msg2, extra=None): print('Method Listener.onTopic11 received: ', repr(msg), repr(msg2), repr(extra)) def onTopic1(self, msg, topic=pub.AUTO_TOPIC): info = 'Method Listener.onTopic1 received "%s" message: %s' print(info % (topic.getName(), repr(msg))) def __call__(self, **kwargs): print('Listener instance received: ', kwargs) listenerObj = Listener() def listenerFn(msg, msg2, extra=None): print('Function listenerFn received: ', repr(msg), repr(msg2), repr(extra)) # ------------ subscribe listeners ------------------ pub.subscribe(listenerObj, pub.ALL_TOPICS) # via its __call__ pub.subscribe(listenerFn, 'topic_1.subtopic_11') pub.subscribe(listenerObj.onTopic11, 'topic_1.subtopic_11') pub.subscribe(listenerObj.onTopic1, 'topic_1') pypubsub-4.0.3/examples/advanced/kwargs_senders.py000066400000000000000000000007101342344706700223550ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub def doSomething1(): pub.sendMessage('topic_1.subtopic_11', msg='message for subtopic 11', msg2='other message', extra=123) def doSomething2(): pub.sendMessage('topic_1', msg='message for topic 1') pub.sendMessage('topic_2.subtopic_21', msg='message for subtopic 2') pypubsub-4.0.3/examples/advanced/kwargs_topics.py000066400000000000000000000021461342344706700222200ustar00rootroot00000000000000# Automatically generated by TopicTreeSpecPrinter(**kwargs). # The kwargs were: # - fileObj: file # - width: 70 # - treeDoc: None # - indentStep: 4 # - footer: '# End of topic tree definition. Note that application may l...' class topic_1: """ Explain when topic_1 should be used """ def msgDataSpec(msg): """ - msg: a text string message for recipient """ class subtopic_11: """ Explain when subtopic_11 should be used """ def msgDataSpec(msg, msg2, extra=None): """ - extra: something optional - msg2: a text string message #2 for recipient """ class topic_2: """ Some something useful about topic2 """ def msgDataSpec(msg=None): """ - msg: a text string """ class subtopic_21: """ description for subtopic 21 """ def msgDataSpec(msg, arg1=None): """ - arg1: UNDOCUMENTED """ # End of topic tree definition. Note that application may load # more than one definitions provider. pypubsub-4.0.3/examples/advanced/kwargs_topics_out.py000066400000000000000000000022071342344706700231050ustar00rootroot00000000000000# Automatically generated by TopicTreeSpecPrinter(**kwargs). # The kwargs were: # - fileObj: TextIOWrapper # - footer: '# End of topic tree definition. Note that application may l...' # - indentStep: 4 # - treeDoc: None # - width: 70 class topic_1: """ Explain when topic_1 should be used """ def msgDataSpec(msg): """ - msg: a text string message for recipient """ class subtopic_11: """ Explain when subtopic_11 should be used """ def msgDataSpec(msg, msg2, extra=None): """ - extra: something optional - msg2: a text string message #2 for recipient """ class topic_2: """ Some something useful about topic2 """ def msgDataSpec(msg=None): """ - msg: a text string """ class subtopic_21: """ description for subtopic 21 """ def msgDataSpec(msg, arg1=None): """ - arg1: UNDOCUMENTED """ # End of topic tree definition. Note that application may load # more than one definitions provider. pypubsub-4.0.3/examples/advanced/notifhandle.py000066400000000000000000000015131342344706700216310ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import sys from pubsub import pub from pubsub.utils.notification import useNotifyByWriteFile, IgnoreNotificationsMixin # create one special notification handler that ignores all except # one type of notification class MyPubsubNotifHandler(IgnoreNotificationsMixin): def notifySubscribe(self, pubListener, topicObj, newSub): newSubMsg = '' if not newSub: newSubMsg = ' was already' msg = 'MyPubsubNotifHandler: listener %s%s subscribed to %s' print(msg % (pubListener.name(), newSubMsg, topicObj.getName())) pub.addNotificationHandler(MyPubsubNotifHandler()) # print(all notifications to stdout) useNotifyByWriteFile(sys.stdout, prefix='NotifyByWriteFile:') pypubsub-4.0.3/examples/basic_kwargs/000077500000000000000000000000001342344706700176565ustar00rootroot00000000000000pypubsub-4.0.3/examples/basic_kwargs/README.txt000066400000000000000000000006521342344706700213570ustar00rootroot00000000000000These two examples demonstrate a simple use of pubsub. There are two examples that can be run from this folder: **console_main.py**: basic console based, uses the console_senders.py and console_listeners.py modules. **wx_main.py**: wxPython GUI application with two windows (win1 and win2) that exchange data without any reference to the other. This example looks for pubsub on your system path so default install ok. pypubsub-4.0.3/examples/basic_kwargs/console_listeners.py000066400000000000000000000017601342344706700237660ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub # ------------ create some listeners -------------- class Listener: def onTopic11(self, msg, extra=None): print('Method Listener.onTopic11 received: ', repr(msg), repr(extra)) def onTopic1(self, msg, topic=pub.AUTO_TOPIC): info = 'Method Listener.onTopic1 received "%s" message: %s' print(info % (topic.getName(), repr(msg))) def __call__(self, **kwargs): print('Listener instance received: ', kwargs) listenerObj = Listener() def listenerFn(msg, extra=None): print('Function listenerFn received: ', repr(msg), repr(extra)) # ------------ subscribe listeners ------------------ pub.subscribe(listenerObj, pub.ALL_TOPICS) # via its __call__ pub.subscribe(listenerFn, 'topic1.subtopic11') pub.subscribe(listenerObj.onTopic11, 'topic1.subtopic11') pub.subscribe(listenerObj.onTopic1, 'topic1') pypubsub-4.0.3/examples/basic_kwargs/console_main.py000066400000000000000000000005451342344706700227020ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import console_listeners import console_senders as senders def run(): print('Using "kwargs" messaging protocol of pubsub v3') senders.doSomething1() senders.doSomething2() if __name__ == '__main__': run() pypubsub-4.0.3/examples/basic_kwargs/console_senders.py000066400000000000000000000010071342344706700234130ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub def doSomething1(): print('--- SENDING topic1.subtopic11 message ---') pub.sendMessage('topic1.subtopic11', msg='message for 11', extra=123) print('---- SENT topic1.subtopic11 message ----') def doSomething2(): print('--- SENDING topic1 message ---') pub.sendMessage('topic1', msg='message for 1') print('---- SENT topic1 message ----') pypubsub-4.0.3/examples/basic_kwargs/wx_main.py000066400000000000000000000034611342344706700216760ustar00rootroot00000000000000""" Adapted from wxPython website at http://wiki.wxpython.org/ModelViewController/. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import wx from pubsub import pub print('pubsub API version', pub.VERSION_API) # notification from pubsub.utils.notification import useNotifyByWriteFile import sys useNotifyByWriteFile(sys.stdout) # the following two modules don't know about each other yet will # exchange data via pubsub: from wx_win1 import View from wx_win2 import ChangerWidget class Model: def __init__(self): self.myMoney = 0 def addMoney(self, value): self.myMoney += value # now tell anyone who cares that the value has been changed pub.sendMessage("money_changed", money=self.myMoney) def removeMoney(self, value): self.myMoney -= value # now tell anyone who cares that the value has been changed pub.sendMessage("money_changed", money=self.myMoney) class Controller: def __init__(self): self.model = Model() # set up the first frame which displays the current Model value self.view1 = View() self.view1.setMoney(self.model.myMoney) # set up the second frame which allows the user to modify the Model's value self.view2 = ChangerWidget() self.view1.Show() self.view2.Show() pub.subscribe(self.changeMoney, 'money_changing') def changeMoney(self, amount): if amount >= 0: self.model.addMoney(amount) else: self.model.removeMoney(-amount) if __name__ == "__main__": app = wx.App() c = Controller() sys.stdout = sys.__stdout__ print('---- Starting main event loop ----') app.MainLoop() print('---- Exited main event loop ----') pypubsub-4.0.3/examples/basic_kwargs/wx_win1.py000066400000000000000000000015601342344706700216260ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import wx from pubsub import pub class View(wx.Frame): def __init__(self, parent=None): wx.Frame.__init__(self, parent, -1, "Main View") sizer = wx.BoxSizer(wx.VERTICAL) text = wx.StaticText(self, -1, "My Money") ctrl = wx.TextCtrl(self, -1, "") sizer.Add(text, 0, wx.EXPAND | wx.ALL) sizer.Add(ctrl, 0, wx.EXPAND | wx.ALL) self.moneyCtrl = ctrl ctrl.SetEditable(False) self.SetSizer(sizer) # subscribe to all "MONEY CHANGED" messages from the Model # to subscribe to ALL messages (topics), omit the second argument below pub.subscribe(self.setMoney, "money_changed") def setMoney(self, money): self.moneyCtrl.SetValue(str(money)) pypubsub-4.0.3/examples/basic_kwargs/wx_win2.py000066400000000000000000000020161342344706700216240ustar00rootroot00000000000000""" Widget from which money can be added or removed from account. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import wx from pubsub import pub class ChangerWidget(wx.Frame): CHANGE = 10 # by how much money changes every time click def __init__(self, parent=None): wx.Frame.__init__(self, parent, -1, "Changer View") sizer = wx.BoxSizer(wx.VERTICAL) self.add = wx.Button(self, -1, "Add Money") self.remove = wx.Button(self, -1, "Remove Money") sizer.Add(self.add, 0, wx.EXPAND | wx.ALL) sizer.Add(self.remove, 0, wx.EXPAND | wx.ALL) self.SetSizer(sizer) self.add.Bind(wx.EVT_BUTTON, self.onAdd) self.remove.Bind(wx.EVT_BUTTON, self.onRemove) def onAdd(self, evt): print('-----') pub.sendMessage("money_changing", amount=self.CHANGE) def onRemove(self, evt): print('-----') pub.sendMessage("money_changing", amount=- self.CHANGE) pypubsub-4.0.3/examples/multithreadloop.py000066400000000000000000000077511342344706700210170ustar00rootroot00000000000000""" This test gives an example of how some computation results from an auxiliary thread could be 'published' via pubsub in a thread-safe manner, in a 'gui'-like application, ie an application where the main thread is in an infinite event loop and supports the callback of user-defined functions when the gui is idle. The worker thread 'work' is to increment a counter as fast as interpreter can handle. Every so often (every resultStep counts), the thread stores the count in a synchronized queue, for later retrieval by the main thread. In parallel to this, the main thread loops forever (or until user interrupts via keyboard), doing some hypothetical work (represented by the sleep(1) call) and calling all registered 'idle' callbacks. The transfer is done by extracting items from the queue and publishing them via pubsub. Oliver Schoenborn May 2009 :copyright: Copyright 2008-2009 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from queue import Queue import time import threading import sys from pubsub import pub __author__ = "schoenb" __date__ = "$31-May-2009 9:11:41 PM$" resultStep = 1000000 # how many counts for thread "result" to be available def threadObserver(transfers, threadObj, count): """Listener that listens for data from testTopic. This function doesn't know where the data comes from (or in what thread it was generated... but threadObj is the thread in which this threadObserver is called and should indicate Main thread).""" print(transfers, threadObj, count / resultStep) pub.subscribe(threadObserver, 'testTopic') def onIdle(): """This should be registered with 'gui' to be called when gui is idle so we get a chance to transfer data from aux thread without blocking the gui. Ie this function must spend as little time as possible so 'gui' remains reponsive.""" thread.transferData() class ParaFunction(threading.Thread): """ Represent a function running in a parallel thread. The thread just increments a counter and puts the counter value on a synchronized queue every resultStep counts. The content of the queue can be published by calling transferData(). """ def __init__(self): threading.Thread.__init__(self) self.running = False # set to True when thread should stop self.count = 0 # our workload: keep counting! self.queue = Queue() # to transfer data to main thread self.transfer = 0 # count how many transfers occurred def run(self): print('aux thread started') self.running = True while self.running: self.count += 1 if self.count % resultStep == 0: self.queue.put(self.count) print('aux thread done') def stop(self): self.running = False def transferData(self): """Send data from aux thread to main thread. The data was put in self.queue by the aux thread, and this queue is a Queue.Queue which is a synchronized queue for inter-thread communication. Note: This method must be called from main thread.""" self.transfer += 1 while not self.queue.empty(): pub.sendMessage('testTopic', transfers=self.transfer, threadObj=threading.currentThread(), count=self.queue.get()) thread = ParaFunction() def main(): idleFns = [] # list of functions to call when 'gui' idle idleFns.append(onIdle) try: thread.start() print('starting event loop') eventLoop = True while eventLoop: time.sleep(1) # pretend that main thread does other stuff for idleFn in idleFns: idleFn() except KeyboardInterrupt: print('Main interrupted, stopping aux thread') thread.stop() except Exception as exc: exc = sys.exc_info()[1] print(exc) print('Exception, stopping aux thread') thread.stop() main() pypubsub-4.0.3/examples/runall.bat000066400000000000000000000020111342344706700171760ustar00rootroot00000000000000echo off rem This script runs all examples. This should be mostly for using the rem examples as regression tests (after all tests have passed in tests rem folder). rem One command line argument is required, the python version number to rem use, no dots: 24 for 2.4, 30 for 3.0, etc. rem rem (C) Oliver Schoenborn 2009 set PY_VER=%1 IF "%1" EQU "" ( SET PY_VER=26 echo Will use Python 2.6. To use other, put version ID as command line arg echo Example: for Python 2.7 put 27, for 3.0 put 30, etc. ) set PYTHON_EXE=python echo python exe is %PYTHON_EXE% echo. echo. echo ######################## basic - kwargs - console ######################### echo. pushd basic_kwargs %PYTHON_EXE% console_main.py popd pause echo. echo. echo ######################## advanced - kwargs - console ######################### echo. pushd advanced %PYTHON_EXE% main_kwargs.py popd pause echo. echo. echo ######################## basic - kwargs - wx ######################### echo. pushd basic_kwargs %PYTHON_EXE% wx_main.py popd pause pypubsub-4.0.3/examples/runall_regression.txt000066400000000000000000000200551342344706700215170ustar00rootroot00000000000000Will use Python 2.6. To use other, put version ID as command line arg Example: for Python 2.7 put 27, for 3.0 put 30, etc. python exe is python ######################## basic - kwargs - console ######################### Using "kwargs" messaging protocol of pubsub v3 --- SENDING topic1.subtopic11 message --- Method Listener.onTopic11 received: 'message for 11' 123 Function listenerFn received: 'message for 11' 123 Method Listener.onTopic1 received "topic1.subtopic11" message: 'message for 11' Listener instance received: {'msg': 'message for 11', 'extra': 123} ---- SENT topic1.subtopic11 message ---- --- SENDING topic1 message --- Method Listener.onTopic1 received "topic1" message: 'message for 1' Listener instance received: {'msg': 'message for 1'} ---- SENT topic1 message ---- Press any key to continue . . . ######################## basic - kwargs - wx ######################### pubsub version 3.1.2.201112.r243 PUBSUB: New topic "money_changed" created PUBSUB: Subscribed listener "View.setMoney" to topic "money_changed" PUBSUB: New topic "money_changing" created PUBSUB: Subscribed listener "Controller.changeMoney" to topic "money_changing" ---- Starting main event loop ---- ----- PUBSUB: Start sending message of topic "money_changing" PUBSUB: Sending message of topic "money_changing" to listener Controller.changeMoney PUBSUB: Start sending message of topic "money_changed" PUBSUB: Sending message of topic "money_changed" to listener View.setMoney PUBSUB: Done sending message of topic "money_changed" PUBSUB: Done sending message of topic "money_changing" ----- PUBSUB: Start sending message of topic "money_changing" PUBSUB: Sending message of topic "money_changing" to listener Controller.changeMoney PUBSUB: Start sending message of topic "money_changed" PUBSUB: Sending message of topic "money_changed" to listener View.setMoney PUBSUB: Done sending message of topic "money_changed" PUBSUB: Done sending message of topic "money_changing" ---- Exited main event loop ---- Press any key to continue . . . ######################## basic - arg1 - console ######################### Using "arg1" messaging protocol of pubsub v3 --- SENDING topic1.subtopic11 message --- Function listenerFn received: 'message for 11' 123 Method Listener.onTopic11 received: 'message for 11' 123 Method Listener.onTopic1 received "topic1.subtopic11" message: 'message for 11' Listener instance received: ---- SENT topic1.subtopic11 message ---- --- SENDING topic1 message --- Method Listener.onTopic1 received "topic1" message: 'message for 1' Listener instance received: ---- SENT topic1 message ---- Press any key to continue . . . ######################## basic - arg1 - wx ######################### Press any key to continue . . . ######################## advanced - kwargs - console ######################### Using "kwargs" messaging protocol of pubsub v3 ------- init ---------- NotifyByWriteFile: New topic "topic_2" created NotifyByWriteFile: New topic "topic_2.subtopic_21" created NotifyByWriteFile: New topic "topic_1" created NotifyByWriteFile: New topic "topic_1.subtopic_11" created MyPubsubNotifHandler: listener Listener_8304 subscribed to ALL_TOPICS NotifyByWriteFile: Subscribed listener "Listener" to topic "ALL_TOPICS" MyPubsubNotifHandler: listener listenerFn_2464 subscribed to topic_1.subtopic_11 NotifyByWriteFile: Subscribed listener "listenerFn" to topic "topic_1.subtopic_11" MyPubsubNotifHandler: listener Listener.onTopic11_8736 subscribed to topic_1.subtopic_11 NotifyByWriteFile: Subscribed listener "Listener.onTopic11" to topic "topic_1.subtopic_11" MyPubsubNotifHandler: listener Listener.onTopic1_8736 subscribed to topic_1 NotifyByWriteFile: Subscribed listener "Listener.onTopic1" to topic "topic_1" ----------------------- NotifyByWriteFile: Start sending message of topic "topic_1.subtopic_11" NotifyByWriteFile: Sending message of topic "topic_1.subtopic_11" to listener Listener.onTopic11 Method Listener.onTopic11 received: 'message for subtopic 11' 'other message' 123 NotifyByWriteFile: Sending message of topic "topic_1.subtopic_11" to listener listenerFn Function listenerFn received: 'message for subtopic 11' 'other message' 123 NotifyByWriteFile: Sending message of topic "topic_1" to listener Listener.onTopic1 Method Listener.onTopic1 received "topic_1.subtopic_11" message: 'message for subtopic 11' NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: {'msg': 'message for subtopic 11', 'extra': 123, 'msg2': 'other message'} NotifyByWriteFile: Done sending message of topic "topic_1.subtopic_11" NotifyByWriteFile: Start sending message of topic "topic_1" NotifyByWriteFile: Sending message of topic "topic_1" to listener Listener.onTopic1 Method Listener.onTopic1 received "topic_1" message: 'message for topic 1' NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: {'msg': 'message for topic 1'} NotifyByWriteFile: Done sending message of topic "topic_1" NotifyByWriteFile: Start sending message of topic "topic_2.subtopic_21" NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: {'msg': 'message for subtopic 2'} NotifyByWriteFile: Done sending message of topic "topic_2.subtopic_21" ------- done ---------- Exporting topic tree to kwargs_topics ------ exiting -------- Press any key to continue . . . ######################## advanced - arg1 - console ######################### Using "arg1" messaging protocol of pubsub v3 ------- init ---------- NotifyByWriteFile: New topic "topic_2" created NotifyByWriteFile: New topic "topic_2.subtopic_21" created NotifyByWriteFile: New topic "topic_1" created NotifyByWriteFile: New topic "topic_1.subtopic_11" created MyPubsubNotifHandler: listener Listener_2608 subscribed to ALL_TOPICS NotifyByWriteFile: Subscribed listener "Listener" to topic "ALL_TOPICS" MyPubsubNotifHandler: listener listenerFn_1024 subscribed to topic_1.subtopic_11 NotifyByWriteFile: Subscribed listener "listenerFn" to topic "topic_1.subtopic_11" MyPubsubNotifHandler: listener Listener.onTopic11_8808 subscribed to topic_1.subtopic_11 NotifyByWriteFile: Subscribed listener "Listener.onTopic11" to topic "topic_1.subtopic_11" MyPubsubNotifHandler: listener Listener.onTopic1_8808 subscribed to topic_1 NotifyByWriteFile: Subscribed listener "Listener.onTopic1" to topic "topic_1" ----------------------- NotifyByWriteFile: Start sending message of topic "topic_1.subtopic_11" NotifyByWriteFile: Sending message of topic "topic_1.subtopic_11" to listener Listener.onTopic11 Method Listener.onTopic11 received: ('message for subtopic 11', 'other message', 123) NotifyByWriteFile: Sending message of topic "topic_1.subtopic_11" to listener listenerFn Function listenerFn received: ('message for subtopic 11', 'other message', 123) NotifyByWriteFile: Sending message of topic "topic_1" to listener Listener.onTopic1 Method Listener.onTopic1 received "topic_1.subtopic_11" message: ('message for subtopic 11', 'other message', 123) NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: ('message for subtopic 11', 'other message', 123) NotifyByWriteFile: Done sending message of topic "topic_1.subtopic_11" NotifyByWriteFile: Start sending message of topic "topic_1" NotifyByWriteFile: Sending message of topic "topic_1" to listener Listener.onTopic1 Method Listener.onTopic1 received "topic_1" message: 'message for topic 1' NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: message for topic 1 NotifyByWriteFile: Done sending message of topic "topic_1" NotifyByWriteFile: Start sending message of topic "topic_2.subtopic_21" NotifyByWriteFile: Sending message of topic "ALL_TOPICS" to listener Listener Listener instance received: message for subtopic 2 NotifyByWriteFile: Done sending message of topic "topic_2.subtopic_21" ------- done ---------- Exporting topic tree to arg1_topics ------ exiting -------- Press any key to continue . . . pypubsub-4.0.3/release.bat000066400000000000000000000015721342344706700155160ustar00rootroot00000000000000echo off REM Run this script: it indicates specific steps to follow, REM and generates the distributions. REM REM Oliver, Dec 2016 echo ============================================================== echo Before continuing, consult the instructions in Release echo section of Dev docs. echo ============================================================== echo Creating source distribution: python setup.py sdist echo ============================================================== echo Creating wheel distribution: python setup.py bdist_wheel echo ============================================================== echo To UPLOAD the dist/* distributions to PyPi, press ENTER, echo OTHERWISE, press ctrl-c: pause twine upload dist/* echo ============================================================== echo Upload completed. echo Follow remaining instructions in Release section of Dev docs. pypubsub-4.0.3/setup.py000066400000000000000000000040361342344706700151160ustar00rootroot00000000000000 from setuptools import setup, find_packages def getPackagesToDistribute(): packages = find_packages('src') import sys setupCmd = sys.argv[1] if setupCmd in ['sdist', 'bdist', 'bdist_egg', 'bdist_wininst']: print( '*'*40 ) print( 'Packaging:', packages) print( '*'*40) return packages def getPubsubVersion(): import sys sys.path.insert(0, 'src') import pubsub return pubsub.__version__ def getInstallRequires(): import sys if sys.version_info < (3,5): return ['typing'] return [] setup( name = 'Pypubsub', version = getPubsubVersion(), description = 'Python Publish-Subscribe Package', keywords = "publish subscribe observer pattern signal signals event events message messages messaging dispatch dispatching", author = 'Oliver Schoenborn (aka "schollii")', author_email = 'oliver.schoenborn@gmail.com', url = 'https://github.com/schollii/pypubsub', license = "BSD License", zip_safe = False, packages = getPackagesToDistribute(), package_dir = {'': 'src'}, package_data = {'pubsub': ['LICENSE_BSD_Simple.txt', 'RELEASE_NOTES.txt']}, install_requires=getInstallRequires(), classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules', ], python_requires = ">=3.3, <4", # use the module docs as the long description: long_description = open('README.rst', 'r').read() ) pypubsub-4.0.3/src/000077500000000000000000000000001342344706700141705ustar00rootroot00000000000000pypubsub-4.0.3/src/contrib/000077500000000000000000000000001342344706700156305ustar00rootroot00000000000000pypubsub-4.0.3/src/contrib/netpubsub.py000066400000000000000000000070021342344706700202100ustar00rootroot00000000000000""" Skeleton code (will not load... must be completed): UDP-based net-centric Pubsub extension so that pubsub can be used over a network of pubsub-powered applications. Extending to use TCP wouldn't be too difficult. Usage: each of your applications that uses pubsub would create a suitable AppListener, then call its sendMessage when wanting to send a message, or call processMessages whenever it is ready to process queued messages from other applications. In addition, one of your applications must be the "pubsub server", or the server could live in its own python program. For it, you would create a PubsubAppServer then call its processMessages() in an infinite loop. Oliver Schoenborn """ from socket import socket, timeout from pubsub import pub class Marshaller: ... depends on your strategy / method... ... for instance, could use unpickle... ... or XML, SOAP, your own format, etc... def pack(self, topic, **args): ... pack topic and args into a data string... return data def unpack(self, data): ... unpack data into a topic and args return topic, msgData def getTopic(self, data): ... get topic from data... return topic class AppListener: def __init__(self, server, readPort, maxWait=0.01): self.__server = server self.__marshaller = Marshaller() self.__udpSocket = socket(... server...) self.__udpSocket.bind(... readPort...) self.__udpSocket.settimeout(maxWait) # want limited blocking def subscribe(self, topic): self.__udpSocket.write('subscribe %s' % topic) def processMessages(self): bufsize = 4096 more = True while more: try: data, sender = self.__udpSocket.recvfrom(bufsize) if sender == self.__server: self.__publish(data) except timeout: more = False def __publish(self, data): topic, argsValues = self.__marshaller.unpack(data) pub.sendMessage(topic, **argsValues) def sendMessage(topic, **msgData): packedData = self.__marshaller.pack(msgData) self.server.write(packedData) class PubsubAppServer: SERVER_PORT = 8743 # pick to suit you def __init__(self): self.__listenerApps = {} # will be list of sets by topic self.__marshaller = Marshaller() self.__udpSocket = socket(...) self.__udpSocket.bind(... SERVER_PORT...) def processMessages(self): bufsize = 4096 more = True while more: try: data, sender = self.__udpSocket.recv(bufsize) if data.startswith('subscribe'): self.__register(sender, data) else: self.__dispatch(data) except timeout: more = False def __register(self, sender, data): topic = data.split(' ')[1] # second item self.__listenerApps.setdefault(topic, set()).add(sender) def __dispatch(self, data): topic = self.__marshaller.getTopic(data) # send to all registered apps for that topic; # if no such topic then do nothing: for listenerApp in self.__listenerApps.get(topic, []): self.__udpSocket.write(data) pypubsub-4.0.3/src/contrib/wx_monitor.py000066400000000000000000000667301342344706700204230ustar00rootroot00000000000000#!python # ----------------------------- # Name: PyPubSubMonitor # Purpose: A pubsub activity monitor # # Author: Josh English # # Date: 20-Aug-2008 # Last Rev: 127 (Last revision this code was checked with) # ------------------------------------- # NOTE: The Tool and Mixin components (including the # docstrings) borrow heavily on Robin Dunn's wxInspectionTool ### Next problem: TreeCtrl doesn't send messages... import wx from pubsub import pub import pubsub.utils as utils from pubsub import pubsubconf as conf conf.setNotifierClass(utils.NotifyByPubsubMessage) pub.setNotificationFlags(all=True) ### MonitorTopics ### Three topics for Pypubsub messages to control the monitor class MonitorTopics(utils.TopicTreeDefnSimple): class monitor: """Messages controlling the monitor""" class show: """Shows or hides the monitor""" show = "Boolean value to show (True) or hide (False) the monitor" _required = ('show',) class hide: """Hides the monitor. Same as message ('monitor.show', show=False)""" class toggle: """Toggles the monitor to be shown or hidden""" pub.addTopicDefnProvider(MonitorTopics()) class MonitorTool: """ The MonitorTool is a singleton based on the wx.lib.inspection.InspectionTool. """ __shared_state = {} def __init__(self): self.__dict__ = self.__shared_state if not hasattr(self, 'initialized'): self.initialized = False pub.subscribe(self.Show, 'monitor.show') print "Made Monitor Tool" tobj = pub.getTopic('monitor.show') print tobj def Init(self, app=None): self._frame = None self._app = app if not self._app: self._app = wx.GetApp() self.initialized = True print "Monitor Tool Init" def Show(self, show): print "Monitor Tool Show" if not self.initialized: self.Init() parent = self._app.GetTopWindow() if not self._frame: self._frame = MonitorFrame(parent=parent) self._frame.Show(show) class MonitorMixin(object): """ This class is intended to be used as a mix-in with the wx.App class. When used it will add the ability to popup a MonitorFrame window. The default key sequence to activate the inspector is Ctrl-Alt-M (or Cmd-Alt-M on Mac) but this can be changed via parameters to the `Init` method, or the application can call `ShowInspectionTool` from other event handlers if desired. To use this class simply derive a class from wx.App and MonitorMixin and then call the `Init` method from the app's OnInit. """ def Init(self, alt=True, cmd=True, shift=False, keycode=ord('M')): self.Bind(wx.EVT_KEY_DOWN, self._OnKeyPress) self._alt = alt self._cmd = cmd self._shift = shift self._keycode = keycode MonitorTool().Init(self) print "Mixin Init" def _OnKeyPress(self, evt): if evt.AltDown() == self._alt and \ evt.CmdDown() == self._cmd and \ evt.ShiftDown() == self._shift and \ evt.GetKeyCode() == self._keycode: self.ShowMonitorTool() else: evt.Skip() def ShowMonitorTool(self): print "Mixin Show" MonitorTool().Show(True) class MonitorFrame(wx.Frame): """MonitorFrame This is the main monitor frame. Interaction between varying panels is done through direct method calls to prevent message clutter """ def __init__(self, parent): wx.Frame.__init__(self, parent, size=(700, 600), title="PyPubSubMonitor") panel = wx.Panel(self) splitter = wx.SplitterWindow(panel, style=wx.SP_BORDER) self.NotifyControl = NotifierPanel(panel) self.TopicTree = TopicTreePanel(splitter) self.Log = LogPanel(splitter) self.Entry = LastLogEntryPanel(panel) self.Count = CountPanel(panel) ## redirect = Redirector(self.Log.getText()) self.Logger = MonitorLogger(pub, self.Log) pub.subscribe(self.psUpdate, pub.ALL_TOPICS) pub.subscribe(self.psShow, 'monitor.show') pub.subscribe(self.psHide, 'monitor.hide') pub.subscribe(self.psToggle, 'monitor.toggle') sizer = wx.BoxSizer(wx.VERTICAL) row = wx.BoxSizer(wx.HORIZONTAL) row.Add(self.NotifyControl, 0, wx.EXPAND | wx.ALL, 3) splitter.SetMinimumPaneSize(20) splitter.SetSashGravity(0.5) splitter.SplitVertically(self.TopicTree, self.Log, 0) row.Add(splitter, 1, wx.EXPAND | wx.GROW | wx.ALL, 3) sizer.Add(row, 1, wx.EXPAND | wx.GROW | wx.ALL, 3) sizer.Add(self.Entry, 0, wx.EXPAND | wx.ALL, 3) sizer.Add(self.Count, 0, wx.EXPAND | wx.ALL, 3) panel.SetSizerAndFit(sizer) self.Layout() self.Refresh() self.Bind(wx.EVT_CLOSE, self.OnClose) print "Frame Init" def OnClose(self, evt): """Turn off notifications before closing down """ pub.setNotificationFlags(all=False) evt.Skip() def psShow(self, show): self.Show(bool(show)) def psHide(self): self.Hide() def psToggle(self): self.Show(not self.IsShown()) def psUpdate(self, msgTopic=pub.AUTO_TOPIC): self.Entry.SetMessage(self.Logger.lastMsg.strip()) self.Entry.SetTopic(self.Logger.lastTopic) self.Entry.SetPSTopic(self.Logger.lastLogTopic.getName()) self.Count.count(self.Logger.lastLogTopic.getName()) class NotifierPanel(wx.Panel): """NotifierPanel Seven toggle buttons to control which kinds of messages the monitor will pay attention to. """ def __init__(self, parent): wx.Panel.__init__(self, parent, name="notifier control") box = wx.StaticBox(self, label="Notifiers") allBtn = wx.ToggleButton(self, label="all") allBtn.SetValue(True) allBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnNotifyAll) self.subBtn = wx.ToggleButton(self, label="subscribe") self.subBtn.SetValue(True) self.subBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnSubscribe) self.unsubBtn = wx.ToggleButton(self, label="unsubscribe") self.unsubBtn.SetValue(True) self.unsubBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnUnsubscribe) self.sendBtn = wx.ToggleButton(self, label="sendMessage") self.sendBtn.SetValue(True) self.sendBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnSendMessage) self.newTBtn = wx.ToggleButton(self, label="newTopic") self.newTBtn.SetValue(True) self.newTBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnNewTopic) self.delTBtn = wx.ToggleButton(self, label="delTopic") self.delTBtn.SetValue(True) self.delTBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnDelTopic) self.deadBtn = wx.ToggleButton(self, label="deadListener") self.deadBtn.SetValue(True) self.deadBtn.Bind(wx.EVT_TOGGLEBUTTON, self.OnDead) btnpad = wx.LEFT | wx.RIGHT | wx.EXPAND sizer = wx.StaticBoxSizer(box, wx.VERTICAL) sizer.Add(allBtn, 0, btnpad, 3) sizer.Add(wx.StaticLine(self, wx.HORIZONTAL), 0, wx.ALL | wx.EXPAND, 2) sizer.Add(self.subBtn, 0, btnpad, 3) sizer.Add(self.unsubBtn, 0, btnpad, 3) sizer.Add(self.sendBtn, 0, btnpad, 3) sizer.Add(wx.StaticLine(self, wx.HORIZONTAL), 0, wx.ALL | wx.EXPAND, 2) sizer.Add(self.newTBtn, 0, btnpad, 3) sizer.Add(self.delTBtn, 0, btnpad, 3) sizer.Add(self.deadBtn, 0, btnpad, 3) self.SetSizerAndFit(sizer) self.Layout() def OnNotifyAll(self, evt): val = evt.IsChecked() self.subBtn.SetValue(val) self.unsubBtn.SetValue(val) self.sendBtn.SetValue(val) self.newTBtn.SetValue(val) self.delTBtn.SetValue(val) self.deadBtn.SetValue(val) pub.setNotificationFlags(all=val) def OnSubscribe(self, evt): pub.setNotificationFlags(subscribe=evt.IsChecked()) def OnUnsubscribe(self, evt): pub.setNotificationFlags(unsubscribe=evt.IsChecked()) def OnSendMessage(self, evt): pub.setNotificationFlags(sendMessage=evt.IsChecked()) def OnNewTopic(self, evt): pub.setNotificationFlags(newTopic=evt.IsChecked()) def OnDelTopic(self, evt): pub.setNotificationFlags(delTopic=evt.IsChecked()) def OnDead(self, evt): pub.setNotificationFlags(deadListener=evt.IsChecked()) class TopicTreePanel(wx.Panel): """MonitorTreePanel Contains the topic tree and a few controls, including an instance of TopicDescriptionPanel """ def __init__(self, parent, *args, **kwargs): wx.Panel.__init__(self, parent, *args, **kwargs) box = wx.StaticBox(self, label="Topic Tree") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) label = wx.StaticText(self, label="RightClick on any item in the tree") ### Create a dummy panel self.tdp = wx.Panel(self) tree = TopicTree(self) refreshButton = wx.Button(self, label="Refresh") refreshButton.Bind(wx.EVT_BUTTON, tree._load) sizer.Add(label) sizer.Add(tree, 1, wx.EXPAND) sizer.Add(refreshButton, 0, wx.EXPAND | wx.ALL, 3) sizer.Add(self.tdp, 0, wx.EXPAND) self.SetSizerAndFit(sizer) self.Layout() def SetTopicDescription(self, tObj): """Replace the current topic description with a new one""" tdp = TopicDescriptionPanel(self, tObj) sizer = self.GetSizer() sizer.Detach(self.tdp) self.tdp.Destroy() sizer.Add(tdp, 0, wx.EXPAND | wx.ALL, 3) self.tdp = tdp self.Layout() self.Refresh() class TopicTreeFiller(utils.ITopicTreeTraverser): """Link a pubsub.utils.ITopicTreeTraverser to a wxTreeCtrl Call traverse(topicObj) to fill """ def __init__(self, wxTree=None, startTopic=None): if wxTree is None: raise RunTimeError, "TopicTreeFiller needs a wx.TreeCtrl to work with" self.Tree = wxTree if startTopic is None: self.startTopic = pub.getTopic(pub.ALL_TOPICS) self.root = self.Tree.AddRoot(str(self.startTopic.getName())) self.lastT = self.root self.Topics = [self.root] def _onTopic(self, topicObj): if topicObj is not self.startTopic: top = self.Tree.AppendItem(self.Topics[-1], str(topicObj.getName())) self.lastT = top def _startChildren(self): self.Topics.append(self.lastT) def _endChildren(self): self.Topics.pop() class TopicTree(wx.TreeCtrl): """Subclass of wxTreeCtrl, sits in a TopicTreePanel object Uses direct method calls, even though this is exactly what pubsub is supposed to do. I don't want to create messasge static in the monitor """ def __init__(self, parent): wx.TreeCtrl.__init__(self, parent) self._load() self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelection) self.Bind(wx.EVT_TREE_ITEM_MENU, self.OnRightClick) def SendPubSubMessage(self, evt): tName = self.GetItemText(self.GetSelection()) tObj = pub.getTopic(str(tName)) dlg = SendMessageDialog(self, tObj) dlg.ShowModal() def ShowListeners(self, evt): tName = self.GetItemText(self.GetSelection()) tObj = pub.getTopic(str(tName)) if tObj.hasListeners(): llist = '\n'.join([repr(l.getCallable()) for l in tObj.getListenersIter()]) else: llist = "No listeners" wx.MessageBox(llist, "Topic Listeners", style=wx.OK) def OnRightClick(self, evt): self.SelectItem(evt.GetItem()) if not hasattr(self, 'sendmessageID'): self.sendmessageID = wx.NewId() self.showlistenersID = wx.NewId() self.Bind(wx.EVT_MENU, self.SendPubSubMessage) self.Bind(wx.EVT_MENU, self.ShowListeners) menu = wx.Menu() menu.Append(self.sendmessageID, "Send Message") menu.Append(self.showlistenersID, "Show Listeners") self.PopupMenu(menu) def OnSelection(self, evt): tName = self.GetItemText(self.GetSelection()) tObj = pub.getTopic(str(tName)) self.Parent.SetTopicDescription(tObj) def _load(self, evt=None): self.DeleteAllItems() trv = TopicTreeFiller(self) trv.traverse(pub.getTopic(pub.ALL_TOPICS)) self.Expand(self.GetRootItem()) class SendMessageDialog(wx.Dialog): """SendMessageDialog create a dialog with the topic description panel and a check and send and cancel options. Currently can only check the arguments, and even that does not work (as of Pypubsub rev 126) because topic.checkArgs does not return the boolean value the docs claim it returns. To get OnSendMessage to work, there needs to be a better way to know what the listener expects for each argument. """ def __init__(self, parent, tObj): wx.Dialog.__init__(self, parent, title="Send Message") self.topicObj = tObj s = wx.BoxSizer(wx.VERTICAL) self.tdp = TopicDescriptionPanel(self, tObj, useTextCtrl=True) checkButton = wx.Button(self, label="Check") checkButton.Bind(wx.EVT_BUTTON, self.CheckTopic) sendButton = wx.Button(self, id=wx.ID_OK, label="Send") sendButton.Bind(wx.EVT_BUTTON, self.OnSendMessage) cancelButton = wx.Button(self, wx.ID_CANCEL) s.Add(self.tdp, 1, wx.EXPAND | wx.ALL, 3) row = wx.BoxSizer(wx.HORIZONTAL) row.Add(checkButton, 0) row.Add(sendButton, 0, wx.LEFT, 6) row.Add(cancelButton, 0, wx.LEFT, 6) s.Add(row, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 6) self.SetSizerAndFit(s) self.Layout() def CheckTopic(self, evt): args = {} for key in self.tdp.topicArgs: args[key] = self.tdp.topicArgs[key].GetValue() ## print args ok = self.topicObj.checkArgs(args) ### currently gets None from topic.checkArgs ### topic.checkArgs should return True or False if ok: wx.MessageBox('Arguments check out', 'Check Args') else: wx.MessageBox("Arguments don't check", "Check Args") def OnSendMessage(self, evt): """OnSendMessage This should create the dialog box, """ wx.MessageBox("This feature is not complete. See the doc string", "Not Implemented") class TopicDescriptionPanel(wx.Panel): """TopicDescriptionPanel(parent, tObj) Creates a panel describing the topic object """ def __init__(self, parent, tObj, useTextCtrl=False): wx.Panel.__init__(self, parent, name="topicDescription") box = wx.StaticBox(self, label="Topic Information") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) dtext = "%s [%s]\n(%d listener%s)" % \ (tObj.getName(), tObj.getDescription(), tObj.getNumListeners(), "" if tObj.getNumListeners() == 1 else "s") self.desc = wx.StaticText(self, label=dtext) self.desc.Wrap(self.Parent.GetSize()[0] - 6) sizer.Add(self.desc, 0, wx.EXPAND | wx.ALL, 2) grid = wx.FlexGridSizer(cols=2, vgap=3, hgap=2) grid.AddGrowableCol(1) req, opt, com = tObj.getArgs() dscs = tObj.getArgDescriptions() self.topicArgs = {} for arg in req + opt: lbl = "%s (%s): " % (arg, "required" if arg in req else "optional") grid.Add(wx.StaticText(self, label=lbl), 0, wx.ALIGN_RIGHT) if useTextCtrl: dsc = wx.TextCtrl(self) dsc.SetToolTipString(dscs.get(arg, "none")) self.topicArgs[arg] = dsc else: dsc = wx.StaticText(self, label=dscs.get(arg, "None")) grid.Add(dsc, 1, wx.ALIGN_LEFT | wx.EXPAND) sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 2) self.SetSizerAndFit(sizer) self.Layout() self.Bind(wx.EVT_SIZE, self.OnSize) # ~ print dir(tObj) ### Some code to explore the types of arguments the listener ### expects, strings, booleans, dictionary, etc. ## if tObj.hasListeners(): ## listener = tObj.getListeners()[0] ## print listener, listener.__class__, listener.getCallable() ## print inspect.getargspec(listener.getCallable()) def OnSize(self, evt): self.desc.Wrap(evt.GetSize()[0] - 6) self.Parent.Refresh() evt.Skip() class MonitorLogger: """ OBSOLETE, needs updating. Monitor logger for 'pubsub.*' topics. These topics are used automatically in various pubsub calls; e.g. when a new topic is created, a 'pubsub.newTopic' message is generated. Note that such messages are generated only if pub.setNotificationFlags() was called. Each method of DefaultLogger can be given to pub.subscribe() as a listener. A method's name indicates which 'pubsub' subtopic it can be listening for. E.g. you would subscribe the DefaultLogger.subscribe method to listen for 'pubsub.subscribe' messages: from pubsub import pub from pubsub import pubsubconf from pubsub.utils import DefaultLogger,NotifyByPubsubMessage pubsubconf.setNotifier(NotifyByPubsubMessage) pub.setNotificationFlags(all=True) logger = DefaultLogger(pub) Any number of instances can be created. By default, the __init__ will subscribe instance to all 'pubsub' subtopics. Class can also be derived to override default behavior. """ prefix = 'PUBSUB: ' import sys def __init__(self, publisher=None, out=sys.stdout): """If publisher is not None, then all self's methods are subscribed to the 'pubsub.*' topics by using publisher.subscribe(). """ self._out = out self.lastMsg = '' self.lastTopic = '' self.lastLogTopic = '' self.counts = dict(send=0, sub=0, unsub=0, delt=0, newt=0, dead=0, all=0) if publisher is not None: pub = publisher pub.subscribe(self.subscribe, 'pubsub.subscribe') pub.subscribe(self.newTopic, 'pubsub.newTopic') pub.subscribe(self.delTopic, 'pubsub.delTopic') pub.subscribe(self.unsubscribe, 'pubsub.unsubscribe') pub.subscribe(self.sendMessage, 'pubsub.sendMessage') pub.subscribe(self.deadListener, 'pubsub.deadListener') pub.subscribe(self.all, 'pubsub') def setOut(self, out): self._out = out def all(self, msgTopic=pub.AUTO_TOPIC): self.counts['all'] += 1 self.lastLogTopic = msgTopic def subscribe(self, topic=None, listener=None, didit=None): """Give this to pub.subscribe() as listener of 'pubsub.subscribe' messages.""" if didit: msg = '%sSubscribed listener %s to topic "%s"\n' else: msg = '%sSubscription of %s to topic "%s" redundant\n' msg = msg % (self.prefix, listener, topic.getName()) self._out.write(msg) self.lastTopic = topic.getName() self.lastLogTopic = 'pubsub.subscribe' self.counts['sub'] += 1 self.lastMsg = msg ## print msg def unsubscribe(self, topic=None, listener=None, listenerRaw=None): """Give this to pub.subscribe() as listener of 'pubsub.unsubscribe' mesages. """ msg = '%sUnsubscribed listener %s from topic "%s"\n' msg = msg % (self.prefix, listener, topic.getName()) self._out.write(msg) self.lastTopic = topic.getName() self.lastLogTopic = 'pubsub.unsubscribe' self.lastMsg = msg self.counts['unsub'] += 1 ## print msg def newTopic(self, topic=None, args=None, description=None, required=None): """Give this to pub.subscribe() as listener of 'pubsub.newTopic' messages. Messages are only generated when pub.newTopic is called explicitly in the code, not on automatic topic generation """ msg = '%sNew topic "%s" created (%s)\n' msg = msg % (self.prefix, topic.getName(), description) self._out.write(msg) self.lastTopic = topic.getName() self.lastLogTopic == 'pubsub.newTopic' self.lastMsg = msg self.counts['newt'] += 1 ## print msg def delTopic(self, name=None): """Give this to pub.subscribe() as listener of 'pubsub.delTopic' messages. """ msg = '%sTopic "%s" destroyed\n' msg = msg % (self.prefix, name) self._out.write(msg) self.lastTopic = name self.lastLogTopic = 'pubsub.delTopic' self.lastMsg = msg self.counts['delt'] += 1 ## print msg def sendMessage(self, topic=None, stage=None): """Give this to pub.subscribe() as listener of 'pubsub.sendMessage' messages. """ if stage == 'pre': msg = '%sSending message of topic "%s"\n' msg = msg % (self.prefix, topic.getName()) self._out.write(msg) self.lastTopic = topic.getName() self.lastLogTopic = 'pubsub.sendMessage' self.lastMsg = msg self.counts['send'] += 1 if stage == 'post': msg = "%sSent '%s' message\n" msg = msg % (self.prefix, topic.getName()) self._out.write(msg) ## print msg def deadListener(self, topic=None, listener=None): """Give this to pub.subscribe() as a listener of 'deadListener' messages.""" msg = "%sListener %s for topic '%s' is dead\n" msg = msg % (self.prefix, listener, topic.getName()) self._out.write(msg) self.lastMsg = msg self.lastTopic = topic.getName() self.lastLogTopic = 'pubsub.deadlistener' self.counts['dead'] += 1 ## print msg ##class Redirector: ## """Redirector(wxTextCtrl) ## Simple wrapper to take output from DefaultLogger (which uses a ## 'write' method to a file-like object) to a wx.TextCtrl (which uses AppendText). ## """ ## def __init__(self,LogCtrl): ## self.text = LogCtrl ## print "Redirector Text",self.text ## print "Redirector Text parent", self.text.Parent ## ## def write(self,text): ## try: ## self.text.AppendText(text) ## except: ## print "cannot print:", text class LogPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, name="log") box = wx.StaticBox(self, label="Log") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) self.Text = wx.TextCtrl(self, style=wx.TE_READONLY | wx.TE_DONTWRAP | wx.TE_MULTILINE) sizer.Add(self.Text, 1, wx.EXPAND | wx.GROW | wx.ALL, 2) self.SetSizerAndFit(sizer) self.Layout() def getText(self): return self.Text def write(self, text): ## print "writing" self.Text.AppendText(text) class LastLogEntryPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, name="last log") box = wx.StaticBox(self, label="Last Log Entry") messageLabel = wx.StaticText(self, label="Message:") self.messageText = wx.TextCtrl(self, name="messageText") topicLabel = wx.StaticText(self, label="Topic:") self.topicText = wx.TextCtrl(self, name="topicText") pstopicLabel = wx.StaticText(self, label="psTopic:") self.pstopicText = wx.TextCtrl(self, name="pstopicText") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) row = wx.BoxSizer(wx.HORIZONTAL) row.Add(messageLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL) row.Add(self.messageText, 1, wx.LEFT | wx.EXPAND, 3) sizer.Add(row, 0, wx.EXPAND) row = wx.BoxSizer(wx.HORIZONTAL) row.Add(topicLabel, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL) row.Add(self.topicText, 1, wx.LEFT | wx.EXPAND, 3) row.Add(pstopicLabel, 0, wx.LEFT | wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL, 6) row.Add(self.pstopicText, 1, wx.LEFT | wx.EXPAND, 3) sizer.Add(row, 0, wx.EXPAND | wx.TOP, 3) self.SetSizerAndFit(sizer) self.Layout() def SetMessage(self, text): self.messageText.SetValue(str(text)) def SetTopic(self, text): self.topicText.SetValue(str(text)) def SetPSTopic(self, text): self.pstopicText.SetValue(str(text)) class CountPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, name="counts") box = wx.StaticBox(self, label="Counts") sizer = wx.StaticBoxSizer(box, wx.VERTICAL) grid = wx.GridSizer(cols=7, hgap=2, vgap=2) lbls = ['all', 'subscribe', 'unsub', 'send', 'newt', 'delt', 'dead'] counts = {} for c in lbls: t = wx.StaticText(self, label="0", name="count_%s" % c, style=wx.ALIGN_CENTRE | wx.ST_NO_AUTORESIZE | wx.SIMPLE_BORDER) grid.Add(t, 0, wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND | wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE_HORIZONTAL, 2) for c in lbls: t = wx.StaticText(self, label=c) grid.Add(t, 0, wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE_HORIZONTAL) sizer.Add(grid, 1, wx.EXPAND | wx.GROW) self.SetSizerAndFit(sizer) self.Layout() def count(self, topicname): ## print "counting:",topicname garbage, topicname = topicname.split('.') if topicname == 'sendMessage': topicname = 'send' if topicname == 'unsubscribe': topicname = 'unsub' if topicname == 'newTopic': topicname = 'newt' if topicname == 'delTopic': topicname = 'delt' if topicname == 'deadListener': topicname = 'dead' lbl = self.FindWindowByName("count_%s" % topicname) if lbl: c = int(lbl.GetLabel()) lbl.SetLabel(str(c + 1)) else: print "something has gone wrong with", topicname alllbl = self.FindWindowByName('count_all') alllbl.SetLabel(str(int(alllbl.GetLabel()) + 1)) def test(): """This test opens the MonitorFrame, as well as a frame with a show and hide button """ class showFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, title="Monitor Toggle") sbtn = wx.Button(self, label="Show Frame") sbtn.Bind(wx.EVT_BUTTON, self.SendShowMsg) hbtn = wx.Button(self, label="Hide Frame") hbtn.Bind(wx.EVT_BUTTON, self.SendHideMsg) s = wx.BoxSizer(wx.VERTICAL) s.Add(sbtn, 0, wx.EXPAND) s.Add(hbtn, 0, wx.EXPAND) self.SetSizerAndFit(s) self.Layout() def SendShowMsg(self, evt): print "pressed show button" pub.sendMessage('monitor.show', show=True) def SendHideMsg(self, evt): print "pressed hide button" pub.sendMessage('monitor.hide') class myApp(wx.App, MonitorMixin): def OnInit(self): print "Making App" MonitorMixin.Init(self) show = showFrame(None) self.SetTopWindow(show) show.Show() show.Raise() print "OnInit end" return True a = myApp(False) print "Made app. Starting Mainloop" a.MainLoop() print "Done with main loop" a.Destroy() pub.delTopic('test') if __name__ == '__main__': print "testing" test() pypubsub-4.0.3/src/pubsub/000077500000000000000000000000001342344706700154705ustar00rootroot00000000000000pypubsub-4.0.3/src/pubsub/LICENSE_BSD_Simple.txt000066400000000000000000000024411342344706700213150ustar00rootroot00000000000000Copyright (c) since 2006, Oliver Schoenborn All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pypubsub-4.0.3/src/pubsub/RELEASE_NOTES.txt000066400000000000000000000033261342344706700202650ustar00rootroot00000000000000Main changes in v4.0.3 (compared to 3.3) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * Support Python 3.5 and 3.6 * Distribution via wheel * Abandon support for Python 2.x and easy_install * Abandon support for (long-ago deprecated) arg1 messaging protocol * Added currying of subscribed listener args * Significant speed improvement for message delivery * Use PEP 484 style of annotations throughout * Support listeners with keyword-only arguments Consequences of these changes: - If your app runs in Python 2.x, you cannot upgrade to pypubsub v4. - If your Python 3.x application uses setupkwargs.py, simply remove the import statement from your app. Your app should run without further changes. If your app won't run after doing this, please post on https://groups.google.com/forum/#!forum/pypubsub. - If your Python 3.x application uses setuparg1.py, this means you are using the long-ago deprecated arg1 API for messaging. Before upgrading to v4 pypubsub, you will have to migrate your app/package to use pypubsub's kwargs API. This is most easily done via pypubsub 3.3, which has functions and docs (http://pypubsub.readthedocs.io/en/stable/usage/howtos/index.html) to help with this task. Once you have completed this migration, you should be able to upgrade to pypubsub v4 without further changes. - The delivery order of sendMessage() has been changed. However as described in the documentation since the very early days of Pypubsub 3, you should design your application to not depend on the order of message delivery to listeners. If you did not follow this very important architectural principle, you should fix your application before upgrading. Then the upgrade will be trivial. Oliver Schoenborn January 2019 pypubsub-4.0.3/src/pubsub/__init__.py000066400000000000000000000003731342344706700176040ustar00rootroot00000000000000""" Pubsub package initialization. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ __version__ = "4.0.3" __all__ = [ 'pub', 'utils', '__version__' ] pypubsub-4.0.3/src/pubsub/core/000077500000000000000000000000001342344706700164205ustar00rootroot00000000000000pypubsub-4.0.3/src/pubsub/core/__init__.py000066400000000000000000000021011342344706700205230ustar00rootroot00000000000000""" Core package of pubsub, holding the publisher, listener, and topic object modules. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from .publisher import Publisher from .callables import ( AUTO_TOPIC, ) from .listener import ( getID as getListenerID, ListenerMismatchError, IListenerExcHandler, Listener, ) from .topicobj import ( Topic, SenderUnknownMsgDataError, SenderMissingReqdMsgDataError, MessageDataSpecError, TopicDefnError, ExcHandlerError, ) from .topicmgr import ( TopicManager, TopicDefnError, TopicNameError, ALL_TOPICS, ) from .topicdefnprovider import ( ITopicDefnProvider, TopicDefnProvider, ITopicDefnDeserializer, UnrecognizedSourceFormatError, exportTopicTreeSpec, TOPIC_TREE_FROM_MODULE, TOPIC_TREE_FROM_STRING, TOPIC_TREE_FROM_CLASS, ) from .topictreetraverser import ( TopicTreeTraverser, TreeTraversal, ) from .notificationmgr import ( INotificationHandler, ) pypubsub-4.0.3/src/pubsub/core/annotations.py000066400000000000000000000021731342344706700213320ustar00rootroot00000000000000""" Miscellaneous utility items :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import Any def annotationType(sig_obj: Any): """ Use to forward declare an annotation type, i.e. a type that will be fully defined later but is not available at the annotation location in the source code. The following example shows a MyType class with a method that accepts a MyType instance; annotating this cannot be done in Python:: class MyType: def copy_from(other: MyType): # interpreter will croak ... The recommended approach is to use a string annotation, but this is rather unnatural in Python code:: class MyType: def copy_from(other: 'MyType'): ... The annotationType function allows a more pythonic syntax: @annotationType class MyType: pass class MyType: def copy_from(other: MyType): ... This decorator doesn't actually do anything to its argument. """ return sig_obj pypubsub-4.0.3/src/pubsub/core/callables.py000066400000000000000000000170151342344706700207200ustar00rootroot00000000000000""" Low level functions and classes related to callables. The AUTO_TOPIC is the "marker" to use in callables to indicate that when a message is sent to those callables, the topic object for that message should be added to the data sent via the call arguments. See the docs in CallArgsInfo regarding its autoTopicArgName data member. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from inspect import ismethod, isfunction, signature, Parameter import sys from types import ModuleType from typing import Tuple, List, Sequence, Callable, Any # Opaque constant used to mark a kwarg of a listener as one to which pubsub should assign the topic of the # message being sent to the listener. This constant should be used by reference; its value is "unique" such that # pubsub can find such kwarg. class AUTO_TOPIC: pass # In the user domain, a listener is any callable, regardless of signature. The return value is ignored, # i.e. the listener will be treated as though it is a Callable[..., None]. Also, the args, "...", must be # consistent with the MDS of the topic to which listener is being subscribed. UserListener = Callable[..., Any] def getModule(obj: Any) -> ModuleType: """ Get the module in which an object was defined. :param obj: the object for which to get module :return: the module object, or the string '__main__' if no module defined for obj (which usually indicates either a builtin, or a definition within main script). """ if hasattr(obj, '__module__'): module = obj.__module__ else: module = '__main__' return module def getID(callable_obj: UserListener) -> Tuple[str, ModuleType]: """ Get "ID" of a callable, in the form of its name and module in which it is defined E.g. getID(Foo.bar) returns ('Foo.bar', 'a.b') if Foo.bar was defined in module a.b. :param callable_obj: a callable, ie function, bound method or callable instance """ sc = callable_obj if ismethod(sc): module = getModule(sc.__self__) obj_name = '%s.%s' % (sc.__self__.__class__.__name__, sc.__func__.__name__) elif isfunction(sc): module = getModule(sc) obj_name = sc.__name__ else: # must be a functor (instance of a class that has __call__ method) module = getModule(sc) obj_name = sc.__class__.__name__ return obj_name, module def getRawFunction(callable_obj: UserListener) -> Tuple[Callable]: """ Get raw function information about a callable. :param callable_obj: any object that can be called :return: function corresponding to callable, and offset is 0 or 1 to indicate whether the function's first argument is 'self' (1) or not (0) :raise ValueError: if callable_obj is not of a recognized type (function, method or object with __call__ method). """ firstArg = 0 if isfunction(callable_obj): # print 'Function', getID(callable_obj) func = callable_obj elif ismethod(callable_obj): # print 'Method', getID(callable_obj) func = callable_obj elif hasattr(callable_obj, '__call__'): # print 'Functor', getID(callable_obj) func = callable_obj.__call__ else: msg = 'type "%s" not supported' % type(callable_obj).__name__ raise ValueError(msg) return func class ListenerMismatchError(ValueError): """ Raised when an attempt is made to subscribe a listener to a topic, but listener does not satisfy the topic's message data specification (MDS). This specification is inferred from the first listener subscribed to a topic, or from an imported topic tree specification (see pub.addTopicDefnProvider()). """ def __init__(self, msg: str, listener: UserListener, *args): idStr, module = getID(listener) msg = 'Listener "%s" (from module "%s") inadequate: %s' % (idStr, module, msg) ValueError.__init__(self, msg) self.args = args self.msg = msg self.module = module self.idStr = idStr def __str__(self): return self.msg class CallArgsInfo: """ Represent the "signature" of a listener of topic messages: which arguments are required vs optional. """ def __init__(self, func: UserListener, ignoreArgs: Sequence[str] = ()): """ :param func: the callable for which to get paramaters info :param ignoreArgs: do not include the given names in the get*Args() return values After construction, - self.acceptsAllKwargs = True if the listener has a **kwargs arg - self.autoTopicArgName will be the name of argument in which to put the Topic object for which pubsub message is sent, or None if auto off. This is identified by a parameter that has a default value of AUTO_TOPIC. For instance, - listener(self, arg1, arg2=AUTO_TOPIC, arg3=None) will have self.allParams = (arg1, arg2, arg3), self.numRequired=1, and self.autoTopicArgName = 'arg2', whereas - listener(self, arg1, arg3=None) will have self.allParams = (arg1, arg3), self.numRequired=1, and self.autoTopicArgName = None. """ requiredArgs = [] optionalArgs = [] self.autoTopicArgName = None self.acceptsAllKwargs = False for argName, param in signature(func).parameters.items(): if argName in ignoreArgs or param.kind == Parameter.VAR_POSITIONAL: continue if param.kind == Parameter.VAR_KEYWORD: self.acceptsAllKwargs = True continue if param.default == Parameter.empty: requiredArgs.append(argName) else: if param.default == AUTO_TOPIC: self.autoTopicArgName = argName else: optionalArgs.append(argName) self.requiredArgs = tuple(requiredArgs) self.optionalArgs = tuple(optionalArgs) self.allParams = self.requiredArgs + self.optionalArgs def getAllArgs(self) -> Tuple[str]: """ Return a tuple of names indicating the complete set of message data (keyword args) that can be given to this listener """ return self.optionalArgs def getOptionalArgs(self) -> Tuple[str]: """ Return a tuple of names indicating which message data (keyword args) are optional when this listener is called. """ return self.optionalArgs def getRequiredArgs(self) -> Tuple[str]: """ Return a tuple of names indicating which message data (keyword args) are required when this listener is called. """ return self.requiredArgs def getArgs(callable_obj: UserListener, ignoreArgs: Sequence[str] = ()) -> CallArgsInfo: """ Get the call parameters of a callable to be used as listener. :param callable_obj: the callable for which to get call parameters :param ignoreArgs: optional list of names of parameters of callable_obj that should not be in the returned object :return: an instance of CallArgsInfo for the given callable_obj :raise ListenerMismatchError: if callable_obj is not a callable, or ignoreArgs has an item that is not a call param of callable """ # figure out what is the actual function object to inspect: try: func = getRawFunction(callable_obj) except ValueError: exc = sys.exc_info()[1] raise ListenerMismatchError(str(exc), callable_obj) return CallArgsInfo(func, ignoreArgs=ignoreArgs) pypubsub-4.0.3/src/pubsub/core/listener.py000066400000000000000000000306441342344706700206260ustar00rootroot00000000000000""" Top-level functionality related to message listeners. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from types import ModuleType from typing import Callable, Mapping, Any, Sequence from .callables import ( getID, getArgs, ListenerMismatchError, CallArgsInfo, AUTO_TOPIC as _AUTO_ARG, UserListener, ) from .weakmethod import getWeakRef, WeakRef from .annotations import annotationType __all__ = [ 'Listener', 'IListenerExcHandler', 'ListenerValidator' ] @annotationType class Topic: pass @annotationType class Listener: """Wrapper of a UserListener""" pass class IListenerExcHandler: """ Interface class base class for any handler given to pub.setListenerExcHandler() Such handler is called whenever a listener raises an exception during a pub.sendMessage(). Example:: from pubsub import pub class MyHandler(pub.IListenerExcHandler): def __call__(self, listenerID, topicObj): ... do something with listenerID ... pub.setListenerExcHandler(MyHandler()) Without an exception handler, the sendMessage() will fail. """ def __call__(self, listenerID: str, topicObj: Topic): raise NotImplementedError('%s must override __call__()' % self.__class__) class Listener: """ Wraps a callable (UserListener) so it can be stored by weak reference and introspected to verify that it adheres to a topic's MDS. A Listener instance has the same hash value as the callable that it wraps. Callables that have 'argName=pub.AUTO_TOPIC' as a kwarg will be given the Topic object for the message sent by sendMessage(). Such a Listener will have wantsTopicObjOnCall() True. Callables that have a '** kargs' argument will receive all message data, not just that for the topic they are subscribed to. Such a listener will have wantsAllMessageData() True. """ AUTO_TOPIC = _AUTO_ARG def __init__(self, callable_obj: UserListener, argsInfo: CallArgsInfo, curriedArgs: Mapping[str, Any] = None, onDead: Callable[[Listener], None] = None): """ Use callable_obj as a listener of topicName. The argsInfo is the return value from a Validator, ie an instance of callables.CallArgsInfo. If given, the onDead will be called with self as parameter, if/when callable_obj gets garbage collected (callable_obj is held only by weak reference). """ # set call policies self.acceptsAllKwargs = argsInfo.acceptsAllKwargs self.curriedArgs = curriedArgs self._autoTopicArgName = argsInfo.autoTopicArgName self._callable = getWeakRef(callable_obj, self.__notifyOnDead) self.__onDead = onDead # save identity now in case callable dies: name, mod = getID(callable_obj) # self.__nameID = name self.__module = mod self.__id = str(id(callable_obj))[-4:] # only last four digits of id self.__hash = hash(callable_obj) def name(self) -> str: """ Return a human readable name for listener, based on the listener's type name and its id (as obtained from id(listener)). If caller just needs name based on type info, specify instance=False. Note that the listener's id() was saved at construction time (since it may get garbage collected at any time) so the return value of name() is not necessarily unique if the callable has died (because id's can be re-used after garbage collection). """ return '%s_%s' % (self.__nameID, self.__id) def typeName(self) -> str: """ Get a type name for the listener. This is a class name or function name, as appropriate. """ return self.__nameID def module(self) -> ModuleType: """ Get the module in which the callable was defined. """ return self.__module def getCallable(self) -> UserListener: """ Get the listener that was given at initialization. Note that this could be None if it has been garbage collected (e.g. if it was created as a wrapper of some other callable, and not stored locally). """ return self._callable() def isDead(self) -> bool: """Return True if this listener died (has been garbage collected)""" return self._callable() is None def wantsTopicObjOnCall(self) -> bool: """True if this listener wants topic object: it has a arg=pub.AUTO_TOPIC""" return self._autoTopicArgName is not None def wantsAllMessageData(self) -> bool: """True if this listener wants all message data: it has a ** kwargs argument""" return self.acceptsAllKwargs def setCurriedArgs(self, **curriedArgs): """ Curry the wrapped listener so it appears to *not* have list(curriedArgs) among its parameters. The curriedArgs key-value pairs will be given to wrapped listener at call time. """ if curriedArgs.keys() != self.curriedArgs.keys(): raise ValueError( "Listener '{}' already subscribed with a different set of pure curried args ({} != {})" .format(self, curriedArgs.keys(), self.curriedArgs.keys())) self.curriedArgs = curriedArgs def _unlinkFromTopic_(self): """Tell self that it is no longer used by a Topic. This allows to break some cyclical references.""" self.__onDead = None def _calledWhenDead(self): raise RuntimeError('BUG: Dead Listener called, still subscribed!') def __notifyOnDead(self, _: WeakRef): """This gets called when listener weak ref has died. Propagate info to Topic.""" notifyDeath = self.__onDead self._unlinkFromTopic_() if notifyDeath is not None: notifyDeath(self) def __eq__(self, rhs: Listener): """ Compare for equality to rhs. This returns true if rhs has our id id(rhs) is same as id(self) or id(callable in self). """ if id(self) == id(rhs): return True c1 = self._callable() try: c2 = rhs._callable() except Exception: # then rhs is not a Listener, compare with c1 return c1 == rhs # both side of == are Listener, but always compare unequal if both dead if c2 is None and c1 is None: return False return c1 == c2 def __ne__(self, rhs: Listener): """Counterpart to __eq__ MUST be defined... equivalent to 'not (self == rhs)'.""" return not self.__eq__(rhs) def __hash__(self): """ Hash is an optimization for dict/set searches, it need not return different numbers for every different object. """ return self.__hash def __str__(self): """String rep is the callable""" return self.__nameID def __call__(self, kwargs: Mapping[str, Any], actualTopic: Topic, allKwargs: Mapping[str, Any] = None): """ Call the listener with **kwargs. Note that it raises RuntimeError if listener is dead. Should always return True (False would require the callable_obj be dead but self hasn't yet been notified of it...). """ if self.acceptsAllKwargs: kwargs = allKwargs or kwargs # if allKwargs is None then use kwargs orig_kwargs = kwargs # combine with curried args; Note: this overrides topic arg if present: if self.curriedArgs: if kwargs: kwargs = kwargs.copy() kwargs.update(self.curriedArgs) else: kwargs = self.curriedArgs if self._autoTopicArgName is not None: if kwargs is orig_kwargs: kwargs = kwargs.copy() kwargs[self._autoTopicArgName] = actualTopic # call: cb = self._callable() if cb is None: self._calledWhenDead() cb(**kwargs) return True class ListenerValidator: """ Validates listeners. It checks whether the listener given to validate() method complies with required and optional arguments specified for topic. Do not accept any required args or *args; accept any **kwarg, and require that the Listener have at least all the kwargs (can have extra) of Topic. """ def __init__(self, topicArgs: Sequence[str], topicKwargs: Sequence[str]): """ :param topicArgs: list of argument names that will be required when sending a message to listener. Hence order of items in topicArgs matters. :param topicKwargs: list of argument names that will be optional, ie given as keyword arguments when sending a message to listener. The list is unordered. """ self._topicArgs = set(topicArgs) self._topicKwargs = set(topicKwargs) def validate(self, listener: UserListener, curriedArgNames: Sequence[str] = None) -> CallArgsInfo: """ Validate that listener (with, optionally, given curried parameters) satisfies the requirements of being a topic listener. :param listener: the callable to validate :param curriedArgNames: the list of parameter names to treat as curried :returns: a CallArgsInfo object containing information about the listener's call arguments, such as whether listener wants topic name (signified by a kwarg value = AUTO_TOPIC in listener signature). :raises ListenerMismatchError: if listener not usable for topic """ paramsInfo = getArgs(listener) self.__validateArgs(listener, paramsInfo, curriedArgNames) return paramsInfo # noinspection PyIncorrectDocstring def isValid(self, listener: UserListener, curriedArgNames: Sequence[str] = None) -> bool: """Same as validate() but returns True/False instead of raising an exception.""" try: self.validate(listener, curriedArgNames=curriedArgNames) return True except ListenerMismatchError: return False def __validateArgs(self, listener: UserListener, paramsInfo: CallArgsInfo, curriedArgNames: Sequence[str]): # accept **kwargs # accept *args # check if listener missing params (only possible if # paramsInfo.acceptsAllKwargs is False) if not paramsInfo.acceptsAllKwargs: allTopicMsgArgs = self._topicArgs | self._topicKwargs allParams = set(paramsInfo.allParams) missingParams = allTopicMsgArgs - allParams if missingParams: msg = 'needs to accept %s more args (%s)' \ % (len(missingParams), ', '.join(missingParams)) raise ListenerMismatchError(msg, listener, missingParams) else: # then can accept that some parameters missing from listener # signature pass if curriedArgNames: unrecognizedCurried = set(curriedArgNames).difference(paramsInfo.allParams) if unrecognizedCurried: msg = 'does not have following args: (%s)' % ', '.join(unrecognizedCurried) raise ListenerMismatchError(msg, listener, unrecognizedCurried) curriedTopicArgs = set(curriedArgNames).intersection(self._topicArgs | self._topicKwargs) if curriedTopicArgs: msg = 'curried args (%s) are topic args, not allowed' % ', '.join(curriedTopicArgs) raise ListenerMismatchError(msg, listener, curriedTopicArgs) # check if there are extra required parameters in listener signature: extraArgs = set(paramsInfo.getRequiredArgs()) - self._topicArgs if extraArgs and curriedArgNames: extraArgs = extraArgs.difference(curriedArgNames) if extraArgs: msg = 'required args (%s) not allowed (could curry them), ' % ','.join(extraArgs) if self._topicArgs: msg += 'topic req\'d args are (%s)' % ', '.join(self._topicArgs) else: msg += 'topic has no required args' # now make sure listener doesn't require params that are optional in TMS: missingDefaultVals = extraArgs.intersection(self._topicKwargs) if missingDefaultVals: msg += ' (params (%s) are req\'d in listener, optional in topic )' % ', '.join(missingDefaultVals) raise ListenerMismatchError(msg, listener, extraArgs) pypubsub-4.0.3/src/pubsub/core/notificationmgr.py000066400000000000000000000175701342344706700222000ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import List, Mapping from .listener import Listener from .topicobj import Topic class INotificationHandler: """ Defines the interface expected by pubsub for pubsub activity notifications. Any instance that supports the same methods, or derives from this class, will work as a notification handler for pubsub events (see pub.addNotificationHandler). """ def notifySubscribe(self, pubListener: Listener, topicObj: Topic, newSub: bool): """ Called when a listener is subscribed to a topic. :param pubListener: the pubsub.core.Listener that wraps subscribed listener. :param topicObj: the pubsub.core.Topic object subscribed to. :param newSub: false if pubListener was already subscribed. """ raise NotImplementedError def notifyUnsubscribe(self, pubListener: Listener, topicObj: Topic): """ Called when a listener is unsubscribed from given topic. :param pubListener: the pubsub.core.Listener that wraps unsubscribed listener. :param topicObj: the pubsub.core.Topic object unsubscribed from. """ raise NotImplementedError def notifyDeadListener(self, pubListener: Listener, topicObj: Topic): """ Called when a listener has been garbage collected. :param pubListener: the pubsub.core.Listener that wraps GC'd listener. :param topicObj: the pubsub.core.Topic object it was subscribed to. """ raise NotImplementedError def notifySend(self, stage: str, topicObj: Topic, pubListener: Listener = None): """ Called multiple times during a sendMessage: once before message sending has started (pre), once for each listener about to be sent the message, and once after all listeners have received the message (post). :param stage: 'pre', 'post', or 'loop'. :param topicObj: the Topic object for the message. :param pubListener: None for pre and post stages; for loop, the listener that is about to be sent the message. """ raise NotImplementedError def notifyNewTopic(self, topicObj: Topic, description: str, required: List[str], argsDocs: Mapping[str, str]): """ Called whenever a new topic is added to the topic tree. :param topicObj: the Topic object for the message. :param description: docstring for the topic. :param required: list of message data names (keys in argsDocs) that are required. :param argsDocs: dictionary of all message data names, with the corresponding docstring. """ raise NotImplementedError def notifyDelTopic(self, topicName: str): """ Called whenever a topic is removed from topic tree. :param topicName: name of topic removed. """ raise NotImplementedError class NotificationMgr: """ Manages notifications for tracing pubsub activity. When pubsub takes a certain action such as sending a message or creating a topic, and the notification flag for that activity is True, all registered notification handlers get corresponding method called with information about the activity, such as which listener subscribed to which topic. See INotificationHandler for which method gets called for each activity. If more than one notification handler has been registered, the order in which they are notified is unspecified (do not rely on it). Note that this manager automatically unregisters all handlers when the Python interpreter exits, to help avoid NoneType exceptions during shutdown. This "shutdown" starts when the last line of app "main" has executed; the Python interpreter then starts cleaning up, garbage collecting everything, which could lead to various pubsub notifications -- by then they should be of no interest -- such as dead listeners, etc. """ def __init__(self, notificationHandler: INotificationHandler = None): self.__notifyOnSend = False self.__notifyOnSubscribe = False self.__notifyOnUnsubscribe = False self.__notifyOnNewTopic = False self.__notifyOnDelTopic = False self.__notifyOnDeadListener = False self.__handlers = [] if notificationHandler is not None: self.addHandler(notificationHandler) self.__atExitRegistered = False def addHandler(self, handler: INotificationHandler): if not self.__atExitRegistered: self.__registerForAppExit() self.__handlers.append(handler) def getHandlers(self) -> List[INotificationHandler]: return self.__handlers[:] def clearHandlers(self): self.__handlers = [] def notifySubscribe(self, *args, **kwargs): if self.__notifyOnSubscribe and self.__handlers: for handler in self.__handlers: handler.notifySubscribe(*args, **kwargs) def notifyUnsubscribe(self, *args, **kwargs): if self.__notifyOnUnsubscribe and self.__handlers: for handler in self.__handlers: handler.notifyUnsubscribe(*args, **kwargs) def notifySend(self, *args, **kwargs): if self.__notifyOnSend and self.__handlers: for handler in self.__handlers: handler.notifySend(*args, **kwargs) def notifyNewTopic(self, *args, **kwargs): if self.__notifyOnNewTopic and self.__handlers: for handler in self.__handlers: handler.notifyNewTopic(*args, **kwargs) def notifyDelTopic(self, *args, **kwargs): if self.__notifyOnDelTopic and self.__handlers: for handler in self.__handlers: handler.notifyDelTopic(*args, **kwargs) def notifyDeadListener(self, *args, **kwargs): if self.__notifyOnDeadListener and self.__handlers: for handler in self.__handlers: handler.notifyDeadListener(*args, **kwargs) def getFlagStates(self) -> Mapping[str, bool]: """Return state of each notification flag, as a dict.""" return dict( subscribe=self.__notifyOnSubscribe, unsubscribe=self.__notifyOnUnsubscribe, deadListener=self.__notifyOnDeadListener, sendMessage=self.__notifyOnSend, newTopic=self.__notifyOnNewTopic, delTopic=self.__notifyOnDelTopic, ) def setFlagStates(self, subscribe: bool = None, unsubscribe: bool = None, deadListener: bool = None, sendMessage: bool = None, newTopic: bool = None, delTopic: bool = None, all: bool = None): """ Set the notification flag on/off for various aspects of pubsub. The kwargs that are None are left at their current value. The 'all', if not None, is set first. E.g. mgr.setFlagStates(all=True, delTopic=False) will toggle all notifications on, but will turn off the 'delTopic' notification. """ if all is not None: # ignore all other arg settings, and set all of them to true: numArgs = 7 # how many args in this method self.setFlagStates(all=None, *((numArgs - 1) * [all])) if sendMessage is not None: self.__notifyOnSend = sendMessage if subscribe is not None: self.__notifyOnSubscribe = subscribe if unsubscribe is not None: self.__notifyOnUnsubscribe = unsubscribe if newTopic is not None: self.__notifyOnNewTopic = newTopic if delTopic is not None: self.__notifyOnDelTopic = delTopic if deadListener is not None: self.__notifyOnDeadListener = deadListener def __registerForAppExit(self): import atexit atexit.register(self.clearHandlers) self.__atExitRegistered = True pypubsub-4.0.3/src/pubsub/core/publisher.py000066400000000000000000000224361342344706700207760ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union from .topicmgr import ( TopicManager, TreeConfig ) from .listener import IListenerExcHandler, Listener, UserListener from .notificationmgr import INotificationHandler TopicFilter = Callable[[str], bool] ListenerFilter = Callable[[Listener], bool] class Publisher: """ Represent the class that send messages to listeners of given topics and that knows how to subscribe/unsubscribe listeners from topics. """ def __init__(self, treeConfig: TreeConfig = None): """ :param treeConfig: the TreeConfig instance to use; if None, a new one is created """ self.__treeConfig = treeConfig or TreeConfig() self.__topicMgr = TopicManager(self.__treeConfig) def getTopicMgr(self) -> TopicManager: """Get the topic manager created for this publisher.""" return self.__topicMgr def getListenerExcHandler(self) -> IListenerExcHandler: """ Get the listener exception handler that was registered via setListenerExcHandler(), or None of none registered. """ return self.__treeConfig.listenerExcHandler def setListenerExcHandler(self, handler: IListenerExcHandler): """Set the function to call when a listener raises an exception during a sendMessage().""" self.__treeConfig.listenerExcHandler = handler def addNotificationHandler(self, handler: INotificationHandler): """Add a handler for tracing pubsub activity.""" self.__treeConfig.notificationMgr.addHandler(handler) def clearNotificationHandlers(self): """ Remove all notification handlers that were added via self.addNotificationHandler(). """ self.__treeConfig.notificationMgr.clearHandlers() def setNotificationFlags(self, **kwargs: Mapping[str, Optional[bool]]): """ Set the notification flags on or off for each type of pubsub activity. The kwargs keys can be any of the following: - subscribe: if True, get notified whenever a listener subscribes to a topic; - unsubscribe: if True, get notified whenever a listener unsubscribes from a topic; - deadListener: if True, get notified whenever a subscribed listener has been garbage-collected; - sendMessage: if True, get notified whenever sendMessage() is called; - newTopic: if True, get notified whenever a new topic is created; - delTopic: if True, get notified whenever a topic is "deleted" from topic tree; - all: set all of the above to the given value (True or False). The kwargs that are not given or None are left at their current value. Those that are False will cause corresponding notification to be silenced. The 'all' is set first, then the others. E.g. mgr.setFlagStates(all=True, delTopic=False) will toggle all notifications on, but will turn off the 'delTopic' notification. """ self.__treeConfig.notificationMgr.setFlagStates(**kwargs) def getNotificationFlags(self) -> Mapping[str, bool]: """Return a dictionary with the notification flag states.""" return self.__treeConfig.notificationMgr.getFlagStates() def setTopicUnspecifiedFatal(self, newVal: bool = True, checkExisting: bool = True) -> bool: """ Changes the creation policy for topics. By default, pubsub will accept topic names for topics that don't have a message data specification (MDS). This default behavior makes pubsub easier to use initially, but allows topic names with typos to go uncaught in common operations such as sendMessage() and subscribe(). In a large application, this can lead to nasty bugs. Pubsub's default behavior is equivalent to setTopicUnspecifiedFatal(false). When called with newVal=True, any future pubsub operation that requires a topic (such as subscribe and sendMessage) will require an MDS; if none is available, pubsub will raise a TopicDefnError exception. If checkExisting is not given or True, all existing topics are validated. A TopicDefnError exception is raised if one is found to be incomplete (has hasMDS() false). Returns previous value of newVal. Note that this method can be used in several ways: 1. Only use it in your application when something is not working as expected: just add a call at the beginning of your app when you have a problem with topic messages not being received (for instance), and remove it when you have fixed the problem. 2. Use it from the beginning of your app and never use newVal=False: add a call at the beginning of your app and you leave it in (forever), and use Topic Definition Providers to provide the listener specifications. These are easy to use via the pub.addTopicDefnProvider(). 3. Use it as in #1 during app development, and once stable, use #2. This is easiest to do in combination with pub.exportTopicTreeSpec(). """ oldVal = self.__treeConfig.raiseOnTopicUnspecified self.__treeConfig.raiseOnTopicUnspecified = newVal if newVal and checkExisting: self.__topicMgr.checkAllTopicsHaveMDS() return oldVal def subscribe(self, listener: UserListener, topicName: str, **curriedArgs) -> Listener: """ Subscribe listener to named topic. Raises ListenerMismatchError if listener isn't compatible with the topic's MDS. Returns (pubsub.core.Listener, success), where success is False if listener was already subscribed. The pub.core.Listener wraps the callable subscribed and provides introspection-based info about the callable. Extra keyword arguments are treated as currying of listener arguments. Example: pub.subscribe(listener1, 'some_topic') pub.subscribe(listener2, 'some_other_topic', a=2, b=3) In the second example, the listener2 will always receive a=2 and b=3 and pubsub treats it as though a and b were curried, i.e. as if the actual listener subscribed were a callable that did not have a or b parameters. Hence if some_other_topic has a or b as message data, subscription will raise a ListenerInadequate error. Note that if 'subscribe' notification is on, the handler's 'notifySubscribe' method is called after subscription. """ topicObj = self.__topicMgr.getOrCreateTopic(topicName) subscribedListener, success = topicObj.subscribe(listener, **curriedArgs) return subscribedListener, success def unsubscribe(self, listener: UserListener, topicName: str): """ Unsubscribe from given topic. Returns the pubsub.core.Listener instance that was used to wrap listener at subscription time. Raises an TopicNameError if topicName doesn't exist. Note that if 'unsubscribe' notification is on, the handler's notifyUnsubscribe() method will be called after unsubscribing. """ topicObj = self.__topicMgr.getTopic(topicName) unsubdLisnr = topicObj.unsubscribe(listener) return unsubdLisnr def unsubAll(self, topicName: str = None, listenerFilter: ListenerFilter = None, topicFilter: Union[str, TopicFilter] = None) -> List[Listener]: """ Unsubscribe all listeners of a topic. :param topicName: if none given, unsub from all topics. :param listenerFilter: filter function to apply to listeners, unsubscribe only the listeners that satisfy listenerFilter(listener: Listener) == True :param topicFilter: topic name, or a filter function to apply to topics; in latter case, only topics that satisfy topicFilter(topic name) == True will be affected :returns: list of all listeners (instances of pub.Listener) that were unsubscribed from the topic tree Note: this method will generate one 'unsubcribe' notification message (see pub.setNotificationFlags()) for each listener unsubscribed. """ unsubdListeners = [] if topicName is None: # unsubscribe all listeners from all topics topicsMap = self.__topicMgr._topicsMap for topicName, topicObj in topicsMap.items(): if topicFilter is None or topicFilter(topicName): tmp = topicObj.unsubscribeAllListeners(listenerFilter) unsubdListeners.extend(tmp) else: topicObj = self.__topicMgr.getTopic(topicName) unsubdListeners = topicObj.unsubscribeAllListeners(listenerFilter) return unsubdListeners def sendMessage(self, topicName: str, **msgData): """ Send a message. :param topicName: name of message topic (dotted or tuple format) :param msgData: message data (must satisfy the topic's MDS) """ topicMgr = self.getTopicMgr() topicObj = topicMgr.getOrCreateTopic(topicName) topicObj.publish(**msgData) pypubsub-4.0.3/src/pubsub/core/topicargspec.py000066400000000000000000000322741342344706700214650ustar00rootroot00000000000000""" Definitions related to message data specification. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ import weakref from typing import Tuple, List, Sequence as Seq, Mapping, Dict, Callable, Any, Optional, Union from .topicutils import stringize, WeakNone from .annotations import annotationType from .topicexc import MessageDataSpecError from .listener import getArgs as getListenerArgs, UserListener ArgsDocs = Dict[str, str] MsgData = Mapping[str, Any] def verifyArgsDifferent(allArgs, allParentArgs, topicName): """ Verify that allArgs does not contain any of allParentArgs. Raise MessageDataSpecError if fail. """ extra = set(allArgs).intersection(allParentArgs) if extra: msg = 'Args %%s already used in parent of "%s"' % topicName raise MessageDataSpecError(msg, tuple(extra)) def verifySubset(all, sub, topicName, extraMsg=''): """ Verify that sub is a subset of all for topicName. Raise MessageDataSpecError if fail. """ notInAll = set(sub).difference(all) if notInAll: args = ','.join(all) msg = 'Params [%s] missing inherited [%%s] for topic "%s"%s' % (args, topicName, extraMsg) raise MessageDataSpecError(msg, tuple(notInAll)) def topicArgsFromCallable(_callable: UserListener, ignoreArgs: Seq[str] = ()) -> Tuple[ArgsDocs, List[str]]: """ Get the topic message data names and list of those that are required, by introspecting given callable. Returns a pair, (args, required) where args is a dictionary of allowed message data names vs docstring, and required states which ones are required rather than optional. """ argsInfo = getListenerArgs(_callable, ignoreArgs=ignoreArgs) required = argsInfo.getRequiredArgs() defaultDoc = 'UNDOCUMENTED' args = dict.fromkeys(argsInfo.allParams, defaultDoc) return args, required class ArgSpecGiven: """ The message data specification (MDS) for a topic. This consists of each argument name that listener should have in its signature, plus which ones are required in any sendMessage(), and a documentation string for each argument. This instance will be transformed into an ArgsInfo object which is basically a superset of that information, needed to ensure that the arguments specifications satisfy pubsub policies for chosen API version. """ SPEC_GIVEN_NONE = 1 # specification not given SPEC_GIVEN_ALL = 3 # all args specified def __init__(self, argsDocs: ArgsDocs = None, reqdArgs: Seq[str] = None): self.reqdArgs = tuple(reqdArgs or ()) if argsDocs is None: self.argsSpecType = ArgSpecGiven.SPEC_GIVEN_NONE self.argsDocs = {} else: self.argsSpecType = ArgSpecGiven.SPEC_GIVEN_ALL self.argsDocs = argsDocs # check that all args marked as required are in argsDocs missingArgs = set(self.reqdArgs).difference(self.argsDocs.keys()) # py3: iter keys ok if missingArgs: msg = 'Params [%s] missing inherited required args [%%s]' % ','.join(argsDocs.keys()) # iter keys ok raise MessageDataSpecError(msg, missingArgs) def setAll(self, allArgsDocs: ArgsDocs, reqdArgs: Seq[str] = None): self.argsDocs = allArgsDocs self.reqdArgs = reqdArgs or () self.argsSpecType = ArgSpecGiven.SPEC_GIVEN_ALL def isComplete(self) -> bool: """Returns True if the definition is usable, false otherwise.""" return self.argsSpecType == ArgSpecGiven.SPEC_GIVEN_ALL def getOptional(self) -> List[str]: """Get the list of optional arguments""" return tuple(set(self.argsDocs.keys()).difference(self.reqdArgs)) def __str__(self): return "%s, %s, %s" % (self.argsDocs, self.reqdArgs, self.argsSpecType) class SenderMissingReqdMsgDataError(RuntimeError): """ Raised when a sendMessage() is missing arguments tagged as 'required' by pubsub topic of message. """ def __init__(self, topicName: str, argNames: Seq[str], missing: Seq[str]): argsStr = ','.join(argNames) missStr = ','.join(missing) msg = "Some required args missing in call to sendMessage('%s', %s): %s" \ % (stringize(topicName), argsStr, missStr) RuntimeError.__init__(self, msg) class SenderUnknownMsgDataError(RuntimeError): """ Raised when a sendMessage() has arguments not listed among the topic's message data specification (MDS). """ def __init__(self, topicName: str, argNames: Seq[str], extra: Seq[str]): argsStr = ','.join(argNames) extraStr = ','.join(extra) msg = "Some optional args unknown in call to sendMessage('%s', %s): %s" \ % (topicName, argsStr, extraStr) RuntimeError.__init__(self, msg) @annotationType class ArgsInfo: pass class ArgsInfo: """ Encode the Message Data Specification (MDS) for a given topic. ArgsInfos form a tree identical to that of Topics in that ArgInfos have a reference to their parent and children ArgInfos, created for the parent and children topics. The only difference between an ArgsInfo and an ArgSpecGiven is that the latter is what "user thinks is ok" whereas former has been validated: the specification for this topic is a strict superset of the specification of its parent, and a strict subset of the specification of each of its children. Also, the instance can be used to check validity and filter arguments. The MDS can be created "empty", ie "incomplete", meaning it cannot yet be used to validate listener subscriptions to topics. """ SPEC_MISSING = 10 # no args given SPEC_COMPLETE = 12 # all args, but not confirmed via user spec def __init__(self, topicNameTuple: Seq[str], specGiven: ArgSpecGiven, parentArgsInfo: ArgsInfo): self.topicNameTuple = topicNameTuple self.allOptional = () # topic message optional arg names self.allDocs = {} # doc for each arg self.allRequired = () # topic message required arg names self.argsSpecType = self.SPEC_MISSING self.parentAI = WeakNone() if parentArgsInfo is not None: self.parentAI = weakref.ref(parentArgsInfo) parentArgsInfo.__addChildAI(self) self.childrenAI = [] if specGiven.isComplete(): self.__setAllArgs(specGiven) if parentArgsInfo is None: assert self.argsAddedToParent is not None else: while not parentArgsInfo.isComplete(): parentArgsInfo = parentArgsInfo.parentAI() self.argsAddedToParent = set(self.getArgs()).difference(parentArgsInfo.getArgs()) def isComplete(self) -> bool: return self.argsSpecType == self.SPEC_COMPLETE def getArgs(self) -> List[str]: return self.allOptional + self.allRequired def numArgs(self) -> int: return len(self.allOptional) + len(self.allRequired) def getReqdArgs(self) -> List[str]: return self.allRequired def getOptArgs(self) -> List[str]: return self.allOptional def getArgsDocs(self) -> ArgsDocs: return self.allDocs.copy() def setArgsDocs(self, docs: ArgsDocs): """docs is a mapping from arg names to their documentation""" if not self.isComplete(): raise RuntimeError('Topic MDS is not complete, cannot set docs!') for arg, doc in docs.items(): self.allDocs[arg] = doc def check(self, msgData: MsgData): """ Check that the message arguments given satisfy the topic message data specification (MDS). :param msgData: the topic message data to check for validity :raise SenderMissingReqdMsgDataError: if some required args are missing or not known :raise SenderUnknownMsgDataError: if some optional args are unknown. """ all = set(msgData) # check that it has all required args needReqd = set(self.allRequired) hasReqd = (needReqd <= all) if not hasReqd: raise SenderMissingReqdMsgDataError( self.topicNameTuple, list(msgData.keys()), needReqd - all) # check that all other args are among the optional spec optional = all - needReqd ok = (optional <= set(self.allOptional)) if not ok: raise SenderUnknownMsgDataError(self.topicNameTuple, list(msgData.keys()), optional - set(self.allOptional)) def filterArgs(self, msgData: MsgData) -> MsgData: """ Returns a dict which contains only those items of msgData which are defined for topic. E.g. if msgData is {a:1, b:'b'} and topic arg spec is ('a',) then return {a:1}. The returned dict is valid only if check(msgData) was called (or check(superset of msgData) was called). :param msgData: the topic message data to filter """ assert self.isComplete() if len(msgData) == self.numArgs(): return msgData # only keep the keys from msgData that are also in topic's kwargs # method 1: SLOWEST # newKwargs = dict( (k,msgData[k]) for k in self.__msgArgs.allOptional if k in msgData ) # newKwargs.update( (k,msgData[k]) for k in self.__msgArgs.allRequired ) # method 2: FAST: # argNames = self.__msgArgs.getArgs() # newKwargs = dict( (key, val) for (key, val) in msgData.iteritems() if key in argNames ) # method 3: FASTEST: argNames = set(self.getArgs()).intersection(msgData) newKwargs = dict((k, msgData[k]) for k in argNames) return newKwargs def hasSameArgs(self, *argNames: Seq[str]) -> bool: """ Returns true if self has all the message arguments given, no more and no less. Order does not matter. So if getArgs() returns ('arg1', 'arg2') then self.hasSameArgs('arg2', 'arg1') will return true. """ return set(argNames) == set(self.getArgs()) def hasParent(self, argsInfo: ArgsInfo) -> bool: """return True if self has argsInfo object as parent""" return self.parentAI() is argsInfo def getCompleteAI(self) -> ArgsInfo: """ Get the closest arg spec, starting from self and moving to parent, that is complete. So if self.isComplete() is True, then returns self, otherwise returns parent (if parent.isComplete()), etc. """ AI = self while AI is not None: if AI.isComplete(): return AI AI = AI.parentAI() # dereference weakref return None def updateAllArgsFinal(self, topicDefn: ArgSpecGiven): """ This can only be called once, if the construction was done with ArgSpecGiven.SPEC_GIVEN_NONE """ assert not self.isComplete() assert topicDefn.isComplete() self.__setAllArgs(topicDefn) def __addChildAI(self, childAI: ArgsInfo): assert childAI not in self.childrenAI self.childrenAI.append(childAI) def __notifyParentCompleted(self): """Parent should call this when parent ArgsInfo has been completed""" assert self.parentAI().isComplete() if self.isComplete(): # verify that our spec is compatible with parent's self.__validateArgsToParent() self.argsAddedToParent = set(self.getArgs()).difference(self.parentAI().getArgs()) else: for argsInfo in self.childrenAI: argsInfo.__notifyAncestorCompleted(self.parentAI()) def __notifyAncestorCompleted(self, parentAI): if self.isComplete(): # verify that our spec is compatible with parent's self.__validateArgsToParent() self.argsAddedToParent = set(self.getArgs()).difference(parentAI.getArgs()) else: for argsInfo in self.childrenAI: argsInfo.__notifyAncestorCompleted(parentAI) def __validateArgsToParent(self): # validate relative to parent arg spec closestParentAI = self.parentAI().getCompleteAI() if closestParentAI is not None: # verify that parent args is a subset of spec given: topicName = stringize(self.topicNameTuple) verifySubset(self.getArgs(), closestParentAI.getArgs(), topicName) verifySubset(self.allRequired, closestParentAI.getReqdArgs(), topicName, ' required args') def __setAllArgs(self, specGiven: ArgSpecGiven): assert specGiven.isComplete() self.allOptional = tuple(specGiven.getOptional()) self.allRequired = specGiven.reqdArgs self.allDocs = specGiven.argsDocs.copy() # doc for each arg self.argsSpecType = self.SPEC_COMPLETE parentArgsInfo = self.parentAI() if parentArgsInfo is None: self.argsAddedToParent = [] else: self.__validateArgsToParent() while not parentArgsInfo.isComplete(): parentArgsInfo = parentArgsInfo.parentAI() self.argsAddedToParent = set(self.getArgs()).difference(parentArgsInfo.getArgs()) # notify our children for childAI in self.childrenAI: childAI.__notifyParentCompleted() pypubsub-4.0.3/src/pubsub/core/topicdefnprovider.py000066400000000000000000000613221342344706700225240ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ import os, re, inspect, io from textwrap import TextWrapper, dedent import sys from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO try: from importlib.util import cache_from_source except ImportError: from imp import cache_from_source try: # raise ImportError from textwrap import indent except ImportError: def indent(text, prefix): new_text = ''.join(((prefix + line) if line.strip() else '\n') for line in text.splitlines(True)) return new_text from .topicargspec import ( topicArgsFromCallable, ArgSpecGiven, ArgsDocs ) from .topictreetraverser import TopicTreeTraverser from .topicexc import UnrecognizedSourceFormatError from .topicobj import Topic class ITopicDefnProvider: """ All topic definition providers added via pub.addTopicDefnProvider() must have this interface. Derived classes must override the getDefn(), getTreeDoc() and topicNames() methods. """ def getDefn(self, topicNameTuple: Sequence[str]) -> Tuple[str, ArgSpecGiven]: """ Must return a pair (string, ArgSpecGiven) for given topic. The first item is a description for topic, the second item contains the message data specification (MDS). Note topic name is in tuple format ('a', 'b', 'c') rather than 'a.b.c'. """ msg = 'Must return (string, ArgSpecGiven), or (None, None)' raise NotImplementedError(msg) def topicNames(self) -> List[str]: """ Return an iterator over topic names available from this provider. Note that the topic names should be in tuple rather than dotted-string format so as to be compatible with getDefn(). """ msg = 'Must return a list of topic names available from this provider' raise NotImplementedError(msg) def getTreeDoc(self) -> str: """Get the docstring for the topic tree.""" msg = 'Must return documentation string for root topic (tree)' raise NotImplementedError(msg) def __iter__(self): """Same as self.topicNames(), do NOT override.""" return iter(self.topicNames()) # name of method in class name assumed to represent topic's listener signature # which will get checked against topic's Message Data Specification (MDS) SPEC_METHOD_NAME = 'msgDataSpec' class ITopicDefnDeserializer: """ Interface class for all topic definition de-serializers that can be accepted by TopicDefnProvider. A deserializer creates a topic tree from something such as file, module, or string. """ class TopicDefn: """ Encapsulate date for a topic definition. Used by getNextTopic(). """ def __init__(self, nameTuple: Sequence[str], description: str, argsDocs: ArgsDocs, required: Sequence[str]): self.nameTuple = nameTuple self.description = description self.argsDocs = argsDocs self.required = required def isComplete(self): return (self.description is not None) and (self.argsDocs is not None) def getTreeDoc(self) -> str: """Get the docstring for the topic tree.""" raise NotImplementedError def getNextTopic(self) -> TopicDefn: """ Get the next topic definition available from the data. The return must be an instance of TopicDefn. Must return None when no topics are left. """ raise NotImplementedError def doneIter(self): """ Called automatically by TopicDefnProvider once it considers the iteration completed. Override this only if deserializer needs to take action, such as closing a file. """ pass def resetIter(self): """ Called by the TopicDefnProvider if it needs to restart the topic iteration. Override this only if special action needed, such as resetting a file pointer to beginning of file. """ pass class TopicDefnDeserialClass(ITopicDefnDeserializer): """ Convert a nested class tree as a topic definition tree. Format: the class name is the topic name, its doc string is its description. The topic's message data specification is determined by inspecting a class method called the same as SPEC_METHOD_NAME. The doc string of that method is parsed to extract the description for each message data. """ def __init__(self, pyClassObj: type = None): """ If pyClassObj is given, it is an object that contains nested classes defining root topics; the root topics contain nested classes defining subtopics; etc. The class name is the topic name, and the class docstring is the topic documentation string. """ self.__rootTopics = [] self.__iterStarted = False self.__nextTopic = iter(self.__rootTopics) self.__rootDoc = None if pyClassObj is not None: self.__rootDoc = pyClassObj.__doc__ topicClasses = self.__getTopicClasses(pyClassObj) for topicName, pyClassObj in topicClasses: self.__addDefnFromClassObj(pyClassObj) def getTreeDoc(self) -> str: return self.__rootDoc def getNextTopic(self) -> ITopicDefnDeserializer.TopicDefn: self.__iterStarted = True try: topicNameTuple, topicClassObj = next(self.__nextTopic) except StopIteration: return None # ok get the info from class if hasattr(topicClassObj, SPEC_METHOD_NAME): protoListener = getattr(topicClassObj, SPEC_METHOD_NAME) argsDocs, required = topicArgsFromCallable(protoListener) if protoListener.__doc__: self.__setArgsDocsFromProtoDocs(argsDocs, protoListener.__doc__) else: # assume definition is implicitly that listener has no args argsDocs = {} required = () desc = None if topicClassObj.__doc__: desc = dedent(topicClassObj.__doc__) return self.TopicDefn(topicNameTuple, desc, argsDocs, required) def resetIter(self): self.__iterStarted = False self.__nextTopic = iter(self.__rootTopics) def getDefinedTopics(self) -> List[str]: return [nt for (nt, defn) in self.__rootTopics] def __addDefnFromClassObj(self, pyClassObj: type): """ Extract a topic definition from a Python class: topic name, docstring, and MDS, and docstring for each message data. The class name is the topic name, assumed to be a root topic, and descends recursively into nested classes to define subtopic etc. """ if self.__iterStarted: raise RuntimeError('addDefnFromClassObj must be called before iteration started!') parentNameTuple = (pyClassObj.__name__,) if pyClassObj.__doc__ is not None: self.__rootTopics.append((parentNameTuple, pyClassObj)) if self.__rootDoc is None: self.__rootDoc = pyClassObj.__doc__ self.__findTopics(pyClassObj, parentNameTuple) # iterator is now out of sync, so reset it; obviously this would # screw up getNextTopic which is why we had to test for self.__iterStarted self.__nextTopic = iter(self.__rootTopics) def __findTopics(self, pyClassObj: type, parentNameTuple: Sequence[str]): assert not self.__iterStarted assert parentNameTuple assert pyClassObj.__name__ == parentNameTuple[-1] topicClasses = self.__getTopicClasses(pyClassObj, parentNameTuple) pyClassObj._topicNameStr = '.'.join(parentNameTuple) # make sure to update rootTopics BEFORE we recurse, so that toplevel # topics come first in the list for parentNameTuple2, topicClassObj in topicClasses: # we only keep track of topics that are documented, so that # multiple providers can co-exist without having to duplicate # information if topicClassObj.__doc__ is not None: self.__rootTopics.append((parentNameTuple2, topicClassObj)) # now can find its subtopics self.__findTopics(topicClassObj, parentNameTuple2) def __getTopicClasses(self, pyClassObj: type, parentNameTuple: Sequence[str] = ()): """Returns a list of pairs, (topicNameTuple, memberClassObj)""" memberNames = dir(pyClassObj) topicClasses = [] for memberName in memberNames: if memberName.startswith('_'): continue # ignore special and non-public methods member = getattr(pyClassObj, memberName) if inspect.isclass(member): topicNameTuple = parentNameTuple + (memberName,) topicClasses.append((topicNameTuple, member)) return topicClasses def __setArgsDocsFromProtoDocs(self, argsDocs: ArgsDocs, protoDocs: str): PAT_ITEM_STR = r'\A-\s*' # hyphen and any number of blanks PAT_ARG_NAME = r'(?P\w*)' PAT_DOC_STR = r'(?P.*)' PAT_BLANK = r'\s*' PAT_ITEM_SEP = r':' argNamePat = re.compile( PAT_ITEM_STR + PAT_ARG_NAME + PAT_BLANK + PAT_ITEM_SEP + PAT_BLANK + PAT_DOC_STR) protoDocs = dedent(protoDocs) lines = protoDocs.splitlines() argName = None namesFound = [] for line in lines: match = argNamePat.match(line) if match: argName = match.group('argName') namesFound.append(argName) argsDocs[argName] = [match.group('doc1')] elif argName: argsDocs[argName].append(line) for name in namesFound: argsDocs[name] = '\n'.join(argsDocs[name]) class TopicDefnDeserialModule(ITopicDefnDeserializer): """ Deserialize a module containing Python source code defining a topic tree. This loads the module and gives it to an instance of TopicDefnDeserialClass. """ def __init__(self, moduleName: str, searchPath: Sequence[str] = None): """ Load the given named module, searched for in searchPath or, if not specified, in sys.path. Give it to a TopicDefnDeserialClass. """ if searchPath is not None: old_path = sys.path sys.path = searchPath try: from importlib import import_module module = import_module(moduleName) finally: if searchPath is not None: sys.path = old_path self.__classDeserial = TopicDefnDeserialClass(module) def getTreeDoc(self) -> str: return self.__classDeserial.getTreeDoc() def getNextTopic(self) -> ITopicDefnDeserializer.TopicDefn: return self.__classDeserial.getNextTopic() def doneIter(self): self.__classDeserial.doneIter() def resetIter(self): self.__classDeserial.resetIter() def getDefinedTopics(self) -> List[str]: return self.__classDeserial.getDefinedTopics() class TopicDefnDeserialString(ITopicDefnDeserializer): """ Deserialize a string containing Python source code defining a topic tree. The string has the same format as expected by TopicDefnDeserialModule. """ def __init__(self, source: str): """ This just saves the string into a temporary file created in os.getcwd(), and the rest is delegated to TopicDefnDeserialModule. The temporary file (module -- as well as its byte-compiled version) will be deleted when the doneIter() method is called. """ source = "class TopicTree:\n" + indent(dedent(source), ' ' * 4) namespace = {} exec(source, namespace) self.__clsDeserial = TopicDefnDeserialClass(namespace['TopicTree']) def getTreeDoc(self) -> str: return self.__clsDeserial.getTreeDoc() def getNextTopic(self) -> ITopicDefnDeserializer.TopicDefn: return self.__clsDeserial.getNextTopic() def doneIter(self): self.__clsDeserial.doneIter() def resetIter(self): self.__clsDeserial.resetIter() def getDefinedTopics(self) -> List[str]: return self.__clsDeserial.getDefinedTopics() TOPIC_TREE_FROM_MODULE = 'module' TOPIC_TREE_FROM_STRING = 'string' TOPIC_TREE_FROM_CLASS = 'class' class TopicDefnProvider(ITopicDefnProvider): """ Default implementation of the ITopicDefnProvider API. This implementation accepts several formats for the topic tree source data and delegates to a registered ITopicDefnDeserializer that converts source data into topic definitions. This provider is instantiated automatically by ``pub.addTopicDefnProvider(source, format)`` when source is *not* an ITopicDefnProvider. Additional de-serializers can be registered via registerTypeForImport(). """ _typeRegistry = {} def __init__(self, source: Any, format: str, **providerKwargs): """ Find the correct de-serializer class from registry for the given format; instantiate it with given source and providerKwargs; get all available topic definitions. """ if format not in self._typeRegistry: raise UnrecognizedSourceFormatError() providerClassObj = self._typeRegistry[format] provider = providerClassObj(source, **providerKwargs) self.__topicDefns = {} self.__treeDocs = provider.getTreeDoc() try: topicDefn = provider.getNextTopic() while topicDefn is not None: self.__topicDefns[topicDefn.nameTuple] = topicDefn topicDefn = provider.getNextTopic() finally: provider.doneIter() def getDefn(self, topicNameTuple: Sequence[str]) -> Tuple[str, ArgSpecGiven]: desc, spec = None, None defn = self.__topicDefns.get(topicNameTuple, None) if defn is not None: assert defn.isComplete() desc = defn.description spec = ArgSpecGiven(defn.argsDocs, defn.required) return desc, spec def topicNames(self) -> Sequence[str]: return self.__topicDefns.keys() def getTreeDoc(self) -> str: return self.__treeDocs @classmethod def registerTypeForImport(cls, typeName: str, providerClassObj: type): """ If a new type of importer is defined for topic definitions, it can be registered with pubsub by providing a name for the new importer (typeName), and the class to instantiate when pub.addTopicDefnProvider(obj, typeName) is called. For instance, :: from pubsub.core.topicdefnprovider import ITopicDefnDeserializer class SomeNewImporter(ITopicDefnDeserializer): ... TopicDefnProvider.registerTypeForImport('some name', SomeNewImporter) # will instantiate SomeNewImporter(source) pub.addTopicDefnProvider(source, 'some name') """ assert issubclass(providerClassObj, ITopicDefnDeserializer) cls._typeRegistry[typeName] = providerClassObj @classmethod def initTypeRegistry(cls): cls.registerTypeForImport(TOPIC_TREE_FROM_MODULE, TopicDefnDeserialModule) cls.registerTypeForImport(TOPIC_TREE_FROM_STRING, TopicDefnDeserialString) cls.registerTypeForImport(TOPIC_TREE_FROM_CLASS, TopicDefnDeserialClass) TopicDefnProvider.initTypeRegistry() def _backupIfExists(filename: str, bak: str): import shutil from pathlib import Path if Path(filename).exists(): backupName = '%s.%s' % (filename, bak) shutil.copy(filename, backupName) defaultTopicTreeSpecHeader = \ """ Topic tree for application. Used via pub.addTopicDefnProvider(thisModuleName). """ defaultTopicTreeSpecFooter = \ """\ # End of topic tree definition. Note that application may load # more than one definitions provider. """ def exportTopicTreeSpec(moduleName: str = None, rootTopic: Union[Topic, str] = None, bak: str = 'bak', moduleDoc: str = None): """ Using TopicTreeSpecPrinter, exports the topic tree rooted at rootTopic to a Python module (.py) file. This module will define module-level classes representing root topics, nested classes for subtopics etc. Returns a string representing the contents of the file. Parameters: - If moduleName is given, the topic tree is written to moduleName.py in os.getcwd(). By default, it is first backed up, it it already exists, using bak as the filename extension. If bak is None, existing module file gets overwritten. - If rootTopic is specified, the export only traverses tree from corresponding topic. Otherwise, complete tree, using pub.getDefaultTopicTreeRoot() as starting point. - The moduleDoc is the doc string for the module ie topic tree. """ if rootTopic is None: from .. import pub rootTopic = pub.getDefaultTopicMgr().getRootAllTopics() elif isinstance(rootTopic, str): from .. import pub rootTopic = pub.getDefaultTopicMgr().getTopic(rootTopic) # create exporter if moduleName is None: capture = io.StringIO() TopicTreeSpecPrinter(rootTopic, fileObj=capture, treeDoc=moduleDoc) return capture.getvalue() else: filename = '%s.py' % moduleName if bak: _backupIfExists(filename, bak) moduleFile = open(filename, 'w') try: TopicTreeSpecPrinter(rootTopic, fileObj=moduleFile, treeDoc=moduleDoc) finally: moduleFile.close() ############################################################## class TopicTreeSpecPrinter: """ Helper class to print the topic tree using the Python class syntax. The "printout" can be sent to any file object (object that has a write() method). If printed to a module, the module can be imported and given to pub.addTopicDefnProvider(module, 'module'). Importing the module also provides code completion of topic names (rootTopic.subTopic can be given to any pubsub function requiring a topic name). """ INDENT_CH = ' ' # INDENT_CH = '.' def __init__(self, rootTopic: Union[str, Topic] = None, fileObj: TextIO = None, width: int = 70, indentStep: int = 4, treeDoc: str = defaultTopicTreeSpecHeader, footer: str = defaultTopicTreeSpecFooter): """ For formatting, can specify the width of output, the indent step, the header and footer to print to override defaults. The destination is fileObj; if none is given, then sys.stdout is used. If rootTopic is given, calls writeAll(rootTopic) at end of __init__. """ self.__traverser = TopicTreeTraverser(self) import sys fileObj = fileObj or sys.stdout self.__destination = fileObj self.__output = [] self.__header = self.__toDocString(treeDoc) self.__footer = dedent(footer) self.__lastWasAll = False # True when last topic done was the ALL_TOPICS self.__width = width self.__wrapper = TextWrapper(width) self.__indentStep = indentStep self.__indent = 0 args = dict(width=width, indentStep=indentStep, treeDoc=treeDoc, footer=self.__footer, fileObj=fileObj) def fmItem(argName, argVal): if isinstance(argVal, str): MIN_OFFSET = 5 lenAV = width - MIN_OFFSET - len(argName) if lenAV > 0: argVal = repr(argVal[:lenAV] + '...') elif argName == 'fileObj': argVal = fileObj.__class__.__name__ return '# - %s: %s' % (argName, argVal) fmtArgs = [fmItem(key, args[key]) for key in sorted(args.keys())] self.__comment = [ '# Automatically generated by %s(**kwargs).' % self.__class__.__name__, '# The kwargs were:', ] self.__comment.extend(fmtArgs) self.__comment.extend(['']) # two empty line after comment if rootTopic is not None: self.writeAll(rootTopic) def getOutput(self) -> str: """ Each line that was sent to fileObj was saved in a list; returns a string which is ``'\\n'.join(list)``. """ return '\n'.join(self.__output) def writeAll(self, topicObj: Topic): """ Traverse each topic of topic tree, starting at topicObj, printing each topic definition as the tree gets traversed. """ self.__traverser.traverse(topicObj) def _accept(self, topicObj: Topic): # accept every topic return True def _startTraversal(self): # output comment self.__wrapper.initial_indent = '# ' self.__wrapper.subsequent_indent = self.__wrapper.initial_indent self.__output.extend(self.__comment) # output header: if self.__header: self.__output.extend(['']) self.__output.append(self.__header) self.__output.extend(['']) def _doneTraversal(self): if self.__footer: self.__output.append('') self.__output.append('') self.__output.append(self.__footer) if self.__destination is not None: self.__destination.write(self.getOutput()) def _onTopic(self, topicObj: Topic): """This gets called for each topic. Print as per specified content.""" # don't print root of tree, it is the ALL_TOPICS builtin topic if topicObj.isAll(): self.__lastWasAll = True return self.__lastWasAll = False self.__output.append('') # empty line # topic name self.__wrapper.width = self.__width head = 'class %s:' % topicObj.getNodeName() self.__formatItem(head) # each extra content (assume constructor verified that chars are valid) self.__printTopicDescription(topicObj) self.__printTopicArgSpec(topicObj) def _startChildren(self): """Increase the indent""" if not self.__lastWasAll: self.__indent += self.__indentStep def _endChildren(self): """Decrease the indent""" if not self.__lastWasAll: self.__indent -= self.__indentStep def __toDocString(self, msg: str) -> str: if not msg: return msg if msg.startswith("'''") or msg.startswith('"""'): return msg return '"""\n%s\n"""' % msg.strip() def __printTopicDescription(self, topicObj: Topic): if topicObj.getDescription(): extraIndent = self.__indentStep self.__formatItem('"""', extraIndent) self.__formatItem(topicObj.getDescription(), extraIndent) self.__formatItem('"""', extraIndent) def __printTopicArgSpec(self, topicObj: Topic): extraIndent = self.__indentStep # generate the message data specification reqdArgs, optArgs = topicObj.getArgs() argsStr = [] if reqdArgs: argsStr.append(", ".join(reqdArgs)) if optArgs: optStr = ', '.join([('%s=None' % arg) for arg in optArgs]) argsStr.append(optStr) argsStr = ', '.join(argsStr) # print it only if there are args; ie if listener() don't print it if argsStr: # output a blank line and protocol self.__formatItem('\n', extraIndent) protoListener = 'def %s(%s):' % (SPEC_METHOD_NAME, argsStr) self.__formatItem(protoListener, extraIndent) # and finally, the args docs extraIndent += self.__indentStep self.__formatItem('"""', extraIndent) # but ignore the arg keys that are in parent args docs: parentMsgKeys = () if topicObj.getParent() is not None: parentMsgKeys = topicObj.getParent().getArgDescriptions().keys() # keys iter ok argsDocs = topicObj.getArgDescriptions() for key in sorted(argsDocs.keys()): if key not in parentMsgKeys: argDesc = argsDocs[key] msg = "- %s: %s" % (key, argDesc) self.__formatItem(msg, extraIndent) self.__formatItem('"""', extraIndent) def __formatItem(self, item: str, extraIndent: int = 0): indent = extraIndent + self.__indent indentStr = self.INDENT_CH * indent lines = item.splitlines() for line in lines: self.__output.append('%s%s' % (indentStr, line)) def __formatBlock(self, text: str, extraIndent: int = 0): self.__wrapper.initial_indent = self.INDENT_CH * (self.__indent + extraIndent) self.__wrapper.subsequent_indent = self.__wrapper.initial_indent self.__output.append(self.__wrapper.fill(text)) pypubsub-4.0.3/src/pubsub/core/topicexc.py000066400000000000000000000055501342344706700206150ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO from .annotations import annotationType @annotationType class Topic: pass class TopicNameError(ValueError): """ Raised when the topic name is not properly formatted or no corresponding Topic object found. """ def __init__(self, name: str, msg: str): ValueError.__init__(self, 'Topic name "%s": %s' % (name, msg)) class TopicDefnError(RuntimeError): """ Raised when an operation requires a topic have an MDS, but it doesn't. See also pub.setTopicUnspecifiedFatal(). """ def __init__(self, topicNameTuple: Sequence[str]): msg = "No topic specification for topic '%s'." % '.'.join(topicNameTuple) RuntimeError.__init__(self, msg + " See pub.addTopicDefnProvider() and/or pub.setTopicUnspecifiedFatal()") class MessageDataSpecError(RuntimeError): """ Raised when an attempt is made to define a topic's Message Data Specification (MDS) to something that is not valid. The keyword names for invalid data go in the 'args' list, and the msg should state the problem and contain "%s" for the args, such as MessageDataSpecError('duplicate args %s', ('arg1', 'arg2')). """ def __init__(self, msg: str, args: Sequence[str]): argsMsg = msg % ','.join(args) RuntimeError.__init__(self, 'Invalid message data spec: ' + argsMsg) class ExcHandlerError(RuntimeError): """ Raised when a listener exception handler (see pub.setListenerExcHandler()) raises an exception. The original exception is contained. """ def __init__(self, badExcListenerID: str, topicObj: Topic, origExc: Exception = None): """ The badExcListenerID is the name of the listener that raised the original exception that handler was attempting to handle. The topicObj is the Topic object for the topic of the sendMessage that had an exception raised. The origExc is the exception that was raised. """ self.badExcListenerID = badExcListenerID import traceback self.exc = traceback.format_exc() msg = 'The exception handler registered with pubsub raised an ' \ + 'exception, *while* handling an exception raised by listener ' \ + ' "%s" of topic "%s"):\n%s' \ % (self.badExcListenerID, topicObj.getName(), self.exc) RuntimeError.__init__(self, msg) class UnrecognizedSourceFormatError(ValueError): """ Raised when a topic definition provider doesn't recognize the format of source input it was given. """ def __init__(self): ValueError.__init__(self, 'Source format not recognized') pypubsub-4.0.3/src/pubsub/core/topicmgr.py000066400000000000000000000452151342344706700206250ustar00rootroot00000000000000""" Code related to the concept of topic tree and its management: creating and removing topics, getting info about a particular topic, etc. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO from .callables import getID, UserListener from .topicutils import ( ALL_TOPICS, tupleize, stringize, ) from .topicexc import ( TopicNameError, TopicDefnError, ) from .topicargspec import ( ArgSpecGiven, topicArgsFromCallable, ArgsInfo, ) from .topicobj import Topic from .listener import IListenerExcHandler from .topicdefnprovider import ITopicDefnProvider from .notificationmgr import NotificationMgr, INotificationHandler # --------------------------------------------------------- __all__ = [ 'TopicManager', 'TopicNameError', 'TopicDefnError', ] ARGS_SPEC_ALL = ArgSpecGiven.SPEC_GIVEN_ALL ARGS_SPEC_NONE = ArgSpecGiven.SPEC_GIVEN_NONE # --------------------------------------------------------- class TreeConfig: """ Each topic tree has its own topic manager and configuration, such as notification and exception handling. """ def __init__(self, notificationHandler: INotificationHandler = None, listenerExcHandler: IListenerExcHandler = None): self.notificationMgr = NotificationMgr(notificationHandler) self.listenerExcHandler = listenerExcHandler self.raiseOnTopicUnspecified = False class TopicManager: """ Manages the registry of all topics and creation/deletion of topics. Note that any method that accepts a topic name can accept it in the 'dotted' format such as ``'a.b.c.'`` or in tuple format such as ``('a', 'b', 'c')``. Any such method will raise a ValueError if name not valid (empty, invalid characters, etc). """ # Allowed return values for isTopicSpecified() TOPIC_SPEC_NOT_SPECIFIED = 0 # false TOPIC_SPEC_ALREADY_CREATED = 1 # all other values equate to "true" but different reason TOPIC_SPEC_ALREADY_DEFINED = 2 def __init__(self, treeConfig: TreeConfig = None): """ The optional treeConfig is an instance of TreeConfig, used to configure the topic tree such as notification settings, etc. A default config is created if not given. This method should only be called by an instance of Publisher (see Publisher.getTopicManager()). """ self.__allTopics = None # root of topic tree self._topicsMap = {} # registry of all topics self.__treeConfig = treeConfig or TreeConfig() self.__defnProvider = _MasterTopicDefnProvider(self.__treeConfig) # define root of all topics assert self.__allTopics is None argsDocs, reqdArgs = {}, () desc = 'Root of all topics' specGiven = ArgSpecGiven(argsDocs, reqdArgs) self.__allTopics = self.__createTopic((ALL_TOPICS,), desc, specGiven=specGiven) def getRootAllTopics(self) -> Topic: """ Get the topic that is parent of all root (ie top-level) topics, for default TopicManager instance created when this module is imported. Some notes: - "root of all topics" topic satisfies isAll()==True, isRoot()==False, getParent() is None; - all root-level topics satisfy isAll()==False, isRoot()==True, and getParent() is getDefaultTopicTreeRoot(); - all other topics satisfy neither. """ return self.__allTopics def addDefnProvider(self, providerOrSource: Any, format=None) -> ITopicDefnProvider: """ Register a topic definition provider. After this method is called, whenever a topic must be created, the first definition provider that has a definition for the required topic is used to instantiate the topic. If providerOrSource is an instance of ITopicDefnProvider, register it as a provider of topic definitions. Otherwise, register a new instance of TopicDefnProvider(providerOrSource, format). In that case, if format is not given, it defaults to TOPIC_TREE_FROM_MODULE. Either way, returns the instance of ITopicDefnProvider registered. """ if isinstance(providerOrSource, ITopicDefnProvider): provider = providerOrSource else: from .topicdefnprovider import TopicDefnProvider, TOPIC_TREE_FROM_MODULE source = providerOrSource provider = TopicDefnProvider(source, format or TOPIC_TREE_FROM_MODULE) self.__defnProvider.addProvider(provider) return provider def clearDefnProviders(self): """Remove all registered topic definition providers""" self.__defnProvider.clear() def getNumDefnProviders(self) -> int: """Get how many topic definitions providers are registered.""" return self.__defnProvider.getNumProviders() def getTopic(self, name: str, okIfNone: bool = False) -> Topic: """ Get the Topic instance for the given topic name. By default, raises an TopicNameError exception if a topic with given name doesn't exist. If okIfNone=True, returns None instead of raising an exception. """ topicNameDotted = stringize(name) # if not name: # raise TopicNameError(name, 'Empty topic name not allowed') obj = self._topicsMap.get(topicNameDotted, None) if obj is not None: return obj if okIfNone: return None # NOT FOUND! Determine what problem is and raise accordingly: # find the closest parent up chain that does exists: parentObj, subtopicNames = self.__getClosestParent(topicNameDotted) assert subtopicNames subtopicName = subtopicNames[0] if parentObj is self.__allTopics: raise TopicNameError(name, 'Root topic "%s" doesn\'t exist' % subtopicName) msg = 'Topic "%s" doesn\'t have "%s" as subtopic' % (parentObj.getName(), subtopicName) raise TopicNameError(name, msg) def getOrCreateTopic(self, name: str, protoListener: UserListener = None) -> Topic: """ Get the Topic instance for topic of given name, creating it (and any of its missing parent topics) as necessary. Pubsub functions such as subscribe() use this to obtain the Topic object corresponding to a topic name. The name can be in dotted or string format (``'a.b.'`` or ``('a','b')``). This method always attempts to return a "complete" topic, i.e. one with a Message Data Specification (MDS). So if the topic does not have an MDS, it attempts to add it. It first tries to find an MDS from a TopicDefnProvider (see addDefnProvider()). If none is available, it attempts to set it from protoListener, if it has been given. If not, the topic has no MDS. Once a topic's MDS has been set, it is never again changed or accessed by this method. Examples:: # assume no topics exist # but a topic definition provider has been added via # pub.addTopicDefnProvider() and has definition for topics 'a' and 'a.b' # creates topic a and a.b; both will have MDS from the defn provider: t1 = topicMgr.getOrCreateTopic('a.b') t2 = topicMgr.getOrCreateTopic('a.b') assert(t1 is t2) assert(t1.getParent().getName() == 'a') def proto(req1, optarg1=None): pass # creates topic c.d with MDS based on proto; creates c without an MDS # since no proto for it, nor defn provider: t1 = topicMgr.getOrCreateTopic('c.d', proto) The MDS can also be defined via a call to subscribe(listener, topicName), which indirectly calls getOrCreateTopic(topicName, listener). """ obj = self.getTopic(name, okIfNone=True) if obj: # if object is not sendable but a proto listener was given, # update its specification so that it is sendable if (protoListener is not None) and not obj.hasMDS(): allArgsDocs, required = topicArgsFromCallable(protoListener) obj.setMsgArgSpec(allArgsDocs, required) return obj # create missing parents nameTuple = tupleize(name) parentObj = self.__createParentTopics(nameTuple) # now the final topic object, args from listener if provided desc, specGiven = self.__defnProvider.getDefn(nameTuple) # POLICY: protoListener is used only if no definition available if specGiven is None: if protoListener is None: desc = 'UNDOCUMENTED: created without spec' else: allArgsDocs, required = topicArgsFromCallable(protoListener) specGiven = ArgSpecGiven(allArgsDocs, required) desc = 'UNDOCUMENTED: created from protoListener "%s" in module %s' % getID(protoListener) return self.__createTopic(nameTuple, desc, parent=parentObj, specGiven=specGiven) def isTopicInUse(self, name: str) -> bool: """ Determine if topic 'name' is in use. True if a Topic object exists for topic name (i.e. message has already been sent for that topic, or a least one listener subscribed), false otherwise. Note: a topic may be in use but not have a definition (MDS and docstring); or a topic may have a definition, but not be in use. """ return self.getTopic(name, okIfNone=True) is not None def hasTopicDefinition(self, name: str) -> bool: """ Determine if there is a definition avaiable for topic 'name'. Return true if there is, false otherwise. Note: a topic may have a definition without being in use, and vice versa. """ # in already existing Topic object: alreadyCreated = self.getTopic(name, okIfNone=True) if alreadyCreated is not None and alreadyCreated.hasMDS(): return True # from provider? nameTuple = tupleize(name) if self.__defnProvider.isDefined(nameTuple): return True return False def checkAllTopicsHaveMDS(self): """ Check that all topics that have been created for their MDS. Raise a TopicDefnError if one is found that does not have one. """ for topic in self._topicsMap.values(): if not topic.hasMDS(): raise TopicDefnError(topic.getNameTuple()) def delTopic(self, name: str) -> bool: """ Delete the named topic, including all sub-topics. Returns False if topic does not exist; True otherwise. Also unsubscribe any listeners of topic and all subtopics. """ # find from which parent the topic object should be removed dottedName = stringize(name) try: # obj = weakref( self._topicsMap[dottedName] ) obj = self._topicsMap[dottedName] except KeyError: return False # assert obj().getName() == dottedName assert obj.getName() == dottedName # notification must be before deletion in case self.__treeConfig.notificationMgr.notifyDelTopic(dottedName) # obj()._undefineSelf_(self._topicsMap) obj._undefineSelf_(self._topicsMap) # assert obj() is None return True def getTopicsSubscribed(self, listener: UserListener) -> List[Topic]: """ Get the list of Topic objects that have given listener subscribed. Note: the listener can also get messages from any sub-topic of returned list. """ assocTopics = [] for topicObj in self._topicsMap.values(): if topicObj.hasListener(listener): assocTopics.append(topicObj) return assocTopics def clearTree(self): """Remove every topic from the topic tree""" for topic in list(self.__allTopics.subtopics): self.delTopic(topic.name) def __getClosestParent(self, topicNameDotted: str) -> Topic: """ Returns a pair, (closest parent, tuple path from parent). The first item is the closest parent Topic that exists. The second one is the list of topic name elements that have to be created to create the given topic. So if topicNameDotted = A.B.C.D, but only A.B exists (A.B.C and A.B.C.D not created yet), then return is (A.B, ['C','D']). Note that if none of the branch exists (not even A), then return will be [root topic, ['A',B','C','D']). Note also that if A.B.C exists, the return will be (A.B.C, ['D']) regardless of whether A.B.C.D exists. """ subtopicNames = [] headTail = topicNameDotted.rsplit('.', 1) while len(headTail) > 1: parentName = headTail[0] subtopicNames.insert(0, headTail[1]) obj = self._topicsMap.get(parentName, None) if obj is not None: return obj, subtopicNames headTail = parentName.rsplit('.', 1) subtopicNames.insert(0, headTail[0]) return self.__allTopics, subtopicNames def __createParentTopics(self, topicName: str) -> Topic: """ This will find which parents need to be created such that topicName can be created (but doesn't create given topic), and creates them. Returns the parent object. """ assert self.getTopic(topicName, okIfNone=True) is None parentObj, subtopicNames = self.__getClosestParent(stringize(topicName)) # will create subtopics of parentObj one by one from subtopicNames if parentObj is self.__allTopics: nextTopicNameList = [] else: nextTopicNameList = list(parentObj.getNameTuple()) for name in subtopicNames[:-1]: nextTopicNameList.append(name) desc, specGiven = self.__defnProvider.getDefn(tuple(nextTopicNameList)) if desc is None: desc = 'UNDOCUMENTED: created as parent without specification' parentObj = self.__createTopic(tuple(nextTopicNameList), desc, specGiven=specGiven, parent=parentObj) return parentObj def __createTopic(self, nameTuple: Sequence[str], desc: str, specGiven: ArgSpecGiven, parent: Topic = None) -> Topic: """ Actual topic creation step. Adds new Topic instance to topic map, and sends notification message (see ``Publisher.addNotificationMgr()``) regarding topic creation. """ if specGiven is None: specGiven = ArgSpecGiven() parentAI = None if parent: parentAI = parent._getListenerSpec() argsInfo = ArgsInfo(nameTuple, specGiven, parentAI) if (self.__treeConfig.raiseOnTopicUnspecified and not argsInfo.isComplete()): raise TopicDefnError(nameTuple) newTopicObj = Topic(self.__treeConfig, nameTuple, desc, argsInfo, parent=parent) # sanity checks: assert newTopicObj.getName() not in self._topicsMap if parent is self.__allTopics: assert len(newTopicObj.getNameTuple()) == 1 else: assert parent.getNameTuple() == newTopicObj.getNameTuple()[:-1] assert nameTuple == newTopicObj.getNameTuple() # store new object and notify of creation self._topicsMap[newTopicObj.getName()] = newTopicObj self.__treeConfig.notificationMgr.notifyNewTopic( newTopicObj, desc, specGiven.reqdArgs, specGiven.argsDocs) return newTopicObj def validateNameHierarchy(topicTuple: Tuple[Topic, ...]): """ Check that names in topicTuple are valid: no spaces, not empty. Raise ValueError if fails check. E.g. ('',) and ('a',' ') would both fail, but ('a','b') would be ok. """ if not topicTuple: topicName = stringize(topicTuple) errMsg = 'empty topic name' raise TopicNameError(topicName, errMsg) for indx, topic in enumerate(topicTuple): errMsg = None if topic is None: topicName = list(topicTuple) topicName[indx] = 'None' errMsg = 'None at level #%s' elif not topic: topicName = stringize(topicTuple) errMsg = 'empty element at level #%s' elif topic.isspace(): topicName = stringize(topicTuple) errMsg = 'blank element at level #%s' if errMsg: raise TopicNameError(topicName, errMsg % indx) class _MasterTopicDefnProvider: """ Stores a list of topic definition providers. When queried for a topic definition, queries each provider (registered via addProvider()) and returns the first complete definition provided, or (None,None). The providers must follow the ITopicDefnProvider API. """ def __init__(self, treeConfig: TreeConfig): self.__providers = [] self.__treeConfig = treeConfig def addProvider(self, provider): """Add given provider IF not already added. """ assert (isinstance(provider, ITopicDefnProvider)) if provider not in self.__providers: self.__providers.append(provider) def clear(self): """Remove all providers added.""" self.__providers = [] def getNumProviders(self) -> int: """Return how many providers added.""" return len(self.__providers) def getDefn(self, topicNameTuple: Sequence[str]) -> Tuple[str, ArgSpecGiven]: """ Returns a pair (docstring, MDS) for the topic. The first item is a string containing the topic's "docstring", i.e. a description string for the topic, or None if no docstring available for the topic. The second item is None or an instance of ArgSpecGiven specifying the required and optional message data for listeners of this topic. """ desc, defn = None, None for provider in self.__providers: tmpDesc, tmpDefn = provider.getDefn(topicNameTuple) if (tmpDesc is not None) and (tmpDefn is not None): assert tmpDefn.isComplete() desc, defn = tmpDesc, tmpDefn break return desc, defn def isDefined(self, topicNameTuple: Sequence[str]) -> bool: """ Returns True only if a complete definition exists, ie topic has a description and a complete message data specification (MDS). """ desc, defn = self.getDefn(topicNameTuple) if desc is None or defn is None: return False if defn.isComplete(): return True return False pypubsub-4.0.3/src/pubsub/core/topicobj.py000066400000000000000000000520261342344706700206100ustar00rootroot00000000000000""" Provide the Topic class. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from weakref import ref as weakref import sys from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO, MutableMapping, \ Iterator, ValuesView from .listener import ( Listener, ListenerValidator, CallArgsInfo, UserListener, ) from .topicutils import ( ALL_TOPICS, stringize, tupleize, validateName, smartDedent, ) from .topicexc import ( TopicDefnError, TopicNameError, ExcHandlerError, ) from .topicargspec import ( ArgsInfo, ArgSpecGiven, MsgData, ArgsDocs, topicArgsFromCallable, MessageDataSpecError, SenderUnknownMsgDataError, SenderMissingReqdMsgDataError, ) from .annotations import annotationType @annotationType class Topic: pass @annotationType class TreeConfig: pass ListenerFilter = Callable[[Listener], bool] class Topic: """ Represent topics in pubsub. Contains information about a topic, including topic's message data specification (MDS), the list of subscribed listeners, docstring for the topic. It allows Python-like access to subtopics (e.g. A.B is subtopic B of topic A). """ def __init__(self, treeConfig: TreeConfig, nameTuple: Tuple[str, ...], description: str, msgArgsInfo: ArgsInfo, parent: Topic = None): """ Create a topic. Should only be called by TopicManager via its getOrCreateTopic() method (which gets called in several places in pubsub, such as sendMessage, subscribe, and newTopic). :param treeConfig: topic tree configuration settings :param nameTuple: topic name, in tuple format (no dots) :param description: "docstring" for topic :param ArgsInfo msgArgsInfo: object that defines MDS for topic :param parent: parent of topic :raises ValueError: invalid topic name """ if parent is None: if nameTuple != (ALL_TOPICS,): msg = 'Only one topic, named %s, can be root of topic tree' raise ValueError(msg % 'pub.ALL_TOPICS') else: validateName(nameTuple) self.__tupleName = nameTuple self.__handlingUncaughtListenerExc = False self._treeConfig = treeConfig self.__validator = None # Registered listeners were originally kept in a Python list; however # a few methods require lookup of the Listener for the given callable, # which is an O(n) operation. A set() could have been more suitable but # there is no way of retrieving an element from a set without iterating # over the set, again an O(n) operation. A dict() is ok too. Because # Listener.__eq__(callable) returns true if the Listener instance wraps # the given callable, and because Listener.__hash__ produces the hash # value of the wrapped callable, calling dict[callable] on a # dict(Listener -> Listener) mapping will be O(1) in most cases: # the dict will take the callables hash, find the list of Listeners that # have that hash, and then iterate over that inner list to find the # Listener instance which satisfies Listener == callable, and will return # the Listener. self.__listeners = dict() # specification: self.__description = None self.setDescription(description) self.__msgArgs = msgArgsInfo if msgArgsInfo.isComplete(): self.__finalize() else: assert not self._treeConfig.raiseOnTopicUnspecified # now that we know the args are fine, we can link to parent self.__parentTopic = None self.__subTopics = {} if parent is None: assert self.hasMDS() else: self.__parentTopic = weakref(parent) assert self.__msgArgs.parentAI() is parent.__msgArgs parent.__adoptSubtopic(self) def setDescription(self, desc: str): """Set the 'docstring' of topic""" self.__description = desc def getDescription(self) -> str: """Return the 'docstring' of topic""" if self.__description is None: return None return smartDedent(self.__description) def setMsgArgSpec(self, argsDocs: ArgsDocs, required: Sequence[str] = ()): """ Specify the message data for topic messages. :param argsDocs: a dictionary of keyword names (message data name) and data 'docstring'; cannot be None :param required: a list of those keyword names, appearing in argsDocs, which are required (all others are assumed optional) Can only be called if this info has not been already set at construction or in a previous call. :raise RuntimeError: if MDS already set at construction or previous call. """ assert self.__parentTopic is not None # for root of tree, this method never called! if argsDocs is None: raise ValueError('Cannot set listener spec to None') if self.__msgArgs is None or not self.__msgArgs.isComplete(): try: specGiven = ArgSpecGiven(argsDocs, required) self.__msgArgs = ArgsInfo(self.__tupleName, specGiven, self.__parentTopic().__msgArgs) except MessageDataSpecError: # discard the lower part of the stack trace exc = sys.exc_info()[1] raise exc self.__finalize() else: raise RuntimeError('Not allowed to call this: msg spec already set!') def getArgs(self) -> Tuple[Sequence[str], Sequence[str]]: """ Returns a pair (reqdArgs, optArgs) where reqdArgs is tuple of names of required message arguments, optArgs is tuple of names for optional arguments. If topic args not specified yet, returns (None, None). """ sendable = self.__msgArgs.isComplete() assert sendable == self.hasMDS() if sendable: return (self.__msgArgs.allRequired, self.__msgArgs.allOptional) return None, None def getArgDescriptions(self) -> ArgsDocs: """Get a map of keyword names to docstrings: documents each MDS element. """ return self.__msgArgs.getArgsDocs() def setArgDescriptions(self, **docs: ArgsDocs): """Set the docstring for each MDS datum.""" self.__msgArgs.setArgsDocs(docs) def hasMDS(self) -> bool: """Return true if this topic has a message data specification (MDS).""" return self.__validator is not None def filterMsgArgs(self, msgData: MsgData, check: bool = False) -> MsgData: """Get the MDS docstrings for each of the spedified kwargs.""" filteredArgs = self.__msgArgs.filterArgs(msgData) # if no check of args yet, do it now: if check: self.__msgArgs.check(filteredArgs) return filteredArgs def isAll(self) -> bool: """ Returns true if this topic is the 'all topics' topic. All root topics behave as though they are child of that topic. """ return self.__tupleName == (ALL_TOPICS,) def isRoot(self) -> bool: """ Returns true if this is a "root" topic, false otherwise. A root topic is a topic whose name contains no dots and which has pub.ALL_TOPICS as parent. """ parent = self.getParent() if parent: return parent.isAll() assert self.isAll() return False def getName(self) -> str: """Return dotted form of full topic name""" return stringize(self.__tupleName) def getNameTuple(self) -> Tuple[str, ...]: """Return tuple form of full topic name""" return self.__tupleName def getNodeName(self) -> str: """Return the last part of the topic name (has no dots)""" name = self.__tupleName[-1] return name def getParent(self) -> Topic: """ Get Topic object that is parent of self (i.e. self is a subtopic of parent). Return none if self is the "all topics" topic. """ if self.__parentTopic is None: return None return self.__parentTopic() def hasSubtopic(self, name: str = None) -> bool: """ Return true only if name is a subtopic of self. If name not specified, return true only if self has at least one subtopic. """ if name is None: return len(self.__subTopics) > 0 return name in self.__subTopics def getSubtopic(self, relName: Union[str, Tuple[str, ...]]) -> Topic: """ Get the specified subtopic object. The relName can be a valid subtopic name, a dotted-name string, or a tuple. """ if not relName: raise ValueError("getSubtopic() arg can't be empty") topicTuple = tupleize(relName) assert topicTuple topicObj = self for topicName in topicTuple: child = topicObj.__subTopics.get(topicName) if child is None: msg = 'Topic "%s" doesn\'t have "%s" as subtopic' % (topicObj.getName(), topicName) raise TopicNameError(relName, msg) topicObj = child return topicObj def getSubtopics(self) -> ValuesView[Topic]: """Get a list of Topic instances that are subtopics of self.""" return self.__subTopics.values() def getNumListeners(self) -> int: """ Return number of listeners currently subscribed to topic. This is different from number of listeners that will get notified since more general topics up the topic tree may have listeners. """ return len(self.__listeners) def hasListener(self, listener: UserListener) -> bool: """Return true if listener is subscribed to this topic.""" return listener in self.__listeners def hasListeners(self) -> bool: """ Return true if there are any listeners subscribed to this topic, false otherwise. """ return bool(self.__listeners) def getListeners(self) -> List[Listener]: """ Get a copy of list of listeners subscribed to this topic. Safe to iterate over while listeners get un/subscribed from this topics (such as while sending a message). """ return list(self.__listeners.keys()) def getListenersIter(self) -> Iterator[Listener]: """ Get an iterator over listeners subscribed to this topic. Do not use if listeners can be un/subscribed while iterating. """ return self.__listeners.keys() def validate(self, listener: UserListener, curriedArgNames: Sequence[str] = None) -> CallArgsInfo: """ Checks whether listener could be subscribed to this topic: if yes, just returns; if not, raises ListenerMismatchError. Note that method raises TopicDefnError if self not hasMDS(). """ if not self.hasMDS(): raise TopicDefnError(self.__tupleName) return self.__validator.validate(listener, curriedArgNames=curriedArgNames) def isValid(self, listener: UserListener, curriedArgNames: Sequence[str] = None) -> bool: """ Return True only if listener could be subscribed to this topic, otherwise returns False. Note that method raises TopicDefnError if self not hasMDS(). """ if not self.hasMDS(): raise TopicDefnError(self.__tupleName) return self.__validator.isValid(listener, curriedArgNames=curriedArgNames) def subscribe(self, listener: UserListener, **curriedArgs) -> Tuple[Listener, bool]: """ Subscribe listener to this topic. Returns a pair (pub.Listener, success). :param curriedArgs: keyword argument to curry the listener arguments at message time; the listener(args) is treated essentially as ``listener(**(args - curriedArgs))``. If the listener was already subscribed, the pure curried args names (curriendArgs.keys() - _overrides_) must be unchanged. :return: True only if listener was not already subscribed; False if it was already subscribed. """ if listener in self.__listeners: assert self.hasMDS() newSub = False subdLisnr = self.__listeners[listener] # subscribe with different curried args; only ok if the keys are the same! if curriedArgs: subdLisnr.setCurriedArgs(**curriedArgs) else: newSub = True if self.__validator is None: args, reqd = topicArgsFromCallable(listener, ignoreArgs=curriedArgs) self.setMsgArgSpec(args, reqd) assert self.__validator is not None argsInfo = self.__validator.validate(listener, curriedArgNames=curriedArgs) weakListener = Listener( listener, argsInfo, curriedArgs=curriedArgs, onDead=self.__onDeadListener) self.__listeners[weakListener] = weakListener subdLisnr = weakListener # notify of subscription self._treeConfig.notificationMgr.notifySubscribe(subdLisnr, self, newSub) return subdLisnr, newSub def unsubscribe(self, listener: UserListener) -> Listener: """ Unsubscribe the specified listener from this topic. Returns the pub.Listener object associated with the listener that was unsubscribed, or None if the specified listener was not subscribed to this topic. Note that this method calls ``notifyUnsubscribe(listener, self)`` on all registered notification handlers (see pub.addNotificationHandler). """ unsubdLisnr = self.__listeners.pop(listener, None) if unsubdLisnr is None: return None unsubdLisnr._unlinkFromTopic_() assert listener == unsubdLisnr.getCallable() # notify of unsubscription self._treeConfig.notificationMgr.notifyUnsubscribe(unsubdLisnr, self) return unsubdLisnr def unsubscribeAllListeners(self, filter: ListenerFilter = None) -> List[Listener]: """ Clears list of subscribed listeners. If filter is given, it must be a function that takes a listener and returns true if the listener should be unsubscribed. Returns the list of Listener for listeners that were unsubscribed. """ unsubd = [] if filter is None: for listener in self.__listeners: listener._unlinkFromTopic_() unsubd = self.__listeners.keys() self.__listeners = {} else: unsubd = [] for listener in list(self.__listeners): if filter(listener): unsubd.append(listener) listener._unlinkFromTopic_() del self.__listeners[listener] # send notification regarding all listeners actually unsubscribed notificationMgr = self._treeConfig.notificationMgr for unsubdLisnr in unsubd: notificationMgr.notifyUnsubscribe(unsubdLisnr, self) return unsubd def publish(self, **msgData): """ This sends message to listeners of parent topics as well. If an exception is raised in a listener, the publish is aborted, except if there is a handler (see pub.setListenerExcHandler). Note that it is important that the PublisherMixin NOT modify any state data during message sending, because in principle it could happen that a listener causes another message of same topic to be sent (presumably, the listener has a way of preventing infinite loop). """ self._treeConfig.notificationMgr.notifySend('pre', self) # check the message data: if self.__validator is not None: self._getListenerSpec().check(msgData) else: assert not self.hasListeners() # get the list of topics from self to root (ALL_TOPICS) topicStack = [self] parent = self.__parentTopic while parent is not None: parent = parent() topicStack.append(parent) parent = parent.__parentTopic # deref weakref # for each topic, send to listeners: msgDataSubset = {} for topicObj in reversed(topicStack): added_args = topicObj.__msgArgs.argsAddedToParent add_to_parent = {k: msgData[k] for k in added_args if k in msgData} msgDataSubset.update(add_to_parent) if topicObj.hasListeners(): self.__sendMessage(msgData, topicObj, msgDataSubset) self._treeConfig.notificationMgr.notifySend('post', self) name = property(getName) parent = property(getParent) subtopics = property(getSubtopics) description = property(getDescription, setDescription) listeners = property(getListeners) numListeners = property(getNumListeners) args = property(getArgs) argDescriptions = property(getArgDescriptions, setArgDescriptions) ############################################################# # # Impementation # ############################################################# def _getListenerSpec(self) -> ArgsInfo: """Only to be called by pubsub package""" return self.__msgArgs def __sendMessage(self, allData: MsgData, topicObj: Topic, data: MsgData): # now send message data to each listener for current topic; # use list of listeners rather than iterator, so that if listeners added/removed during # send loop, no runtime exception (performance hit is marginal): for listener in topicObj.getListeners(): try: self._treeConfig.notificationMgr.notifySend('in', topicObj, pubListener=listener) listener(data, self, allData) except Exception: # if exception handling is on, handle, otherwise re-raise handler = self._treeConfig.listenerExcHandler if handler is None or self.__handlingUncaughtListenerExc: raise # try handling the exception so we can continue the send: try: self.__handlingUncaughtListenerExc = True handler(listener.name(), topicObj) self.__handlingUncaughtListenerExc = False except Exception: exc = sys.exc_info()[1] # print 'exception raised', exc self.__handlingUncaughtListenerExc = False raise ExcHandlerError(listener.name(), topicObj, exc) def __finalize(self): """ Finalize the topic specification, which currently means creating the listener validator for this topic. This allows calls to subscribe() to validate that listener adheres to topic's message data specification (MDS). """ assert self.__msgArgs.isComplete() assert not self.hasMDS() # must make sure can adopt a validator required = self.__msgArgs.allRequired optional = self.__msgArgs.allOptional self.__validator = ListenerValidator(required, list(optional)) assert not self.__listeners def _undefineSelf_(self, topicsMap: MutableMapping[str, Topic]): """Called by topic manager when deleting a topic.""" if self.__parentTopic is not None: self.__parentTopic().__abandonSubtopic(self.__tupleName[-1]) self.__undefineBranch(topicsMap) def __undefineBranch(self, topicsMap: MutableMapping[str, Topic]): """ Unsubscribe all our listeners, remove all subtopics from self, then detach from parent. Parent is not notified, because method assumes it has been called by parent """ # print 'Remove %s listeners (%s)' % (self.getName(), self.getNumListeners()) self.unsubscribeAllListeners() self.__parentTopic = None for subName, subObj in self.__subTopics.items(): assert isinstance(subObj, Topic) # print 'Unlinking %s from parent' % subObj.getName() subObj.__undefineBranch(topicsMap) self.__subTopics = {} del topicsMap[self.getName()] def __adoptSubtopic(self, topicObj: Topic): """Add topicObj as child topic.""" assert topicObj.__parentTopic() is self attrName = topicObj.getNodeName() self.__subTopics[attrName] = topicObj def __abandonSubtopic(self, name: str): """The given subtopic becomes orphan (no parent).""" topicObj = self.__subTopics.pop(name) assert topicObj.__parentTopic() is self def __onDeadListener(self, listener: Listener): """One of our subscribed listeners has died, so remove it and notify""" pubListener = self.__listeners.pop(listener) self._treeConfig.notificationMgr.notifyDeadListener(pubListener, self) def __str__(self): return "%s(%s)" % (self.getName(), self.getNumListeners()) pypubsub-4.0.3/src/pubsub/core/topictreetraverser.py000066400000000000000000000122701342344706700227300ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from enum import IntEnum from .topicobj import Topic class ITopicTreeVisitor: """ Derive from ITopicTreeVisitor and override one or more of the self._*() methods. Give an instance to an instance of TopicTreeTraverser. """ def _accept(self, topicObj: Topic): """ Override this to filter nodes of topic tree. Must return True (accept node) of False (reject node). Note that rejected nodes cause traversal to move to next branch (no children traversed). """ return True def _startTraversal(self): """Override this to define what to do when traversal() starts.""" pass def _onTopic(self, topicObj): """Override this to define what to do for each node.""" pass def _startChildren(self): """ Override this to take special action whenever a new level of the topic hierarchy is started (e.g., indent some output). """ pass def _endChildren(self): """ Override this to take special action whenever a level of the topic hierarchy is completed (e.g., dedent some output). """ pass def _doneTraversal(self): """Override this to take special action when traversal done.""" pass class TreeTraversal(IntEnum): """MAP is sequential through topic manager\'s topics map; the other two, through topic tree.""" DEPTH, BREADTH, MAP = range(3) class TopicTreeTraverser: """ Supports taking action on every topic in the topic tree. The traverse() method traverses a topic tree and calls visitor._onTopic() for each topic in the tree that satisfies visitor._accept(). Additionally it calls visitor._startChildren() whenever it starts traversing the subtopics of a topic, and visitor._endChildren() when it is done with the subtopics. Finally, it calls visitor._doneTraversal() when traversal has been completed. The visitor must therefore adhere to the ITopicTreeVisitor interface. """ def __init__(self, visitor: ITopicTreeVisitor = None): """The visitor, if given, must adhere to API of ITopicTreeVisitor.""" self.__handler = visitor def setVisitor(self, visitor: ITopicTreeVisitor): """The visitor must adhere to API of ITopicTreeVisitor.""" self.__handler = visitor def traverse(self, topicObj: Topic, how: TreeTraversal = TreeTraversal.DEPTH, onlyFiltered: bool = True): """ Start traversing tree at topicObj. Note that topicObj is a Topic object, not a topic name. The how defines if tree should be traversed breadth or depth first. If onlyFiltered is False, then all nodes are accepted (_accept(node) not called). This method can be called multiple times. """ if how == TreeTraversal.MAP: raise NotImplementedError('not yet available') self.__handler._startTraversal() if how == TreeTraversal.BREADTH: self.__traverseBreadth(topicObj, onlyFiltered) else: assert how == TreeTraversal.DEPTH self.__traverseDepth(topicObj, onlyFiltered) self.__handler._doneTraversal() def __traverseBreadth(self, topicObj: Topic, onlyFiltered: bool): visitor = self.__handler def extendQueue(subtopics): topics.append(visitor._startChildren) topics.extend(subtopics) topics.append(visitor._endChildren) topics = [topicObj] while topics: topicObj = topics.pop(0) if topicObj in (visitor._startChildren, visitor._endChildren): topicObj() continue if onlyFiltered: if visitor._accept(topicObj): extendQueue(topicObj.getSubtopics()) visitor._onTopic(topicObj) else: extendQueue(topicObj.getSubtopics()) visitor._onTopic(topicObj) def __traverseDepth(self, topicObj: Topic, onlyFiltered: bool): visitor = self.__handler def extendStack(topicTreeStack, subtopics): topicTreeStack.insert(0, visitor._endChildren) # marker functor # put subtopics in list in alphabetical order subtopicsTmp = list(subtopics) subtopicsTmp.sort(reverse=True, key=topicObj.__class__.getName) for sub in subtopicsTmp: topicTreeStack.insert(0, sub) # this puts them in reverse order topicTreeStack.insert(0, visitor._startChildren) # marker functor topics = [topicObj] while topics: topicObj = topics.pop(0) if topicObj in (visitor._startChildren, visitor._endChildren): topicObj() continue if onlyFiltered: if visitor._accept(topicObj): extendStack(topics, topicObj.getSubtopics()) visitor._onTopic(topicObj) else: extendStack(topics, topicObj.getSubtopics()) visitor._onTopic(topicObj) pypubsub-4.0.3/src/pubsub/core/topicutils.py000066400000000000000000000076331342344706700212020ustar00rootroot00000000000000""" Various utilities used by topic-related modules. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from textwrap import TextWrapper, dedent import sys from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO from .topicexc import TopicNameError __all__ = [] UNDERSCORE = '_' # topic name can't start with this # just want something unlikely to clash with user's topic names ALL_TOPICS = 'ALL_TOPICS' class WeakNone: """ Pretend to be a weak reference to nothing. Used by ArgsInfos to refer to parent when None so no if-else blocks needed. """ def __call__(self): return None def smartDedent(paragraph: str) -> str: """ Dedent paragraph using textwrap.dedent(), but properly dedents even if the first line of paragraph does not contain blanks. This handles the case where a user types a documentation string as '''A long string spanning several lines.''' """ if paragraph.startswith(' '): para = dedent(paragraph) else: lines = paragraph.split('\n') exceptFirst = dedent('\n'.join(lines[1:])) para = lines[0] + exceptFirst return para import re _validNameRE = re.compile(r'[-0-9a-zA-Z]\w*') def validateName(topicName: str): """Raise TopicNameError if nameTuple not valid as topic name.""" topicNameTuple = tupleize(topicName) if not topicNameTuple: reason = 'name tuple must have at least one item!' raise TopicNameError(None, reason) class topic: pass for subname in topicNameTuple: if not subname: reason = 'can\'t contain empty string or None' raise TopicNameError(topicNameTuple, reason) if subname.startswith(UNDERSCORE): reason = 'must not start with "%s"' % UNDERSCORE raise TopicNameError(topicNameTuple, reason) if subname == ALL_TOPICS: reason = 'string "%s" is reserved for root topic' % ALL_TOPICS raise TopicNameError(topicNameTuple, reason) if _validNameRE.match(subname) is None: reason = 'element #%s ("%s") has invalid characters' % \ (1 + list(topicNameTuple).index(subname), subname) raise TopicNameError(topicNameTuple, reason) def stringize(topicName: Sequence[str]) -> str: """ If topicName is a string, just return it as is. If it is a topic definition object (ie an object that has 'msgDataSpec' as data member), return the dotted name of corresponding topic. Otherwise, assume topicName is a tuple and convert it to to a dotted name i.e. ('a','b','c') => 'a.b.c'. Empty name is not allowed (ValueError). The reverse operation is tupleize(topicName). """ if isinstance(topicName, str): return topicName if hasattr(topicName, "_topicNameStr"): return topicName._topicNameStr try: name = '.'.join(topicName) except Exception: exc = sys.exc_info()[1] raise TopicNameError(topicName, str(exc)) return name def tupleize(topicName: str) -> Tuple[str, ...]: """ If topicName is a tuple of strings, just return it as is. Otherwise, convert it to tuple, assuming dotted notation used for topicName. I.e. 'a.b.c' => ('a','b','c'). Empty topicName is not allowed (ValueError). The reverse operation is stringize(topicNameTuple). """ # assume name is most often str; if more often tuple, # then better use isinstance(name, tuple) if hasattr(topicName, "msgDataSpec"): topicName = topicName._topicNameStr if isinstance(topicName, str): topicTuple = tuple(topicName.split('.')) else: topicTuple = tuple(topicName) # assume already tuple of strings if not topicTuple: raise TopicNameError(topicTuple, "Topic name can't be empty!") return topicTuple pypubsub-4.0.3/src/pubsub/core/weakmethod.py000066400000000000000000000025501342344706700211240ustar00rootroot00000000000000""" This module hides the source of implementation of weak ref to a method: for Python 3.4, it is Python's weakref module; for earlier Python, it is the weakrefmethod module from PyPI. Prior to pypubsub 4.0, WeakMethod was a custom class that adhered to the WeakRef API. Use the getWeakRef(object) module function to create the proper type of weak reference (weakref.WeakRef or WeakMethod) for given object. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from inspect import ismethod from weakref import ref as WeakRef # for weakly bound methods: try: from weakref import WeakMethod except: from weakrefmethod import WeakMethod # type hinting: from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO WeakObjOrMethod = Union[WeakMethod, WeakRef] DeadRefObserver = Callable[[WeakObjOrMethod], None] def getWeakRef(obj, notifyDead: DeadRefObserver = None): """ Get a weak reference to obj. If obj is a bound method, a WeakMethod object, that behaves like a WeakRef, is returned; if it is anything else a WeakRef is returned. If obj is an unbound method, a ValueError will be raised. """ if ismethod(obj): createRef = WeakMethod else: createRef = WeakRef return createRef(obj, notifyDead) pypubsub-4.0.3/src/pubsub/pub.py000066400000000000000000000136521342344706700166370ustar00rootroot00000000000000""" This is the main entry-point to pubsub's core functionality. The :mod:`~pubsub.pub` module supports: * messaging: publishing and receiving messages of a given topic * tracing: tracing pubsub activity in an application * trapping exceptions: dealing with "badly behaved" listeners (ie that leak exceptions) * specificatio of topic tree: defining (or just documenting) the topic tree of an application; message data specification (MDS) The recommended usage is :: from pubsub import pub // use pub functions: pub.sendMessage(...) Note that this module creates a "default" instance of pubsub.core.Publisher and binds several local functions to some of its methods and those of the pubsub.core.TopicManager instance that it contains. However, an application may create as many independent instances of Publisher as required (for instance, one in each thread; with a custom queue to mediate message transfer between threads). """ """ :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ VERSION_API = 4 #: major API version VERSION_SVN = "$Rev: 243 $".split()[1] # DO NOT CHANGE: automatically updated by VCS from typing import List from .core import ( Publisher, AUTO_TOPIC, ListenerMismatchError, TopicDefnError, IListenerExcHandler, ExcHandlerError, TopicDefnError, TopicNameError, UnrecognizedSourceFormatError, SenderUnknownMsgDataError, SenderMissingReqdMsgDataError, TopicManager, ALL_TOPICS, Topic, MessageDataSpecError, exportTopicTreeSpec, TOPIC_TREE_FROM_MODULE, TOPIC_TREE_FROM_STRING, TOPIC_TREE_FROM_CLASS, TopicTreeTraverser, INotificationHandler, ) __all__ = [ # listener stuff: 'subscribe', 'unsubscribe', 'unsubAll', 'isSubscribed', 'isValid', 'validate', 'ListenerMismatchError', 'AUTO_TOPIC', 'IListenerExcHandler', 'getListenerExcHandler', 'setListenerExcHandler', 'ExcHandlerError', # topic stuff: 'ALL_TOPICS', 'Topic', 'topicTreeRoot', 'topicsMap', 'TopicManager', 'getDefaultTopicMgr', # topioc defn provider stuff 'addTopicDefnProvider', 'clearTopicDefnProviders', 'getNumTopicDefnProviders', 'TOPIC_TREE_FROM_MODULE', 'TOPIC_TREE_FROM_CLASS', 'TOPIC_TREE_FROM_STRING', 'exportTopicTreeSpec', 'instantiateAllDefinedTopics' 'TopicDefnError', 'TopicNameError', 'setTopicUnspecifiedFatal', # publisher stuff: 'sendMessage', # misc: 'addNotificationHandler', 'setNotificationFlags', 'getNotificationFlags', 'clearNotificationHandlers', 'TopicTreeTraverser', ] # --------- Publisher singleton and bound methods ------------------------------------ _publisher = Publisher() subscribe = _publisher.subscribe unsubscribe = _publisher.unsubscribe unsubAll = _publisher.unsubAll sendMessage = _publisher.sendMessage getListenerExcHandler = _publisher.getListenerExcHandler setListenerExcHandler = _publisher.setListenerExcHandler addNotificationHandler = _publisher.addNotificationHandler clearNotificationHandlers = _publisher.clearNotificationHandlers setNotificationFlags = _publisher.setNotificationFlags getNotificationFlags = _publisher.getNotificationFlags setTopicUnspecifiedFatal = _publisher.setTopicUnspecifiedFatal def getDefaultPublisher() -> Publisher: """ Get the Publisher instance created by default when this module is imported. See the module doc for details about this instance. """ return _publisher # ---------- default TopicManager instance and bound methods ------------------------ _topicMgr = _publisher.getTopicMgr() topicTreeRoot = _topicMgr.getRootAllTopics() topicsMap = _topicMgr._topicsMap def isValid(listener, topicName, curriedArgNames=None) -> bool: """ Return true only if listener can subscribe to messages of given topic. If curriedArgNames can be a list of parameters of the given listener, that should be assumed curried (i.e. actual listener signature is signature of given listener minus curried args). """ return _topicMgr.getTopic(topicName).isValid(listener, curriedArgNames=curriedArgNames) def validate(listener, topicName, curriedArgNames=None): """ Checks if listener can subscribe to topicName. If not, raises ListenerMismatchError, otherwise just returns. The curriedArgNames is same as for isValid(). """ _topicMgr.getTopic(topicName).validate(listener, curriedArgNames=curriedArgNames) def isSubscribed(listener, topicName) -> bool: """ Returns true if listener has subscribed to topicName, false otherwise. WARNING: a false return is not a guarantee that listener won't get messages of topicName: it could receive messages of a subtopic of topicName. """ return _topicMgr.getTopic(topicName).hasListener(listener) def getDefaultTopicMgr() -> TopicManager: """ Get the TopicManager instance created by default when this module is imported. This function is a shortcut for ``pub.getDefaultPublisher().getTopicMgr()``. """ return _topicMgr addTopicDefnProvider = _topicMgr.addDefnProvider clearTopicDefnProviders = _topicMgr.clearDefnProviders getNumTopicDefnProviders = _topicMgr.getNumDefnProviders def instantiateAllDefinedTopics(provider) -> List[Topic]: """ Loop over all topics of given provider and "instantiate" each topic, thus forcing a parse of the topics documentation, message data specification (MDS), comparison with parent MDS, and MDS documentation. Without this function call, an error among any of those characteristics will manifest only if the a listener is registered on it. """ all_topics = [] for topic_name in provider: _topicMgr.getOrCreateTopic(topic_name) all_topics.append(topic_name) return all_topics # --------------------------------------------------------------------------- pypubsub-4.0.3/src/pubsub/utils/000077500000000000000000000000001342344706700166305ustar00rootroot00000000000000pypubsub-4.0.3/src/pubsub/utils/__init__.py000066400000000000000000000011321342344706700207360ustar00rootroot00000000000000""" Provides utility functions and classes that are not required for using pubsub but are likely to be very useful. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from .topictreeprinter import printTreeDocs from .notification import ( useNotifyByPubsubMessage, useNotifyByWriteFile, IgnoreNotificationsMixin, ) from .exchandling import ExcPublisher __all__ = [ 'printTreeDocs', 'useNotifyByPubsubMessage', 'useNotifyByWriteFile', 'IgnoreNotificationsMixin', 'ExcPublisher' ] pypubsub-4.0.3/src/pubsub/utils/exchandling.py000066400000000000000000000102101342344706700214600ustar00rootroot00000000000000""" Some utility classes for exception handling of exceptions raised within listeners: - TracebackInfo: convenient way of getting stack trace of latest exception raised. The handler can create the instance to retrieve the stack trace and then log it, present it to user, etc. - ExcPublisher: example handler that publishes a message containing traceback info :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ import sys, traceback from typing import Tuple, List, Sequence, Mapping, Dict, Callable, Any, Optional, Union, TextIO from ..core.listener import IListenerExcHandler from ..core.topicmgr import TopicManager class TracebackInfo: """ Represent the traceback information for when an exception is raised -- but not caught -- in a listener. The complete traceback cannot be stored since this leads to circular references (see docs for sys.exc_info()) which keeps listeners alive even after the application is no longer referring to them. Instances of this object are given to listeners of the 'uncaughtExcInListener' topic as the excTraceback kwarg. The instance calls sys.exc_info() to get the traceback info but keeps only the following info: * self.ExcClass: the class of exception that was raised and not caught * self.excArg: the argument given to exception when raised * self.traceback: list of quadruples as returned by traceback.extract_tb() Normally you just need to call one of the two getFormatted() methods. """ def __init__(self): tmpInfo = sys.exc_info() self.ExcClass = tmpInfo[0] self.excArg = tmpInfo[1] # for the traceback, skip the first 3 entries, since they relate to # implementation details for pubsub. tb_list = traceback.extract_tb(tmpInfo[2]) IGNORE_FRAMES = 2 import re assert re.search(r'pubsub.core.listener\.py', tb_list[IGNORE_FRAMES - 1][0]) self.traceback = tb_list[IGNORE_FRAMES:] # help avoid circular refs del tmpInfo def getFormattedList(self) -> List[str]: """ Get a list of strings as returned by the traceback module's format_list() and format_exception_only() functions. """ tmp = traceback.format_list(self.traceback) tmp.extend(traceback.format_exception_only(self.ExcClass, self.excArg)) return tmp def getFormattedString(self) -> str: """ Get a string similar to the stack trace that gets printed to stdout by Python interpreter when an exception is not caught. """ return ''.join(self.getFormattedList()) def __str__(self): return self.getFormattedString() class ExcPublisher(IListenerExcHandler): """ Example exception handler that simply publishes the exception traceback. The messages will have topic name given by topicUncaughtExc. """ # name of the topic topicUncaughtExc = 'uncaughtExcInListener' def __init__(self, topicMgr: TopicManager = None): """ If topic manager is specified, will automatically call init(). Otherwise, caller must call init() after pubsub imported. See pub.setListenerExcHandler(). """ if topicMgr is not None: self.init(topicMgr) def init(self, topicMgr: TopicManager): """ Must be called only after pubsub has been imported since this handler creates a pubsub topic. """ obj = topicMgr.getOrCreateTopic(self.topicUncaughtExc) obj.setDescription('generated when a listener raises an exception') obj.setMsgArgSpec(dict( listenerStr='string representation of listener', excTraceback='instance of TracebackInfo containing exception info')) self.__topicObj = obj def __call__(self, listenerID: str, topicObj): """ Handle the exception raised by given listener. Send the Traceback to all subscribers of topic self.topicUncaughtExc. """ tbInfo = TracebackInfo() self.__topicObj.publish(listenerStr=listenerID, excTraceback=tbInfo) pypubsub-4.0.3/src/pubsub/utils/misc.py000066400000000000000000000021751342344706700201420ustar00rootroot00000000000000""" Provides useful functions and classes. Most useful are probably printTreeDocs and printTreeSpec. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ import sys __all__ = ('printImported', 'Callback') def printImported(): """Output a list of pubsub modules imported so far""" ll = [mod for mod in sys.modules.keys() if mod.find('pubsub') >= 0] # iter keys ok ll.sort() print('\n'.join(ll)) class Callback: """ This can be used to wrap functions that are referenced by class data if the data should be called as a function. E.g. given >>> def func(): pass >>> class A: ....def __init__(self): self.a = func then doing >>> boo=A(); boo.a() will fail since Python will try to call a() as a method of boo, whereas a() is a free function. But if you have instead "self.a = Callback(func)", then "boo.a()" works as expected. """ def __init__(self, callable_obj): self.__callable = callable_obj def __call__(self, *args, **kwargs): return self.__callable(*args, **kwargs) pypubsub-4.0.3/src/pubsub/utils/notification.py000066400000000000000000000322271342344706700216760ustar00rootroot00000000000000""" Provide an interface class for handling pubsub notification messages, and an example class (though very useful in practice) showing how to use it. Notification messages are generated by pubsub - if a handler has been configured via pub.addNotificationHandler() - when pubsub does certain tasks, such as when a listener subscribes to or unsubscribes from a topic Derive from this class to handle notification events from various parts of pubsub. E.g. when a listener subscribes, unsubscribes, or dies, a notification handler, if you specified one via pub.addNotificationHandler(), is given the relevant information. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ from typing import List, Mapping, Any, TextIO from ..core import TopicManager, INotificationHandler, Listener, Topic, Publisher class IgnoreNotificationsMixin(INotificationHandler): """ Derive your Notifications handler from this class if your handler just wants to be notified of one or two types of pubsub events. Then just override the desired methods. The rest of the notifications will automatically be ignored. """ def notifySubscribe(self, pubListener: Listener, topicObj: Topic, newSub: bool): pass def notifyUnsubscribe(self, pubListener: Listener, topicObj: Topic): pass def notifyDeadListener(self, pubListener: Listener, topicObj: Topic): pass def notifySend(self, stage: str, topicObj: Topic, pubListener: Listener = None): pass def notifyNewTopic(self, topicObj: Topic, description: str, required: List[str], argsDocs: Mapping[str, str]): pass def notifyDelTopic(self, topicName: str): pass class NotifyByWriteFile(INotificationHandler): """ Print a message to stdout when a notification is received. """ defaultPrefix = 'PUBSUB:' def __init__(self, fileObj: TextIO = None, prefix: str = None): """ Will write to stdout unless fileObj given. Will use defaultPrefix as prefix for each line output, unless prefix specified. """ self.__pre = prefix or self.defaultPrefix if fileObj is None: import sys self.__fileObj = sys.stdout else: self.__fileObj = fileObj def changeFile(self, fileObj): self.__fileObj = fileObj def notifySubscribe(self, pubListener: Listener, topicObj: Topic, newSub: bool): if newSub: msg = '%s Subscribed listener "%s" to topic "%s"\n' else: msg = '%s Subscription of "%s" to topic "%s" redundant\n' msg = msg % (self.__pre, pubListener, topicObj.getName()) self.__fileObj.write(msg) def notifyUnsubscribe(self, pubListener: Listener, topicObj: Topic): msg = '%s Unsubscribed listener "%s" from topic "%s"\n' msg = msg % (self.__pre, pubListener, topicObj.getName()) self.__fileObj.write(msg) def notifyDeadListener(self, pubListener: Listener, topicObj: Topic): msg = '%s Listener "%s" of Topic "%s" has died\n' \ % (self.__pre, pubListener, topicObj.getName()) # a bug apparently: sometimes on exit, the stream gets closed before # and leads to a TypeError involving NoneType self.__fileObj.write(msg) def notifySend(self, stage: str, topicObj: Topic, pubListener: Listener = None): if stage == 'in': msg = '%s Sending message of topic "%s" to listener %s\n' % (self.__pre, topicObj.getName(), pubListener) elif stage == 'pre': msg = '%s Start sending message of topic "%s"\n' % (self.__pre, topicObj.getName()) else: msg = '%s Done sending message of topic "%s"\n' % (self.__pre, topicObj.getName()) self.__fileObj.write(msg) def notifyNewTopic(self, topicObj: Topic, description: str, required: List[str], argsDocs: Mapping[str, str]): msg = '%s New topic "%s" created\n' % (self.__pre, topicObj.getName()) self.__fileObj.write(msg) def notifyDelTopic(self, topicName: str): msg = '%s Topic "%s" destroyed\n' % (self.__pre, topicName) self.__fileObj.write(msg) class NotifyByPubsubMessage(INotificationHandler): """ Handle pubsub notification messages by generating messages of a 'pubsub.' subtopic. Also provides an example of how to create a notification handler. Use it by calling:: import pubsub.utils pubsub.utils.useNotifyByPubsubMessage() ... pub.setNotificationFlags(...) # optional E.g. whenever a listener is unsubscribed, a 'pubsub.unsubscribe' message is generated. If you have subscribed a listener of this topic, your listener will be notified of what listener unsubscribed from what topic. """ topicRoot = 'pubsub' topics = dict( send='%s.sendMessage' % topicRoot, subscribe='%s.subscribe' % topicRoot, unsubscribe='%s.unsubscribe' % topicRoot, newTopic='%s.newTopic' % topicRoot, delTopic='%s.delTopic' % topicRoot, deadListener='%s.deadListener' % topicRoot) def __init__(self, topicMgr: TopicManager = None): self._pubTopic = None self.__sending = False # used to guard against infinite loop if topicMgr is not None: self.createNotificationTopics(topicMgr) def createNotificationTopics(self, topicMgr: TopicManager): """ Create the notification topics. The root of the topics created is self.topicRoot. The topicMgr is (usually) pub.topicMgr. """ # see if the special topics have already been defined try: topicMgr.getTopic(self.topicRoot) except ValueError: # no, so create them self._pubTopic = topicMgr.getOrCreateTopic(self.topicRoot) self._pubTopic.setDescription('root of all pubsub-specific topics') _createTopics(self.topics, topicMgr) def notifySubscribe(self, pubListener: Listener, topicObj: Topic, newSub: bool): if (self._pubTopic is None) or self.__sending: return pubTopic = self._pubTopic.getSubtopic('subscribe') if topicObj is not pubTopic: kwargs = dict(listener=pubListener, topic=topicObj, newSub=newSub) self.__doNotification(pubTopic, kwargs) def notifyUnsubscribe(self, pubListener: Listener, topicObj: Topic): if (self._pubTopic is None) or self.__sending: return pubTopic = self._pubTopic.getSubtopic('unsubscribe') if topicObj is not pubTopic: kwargs = dict( topic=topicObj, listenerRaw=pubListener.getCallable(), listener=pubListener) self.__doNotification(pubTopic, kwargs) def notifyDeadListener(self, pubListener: Listener, topicObj: Topic): if (self._pubTopic is None) or self.__sending: return pubTopic = self._pubTopic.getSubtopic('deadListener') kwargs = dict(topic=topicObj, listener=pubListener) self.__doNotification(pubTopic, kwargs) def notifySend(self, stage: str, topicObj: Topic, pubListener: Listener = None): """ Stage must be 'pre' or 'post'. Note that any pubsub sendMessage operation resulting from this notification (which sends a message; listener could handle by sending another message!) will NOT themselves lead to a send notification. """ if (self._pubTopic is None) or self.__sending: return sendMsgTopic = self._pubTopic.getSubtopic('sendMessage') if stage == 'pre' and (topicObj is sendMsgTopic): msg = 'Not allowed to send messages of topic %s' % topicObj.getName() raise ValueError(msg) self.__doNotification(sendMsgTopic, dict(topic=topicObj, stage=stage)) def notifyNewTopic(self, topicObj: Topic, description: str, required: List[str], argsDocs: Mapping[str, str]): if (self._pubTopic is None) or self.__sending: return pubTopic = self._pubTopic.getSubtopic('newTopic') kwargs = dict(topic=topicObj, description=description, required=required, args=argsDocs) self.__doNotification(pubTopic, kwargs) def notifyDelTopic(self, topicName: str): if (self._pubTopic is None) or self.__sending: return pubTopic = self._pubTopic.getSubtopic('delTopic') self.__doNotification(pubTopic, dict(name=topicName)) def __doNotification(self, pubTopic: Topic, kwargs: Mapping[str, Any]): self.__sending = True try: pubTopic.publish(**kwargs) finally: self.__sending = False def _createTopics(topicMap: Mapping[str, str], topicMgr: TopicManager): """ Create notification topics. These are used when some of the notification flags have been set to True (see pub.setNotificationFlags(). The topicMap is a dict where key is the notification type, and value is the topic name to create. Notification type is a string in ('send', 'subscribe', 'unsubscribe', 'newTopic', 'delTopic', 'deadListener'). """ def newTopic(_name, _desc, _required=None, **argsDocs): topic = topicMgr.getOrCreateTopic(_name) topic.setDescription(_desc) topic.setMsgArgSpec(argsDocs, _required) newTopic( _name=topicMap['subscribe'], _desc='whenever a listener is subscribed to a topic', topic='topic that listener has subscribed to', listener='instance of pub.Listener containing listener', newSub='false if listener was already subscribed, true otherwise') newTopic( _name=topicMap['unsubscribe'], _desc='whenever a listener is unsubscribed from a topic', topic='instance of Topic that listener has been unsubscribed from', listener='instance of pub.Listener unsubscribed; None if listener not found', listenerRaw='listener unsubscribed') newTopic( _name=topicMap['send'], _desc='sent at beginning and end of sendMessage()', topic='instance of topic for message being sent', stage='stage of send operation: "pre" or "post" or "in"', listener='which listener being sent to') newTopic( _name=topicMap['newTopic'], _desc='whenever a new topic is defined', topic='instance of Topic created', description='description of topic (use)', args='the argument names/descriptions for arguments that listeners must accept', required='which args are required (all others are optional)') newTopic( _name=topicMap['delTopic'], _desc='whenever a topic is deleted', name='full name of the Topic instance that was destroyed') newTopic( _name=topicMap['deadListener'], _desc='whenever a listener dies without having unsubscribed', topic='instance of Topic that listener was subscribed to', listener='instance of pub.Listener containing dead listener') def useNotifyByPubsubMessage(publisher: Publisher = None, all: bool = True, **kwargs): """ Will cause all of pubsub's notifications of pubsub "actions" (such as new topic created, message sent, listener subscribed, etc) to be sent out as messages. Topic will be 'pubsub' subtopics, such as 'pubsub.newTopic', 'pubsub.delTopic', 'pubsub.sendMessage', etc. The 'all' and kwargs args are the same as pubsub's setNotificationFlags(), except that 'all' defaults to True. The publisher is rarely needed: * The publisher must be specfied if pubsub is not installed on the system search path (ie from pubsub import ... would fail or import wrong pubsub -- such as if pubsub is within wxPython's wx.lib package). Then pbuModule is the pub module to use:: from wx.lib.pubsub import pub from wx.lib.pubsub.utils import notification notification.useNotifyByPubsubMessage() """ if publisher is None: from .. import pub publisher = pub.getDefaultPublisher() topicMgr = publisher.getTopicMgr() notifHandler = NotifyByPubsubMessage(topicMgr) publisher.addNotificationHandler(notifHandler) publisher.setNotificationFlags(all=all, **kwargs) def useNotifyByWriteFile(fileObj: TextIO = None, prefix: str = None, publisher: Publisher = None, all: bool = True, **kwargs): """ Will cause all pubsub notifications of pubsub "actions" (such as new topic created, message sent, listener died etc) to be written to specified file (or stdout if none given). The fileObj need only provide a 'write(string)' method. The first two arguments are the same as those of NotifyByWriteFile constructor. The 'all' and kwargs arguments are those of pubsub's setNotificationFlags(), except that 'all' defaults to True. See useNotifyByPubsubMessage() for an explanation of pubModule (typically only if pubsub inside wxPython's wx.lib) """ if publisher is None: from .. import pub publisher = pub.getDefaultPublisher() notifHandler = NotifyByWriteFile(fileObj, prefix) publisher.addNotificationHandler(notifHandler) publisher.setNotificationFlags(all=all, **kwargs) pypubsub-4.0.3/src/pubsub/utils/topictreeprinter.py000066400000000000000000000173761342344706700226220ustar00rootroot00000000000000""" 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) pypubsub-4.0.3/src/pubsub/utils/xmltopicdefnprovider.py000066400000000000000000000206611342344706700234560ustar00rootroot00000000000000""" Contributed by Joshua R English, adapted by Oliver Schoenborn to be consistent with pubsub API. An extension for pubsub (http://pubsub.sourceforge.net) so topic tree specification can be encoded in XML format rather than pubsub's default Python nested class format. To use: xml = ''' Test showing topic hierarchy and inheritance Parent with a parameter and subtopics given name surname This is the first child A nickname ''' These topic definitions are loaded through an XmlTopicDefnProvider: pub.addTopicDefnProvider( XmlTopicDefnProvider(xml) ) The XmlTopicDefnProvider also accepts a filename instead of XML string: provider = XmlTopicDefnProvider("path/to/XMLfile.xml", TOPIC_TREE_FROM_FILE) pub.addTopicDefnProvider( provider ) Topics can be exported to an XML file using the exportTopicTreeSpecXml function. This will create a text file for the XML and return the string representation of the XML tree. :copyright: Copyright since 2013 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE_BSD_Simple.txt for details. """ __author__ = 'Joshua R English' __revision__ = 6 __date__ = '2013-07-27' from ..core.topictreetraverser import ITopicTreeVisitor from ..core.topicdefnprovider import ( ITopicDefnProvider, ArgSpecGiven, TOPIC_TREE_FROM_STRING, ) try: from elementtree import ElementTree as ET except ImportError: try: # for Python 2.4, must use cElementTree: from xml.etree import ElementTree as ET except ImportError: from cElementTree import ElementTree as ET __all__ = [ 'XmlTopicDefnProvider', 'exportTopicTreeSpecXml', 'TOPIC_TREE_FROM_FILE' ] def _get_elem(elem): """ Assume an ETree.Element object or a string representation. Return the ETree.Element object """ if not ET.iselement(elem): try: elem = ET.fromstring(elem) except: print("Value Error", elem) raise ValueError("Cannot convert to element") return elem TOPIC_TREE_FROM_FILE = 'file' class XmlTopicDefnProvider(ITopicDefnProvider): class XmlParserError(RuntimeError): pass class UnrecognizedSourceFormatError(ValueError): pass def __init__(self, xml, format=TOPIC_TREE_FROM_STRING): self._topics = {} self._treeDoc = '' if format == TOPIC_TREE_FROM_FILE: self._parse_tree(_get_elem(open(xml, mode="r").read())) elif format == TOPIC_TREE_FROM_STRING: self._parse_tree(_get_elem(xml)) else: raise UnrecognizedSourceFormatError() def _parse_tree(self, tree): doc_node = tree.find('description') if doc_node is None: self._treeDoc = "UNDOCUMENTED" else: self._treeDoc = ' '.join(doc_node.text.split()) for node in tree.findall('topic'): self._parse_topic(node) def _parse_topic(self, node, parents=None, specs=None, reqlist=None): parents = parents or [] specs = specs or {} reqlist = reqlist or [] descNode = node.find('description') if descNode is None: desc = "UNDOCUMENTED" else: desc = ' '.join(descNode.text.split()) node_id = node.get('id') if node_id is None: raise XmlParserError("topic element must have an id attribute") for this in (node.findall('listenerspec/arg')): this_id = this.get('id') if this_id is None: raise XmlParserError("arg element must have an id attribute") this_desc = this.text.strip() this_desc = this_desc or "UNDOCUMENTED" this_desc = ' '.join(this_desc.split()) specs[this_id] = this_desc if this.get('optional', '').lower() not in ['true', 't', 'yes', 'y']: reqlist.append(this_id) defn = ArgSpecGiven(specs, tuple(reqlist)) parents.append(node.get('id')) self._topics[tuple(parents)] = desc, defn for subtopic in node.findall('topic'): self._parse_topic(subtopic, parents[:], specs.copy(), reqlist[:]) def getDefn(self, topicNameTuple): return self._topics.get(topicNameTuple, (None, None)) def topicNames(self): return self._topics.keys() # dict_keys iter in 3, list in 2 def getTreeDoc(self): return self._treeDoc class XmlVisitor(ITopicTreeVisitor): def __init__(self, elem): self.tree = elem self.known_topics = [] def _startTraversal(self): self.roots = [self.tree] def _onTopic(self, topicObj): if topicObj.isAll(): self.last_elem = self.tree return if self.roots: this_elem = ET.SubElement(self.roots[-1], 'topic', {'id': topicObj.getNodeName()}) else: this_elem = ET.Element('topic', {'id': topicObj.getNodeName()}) req, opt = topicObj.getArgs() req = req or () opt = opt or () desc_elem = ET.SubElement(this_elem, 'description') topicDesc = topicObj.getDescription() if topicDesc: desc_elem.text = ' '.join(topicDesc.split()) else: desc_elem.text = "UNDOCUMENTED" argDescriptions = topicObj.getArgDescriptions() # pubsub way of getting known_args known_args = [] parent = topicObj.getParent() while parent: if parent in self.known_topics: p_req, p_opt = parent.getArgs() if p_req: known_args.extend(p_req) if p_opt: known_args.extend(p_opt) parent = parent.getParent() # there is probably a cleaner way to do this if req or opt: spec = ET.SubElement(this_elem, 'listenerspec') for arg in req: if arg in known_args: continue arg_elem = ET.SubElement(spec, 'arg', {'id': arg}) arg_elem.text = ' '.join(argDescriptions.get(arg, 'UNDOCUMENTED').split()) for arg in opt: if arg in known_args: continue arg_elem = ET.SubElement(spec, 'arg', {'id': arg, 'optional': 'True'}) arg_elem.text = ' '.join(argDescriptions.get(arg, 'UNDOCUMENTED').split()) self.last_elem = this_elem self.known_topics.append(topicObj) def _startChildren(self): self.roots.append(self.last_elem) def _endChildren(self): self.roots.pop() ## http://infix.se/2007/02/06/gentlemen-indent-your-xml def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " for e in elem: indent(e, level + 1) if not e.tail or not e.tail.strip(): e.tail = i + " " if not e.tail or not e.tail.strip(): e.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i else: elem.tail = "\n" def exportTopicTreeSpecXml(moduleName=None, rootTopic=None, bak='bak', moduleDoc=None): """ If rootTopic is None, then pub.getDefaultTopicTreeRoot() is assumed. """ if rootTopic is None: from .. import pub rootTopic = pub.getDefaultTopicTreeRoot() elif isinstance(rootTopic, str): from .. import pub rootTopic = pub.getTopic(rootTopic) tree = ET.Element('topicdefntree') if moduleDoc: mod_desc = ET.SubElement(tree, 'description') mod_desc.text = ' '.join(moduleDoc.split()) traverser = pub.TopicTreeTraverser(XmlVisitor(tree)) traverser.traverse(rootTopic) indent(tree) if moduleName: filename = '%s.xml' % moduleName if bak: pub._backupIfExists(filename, bak) fulltree = ET.ElementTree(tree) fulltree.write(filename, "utf-8", True) return ET.tostring(tree) pypubsub-4.0.3/tests/000077500000000000000000000000001342344706700145435ustar00rootroot00000000000000pypubsub-4.0.3/tests/perf.py000066400000000000000000000165541342344706700160640ustar00rootroot00000000000000""" Measure performance of pubsub, so that impact of proposed performance enhancing algorithms can be proven. Measure with python -m timeit -n1 "import perf; perf.runTest()" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pathlib import Path from time import perf_counter from typing import Tuple from pubsub import pub from pubsub.core import Listener topicMgr = pub.getDefaultTopicMgr() def loop_subscribe(topic_names, listeners): topicMgr = pub.getDefaultTopicMgr() topicMgr.clearTree() num_samples = 1000 start = perf_counter() for count in range(num_samples): for listener in listeners: for topic_name in topic_names: pub.subscribe(listener, topic_name) tot_time = (perf_counter() - start) return round(tot_time, 2) def perf_subscribe(): """ Subscription involves inspecting listener to (upon first subscription to a topic) create the topic and its MDS or (upon subsequent subscriptions to same topic) verify its signature is compatible with topic MDS, then adding listener to topic listeners, and adding topic to topics mapping (of name to topic object). This suggests that complexity of signature and complexity of topic name are the two axes of performance for subscriptions, as the creation of a topic is not likely to be nearly as common as subscriptions to existing topics. Results indicate that deeper topics and listeners have only marginal (ie insignificant) overhead over shallow topics and listeners: - root topic and no-args listeners: 6.77 - deep topics and no-args listeners: 6.83 - root topics and many-args listeners: 6.8 - deep topics and many-args listeners: 6.83 """ print("-"*40) print("Performance measurement for subscribing:") num_topics = 100 num_listeners = 10 # root topic and no-args listeners: topic_names = ['some_topic_' + str(topic_index) for topic_index in range(num_topics)] listeners = [lambda: n for n in range(num_listeners)] print('root topic and no-args listeners:', loop_subscribe(topic_names, listeners)) # deep topics and no-args listeners: topic_names = ['rt.st.sst.ssst.leaf_topic_' + str(topic_index) for topic_index in range(num_topics)] print('deep topics and no-args listeners:', loop_subscribe(topic_names, listeners)) # root topics and many-args listeners: topic_names = ['some_topic_' + str(topic_index) for topic_index in range(num_topics)] listeners = [lambda x, y, z, a, b, c: n for n in range(num_listeners)] print('root topics and many-args listeners:', loop_subscribe(topic_names, listeners)) # deep topics and many-args listeners: topic_names = ['rt.st.sst.ssst.leaf_topic_' + str(topic_index) for topic_index in range(num_topics)] print('deep topics and many-args listeners:', loop_subscribe(topic_names, listeners)) def loop_send(subscriptions: Tuple[Listener, str], messages): topicMgr = pub.getDefaultTopicMgr() topicMgr.clearTree() for listener, topic_name in subscriptions: pub.subscribe(listener, topic_name) num_samples = 1000 start = perf_counter() for count in range(num_samples): for topic_name, kwargs in messages: pub.sendMessage(topic_name, **kwargs) tot_time = (perf_counter() - start) return round(tot_time, 2) def perf_send(): """ Sending message involves calling each listener with data, and going up the tree to root actor each level has fewer data so data must be filtered out. ---------------------------------------- Performance measurement for sending: listeners ['obs1', 'obs2', 'obs3', 'obs4', 'obs5', 'obs6', 'obs7', 'obs8'] topic names ['t1', 't1.t2', ..., 't1.t2.t3.t4.t5.t6.t7.t8'] with depth 8: with depth 8: % less X faster 8 data 9.31 8 data 4.76 49 2.0 4 data 8.48 4 data 4.48 47 1.9 2 data 8.23 2 data 4.5 45 1.8 1 data 8.26 1 data 4.31 48 1.9 no data 8.05 no data 4.27 47 1.8 with depth 4: with depth 4: 4 data 4.4 4 data 2.63 40 1.7 2 data 3.96 2 data 2.56 35 1.5 1 data 3.99 1 data 2.51 37 1.6 no data 4.1 no data 2.43 40 1.7 with depth 2: with depth 2: 2 data 2.28 2 data 1.61 29 1.4 1 data 1.84 1 data 1.57 15 1.2 no data 2.18 no data 1.52 30 1.4 with depth 1: with depth 1: 1 data 1.2 1 data 1.1 8 1.1 no data 1.24 no data 1.05 15 1.2 """ print("-"*40) print("Performance measurement for sending:") # root topic and no-args listeners: def obs1(arg1=None): pass def obs2(arg1=None, arg2=None): pass def obs3(arg1=None, arg2=None, arg3=None): pass def obs4(arg1=None, arg2=None, arg3=None, arg4=None): pass def obs5(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None): pass def obs6(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None): pass def obs7(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None, arg7=None): pass def obs8(arg1=None, arg2=None, arg3=None, arg4=None, arg5=None, arg6=None, arg7=None, arg8=None): pass local_objs = locals().copy() listeners = [local_objs['obs' + str(n)] for n in range(1, 9)] print('listeners', [cb.__name__ for cb in listeners]) topic_names = ['t1'] for index in range(2, 9): topic_names.append(topic_names[-1] + ".t" + str(index)) print('topic names', topic_names) num_messages = 100 def sub_test(topic_names): subscriptions = [(obs, name) for obs, name in zip(listeners, topic_names)] num_topics = len(topic_names) num_subs = len(subscriptions) print('with depth {}:'.format(num_topics)) if len(topic_names) >= 8: msg_data = dict(arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6, arg7=7, arg8=8) messages = [(topic_names[-1], msg_data)] * num_messages print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages)) if len(topic_names) >= 4: msg_data = dict(arg1=1, arg2=2, arg3=3, arg4=4) messages = [(topic_names[-1], msg_data)] * num_messages print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages)) if len(topic_names) >= 2: msg_data = dict(arg1=1, arg2=2) messages = [(topic_names[-1], msg_data)] * num_messages print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages)) if len(topic_names) >= 1: msg_data = dict(arg1=1) messages = [(topic_names[-1], msg_data)] * num_messages print(' {} data'.format(len(msg_data)), loop_send(subscriptions, messages)) messages = [(topic_names[-1], {})] * num_messages print(' {} data'.format('no'), loop_send(subscriptions, messages)) sub_test(topic_names) sub_test(topic_names[:4]) sub_test(topic_names[:2]) sub_test(topic_names[:1]) if __name__ == '__main__': perf_subscribe() perf_send()pypubsub-4.0.3/tests/pytest.ini000066400000000000000000000000411342344706700165670ustar00rootroot00000000000000[pytest] python_files = test*.py pypubsub-4.0.3/tests/suite/000077500000000000000000000000001342344706700156745ustar00rootroot00000000000000pypubsub-4.0.3/tests/suite/my_import_topics.py000066400000000000000000000006531342344706700216520ustar00rootroot00000000000000class root_topic_1: """root topic 1""" class subtopic_2: "subtopic 2" class subsubtopic_21: """Sub sub topic 1 of subtopic 2.""" def msgDataSpec(arg1, arg2=None, arg4=None): """ - arg4: doc for arg4 """ pass class root_topic_2: 'docs for root_topic_2' class subtopic_21: 'docs for subtopic_21' pypubsub-4.0.3/tests/suite/raisinglistener.py000066400000000000000000000002371342344706700214520ustar00rootroot00000000000000def getRaisingListener(): def raisingListener(): def nested(): raise RuntimeError2('test') nested() return raisingListenerpypubsub-4.0.3/tests/suite/test1_listener.py000066400000000000000000000270341342344706700212210ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import gc import sys import pytest from pubsub.core.weakmethod import WeakMethod from pubsub.core import listener from pubsub.core.listener import ( Listener, ListenerMismatchError, CallArgsInfo, getArgs, ListenerValidator) def test_ArgsInfo(): def listener0(msgTopic = Listener.AUTO_TOPIC): pass CallArgsInfo(listener0) def listener1(arg1, msgTopic = Listener.AUTO_TOPIC): pass CallArgsInfo(listener1) def listenerWithHints(arg1, arg2, *, kwarg1, kwarg2=4, **kwargs): pass c = CallArgsInfo(listenerWithHints) assert c.acceptsAllKwargs == True assert c.requiredArgs == ('arg1', 'arg2', 'kwarg1') assert c.optionalArgs == ('kwarg2',) assert c.getOptionalArgs() == c.optionalArgs assert c.getRequiredArgs() == c.requiredArgs def listenerWithHints2(arg1, arg2=2, *, kwarg1, kwarg2=4, **kwargs): pass c = CallArgsInfo(listenerWithHints2) assert c.acceptsAllKwargs == True assert c.requiredArgs == ('arg1', 'kwarg1') assert c.optionalArgs == ('arg2', 'kwarg2') assert c.getOptionalArgs() == c.optionalArgs assert c.getRequiredArgs() == c.requiredArgs class ArgsInfoMock: def __init__(self, autoTopicArgName=None): self.autoTopicArgName = autoTopicArgName self.acceptsAllKwargs = False def test_Validation0(): # Test when ValidatorSameKwargsOnly used, ie when args in # listener and topic must be exact match (unless *arg). AA = Listener.AUTO_TOPIC # test for topic that has no arg/kwargs in topic message spec (TMS) def same(): pass def varargs(*args, **kwargs): pass def autoArg(msgTopic=AA): pass def extraArg(a): pass def extraKwarg(a=1): pass # no arg/kwarg in topic message spec (TMS) validator = ListenerValidator([], []) validate = validator.validate validate(same) # ok: same validate(varargs) # ok: *args/**kwargs validate(autoArg) # ok: extra but AUTO_TOPIC pytest.raises(ListenerMismatchError, validate, extraArg) # E: extra arg validate(extraArg, curriedArgNames=('a',)) # ok: extra is curried validate(extraKwarg) # ok: extra but AUTO_TOPIC def test_Validation1(): # one arg/kwarg in topic validator = ListenerValidator(['a'], ['b']) validate = validator.validate def same(a, b=1): pass def same2(a=2, b=1): pass def varkwargs(**kwargs): pass def varkwargs_a(a, **kwargs): pass def extra_kwarg1(a, b=1, c=2): pass def opt_reqd(b, **kwargs): pass def missing_arg(b=1): pass def missing_kwarg(a): pass def extra_kwarg2(*args, **kwargs): pass def extra_arg1(a,c,b=1): pass def extra_arg2(a,b,c=2): pass validate(same) # ok: same validate(same2) # ok: same even if a now has default value validate(varkwargs_a) # ok: has **kwargs validate(varkwargs) # ok: has **kwargs validate(extra_kwarg1) # ok: extra arg has default value validate(extra_kwarg2) # ok: can accept anything pytest.raises( ListenerMismatchError, validate, opt_reqd) # E: b now required pytest.raises( ListenerMismatchError, validate, missing_arg) # E: missing arg pytest.raises( ListenerMismatchError, validate, missing_kwarg) # E: missing kwarg pytest.raises( ListenerMismatchError, validate, extra_arg1) # E: extra arg pytest.raises( ListenerMismatchError, validate, extra_arg2) # E: extra arg def test_IsCallable(): # Test the proper trapping of non-callable and certain types of # callable objects. # validate different types of callables validator = ListenerValidator([], []) # not a function: notAFunc = 1 # just pick something that is not a function pytest.raises(ListenerMismatchError, validator.validate, notAFunc) # a regular function: def aFunc(): pass validator.validate(aFunc) # a functor and a method class Foo(object): def __call__(self): pass def meth(self): pass foo = Foo() validator.validate(foo) validator.validate(foo.meth) def test_WantTopic(): # Test the correct determination of whether want topic # auto-passed during sendMessage() calls. # first check proper breakdown of listener args: def listener(a, b=1): pass argsInfo = CallArgsInfo(listener) assert None == argsInfo.autoTopicArgName msgTopic = 'auto' class MyListener: def method(self, a, b=1, auto=Listener.AUTO_TOPIC): pass listener = MyListener() argsInfo = getArgs(listener.method) assert msgTopic == argsInfo.autoTopicArgName assert ('a','b') == argsInfo.allParams class MyFunctor: def __call__(self, a, b=1, auto=Listener.AUTO_TOPIC): pass listener = MyFunctor() argsInfo = getArgs(listener) assert msgTopic == argsInfo.autoTopicArgName assert ('a','b') == argsInfo.allParams # now some white box testing of validator that makes use of args info: def checkWantTopic(validate, listener, wantTopicAsArg=None): argsInfo = getArgs(listener) assert argsInfo.autoTopicArgName == wantTopicAsArg validate(listener) validator = ListenerValidator([], ['a']) validate = validator.validate def noWant(a=1): pass def want1(a=1, auto=Listener.AUTO_TOPIC): pass checkWantTopic(validate, noWant) checkWantTopic(validate, want1, msgTopic) validator = ListenerValidator(['a'], ['b']) validate = validator.validate def noWant2(a, b=1): pass def want2(a, auto=Listener.AUTO_TOPIC, b=1): pass checkWantTopic(validate, noWant2) checkWantTopic(validate, want2, msgTopic) # topic that has Listener.AUTO_TOPIC as an arg rather than kwarg validator = ListenerValidator([msgTopic], ['b']) validate = validator.validate def noWant3(auto, b=1): pass checkWantTopic(validate, noWant3) def test_weakref(): from weakref import ref as weakref from inspect import isfunction, ismethod class Foo: def instanceMethod(self): pass @classmethod def classMethod(cls): pass def __call__(self): pass assert isfunction(Foo.instanceMethod) wr = weakref(Foo.instanceMethod) assert wr() is not None, 'Foo.instanceMethod' assert ismethod(Foo.classMethod) wr = weakref(Foo.classMethod) gc.collect() # for pypy: the gc doesn't work the same as cpython's assert wr() is None, 'Foo.classMethod' foo = Foo() fooWR = weakref(foo) assert fooWR() is not None, 'foo' assert ismethod(foo.instanceMethod) wr = weakref(foo.instanceMethod) gc.collect() # for pypy: the gc doesn't work the same as cpython's assert wr() is None, 'foo.instanceMethod' assert ismethod(foo.classMethod) wr = weakref(foo.classMethod) gc.collect() # for pypy: the gc doesn't work the same as cpython's assert wr() is None, 'foo.classMethod' del foo gc.collect() assert fooWR() is None, 'foo' def test_DOAListeners_1(): # Test "dead on arrival" # test DOA of unbound method def getListener1(): class DOA: def tmpFn(self): pass return Listener( DOA.tmpFn, ArgsInfoMock() ) unbound = getListener1() assert not unbound.isDead() def test_DOAListeners_2(): # test DOA of tmp callable: def fn(): pass class Wrapper: def __init__(self, func): self.func = func def __call__(self): pass def onDead(listenerObj): pass # check dead-on-arrival when no death callback specified: doa1 = Listener( Wrapper(fn), ArgsInfoMock() ) gc.collect() # for pypy: the gc doesn't work the same as cpython's assert doa1.getCallable() is None assert doa1.isDead() pytest.raises(RuntimeError, doa1, None, {}) # check dead-on-arrival when a death callback specified: doa2 = Listener( Wrapper(fn), ArgsInfoMock(), onDead ) gc.collect() # for pypy: the gc doesn't work the same as cpython's assert doa2.getCallable() is None assert doa2.isDead() pytest.raises(RuntimeError, doa2, None, {}) def test_ListenerEq(): # Test equality tests of two listeners def listener1(): pass def listener2(): pass l1 = Listener(listener1, ArgsInfoMock()) l2 = Listener(listener2, ArgsInfoMock()) # verify that Listener can be compared for equality to another Listener, weakref, or callable assert l1 == l1 assert l1 != l2 assert l1 == listener1 assert l1 != listener2 ll = [l1] assert listener1 in ll assert listener2 not in ll assert ll.index(listener1) == 0 # now for class method listener: class MyListener: def __call__(self): pass def meth(self): pass listener3 = MyListener() l3 = Listener(listener3, ArgsInfoMock() ) assert l3 != l1 assert l3 != l2 assert l3 != listener2 assert l3 == l3 assert l3 == listener3 assert l3 != listener3.__call__ l4 = Listener(listener3.meth, ArgsInfoMock() ) assert l4 == l4 assert l4 != l3 assert l4 != l2 assert l4 != listener3.__call__ assert l4 == listener3.meth def test_DyingListenersClass(): # Test notification callbacks when listener dies # test dead listener notification def onDead(weakListener): lsrs.remove(weakListener) def listener1(): pass def listener2(): pass def listener3(): pass lsrs = [] lsrs.append( Listener(listener1, ArgsInfoMock(False), onDead=onDead) ) lsrs.append( Listener(listener2, ArgsInfoMock(False), onDead=onDead) ) lsrs.append( Listener(listener3, ArgsInfoMock(False), onDead=onDead) ) # now force some listeners to die, verify lsrs list assert len(lsrs) == 3 del listener1 gc.collect() # for pypy: the gc doesn't work the same as cpython's assert len(lsrs) == 2 assert lsrs[0] == listener2 assert lsrs[1] == listener3 del listener2 gc.collect() # for pypy: the gc doesn't work the same as cpython's assert len(lsrs) == 1 assert lsrs[0] == listener3 del listener3 gc.collect() # for pypy: the gc doesn't work the same as cpython's assert len(lsrs) == 0 def test_getArgsBadListener(): pytest.raises( ListenerMismatchError, getArgs, 1) try: getArgs(1) except ListenerMismatchError: exc = sys.exc_info()[1] msg = 'Listener "int" (from module "__main__") inadequate: type "int" not supported' assert str(exc) == msg def test_weakMethod(): class Foo: def meth(self): pass foo = Foo() wm = WeakMethod(foo.meth) str(wm) def test_testNaming(): aiMock = ArgsInfoMock() # define various type of listeners def fn(): pass class Foo: def __call__(self): pass def meth(self): pass ll = Listener(fn, aiMock) assert ll.typeName() == "fn" assert ll.module() == "test1_listener" assert not ll.wantsTopicObjOnCall() foo = Foo() ll = Listener(foo, aiMock) assert ll.typeName() == "Foo" assert ll.module() == "test1_listener" assert not ll.wantsTopicObjOnCall() ll = Listener(foo.meth, ArgsInfoMock('argName')) assert ll.typeName() == "Foo.meth" assert ll.module() == "test1_listener" assert ll.wantsTopicObjOnCall() def test_call(): aiMock = ArgsInfoMock() result = [] def fn(a, b, c=1, d=2, **e): result.append((a,b,c,d,e)) listener = Listener(fn, aiMock) listener(dict(a=123, b=456), 'test_topic') assert result[0] == (123, 456, 1, 2, {}) listener = Listener(fn, aiMock, curriedArgs=dict(b=4, d=5, f=6)) listener(dict(a=123), 'test_topic') assert result[1] == (123, 4, 1, 5, {'f': 6}) pypubsub-4.0.3/tests/suite/test2_spec.py000066400000000000000000000072031342344706700203230ustar00rootroot00000000000000""" Test topicargspec. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import pytest from pubsub.core.topicargspec import ( ArgsInfo, ArgSpecGiven, SenderMissingReqdMsgDataError ) from pubsub.core import SenderUnknownMsgDataError #class Test2_specTestCase: #def setUp(self): # self.foo = Test2_spec() # #def tearDown(self): # self.foo.dispose() # self.foo = None def test_create(): # root td1 = ArgSpecGiven( dict() ) ai1 = ArgsInfo(('t1',), td1, None) assert ai1.isComplete() assert ai1.numArgs() == 0 assert ai1.getArgs() == () assert ai1.getCompleteAI() is ai1 # sub, complete td2 = ArgSpecGiven( argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'), reqdArgs = ('arg2',)) ai2 = ArgsInfo(('t1','st1'), td2, ai1) assert ai2.isComplete() assert ai2.numArgs() == 2 assert ai2.getArgs() == ('arg1', 'arg2') assert ai2.getCompleteAI() is ai2 # sub, missing td2.argsSpecType = ArgSpecGiven.SPEC_GIVEN_NONE ai4 = ArgsInfo(('t1','st3'), td2, ai1) assert not ai4.isComplete() assert ai4.numArgs() == 0 assert ai4.getArgs() == () assert ai4.getCompleteAI() is ai1 # sub, of incomplete spec, given ALL args td3 = ArgSpecGiven( argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'), reqdArgs = ('arg2',)) ai5 = ArgsInfo(('t1','st3','sst1'), td3, ai4) assert ai5.isComplete() assert ai5.numArgs() == 2 assert ai5.hasSameArgs('arg1', 'arg2') assert ai5.getCompleteAI() is ai5 def test_update(): td1 = ArgSpecGiven( dict() ) td2 = ArgSpecGiven() td4 = ArgSpecGiven() td5 = ArgSpecGiven( argsDocs = dict( arg1='doc for arg1', arg2='doc for arg2', arg3='doc for arg3', arg4='doc for arg4'), reqdArgs = ('arg4','arg2')) ai1 = ArgsInfo(('t1',), td1, None) # root, complete ai2 = ArgsInfo(('t1','st1'), td2, ai1) # sub 1, empty ai4 = ArgsInfo(('t1','st1','sst2'), td4, ai2) # empty sub of sub 1 ai5 = ArgsInfo(('t1','st1','sst3'), td5, ai2) # completed sub of sub 1 # check assumptions before we start: assert not ai2.isComplete() assert not ai4.isComplete() assert ai5.isComplete() assert ai2.numArgs() == 0 assert ai4.numArgs() == 0 assert ai5.numArgs() == 4 # pretend we have an update for ai2: all args now available ai2.updateAllArgsFinal( ArgSpecGiven( dict(arg1='doc for arg1', arg2='doc for arg2'), ('arg2',)) ) assert ai2.isComplete() assert ai2.numArgs() == 2 assert ai2.hasSameArgs('arg1', 'arg2') assert ai2.getCompleteAI() is ai2 assert not ai4.isComplete() assert ai2.numArgs() == 2 assert ai4.numArgs() == 0 assert ai5.numArgs() == 4 assert ai4.getCompleteAI() is ai2 assert ai2.hasSameArgs('arg1', 'arg2') assert ai5.hasSameArgs('arg1', 'arg2', 'arg3', 'arg4') def test_filter(): td = ArgSpecGiven( argsDocs = dict(arg1='doc for arg1', arg2='doc for arg2'), reqdArgs = ('arg2',)) ai = ArgsInfo(('t1',), td, None) # check: argsMissingReqd = {} pytest.raises(SenderMissingReqdMsgDataError, ai.check, argsMissingReqd) argsExtraOpt = dict(arg2=2, arg5=5) pytest.raises(SenderUnknownMsgDataError, ai.check, argsExtraOpt) args = dict(arg1=1, arg2=2) ai.check(args) # filter: msgArgs = dict(arg1=1, arg2=2) argsOK = msgArgs.copy() assert ai.filterArgs( msgArgs ) == argsOK msgArgs.update(arg3=3, arg4=4) assert ai.filterArgs( msgArgs ) == argsOK pypubsub-4.0.3/tests/suite/test2a_topic.py000066400000000000000000000106501342344706700206500ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import pytest from pubsub.core.topicobj import Topic from pubsub.core.topicmgr import TreeConfig from pubsub.core.topicutils import ALL_TOPICS from pubsub.core.topicargspec import ArgsInfo, ArgSpecGiven from pubsub.core.listener import ListenerMismatchError from pubsub.core.topicexc import MessageDataSpecError rootTopic = None treeConfig = TreeConfig() def test_CreateRoot(): # # Test create and then modify state of a topic object # nameTuple = ('root',) description = 'root description' msgArgsInfo = None # when parent is None, only nameTuple=ALL_TOPICS is allowed, thereby # guaranteeing that only one tree root can be created pytest.raises(ValueError, Topic, treeConfig, nameTuple, description, msgArgsInfo) # create the ALL TOPICS topic; it has no message args nameTuple = (ALL_TOPICS,) argSpec = ArgSpecGiven( dict() ) msgArgsInfo = ArgsInfo(nameTuple, argSpec, None) obj = Topic(treeConfig, nameTuple, description, msgArgsInfo) # verify its state is as expected after creation: assert obj.getListeners() == [] assert obj.getNumListeners() == 0 assert obj.hasListeners() == False def listener1(): pass def listener2(): pass def badListener1(arg1): pass # extra required arg def listener3(arg1=None): pass # extra is optional assert obj.isValid(listener1) assert not obj.isValid(badListener1) assert obj.isValid(listener3) global rootTopic rootTopic = obj def test_SubUnsub(): # # Test subscription and unsubscription of listeners # def listener1(): pass def listener2(): pass obj = rootTopic # now modify its state by subscribing listeners obj.subscribe(listener1) obj.subscribe(listener2) obj.hasListener(listener1) obj.hasListener(listener2) assert obj.hasListeners() == True assert set(obj.getListenersIter()) == set([listener1, listener2]) assert obj.getNumListeners() == 2 # try to subscribe an invalid listener def badListener(arg1): pass # extra required arg pytest.raises(ListenerMismatchError, obj.subscribe, badListener) # try unsubscribe obj.unsubscribe(listener1) assert obj.hasListeners() assert obj.getListeners() == [listener2] assert obj.getNumListeners() == 1 # try unsubscribe all, with filtering obj.subscribe(listener1) def listener3(): pass obj.subscribe(listener3) assert obj.getNumListeners() == 3 def ff(listener): # use != since it is defined in terms of ==; also, put listener # on RHS to verify works even when Listener used on RHS return listener2 != listener obj.unsubscribeAllListeners(filter=ff) assert obj.getNumListeners() == 1 assert obj.getListeners() == [listener2] obj.subscribe(listener1) obj.subscribe(listener3) assert obj.getNumListeners() == 3 obj.unsubscribeAllListeners() assert obj.getNumListeners() == 0 def test_CreateChild(): # # Test creation of a child topic, subscription of listeners # nameTuple = ('childOfAll',) description = 'child description' argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc') reqdArgs = ('arg2',) argSpec = ArgSpecGiven(argsDocs=argsDocs, reqdArgs = reqdArgs) msgArgsInfo = ArgsInfo(nameTuple, argSpec, rootTopic._getListenerSpec()) parent = Topic(treeConfig, nameTuple, description, msgArgsInfo, parent=rootTopic) assert parent.getParent() is rootTopic # now create a child of child with wrong arguments so we can test exceptions nameTuple = ('childOfAll', 'grandChild') description = 'grandchild description' def tryCreate(ad, r): argSpec = ArgSpecGiven(argsDocs=ad, reqdArgs = r) msgArgsInfo = ArgsInfo(nameTuple, argSpec, parent._getListenerSpec()) obj = Topic(treeConfig, nameTuple, description, msgArgsInfo, parent=parent) # test when all OK argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc') reqdArgs = ('arg2',) tryCreate(argsDocs, reqdArgs) # test when requiredArg wrong reqdArgs = ('arg3',) pytest.raises(MessageDataSpecError, tryCreate, argsDocs, reqdArgs) reqdArgs = () pytest.raises(MessageDataSpecError, tryCreate, argsDocs, reqdArgs) # test when missing opt arg argsDocs = dict(arg1='arg1 desc', arg2='arg2 desc') reqdArgs = ('arg2',) pypubsub-4.0.3/tests/suite/test2b_topicmgr.py000066400000000000000000000406221342344706700213610ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import io import sys import pytest from pubsub import pub from pubsub.pub import ( ALL_TOPICS, MessageDataSpecError, TopicNameError, TopicDefnError, ) from pubsub.core import ( ITopicDefnProvider, TopicTreeTraverser, TreeTraversal, ) from pubsub.core.topicmgr import ( ArgSpecGiven, ) from pubsub.core.topicutils import ( TopicNameError, validateName, ) topicMgr = pub.getDefaultTopicMgr() from pubsub.utils.topictreeprinter import ( printTreeDocs, ITopicTreeVisitor, ) class TestTopicMgr0_Basic: """ Only tests TopicMgr methods. This must use some query methods on topic objects to validate that TopicMgr did it's job properly. """ def failTopicName(self, name): pytest.raises(TopicNameError, validateName, name) def test_GoodTopicNames(self): # # Test that valid topic names are accepted by pubsub' # validateName('test.asdf') validateName('test.a') validateName('test.a.b') def test_BadTopicNames(self): # # Test that invalid topic names are rejected by pubsub # # parts of topic name are 'empty' self.failTopicName( '' ) self.failTopicName( ('',) ) self.failTopicName( ('test','asdf','') ) self.failTopicName( ('test','a', None) ) # parts of topic name have invalid char self.failTopicName( ('test','a','b','_') ) self.failTopicName( ('(aa',) ) self.failTopicName( (ALL_TOPICS,) ) def test_clear(self): topicMgr.getOrCreateTopic('topic1') topicMgr.getOrCreateTopic('topic2') topicMgr.getOrCreateTopic('topic3') topicMgr.getOrCreateTopic('topic4') topicMgr.getOrCreateTopic('topic3.topic31') topicMgr.getOrCreateTopic('topic3.topic32') topicMgr.getOrCreateTopic('topic4') assert set(t.name for t in topicMgr.getRootAllTopics().subtopics) == set(['topic1', 'topic2', 'topic3', 'topic4']) topicMgr.clearTree() assert list(topicMgr.getRootAllTopics().subtopics) == [] class TestTopicMgr1_GetOrCreate_NoDefnProv: """Only tests TopicMgr methods. This must use some query methods on topic objects to validate that TopicMgr did it's job properly.""" def test_NoProtoListener(self): # # Test the getOrCreateTopic without proto listener # def verifyNonSendable(topicObj, nameTuple, parent): """Any non-sendable topic will satisfy these conditions:""" assert not topicMgr.hasTopicDefinition(nameTuple) assert topicMgr.isTopicInUse(nameTuple) assert not topicObj.hasMDS() assert topicObj.getListeners() == [] assert topicObj.getNameTuple() == nameTuple assert topicObj.getNumListeners() == 0 assert topicObj.getParent() is parent assert topicObj.getNodeName() == topicObj.getNameTuple()[-1] def foobar(): pass assert not topicObj.hasListener(foobar) assert not topicObj.hasListeners() assert not topicObj.hasSubtopic('asdfafs') assert not topicObj.isAll() pytest.raises(TopicDefnError, topicObj.isValid, foobar) pytest.raises(TopicDefnError, topicObj.validate, foobar) # check that getTopic and getOrCreateTopic won't create again: assert topicMgr.getOrCreateTopic(nameTuple) is topicObj assert topicMgr.getTopic(nameTuple) is topicObj # test with a root topic rootName = 'GetOrCreate_NoProtoListener' tName = rootName # verify doesn't exist yet assert topicMgr.getTopic(tName, True) is None # ok create it, unsendable rootTopic = topicMgr.getOrCreateTopic(tName) verifyNonSendable(rootTopic, (rootName,), topicMgr.getRootAllTopics()) DESC_NO_SPEC = 'UNDOCUMENTED: created without spec' assert rootTopic.getDescription() == DESC_NO_SPEC assert rootTopic.isRoot() assert list(rootTopic.getSubtopics()) == [] assert not rootTopic.isAll() assert not rootTopic.hasSubtopic() # test with a subtopic tName1 = (rootName, 'stB') tName2 = tName1 + ('sstC',) assert topicMgr.getTopic(tName1, True) is None assert topicMgr.getTopic(tName2, True) is None subsubTopic = topicMgr.getOrCreateTopic(tName2) # verify that parent was created implicitly subTopic = topicMgr.getTopic(tName1) verifyNonSendable(subTopic, tName1, rootTopic) verifyNonSendable(subsubTopic, tName2, subTopic) assert subsubTopic.getDescription() == DESC_NO_SPEC DESC_PARENT_NO_SPEC = 'UNDOCUMENTED: created as parent without specification' assert subTopic.getDescription() == DESC_PARENT_NO_SPEC assert list(rootTopic.getSubtopics()) == [subTopic] assert rootTopic.hasSubtopic() assert list(subTopic.getSubtopics()) == [subsubTopic] assert subTopic.hasSubtopic() assert list(subsubTopic.getSubtopics()) == [] assert not subsubTopic.hasSubtopic() # check that getTopic raises expected exception when undefined topic: tName = 'Undefined' pytest.raises(TopicNameError, topicMgr.getTopic, tName) tName = rootName + '.Undefined' pytest.raises(TopicNameError, topicMgr.getTopic, tName) def test_WithProtoListener(self): # # Test the getOrCreateTopic with proto listener # rootName = 'GetOrCreate_WithProtoListener' tName = rootName # verify doesn't exist yet assert topicMgr.getTopic(tName, True) is None def protoListener(arg1, arg2=None): pass # ok create it, sendable rootTopic = topicMgr.getOrCreateTopic(tName, protoListener) # check that getTopic and getOrCreateTopic won't create again: assert topicMgr.getOrCreateTopic(tName) is rootTopic assert topicMgr.getTopic(tName) is rootTopic assert rootTopic.hasMDS() assert topicMgr.hasTopicDefinition(tName) expectDesc = 'UNDOCUMENTED: created from protoListener "protoListener" in module test2b_topicmgr' assert rootTopic.getDescription() == expectDesc #print '*** DESC ***', rootTopic.getDescription() # check that topic created can discern between good and bad listener assert rootTopic.isValid(protoListener) def badListener1(): pass # missing required arg def badListener2(arg2): pass # opt arg is required def badListener3(arg1, arg3): pass # extra required arg assert not rootTopic.isValid(badListener1) assert not rootTopic.isValid(badListener2) assert not rootTopic.isValid(badListener3) # verify that missing parent created is not sendable, child is def protoListener2(arg1, arg2=None): pass tName = (tName, 'stA', 'sstB') subsubTopic = topicMgr.getOrCreateTopic(tName, protoListener2) subTopic = topicMgr.getTopic( tName[:-1] ) assert not topicMgr.hasTopicDefinition( tName[:-1] ) assert topicMgr.hasTopicDefinition( tName ) assert subsubTopic.isValid(protoListener2) class TestTopicMgr2_GetOrCreate_DefnProv: """ Test TopicManager when one or more definition providers can provide for some topic definitions. """ def test_DefnProvider(self): # # Test the addition and clearing of definition providers # class DefnProvider(ITopicDefnProvider): pass dp1 = DefnProvider() dp2 = DefnProvider() topicMgr.addDefnProvider(dp1) assert 1 == topicMgr.getNumDefnProviders() topicMgr.addDefnProvider(dp1) assert 1 == topicMgr.getNumDefnProviders() topicMgr.addDefnProvider(dp2) assert 2 == topicMgr.getNumDefnProviders() topicMgr.addDefnProvider(dp2) assert 2 == topicMgr.getNumDefnProviders() topicMgr.addDefnProvider(dp1) assert 2 == topicMgr.getNumDefnProviders() topicMgr.clearDefnProviders() assert 0 == topicMgr.getNumDefnProviders() topicMgr.addDefnProvider(dp1) assert 1 == topicMgr.getNumDefnProviders() topicMgr.clearDefnProviders() def test_UseProvider(self): # # Test the use of definition providers for topics. We create # two so we can check that more than one can work together. # One provides good definitions, one provides some with errors. # class DefnProvider(ITopicDefnProvider): """ Provide definitions for a root topic, subtopic, and one subtopic whose parent is not defined here. It is easier to use sub-only definitions. """ def __init__(self): self.defns = { ('a',) : (dict(arg1='arg1 desc', arg2='arg2 desc'), ('arg1',) ), ('a', 'b') : (dict(arg1='arg1 desc', arg2='arg2 desc', arg3='arg3 desc', arg4='arg2 desc'), ('arg1', 'arg3',) ), # parent doesn't have defn ('a', 'c', 'd') : ( dict(arg1='arg1 desc', arg2='arg2 desc', arg3='arg3 desc', arg4='arg4 desc', arg5='arg5 desc', arg6='arg6 desc'), ('arg1', 'arg3', 'arg5',)), } def getDefn(self, topicNameTuple): if topicNameTuple not in self.defns: return None, None defn = ArgSpecGiven() defn.setAll( * self.defns[topicNameTuple] ) desc = '%s desc' % '.'.join(topicNameTuple) return desc, defn class DefnProviderErr(ITopicDefnProvider): """ Provide some definitions that have wrong arg spec. It is easier to use the 'all-spec' for definitions, which provides an opportunity for a different method of ArgSpecGiven. """ def __init__(self): self.defns = { ('a', 'err1') : (# missing arg2 dict(arg1=''), ('arg1',) ), ('a', 'err2') : (# missing arg1 dict(arg2=''), ), ('a', 'err3') : (# arg1 is no longer required dict(arg1='', arg2=''), ), } def getDefn(self, topicNameTuple): if topicNameTuple not in self.defns: return None, None defn = ArgSpecGiven() defn.setAll( * self.defns[topicNameTuple] ) desc = '%s desc' % '.'.join(topicNameTuple) return desc, defn topicMgr.addDefnProvider( DefnProvider() ) topicMgr.addDefnProvider( DefnProviderErr() ) # create some topics that will use defn provider topic = topicMgr.getOrCreateTopic('a') assert topic.getDescription() == 'a desc' assert topic.hasMDS() topic = topicMgr.getOrCreateTopic('a.b') assert topic.getDescription() == 'a.b desc' assert topic.hasMDS() topic = topicMgr.getOrCreateTopic('a.c.d') assert topic.getDescription() == 'a.c.d desc' assert topic.hasMDS() assert not topicMgr.hasTopicDefinition('a.c') # check parent = topicMgr.getTopic('a.c') assert not parent.hasMDS() def protoListener(arg1, arg3, arg2=None, arg4=None): pass parent = topicMgr.getOrCreateTopic('a.c', protoListener) assert parent.hasMDS() assert topic.hasMDS() # now the erroneous ones: def testRaises(topicName, expectMsg): pytest.raises(MessageDataSpecError, topicMgr.getOrCreateTopic, topicName) try: assert topicMgr.getOrCreateTopic(topicName) is None except MessageDataSpecError: # ok, did raise but is it correct message? exc = sys.exc_info()[1] try: str(exc).index(expectMsg) except ValueError: msg = 'Wrong message, expected \n "%s", got \n "%s"' raise RuntimeError(msg % (expectMsg, str(exc)) ) testRaises('a.err1', 'Params [arg1] missing inherited [arg2] for topic "a.err1"') testRaises('a.err2', 'Params [arg2] missing inherited [arg1] for topic "a.err2"') testRaises('a.err3', 'Params [] missing inherited [arg1] for topic "a.err3" required args') def test_DelTopic(self): # # Test topic deletion # topicMgr.getOrCreateTopic('delTopic.b.c.d.e') assert topicMgr.getTopic('delTopic.b.c.d.e') is not None assert topicMgr.getTopic('delTopic.b.c.d').hasSubtopic('e') assert topicMgr.getTopic('delTopic.b').hasSubtopic('c') topicMgr.delTopic('delTopic.b.c') assert not topicMgr.getTopic('delTopic.b').hasSubtopic('c') assert topicMgr.getTopic('delTopic.b.c.d.e', okIfNone=True) is None assert topicMgr.getTopic('delTopic.b.c.d', okIfNone=True) is None assert topicMgr.getTopic('delTopic.b.c', okIfNone=True) is None class TestTopicMgr3_TreeTraverser: expectedOutput = '''\ \\-- Topic "a2" \\-- Topic "a" \\-- Topic "a" \\-- Topic "b" \\-- Topic "b" \\-- Topic "a" \\-- Topic "b"''' def test1(self): # # Test printing of topic tree # root = topicMgr.getOrCreateTopic('a2') topicMgr.getOrCreateTopic('a2.a.a') topicMgr.getOrCreateTopic('a2.a.b') topicMgr.getOrCreateTopic('a2.b.a') topicMgr.getOrCreateTopic('a2.b.b') buffer = io.StringIO() printTreeDocs(rootTopic=root, width=70, fileObj=buffer) #print buffer.getvalue() assert buffer.getvalue() == self.expectedOutput def test2(self): # # Test traversing with and without filtering, breadth and depth # class MyTraverser(ITopicTreeVisitor): def __init__(self): self.traverser = TopicTreeTraverser(self) self.calls = '' self.topics = [] def traverse(self, rootTopic, **kwargs): self.traverser.traverse(rootTopic, **kwargs) def __append(self, val): self.calls = self.calls + str(val) def _startTraversal(self): self.__append(1) def _accept(self, topicObj): self.__append(2) # only accept topics at root or second level tree, or if tailName() is 'A' return len(topicObj.getNameTuple()) <= 2 or topicObj.getNodeName() == 'A' def _onTopic(self, topicObj): self.__append(3) self.topics.append(topicObj.getNodeName()) def _startChildren(self): self.__append(4) def _endChildren(self): self.__append(5) def _doneTraversal(self): self.__append(6) root = topicMgr.getOrCreateTopic('traversal') topicMgr.getOrCreateTopic('traversal.a.A') topicMgr.getOrCreateTopic('traversal.a.B.foo') topicMgr.getOrCreateTopic('traversal.b.C') topicMgr.getOrCreateTopic('traversal.b.D.bar') def exe(expectCalls, expectTopics, **kwargs): traverser = MyTraverser() traverser.traverse(root, **kwargs) #print traverser.calls #print traverser.topics assert set(traverser.topics) == set(expectTopics) assert set(traverser.calls) == set(expectCalls) exe(expectCalls = '13434345343455534345343455556', expectTopics = ['traversal', 'a', 'A', 'B', 'foo', 'b', 'C', 'D', 'bar'], onlyFiltered = False) exe(expectCalls = '13433543354335454354543545456', expectTopics = ['traversal', 'a', 'b', 'A', 'B', 'C', 'D', 'foo', 'bar'], how = TreeTraversal.BREADTH, onlyFiltered = False) exe(expectCalls = '123423423452523422556', expectTopics = ['traversal','a','A','b']) exe(expectCalls = '123423235423254225456', expectTopics = ['traversal','a','b','A'], how = TreeTraversal.BREADTH) pypubsub-4.0.3/tests/suite/test2c_notify.py000066400000000000000000000112731342344706700210460ustar00rootroot00000000000000""" This one tests NotifyByWriteFile and custom notification handler :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import io import gc from difflib import unified_diff # setup notification and logging from pubsub import pub from pubsub.utils.notification import useNotifyByWriteFile from pubsub.core import INotificationHandler topicMgr = pub.getDefaultTopicMgr() def captureStdout(): capture = io.StringIO() useNotifyByWriteFile( fileObj = capture ) return capture def testNotifyByPrint(): capture = captureStdout() def listener1(arg1): pass pub.subscribe(listener1, 'baz') pub.sendMessage('baz', arg1=123) pub.unsubscribe(listener1, 'baz') def doa(): def listener2(): pass pub.subscribe(listener2, 'bar') doa() # listener2 should be gc'd gc.collect() # for pypy: the gc doesn't work the same as cpython's topicMgr.delTopic('baz') expect = """\ PUBSUB: New topic "baz" created PUBSUB: Subscribed listener "listener1" to topic "baz" PUBSUB: Start sending message of topic "baz" PUBSUB: Sending message of topic "baz" to listener listener1 PUBSUB: Done sending message of topic "baz" PUBSUB: Unsubscribed listener "listener1" from topic "baz" PUBSUB: New topic "bar" created PUBSUB: Subscribed listener "listener2" to topic "bar" PUBSUB: Listener "listener2" of Topic "bar" has died PUBSUB: Topic "baz" destroyed """ captured = capture.getvalue() #print captured #print repr(expect) assert captured == expect, \ '\n'.join( unified_diff(expect.splitlines(), captured.splitlines(), n=0) ) def testFlagChanges(): savedFlags = pub.getNotificationFlags() pub.setNotificationFlags(all=True, sendMessage=False, deadListener=False) flags = pub.getNotificationFlags() assert not flags['sendMessage'] assert not flags['deadListener'] assert flags['newTopic'] assert flags['delTopic'] assert flags['subscribe'] assert flags['unsubscribe'] pub.setNotificationFlags(subscribe=False, deadListener=True) flags = pub.getNotificationFlags() assert not flags['sendMessage'] assert not flags['subscribe'] assert flags['newTopic'] assert flags['delTopic'] assert flags['deadListener'] assert flags['unsubscribe'] pub.setNotificationFlags(all=False, subscribe=True, unsubscribe=True) flags = pub.getNotificationFlags() assert not flags['sendMessage'] assert not flags['deadListener'] assert not flags['newTopic'] assert not flags['delTopic'] assert flags['subscribe'] assert flags['unsubscribe'] pub.setNotificationFlags(** savedFlags) def testNotifications(): class Handler(INotificationHandler): def __init__(self): self.resetCounts() def resetCounts(self): self.counts = dict(send=0, sub=0, unsub=0, delt=0, newt=0, dead=0, all=0) def notifySubscribe(self, pubListener, topicObj, newSub): self.counts['sub'] += 1 def notifyUnsubscribe(self, pubListener, topicObj): self.counts['unsub'] += 1 def notifyDeadListener(self, pubListener, topicObj): self.counts['dead'] += 1 def notifySend(self, stage, topicObj, pubListener=None): if stage == 'pre': self.counts['send'] += 1 def notifyNewTopic(self, topicObj, description, required, argsDocs): self.counts['newt'] += 1 def notifyDelTopic(self, topicName): self.counts['delt'] += 1 notifiee = Handler() pub.addNotificationHandler(notifiee) pub.setNotificationFlags(all=True) def verify(**ref): gc.collect() # for pypy: the gc doesn't work the same as cpython's for key, val in notifiee.counts.items(): if key in ref: assert val == ref[key], "\n%s\n%s" % (notifiee.counts, ref) else: assert val == 0, "%s = %s, expected 0" % (key, val) notifiee.resetCounts() verify() def testListener(): pass def testListener2(): pass def testListener3(): pass class TestListener: def __call__(self): pass def __del__(self): pass testListener = TestListener() topicMgr = pub.getDefaultTopicMgr() topicMgr.getOrCreateTopic('newTopic') verify(newt=1) pub.subscribe(testListener, 'newTopic') pub.subscribe(testListener2, 'newTopic') pub.subscribe(testListener3, 'newTopic') verify(sub=3) pub.sendMessage('newTopic') verify(send=1) verify(dead=0) del testListener del testListener3 verify(dead=2) pub.unsubscribe(testListener2,'newTopic') verify(unsub=1) topicMgr.delTopic('newTopic') verify(delt=1) pypubsub-4.0.3/tests/suite/test2c_notify2.py000066400000000000000000000065211342344706700211300ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from pubsub import pub from pubsub.utils.notification import useNotifyByPubsubMessage topicMgr = pub.getDefaultTopicMgr() def test_NotificationTopics(): assert not topicMgr.getTopic('pubsub', okIfNone=True) useNotifyByPubsubMessage() assert topicMgr.getTopic('pubsub') assert topicMgr.getTopic('pubsub').hasSubtopic() pubsubTopicNames = [obj.getName() for obj in topicMgr.getTopic('pubsub').getSubtopics()] assert ( set( pubsubTopicNames ) == set(['pubsub.sendMessage', 'pubsub.deadListener', 'pubsub.subscribe', 'pubsub.unsubscribe', 'pubsub.newTopic', 'pubsub.delTopic'])) def test_SubscribeNotify(): class MyListener: countSub = 0 countUnsub = 0 def listenerSub(self, msgTopic=pub.AUTO_TOPIC, listener=None, topic=None, newSub=None): assert msgTopic.getName() == 'pubsub.subscribe' assert topic.getName() in ('pubsub.unsubscribe', 'testSubscribeNotify') if newSub: self.countSub += 1 def listenerUnsub(self, msgTopic=pub.AUTO_TOPIC, topic=None, listener=None, listenerRaw=None): assert topic.getName() in ('testSubscribeNotify', 'pubsub.subscribe' ) assert msgTopic.getName() == 'pubsub.unsubscribe' if listener is not None: self.countUnsub += 1 def listenerTest(self): raise NotImplementedError # should never get here pub.setNotificationFlags(subscribe=True, unsubscribe=True) topicMgr.getOrCreateTopic('testSubscribeNotify') tmp = MyListener() pub.subscribe(tmp.listenerSub, 'pubsub.subscribe') assert tmp.countSub == 0 # don't notify of self subscription assert tmp.countUnsub == 0 sl, ok = pub.subscribe(tmp.listenerUnsub, 'pubsub.unsubscribe') assert ok assert tmp.countSub == 1 assert tmp.countUnsub == 0 pub.subscribe(tmp.listenerTest, 'testSubscribeNotify') #assert_equal(tmp.countSub, 2) assert tmp.countUnsub == 0 pub.unsubscribe(tmp.listenerTest, 'testSubscribeNotify') #assert_equal(tmp.countSub, 2) assert tmp.countUnsub == 1 pub.unsubscribe(tmp.listenerSub, 'pubsub.subscribe') assert tmp.countSub == 2 assert tmp.countUnsub == 2 pub.unsubscribe(tmp.listenerUnsub, 'pubsub.unsubscribe') assert tmp.countSub == 2 assert tmp.countUnsub == 2 # don't notify of self unsubscription def test_SendNotify(): # trap the pubsub.sendMessage topic: class SendHandler: def __init__(self): self.pre = self.post = 0 def __call__(self, topic=None, stage=None, listener=None, msgTopic=pub.AUTO_TOPIC): if stage == 'pre': self.pre += 1 else: self.post += 1 assert msgTopic.getName() == 'pubsub.sendMessage' assert topic.getName() == 'testSendNotify' sh = SendHandler() pub.subscribe(sh, 'pubsub.sendMessage') pub.setNotificationFlags(sendMessage=True) # generate a message that will cause pubsub.sendMessage to be generated too assert sh.pre == 0 assert sh.post == 0 topicMgr.getOrCreateTopic('testSendNotify') pub.sendMessage('testSendNotify') assert sh.pre == 1 assert sh.post == 1 pypubsub-4.0.3/tests/suite/test2d_except.py000066400000000000000000000114521342344706700210260ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import gc import pytest from pubsub import pub topicMgr = pub.getDefaultTopicMgr() def throws(): raise RuntimeError('test') def testHandleExcept1a(): from pubsub.utils.exchandling import ExcPublisher excPublisher = ExcPublisher( pub.getDefaultTopicMgr() ) pub.setListenerExcHandler(excPublisher) # create a listener that raises an exception: from raisinglistener import getRaisingListener raisingListener = getRaisingListener() pub.setNotificationFlags(all=False) pub.subscribe(raisingListener, 'testHandleExcept1a') # first test when a listener raises an exception and exception listener also raises! class BadUncaughtExcListener: def __call__(self, listenerStr=None, excTraceback=None): raise RuntimeError('bad exception listener!') handler = BadUncaughtExcListener() pub.subscribe(handler, ExcPublisher.topicUncaughtExc) pytest.raises(pub.ExcHandlerError, pub.sendMessage, 'testHandleExcept1a') pub.unsubscribe(handler, ExcPublisher.topicUncaughtExc) def testHandleExcept1b(): # create a listener that raises an exception: from raisinglistener import getRaisingListener raisingListener = getRaisingListener() pub.subscribe(raisingListener, 'testHandleExcept1b') # subscribe a good exception listener and validate # create the listener for uncaught exceptions in listeners: class UncaughtExcListener: def __call__(self, listenerStr=None, excTraceback=None): # verify that information received; first the listenerStr assert listenerStr.startswith('raisingListener') # next the traceback: tb = excTraceback.traceback assert len(tb) == 2 def validateTB(tbItem, eFN, eLine, eFnN): assert tbItem[0].endswith(eFN), '%s !~ %s' % (tbItem[0], eFN) assert tbItem[1] == eLine assert tbItem[2] == eFnN validateTB(tb[0], 'raisinglistener.py', 5, 'raisingListener') validateTB(tb[1], 'raisinglistener.py', 4, 'nested') # next the formatted traceback: assert len( excTraceback.getFormattedList() ) == len(tb)+1 # finally the string for formatted traceback: msg = excTraceback.getFormattedString() #print 'Msg "%s"' % msg assert msg.startswith(' File') assert msg.endswith("name 'RuntimeError2' is not defined\n") from pubsub.utils.exchandling import ExcPublisher topic = topicMgr.getTopic( ExcPublisher.topicUncaughtExc ) assert not topic.hasListeners() handler = UncaughtExcListener() pub.subscribe(handler, ExcPublisher.topicUncaughtExc) pub.sendMessage('testHandleExcept1b') # verify that listener isn't stuck in a cyclic reference by sys.exc_info() del raisingListener gc.collect() # for pypy: the gc doesn't work the same as cpython's assert not topicMgr.getTopic('testHandleExcept1b').hasListeners() pub.unsubscribe(handler, ExcPublisher.topicUncaughtExc) def testHandleExcept2(): #Test sendMessage when one handler, then change handler and verify changed testTopic = 'testTopics.testHandleExcept2' pub.subscribe(throws, testTopic) pub.setListenerExcHandler(None) #pubsub.utils.notification.useNotifyByWriteFile() #assert_equal( topicMgr.getTopic(testTopic).getNumListeners(), 1 ) expect = None def validate(className): global expect assert expect == className expect = None class MyExcHandler: def __call__(self, listener, topicObj): validate(self.__class__.__name__) class MyExcHandler2: def __call__(self, listener, topicObj): validate(self.__class__.__name__) def doHandling(HandlerClass): global expect expect = HandlerClass.__name__ #'MyExcHandler' excHandler = HandlerClass() pub.setListenerExcHandler(excHandler) pub.sendMessage(testTopic) assert expect is None doHandling(MyExcHandler) doHandling(MyExcHandler2) # restore to no handling and verify: pub.setListenerExcHandler(None) pytest.raises( RuntimeError, pub.sendMessage, testTopic) def testNoExceptionHandling1(): pub.setListenerExcHandler(None) def raises(): raise RuntimeError('test') topicMgr.getOrCreateTopic('testNoExceptionTrapping') pub.subscribe(raises, 'testNoExceptionTrapping') pytest.raises( RuntimeError, pub.sendMessage, 'testNoExceptionTrapping') def testNoExceptionHandling2(): testTopic = 'testTopics.testNoExceptionHandling' pub.subscribe(throws, testTopic) assert pub.getListenerExcHandler() is None pytest.raises( RuntimeError, pub.sendMessage, testTopic) pypubsub-4.0.3/tests/suite/test3b_sel_default.py000066400000000000000000000001161342344706700220170ustar00rootroot00000000000000def test_import(): from pubsub import pub assert pub.VERSION_API == 4 pypubsub-4.0.3/tests/suite/test3c_pubsub3.py000066400000000000000000000372461342344706700211320ustar00rootroot00000000000000""" Except for one test, this file tests with auto-creation of topics disabled, as it is more rigorous for testing purposes. :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import gc import pytest import pubsub.core.topicargspec from pubsub import pub from pubsub.utils.notification import IgnoreNotificationsMixin from pubsub.core import getListenerID, ListenerMismatchError topicMgr = pub.getDefaultTopicMgr() def testDOAListenerPubsub(): # Verify that a 'temporary' listener (one that will be garbage collected # as soon as subscribe() returns because there are no strong references to # it) gets immediately unregistered def listener(): pass class Wrapper: def __init__(self, func): self.func = func def __call__(self): pass pub.subscribe( Wrapper(listener), 'testDOAListenerPubsub') gc.collect() # for pypy: the gc doesn't work the same as cpython's assert not topicMgr.getTopic('testDOAListenerPubsub').hasListeners() assert pub.isValid(listener, 'testDOAListenerPubsub') def testDeadListener(): # create a listener for listeners that have died class DeathListener(IgnoreNotificationsMixin): listenerStr = '' def notifyDeadListener(self, pubListener, topicObj): assert topicObj.getName() == 'sadTopic' #import pdb; pdb.set_trace() #print 'hi again' DeathListener.listenerStr = pubListener.name() dl = DeathListener() pub.addNotificationHandler( dl ) pub.setNotificationFlags(deadListener=True) # define a topic, subscribe to it, and kill its listener: class TempListener: def __call__(self, **kwargs): pass def __del__(self): # print 'being deleted' pass #def tempListener(): pass tempListener = TempListener() expectLisrStr, _ = getListenerID(tempListener) pub.subscribe(tempListener, 'sadTopic') del tempListener # verify: gc.collect() # for pypy: the gc doesn't work the same as cpython's assert DeathListener.listenerStr.startswith(expectLisrStr), \ '"%s" !~ "%s"' % (DeathListener.listenerStr, expectLisrStr) pub.addNotificationHandler(None) pub.clearNotificationHandlers() def testSubscribe(): topicName = 'testSubscribe' def proto(a, b, c=None): pass topicMgr.getOrCreateTopic(topicName, proto) def listener(a, b, c=None): pass # verify that pub.isValid() works too pub.validate(listener, topicName) assert pub.isValid(listener, topicName) assert topicMgr.getTopic(topicName).getNumListeners() == 0 assert topicMgr.getTopicsSubscribed(listener) == [] assert not pub.isSubscribed(listener, topicName) assert pub.subscribe(listener, topicName) assert pub.isSubscribed(listener, topicName) def topicNames(listener): return [t.getName() for t in topicMgr.getTopicsSubscribed(listener)] assert topicNames(listener) == [topicName] # should do nothing if already subscribed: assert not pub.subscribe(listener, topicName)[1] assert topicMgr.getTopic(topicName).getNumListeners() == 1 # test topicMgr.getTopicsSubscribed() pub.subscribe(listener, 'lt2', ) assert set(topicNames(listener)) == set([topicName,'lt2']) pub.subscribe(listener, 'lt1.lst1') assert set(topicNames(listener)) == set([topicName,'lt2','lt1.lst1']) # test ALL_TOPICS def listenToAll(): pass pub.subscribe(listenToAll, pub.ALL_TOPICS) assert topicNames(listenToAll) == [pub.ALL_TOPICS] # test type hints in listeners: def listenerWithHints(a: int, b: bool, c: str = 2): pass topicForHintedListeners = 'topicForHints' topicMgr.getOrCreateTopic(topicForHintedListeners, listenerWithHints) assert not pub.isSubscribed(listenerWithHints, topicForHintedListeners) pub.subscribe(listenerWithHints, topicForHintedListeners) assert pub.subscribe(listenerWithHints, topicForHintedListeners) assert pub.isSubscribed(listenerWithHints, topicForHintedListeners) def testMissingReqdArgs(): def proto(a, b, c=None): pass topicMgr.getOrCreateTopic('missingReqdArgs', proto) pytest.raises(pubsub.core.topicargspec.SenderMissingReqdMsgDataError, pub.sendMessage, 'missingReqdArgs', a=1) def testSendTopicWithMessage(): class MyListener: def __init__(self): self.count = 0 self.heardTopic = False self.listen2Topics = [] def listen0(self): pass def listen1(self, **kwarg): self.count += 1 self.heardTopic = True def listen2(self, msgTopic=pub.AUTO_TOPIC, **kwarg): self.listen2Topics.append(msgTopic.getName()) my = MyListener() pub.subscribe(my.listen0, 'testSendTopic') pub.subscribe(my.listen1, 'testSendTopic') pub.subscribe(my.listen2, 'testSendTopic') pub.sendMessage('testSendTopic') assert my.count == 1 assert my.heardTopic == True pub.subscribe(my.listen0, 'testSendTopic.subtopic') pub.subscribe(my.listen1, 'testSendTopic.subtopic') pub.subscribe(my.listen2, 'testSendTopic.subtopic') pub.sendMessage('testSendTopic.subtopic') assert my.count == 3 assert [] == [topic for topic in my.listen2Topics if topic not in ('testSendTopic', 'testSendTopic.subtopic')] # type hints on listeners: result = [] def listenerWithHints(a: int, b: bool, c: str = 2): result.append((a, b, c)) topicForHintedListeners = 'topicForHints' pub.subscribe(listenerWithHints, topicForHintedListeners) assert pub.subscribe(listenerWithHints, topicForHintedListeners) pub.sendMessage(topicForHintedListeners, b=456, a=123, c='hello') assert result == [(123, 456, 'hello')] def testOptionalArgs(): # first function registered determines topic MDS (message data spec) # here we first register a more "permissive" listener, ie one that has default values for args # that the second listener does not; therefore pubsub should refuse subscribing second listener # to same topic def myFunction1(arg1, arg2=2, *, kwarg1, kwarg2=4, **kwargs): pass def myFunction2(arg1, arg2, *, kwarg1, kwarg2=4, **kwargs): pass pub.subscribe(myFunction1, 'testKeywordOnlyArgs') pytest.raises(ListenerMismatchError, pub.subscribe, myFunction2, 'testKeywordOnlyArgs') def testKeywordOnlyArgsStar(): def capture(funcName, *args): result[funcName] = args def myFunction1(arg1, arg2, *, kwarg1, kwarg2=4, **kwargs): capture('myFunction1', arg1, arg2, kwarg1, kwarg2, kwargs) def myFunction2(arg1, arg2, *arg3, kwarg1, kwarg2=4, **kwargs): capture('myFunction2', arg1, arg2, arg3, kwarg1, kwarg2, kwargs) pub.subscribe(myFunction1, 'testKeywordOnlyArgsStar') pub.subscribe(myFunction2, 'testKeywordOnlyArgsStar') result = {} pub.sendMessage('testKeywordOnlyArgsStar', arg1=1, arg2=2, kwarg1=3) assert result == dict(myFunction1=(1, 2, 3, 4, {}), myFunction2=(1, 2, (), 3, 4, {})) def testKeywordOnlyArgsStarAfterOpt(): def capture(funcName, *args): result[funcName] = args def myFunction1(arg1, arg2=2, *, kwarg1, kwarg2=4, **kwargs): capture('myFunction1', arg1, arg2, kwarg1, kwarg2, kwargs) def myFunction2(arg1, arg2=2, *arg3, kwarg1, kwarg2=4, **kwargs): capture('myFunction2', arg1, arg2, arg3, kwarg1, kwarg2, kwargs) pub.subscribe(myFunction1, 'testKeywordOnlyArgsStarAfterOpt') pub.subscribe(myFunction2, 'testKeywordOnlyArgsStarAfterOpt') result = dict() pub.sendMessage('testKeywordOnlyArgsStarAfterOpt', arg1=1, kwarg1=3) assert result == dict(myFunction1=(1, 2, 3, 4, {}), myFunction2=(1, 2, (), 3, 4, {})) def testKeywordOnlyArgsNoDefaults(): def capture(funcName, *args): result[funcName] = args def myFunction1(*, kwarg1, kwarg2): capture('myFunction1', kwarg1, kwarg2) def myFunction2(*args, kwarg1, kwarg2): capture('myFunction2', args, kwarg1, kwarg2) def myFunction3(*, kwarg1, kwarg2, **kwargs): capture('myFunction3', kwarg1, kwarg2, kwargs) def myFunction4(*args, kwarg1, kwarg2, **kwargs): capture('myFunction4', args, kwarg1, kwarg2, kwargs) pub.subscribe(myFunction1, 'testKeywordOnlyArgsNoDefaults') pub.subscribe(myFunction2, 'testKeywordOnlyArgsNoDefaults') pub.subscribe(myFunction3, 'testKeywordOnlyArgsNoDefaults') pub.subscribe(myFunction4, 'testKeywordOnlyArgsNoDefaults') result = {} pub.sendMessage('testKeywordOnlyArgsNoDefaults', kwarg1=1, kwarg2=2) assert result == dict(myFunction1=(1, 2), myFunction2=((), 1, 2), myFunction3=(1, 2, {}), myFunction4=((), 1, 2, {})) def testAcceptAllArgs(): def listen(arg1=None): pass def listenAllArgs(arg1=None, **kwargs): pass def listenAllArgs2(arg1=None, msgTopic=pub.AUTO_TOPIC, **kwargs): pass pub.subscribe(listen, 'testAcceptAllArgs') pub.subscribe(listenAllArgs, 'testAcceptAllArgs') pub.subscribe(listenAllArgs2, 'testAcceptAllArgs') pub.subscribe(listenAllArgs2, 'testAcceptAllArgs.subtopic') pub.subscribe(listenAllArgs, 'testAcceptAllArgs.subtopic') def testSubscribeCurriedListeners(): result = [] def proto_listener(a, b, c=1, d=2): pass def listener1(a, b, c=1, d=2, nonTopicArg=3): result.append((a, b, c, d)) # no currying: pub.subscribe(proto_listener, 'topic1') pub.subscribe(listener1, 'topic1') # ok: pub.subscribe(proto_listener, 'topic2') pub.subscribe(listener1, 'topic2', nonTopicArg=4) # curried arg typo: pub.subscribe(proto_listener, 'topic3') pytest.raises(ListenerMismatchError, pub.subscribe, listener1, 'topic3', invalidArg=4) # curried arg is in topic args: pub.subscribe(proto_listener, 'topic4') pytest.raises(ListenerMismatchError, pub.subscribe, listener1, 'topic4', a=4) def testSendWhenCurriedArgs(): result = [] def listen(a, b, c, d=0, e=0): result.append((a, b, c, d, e)) wrapped = pub.subscribe(listen, 'testCurriedArgs', b=2, d=4) assert wrapped[0].curriedArgs pub.sendMessage('testCurriedArgs', a=1, c=3) assert result[0] == (1, 2, 3, 4, 0) def testUnsubAll(): def lisnr1(): pass def lisnr2(): pass class MyListener: def __call__(self): pass def meth(self): pass def __hash__(self): return 123 lisnr3 = MyListener() lisnr4 = lisnr3.meth def lisnrSub(listener=None, topic=None, newSub=None): pass pub.subscribe(lisnrSub, 'pubsub.subscribe') assert topicMgr.getTopic('pubsub.subscribe').getNumListeners() == 1 def subAll(): pub.subscribe(lisnr1, 'testUnsubAll') pub.subscribe(lisnr2, 'testUnsubAll') pub.subscribe(lisnr3, 'testUnsubAll') pub.subscribe(lisnr4, 'testUnsubAll') assert topicMgr.getTopic('testUnsubAll').getNumListeners() == 4 def filter(lisnr): passes = str(lisnr).endswith('meth') return passes # test unsub many non-pubsub topic listeners subAll() pub.unsubAll('testUnsubAll') assert topicMgr.getTopic('testUnsubAll').getNumListeners() == 0 assert topicMgr.getTopic('pubsub.subscribe').getNumListeners() == 1 # now same but with filter: subAll() unsubed = pub.unsubAll('testUnsubAll', listenerFilter=filter) assert topicMgr.getTopic('testUnsubAll').getNumListeners() == 3 assert topicMgr.getTopic('pubsub.subscribe').getNumListeners() == 1 # test unsub all listeners of all topics subAll() assert topicMgr.getTopic('testUnsubAll').getNumListeners() == 4 unsubed = pub.unsubAll(listenerFilter=filter) assert unsubed == [lisnr4] assert topicMgr.getTopic('testUnsubAll').getNumListeners() == 3 assert topicMgr.getTopic('pubsub.subscribe').getNumListeners() == 1 unsubed = set( pub.unsubAll() ) expect = set([lisnr1, lisnrSub, lisnr3, lisnr2]) # at least all the 'expected' ones were unsub'd; will be others if this # test is run after other unit tests in same py.test run assert unsubed >= expect def testSendForUndefinedTopic(): pub.sendMessage('testSendForUndefinedTopic') assert topicMgr.getTopic('testSendForUndefinedTopic') assert topicMgr.getTopic('testSendForUndefinedTopic').getArgs() == (None, None) # must also check for subtopics if parents have listeners since # filtering of args is affected def listener(): pass pub.subscribe(listener, 'testSendForUndefinedTopic') pub.sendMessage('testSendForUndefinedTopic.subtopic', msg='something') def testTopicUnspecifiedError(): pytest.raises(pub.TopicDefnError, pub.setTopicUnspecifiedFatal) pub.setTopicUnspecifiedFatal(checkExisting=False) def fn(): pass LSI = pub.TopicDefnError pytest.raises(LSI, pub.sendMessage, 'testTopicUnspecifiedError') pytest.raises(LSI, pub.subscribe, fn, 'testTopicUnspecifiedError') pub.setTopicUnspecifiedFatal(False) pub.sendMessage('testTopicUnspecifiedError') pub.subscribe(fn, 'testTopicUnspecifiedError') def testArgSpecDerivation(): def ok_0(): pass def ok_1(arg1): pass def err_11(arg1=None): pass # required can't become optional! def err_12(arg2): pass # parent's arg1 missing def ok_2(arg1=None): pass def ok_21(arg1): pass # optional can become required def err_22(arg2): pass # parent's arg1 missing # with getOrCreateTopic(topic, proto), the 'required args' set # is garanteed to be a subset of 'all args' topicMgr.getOrCreateTopic('tasd', ok_0) topicMgr.getOrCreateTopic('tasd.t_1', ok_1) pytest.raises( pub.MessageDataSpecError, topicMgr.getOrCreateTopic, 'tasd.t_1.t_11', err_11) pytest.raises( pub.MessageDataSpecError, topicMgr.getOrCreateTopic, 'tasd.t_1.t_12', err_12) topicMgr.getOrCreateTopic('tasd.t_2', ok_2) topicMgr.getOrCreateTopic('tasd.t_2.t_21', ok_21) pytest.raises( pub.MessageDataSpecError, topicMgr.getOrCreateTopic, 'tasd.t_2.t_22', err_22) print() def testListenerChangesListenerList(): """pubsub supports un/subscribing of listeners while sendMessage in progress. This requires that the TopicManager instance properly loop over listeners, via a copy of list instead of iterator on list (since lists can't be modified while iteration in progress. This test verifies that listener receiving message can subscribe another listener to same topic, and can unsubscribe self while handling message. """ class Listeners: callCountForNewListener = 0 callCountForChanger = 0 def newListener(self): self.callCountForNewListener += 1 pub.unsubscribe(self.newListener, 'test.change-listeners') def changer(self): # first time, subscribe new listener; if don't have this, will fail in # py3 because order of listeners opposite, so unsub will happen before # the sub (which succeeds even if no listeners) and newListener will # remain subscribed. if self.callCountForChanger == 0: pub.subscribe(self.newListener, 'test.change-listeners') self.callCountForChanger += 1 testListeners = Listeners() pub.subscribe(testListeners.changer, 'test.change-listeners') topic = pub.getDefaultTopicMgr().getTopic('test.change-listeners') pub.sendMessage('test.change-listeners') assert testListeners.callCountForChanger == 1 assert testListeners.callCountForNewListener == 0 assert topic.getNumListeners() == 2 pub.sendMessage('test.change-listeners') assert testListeners.callCountForChanger == 2 assert testListeners.callCountForNewListener == 1 assert topic.getNumListeners() == 1 pub.sendMessage('test.change-listeners') assert testListeners.callCountForChanger == 3 assert testListeners.callCountForNewListener == 1 pypubsub-4.0.3/tests/suite/test3d_defaultlog.py000066400000000000000000000006551342344706700216700ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ import io from pubsub.utils import notification def testNotifications(): capture = io.StringIO() logger = notification.useNotifyByWriteFile(capture) from pubsub import pub def block(): def listener(): pass pub.subscribe(listener, 'testNotifications') block() pypubsub-4.0.3/tests/suite/test4_prov_module_expect.py000066400000000000000000000015241342344706700232760ustar00rootroot00000000000000# Automatically generated by TopicTreeSpecPrinter(**kwargs). # The kwargs were: # - fileObj: file # - footer: '# End of topic tree definition. Note that application may l...' # - indentStep: 4 # - treeDoc: 'Tree docs, can be anything you want....' # - width: 70 """ Tree docs, can be anything you want. """ class root_topic_1b: """ Root topic 1. """ class subtopic_1: """ Sub topic 1 of root topic. Docs rely on one blank line for topic doc, and indentation for each argument doc. """ def msgDataSpec(arg1, arg2=None): """ - arg1: some multiline doc for arg1 - arg2: some multiline doc for arg2 """ # End of topic tree definition. Note that application may load # more than one definitions provider. pypubsub-4.0.3/tests/suite/test4_provider.py000066400000000000000000000250471342344706700212330ustar00rootroot00000000000000""" :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. :license: BSD, see LICENSE.txt for details. """ from typing import Any from textwrap import dedent from pathlib import Path import sys from pubsub.core import TopicNameError try: from importlib.util import cache_from_source except ImportError: from imp import cache_from_source import pytest from pubsub import pub try: Path.write_text # new in Python 3.5 except AttributeError: def write_text(path: Path, text: str): with path.open('w') as f: f.write(text) Path.write_text = write_text topicMgr = pub.getDefaultTopicMgr() def try_call(max_times: int, func: callable, *args: Any, func_on_fail: callable = None) -> int: """ Try to call a function. :param func: the function to call :param args: arguments to give to function :param max_times: maximum number of attempts :param func_on_fail: when call fails, call this function :return: # of calls left (so if 0, ran out of calls, i.e. tried more than max_times) """ retries = 0 while True: try: if retries >= max_times: break func(*args) break except Exception as exc: retries += 1 if func_on_fail is not None: func_on_fail(exc) return max_times - retries def cleanup_py_files(path): """Cleanup python file and associated byte-compiled file""" path = Path(path) if path.exists(): path.unlink() assert not path.exists() cached_file = Path(cache_from_source(str(path))) if cached_file.exists(): cached_file.unlink() assert not cached_file.exists() def clear_topic_tree(): root = topicMgr.getRootAllTopics() for topic in list(root.getSubtopics()): topicMgr.delTopic(topic.getName()) topicMgr.clearDefnProviders() def create_all_defined_topics(*args): prov = pub.addTopicDefnProvider(*args) return pub.instantiateAllDefinedTopics(prov) topicDefns2 = ''' class root_topic_1: class subtopic_1: class subsubtopic_11: """ Sub sub topic 1 of sub topic 1. Only need to doc the extra args. """ def msgDataSpec(arg1, arg3, arg2=None, arg4=None): """ - arg3: doc for arg3 - arg4: doc for arg4 """ pass ''' class topicDefns3: class root_topic_1: """Root topic 1""" class subtopic_1: """ Sub topic 1 of root topic. Docs rely on one blank line for topic doc, and indentation for each argument doc. """ def msgDataSpec(arg1, arg2=None): """ - arg1: some multiline doc for arg1 - arg2: some multiline doc for arg2 """ pass class subsubtopic_12: """Sub sub topic 2 of sub topic 1.""" def msgDataSpec(arg1, argA, arg2=None, argB=None): """ - argA: doc for argA - argB: doc for argB """ pass failed_imports = [] num_tries = 2000 def teardown_module(): #print(len(failed_imports) / num_tries) pass # @pytest.mark.parametrize('dummy', range(num_tries)) # def test_import(dummy): # from importlib import import_module # pytest.raises(ImportError, import_module, 'some_module') # # some_module_path = Path('some_module.py') # some_module_path.write_text("") # assert some_module_path.exists() # assert 'some_module' not in sys.modules # assert str(some_module_path.parent.resolve()) in sys.path # try: # import_module('some_module') # del sys.modules['some_module'] # except ImportError: # failed_imports.append(dummy) # # assert try_call(1000, cleanup_py_files, some_module_path) # def test_provider(): clear_topic_tree() # create several providers that provide for different subsets of a tree: pub.addTopicDefnProvider('my_import_topics') pub.addTopicDefnProvider(topicDefns2, pub.TOPIC_TREE_FROM_STRING) pub.addTopicDefnProvider(topicDefns3, pub.TOPIC_TREE_FROM_CLASS) # adding the providers loaded the specifications, but did not create any topics: pytest.raises(TopicNameError, topicMgr.getTopic, 'root_topic_1') pytest.raises(TopicNameError, topicMgr.getTopic, 'root_topic_1.subtopic_2') # the following will create topics based on the providers added: assert topicMgr.getOrCreateTopic('root_topic_1').hasMDS() assert topicMgr.getOrCreateTopic('root_topic_1.subtopic_1').hasMDS() assert topicMgr.getOrCreateTopic('root_topic_1.subtopic_1.subsubtopic_11').hasMDS() assert topicMgr.getOrCreateTopic('root_topic_1.subtopic_1.subsubtopic_12').hasMDS() assert topicMgr.getOrCreateTopic('root_topic_1.subtopic_2').hasMDS() assert topicMgr.getOrCreateTopic('root_topic_1.subtopic_2.subsubtopic_21').hasMDS() # create some listeners and validate them even though none have been subscribed so the MDS # will be used in order to validate them: def sub(arg1=678): pass def sub_2(arg1=987, arg2=123): pass def sub_21(arg1, arg2=None, arg4=None): pass def isValid(topicName, listener): topic = topicMgr.getTopic(topicName) assert topic.getDescription() assert topic.hasMDS() return topic.isValid(listener) assert isValid('root_topic_1', sub) assert isValid('root_topic_1.subtopic_2', sub_2) assert isValid('root_topic_1.subtopic_2.subsubtopic_21', sub_21) def sub_21_bad(arg4): # required arg4 rather than optional! pass assert not topicMgr.getTopic('root_topic_1.subtopic_2.subsubtopic_21').isValid(sub_21_bad) @pytest.mark.parametrize('repeat', range(100)) def test_export(repeat): # create a topic tree from a couple of the topic providers: clear_topic_tree() all_topics = create_all_defined_topics(topicDefns2, pub.TOPIC_TREE_FROM_STRING) all_topics.extend(create_all_defined_topics(topicDefns3, pub.TOPIC_TREE_FROM_CLASS)) # export topic tree: try_call(100, pub.exportTopicTreeSpec, 'my_exported_topics') # import exported tree: clear_topic_tree() all_topics2 = create_all_defined_topics('my_exported_topics') # verify: assert sorted(all_topics) == sorted(all_topics2) try_call(100, cleanup_py_files, 'my_exported_topics.py') def test_string_prov_export(): clear_topic_tree() importStr = ''' """Tree docs, can be anything you want.""" class root_topic_1: """Root topic 1.""" class subtopic_1: """ Sub topic 1 of root topic. Docs rely on one blank line for topic doc, and indentation for each argument doc. """ def msgDataSpec(arg1, arg2=None): """ - arg1: some multiline doc for arg1 - arg2: some multiline doc for arg2 """ pass class root_topic_2: """Root topic 2.""" ''' pub.clearTopicDefnProviders() provider = pub.addTopicDefnProvider(importStr, pub.TOPIC_TREE_FROM_STRING) treeDoc = provider.getTreeDoc() assert treeDoc == """Tree docs, can be anything you want.""" root = topicMgr.getOrCreateTopic('root_topic_1.subtopic_1') assert root is not None assert topicMgr.getOrCreateTopic('root_topic_2').hasMDS() # few sanity checks def sub_1(arg1, arg2=None): pass assert root.hasMDS() assert pub.isValid(sub_1, 'root_topic_1.subtopic_1') # export tree exported = pub.exportTopicTreeSpec(rootTopic='root_topic_1', moduleDoc=treeDoc) # print(exported) expectExport = '''\ # Automatically generated by TopicTreeSpecPrinter(**kwargs). # The kwargs were: # - fileObj: StringIO # - footer: '# End of topic tree definition. Note that application may l...' # - indentStep: 4 # - treeDoc: 'Tree docs, can be anything you want....' # - width: 70 """ Tree docs, can be anything you want. """ class root_topic_1: """ Root topic 1. """ class subtopic_1: """ Sub topic 1 of root topic. Docs rely on one blank line for topic doc, and indentation for each argument doc. """ def msgDataSpec(arg1, arg2=None): """ - arg1: some multiline doc for arg1 - arg2: some multiline doc for arg2 """ # End of topic tree definition. Note that application may load # more than one definitions provider. ''' # check there are no differences from difflib import context_diff, ndiff diffs = ndiff(dedent(expectExport).splitlines(), exported.splitlines()) diffs = [d for d in diffs if not d.startswith(' ')] assert diffs == ['- ', '+ '] # now for module: provider = pub.addTopicDefnProvider('test4_prov_module_expect') pub.instantiateAllDefinedTopics(provider) modDoc = provider.getTreeDoc() assert modDoc.startswith('\nTree docs, can be anything you') pub.exportTopicTreeSpec('test4_prov_module_actual', rootTopic='root_topic_1b', moduleDoc=treeDoc) lines1 = open('test4_prov_module_actual.py', 'r').readlines() lines2 = open('test4_prov_module_expect.py', 'r').readlines() diffs = ndiff(lines1, lines2) diffs = [d for d in diffs if not d.startswith(' ')] assert not list(diffs) or list(diffs) == ['- # - fileObj: TextIOWrapper\n', '+ # - fileObj: file\n'] Path('test4_prov_module_actual.py').unlink() def test_module_as_class(): clear_topic_tree() assert topicMgr.getTopic('root_topic_1', True) is None assert topicMgr.getTopic('root_topic_2.sub_topic_21', True) is None # noinspection PyUnresolvedReferences import my_import_topics provider = pub.addTopicDefnProvider(my_import_topics, pub.TOPIC_TREE_FROM_CLASS) pub.instantiateAllDefinedTopics(provider) assert topicMgr.getTopic('root_topic_1') is not None assert topicMgr.getTopic('root_topic_2.subtopic_21') is not None pub.sendMessage(my_import_topics.root_topic_1) pypubsub-4.0.3/tests/suite/test5_xmlprovider.py000066400000000000000000000055501342344706700217520ustar00rootroot00000000000000#!/usr/bin/env python from pubsub import pub from pubsub.utils.xmltopicdefnprovider import ( XmlTopicDefnProvider, TOPIC_TREE_FROM_FILE, exportTopicTreeSpecXml ) topicMgr = pub.getDefaultTopicMgr() def test_xml_from_file(): pub.clearTopicDefnProviders() provider = XmlTopicDefnProvider('xmlprovider_topics.xml', TOPIC_TREE_FROM_FILE) assert topicMgr.getTopic('parent', True) is None assert topicMgr.getTopic('parent.child', True) is None assert topicMgr.getOrCreateTopic('parent') is not None assert topicMgr.getOrCreateTopic('parent.child') is not None def test_xml_import(): pub.clearTopicDefnProviders() topicMgr.delTopic('parent') # verify pre: assert topicMgr.getTopic('parent', True) is None assert topicMgr.getTopic('parent.child', True) is None provider = XmlTopicDefnProvider('xmlprovider_topics.xml', TOPIC_TREE_FROM_FILE) pub.addTopicDefnProvider( provider ) # force instantiation of two topic definitions that were defined in xml: pub.sendMessage('parent', lastname='') pub.sendMessage('parent.child', lastname='', nick='') # verify post: assert topicMgr.getTopic('parent') is not None assert topicMgr.getTopic('parent.child') is not None def test_xml_string_import(): xml=""" Test Topics showing hierarchy and topic inheritance Parent with a parameter and subtopics surname given name This is the first child A nickname """ topicMgr.delTopic('parent') pub.clearTopicDefnProviders() assert topicMgr.getTopic('parent', True) is None assert topicMgr.getTopic('parent.child', True) is None provider = XmlTopicDefnProvider(xml) pub.addTopicDefnProvider( provider ) # to force instantiation of two topic definitions that were defined in xml, # this time we just instantiate all of them: pub.instantiateAllDefinedTopics(provider) assert topicMgr.getTopic('parent') is not None assert topicMgr.getTopic('parent.child') is not None def test_xml_topics(): # validate that topic specs were properly parsed def isValid(topicName, listener): topic = topicMgr.getTopic(topicName) assert topic.getDescription() is not None assert topic.hasMDS() return topic.isValid(listener) def hello(lastname, name=None): pass def friend(lastname, nick, name=None): pass assert isValid('parent', hello) assert isValid('parent.child', friend) pypubsub-4.0.3/tests/suite/xmlprovider_topics.xml000066400000000000000000000011361342344706700223530ustar00rootroot00000000000000 Test Topics showing hierarchy and topic inheritance Parent with a parameter and subtopics surname given name This is the first child A nickname pypubsub-4.0.3/tools/000077500000000000000000000000001342344706700145415ustar00rootroot00000000000000pypubsub-4.0.3/tools/compare_timings.py000066400000000000000000000062061342344706700202770ustar00rootroot00000000000000''' Timing helper function to time several Python statements. It makes using the timeit module much more convenient. The only important function is times(), to which you provide the number of iterations to time, an initialization statement (typically, to initialize some locals), and the list of statements to time. E.g. times(init='l=range(1,1000)', s1='if l!=[]: pass', s2='if l: pass', s3='if len(l): pass') will print(out) s2 => 0.046 s1 => 0.086 s3 => 0.121 showing that s2 is the fastest (in fact, two to three times faster than the alternatives). ''' from timeit import Timer class Timing: def __init__(self, name, num, init, statement): self.__timer = Timer(statement, init) self.__num = num self.name = name self.statement = statement self.__result = None def timeit(self): self.__result = self.__timer.timeit(self.__num) def getResult(self): return self.__result def times(num=1000000, reverse=False, init='', **statements): '''The num is the number of times that each statement will be executed. The init statement is executed only once for statement and is not part of the timing result. The statements kwarg is a dict of statements to time, where key is a 'name' for the statement, and value is the statement (as a string). Prints the timings from smallest to largest (unless reverse=True, then opposite). ''' # time each statement timings = [] for n, s in statements.iteritems(): t = Timing(n, num, init, s) t.timeit() timings.append(t) # print(results) timings.sort(key=Timing.getResult, reverse=reverse) for t in timings: print(" %10s => %.3f s" % (t.name, t.getResult())) if __name__ == '__main__': inits = ('l=range(1,1000)', 'l=[]') for ii in inits: print('\nFor %s:' % ii) times(init=ii, boolean = 'if l: pass', empty = 'if l!=[]: pass', len_ = 'if len(l): pass') print('\nFor list -> dict:') times(100, init='l=range(1,1000)', zip = 'dict(zip(l[::2], l[1::2]))', listcomp = 'dict((l[i],l[i+1]) for i in range(0,len(l)-1,2))') print('\nFor hasattr vs except:') times(10000, init='class Foo: pass\nfoo = Foo()', hasattr = 'if hasattr(foo, "a"): a = getattr(foo, "a")', excep = 'try: a = foo.a\nexcept Exception: pass') print('\nFor cost of enumerate:') times(100, init='l=range(1,1000)\ndef f(v): return (v*10-50)/2', enum = 'for i, v in enumerate(l): f(v)', noenum = 'for v in l: f(v)', count = 'ii=0\nfor v in l: \n f(v)\nii += 1') print('\nFor slice before/in loop:') times(100, init='l=range(0,10000)\nl2=range(0,1000)', before10k = 'l3=l[9000:]\nfor i in l3: pass', before1k = 'l3=l2[1:]\nfor i in l3: pass', inloop10k = 'for i in l[9000:]: pass', inloop1k = 'for i in l2[1:]: pass', range = 'for i in xrange(9000,10000): l[i]', nocopy = 'for i in l2: pass')pypubsub-4.0.3/tox.ini000066400000000000000000000011551342344706700147160ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py33, py34, py35, py36, py37 toxworkdir=C:\temp\tox\pypubsub [testenv] usedevelop = True changedir = tests/suite commands = py.test install_command = pip install {opts} {packages} --trusted-host pypi.python.org deps = pytest [testenv:py33] deps = pytest typing enum34 pathlib weakrefmethod [testenv:py34] deps = pytest typing