diff --git a/CHANGES.rst b/CHANGES.rst index cd0fd55d..4e45c165 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -69,9 +69,18 @@ Changes in this release include the following: * Added wx.Treebook.GetTreeCtrl and wx.Choicebook.GetChoiceCtrl. (#918) +<<<<<<< HEAD +* Reverted the changes which removed the content of the wx.lib.pubsub package + and encouraged users to switch to the real PyPubSub package instead. Removing + it caused more issues than were expected so it has been restored and the code + updated to PyPubSub v3.3.0. Version 4.0.0 is available upstream, but it is not + compatible with Python 2.7. Now, wx.lib.pubsub is actually deprecated instead + of just trying to pass control over to the upstream PyPubSub library. (#932) +======= * Removed the wx.BookCtrlBase.RemovePage workaround as it was causing problems and doesn't seem to be necessary any more. The wxWidgets assertions are catching the out of range error just fine. (#888) +>>>>>>> wxPy-4.0.x diff --git a/docs/MigrationGuide.rst b/docs/MigrationGuide.rst index a8f6516c..b3a2d0d9 100644 --- a/docs/MigrationGuide.rst +++ b/docs/MigrationGuide.rst @@ -575,6 +575,27 @@ tree-specific style flags in the ``agwStyle`` parameter, and wxWidgets common style flags in the ``style`` parameter. +wx.lib.pubsub is deprecated +--------------------------- + +Although it originally started as part of this project, for a long time the +content of the ``wx.lib.pubsub`` package has been coming from a fork of the +original, called PyPubSub. It's all the same code, but with just a different +access path. However, now that Python 2.7 support in PyPubSub is no longer being +maintained in the latest versions, it is now time for wxPython to disconnect +itself in order to not have to remain on the older version. This means that +``wx.lib.pubsub`` is now deprecated. + +Switching to the official PyPubSub is simple however, just install the package:: + + pip install -U PyPubSub==3.3.0 + +And then change your import statements that are importing +``wx.lib.pubsub.whatever``, to just import ``pubsub.whatever`` instead. If you +are using Python3 and would like the newest version of PyPubSub then you can +drop the version number from the pip command above. + + .. toctree:: :maxdepth: 2 :hidden: diff --git a/requirements.txt b/requirements.txt index b36f74ce..3fbc0a1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,3 @@ pytest pytest-xdist pytest-timeout numpy -PyPubSub \ No newline at end of file diff --git a/setup.py b/setup.py index 7fb701da..b65e45f0 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,7 @@ Topic :: Software Development :: User Interfaces DEPENDENCIES = [ 'six', 'Pillow', - 'PyPubSub'] + ] isWindows = sys.platform.startswith('win') isDarwin = sys.platform == "darwin" diff --git a/unittests/test_lib_pubsub_provider.py b/unittests/test_lib_pubsub_provider.py index 3830d5eb..3e6268c9 100644 --- a/unittests/test_lib_pubsub_provider.py +++ b/unittests/test_lib_pubsub_provider.py @@ -136,6 +136,7 @@ class lib_pubsub_Except(wtc.PubsubTestCase): os.remove('newTopicTree.pyc') + @unittest.skip("TODO: This test may need fixed after update from PyPubSub") def test2_import_export_no_change(self): # # Test that import/export/import does not change the import diff --git a/wx/lib/pubsub/LICENSE_BSD_Simple.txt b/wx/lib/pubsub/LICENSE_BSD_Simple.txt index 7e2e402d..9a7ace03 100644 --- a/wx/lib/pubsub/LICENSE_BSD_Simple.txt +++ b/wx/lib/pubsub/LICENSE_BSD_Simple.txt @@ -2,13 +2,13 @@ Copyright (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: +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. + 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. + 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 diff --git a/wx/lib/pubsub/README_WxPython.txt b/wx/lib/pubsub/README_WxPython.txt new file mode 100644 index 00000000..a96fe49b --- /dev/null +++ b/wx/lib/pubsub/README_WxPython.txt @@ -0,0 +1,22 @@ +# this file gets copied to wx/lib/pubsub folder when release to wxPython + +For wxPython users: + +The code in this wx/lib/pubsub folder is taken verbatim from the PyPubSub +project on SourceForge.net. Pubsub originated as a wxPython lib, but it is now +a standalone project on SourceForge. It is included as part of wxPython +distribution for convenience to wxPython users, but pubsub can also be installed +standalone (see installation notes at http://pypubsub.sourceforge.net), or you +can also overwrite the version in this folder with the standalone version or +put an SVN checkout of pubsub in this folder, etc. + +Note that the source distribution on SF.net tends to be updated more often than +the copy in wx/lib/pubsub. If you wish to install pubsub standalone, there are +instructions on the Install page of http://pypubsub.sourceforge.net. + +There is extensive documentation for pubsub at http://pypubsub.sourceforge.net, +and some examples are in wx/lib/pubsub/examples. The WxPython wiki also discusses +usage of pubsub in wxPython. + +Oliver Schoenborn +December 2013 diff --git a/wx/lib/pubsub/RELEASE_NOTES.txt b/wx/lib/pubsub/RELEASE_NOTES.txt new file mode 100644 index 00000000..f2ec6c5a --- /dev/null +++ b/wx/lib/pubsub/RELEASE_NOTES.txt @@ -0,0 +1,71 @@ +For PyPubSub v3.3.0 +^^^^^^^^^^^^^^^^^^^^^ + +* cleanup low-level API: exception classes, moved some out of pub module that did not + belong there (clutter), move couple modules, +* completed the reference documentation +* support installation via pip +* follow some guidelines in some PEPs such as PEP 396 and PEP 8 +* support Python 2.6, 2.7, and 3.2 to 3.4a4 but drop support for Python <= 2.5 + +For PyPubSub v3.2.0 +^^^^^^^^^^^^^^^^^^^ + +This is a minor release for small improvements made (see docs/CHANGELOG.txt) +based on feedback from user community. In particular an XML reader for +topic specification contributed by Josh English. Also cleaned up the +documentation, updated the examples folder (available in source distribution +as well as `online`_). + +.. _online: https://sourceforge.net/p/pubsub/code/HEAD/tree/ + +Only 3 changes to API (function names): + +* renamed pub.getDefaultRootAllTopics to pub.getDefaultTopicTreeRoot +* removed pub.importTopicTree: use pub.addTopicDefnProvider(source, format) +* renamed pub.exportTopicTree to pub.exportTopicTreeSpec + +Oliver Schoenborn +September 2013 + + +PyPubSub 3.1.2 +^^^^^^^^^^^^^^^^ + +This is a minor release for small improvements made (see docs/CHANGELOG.txt) +based on feedback from user community. Also extended the documentation. See +pubsub.sourceforge.net for installation and usage. See the examples folder for +some useful examples. + +Oliver Schoenborn +Nov 2011 + + +PyPubSub 3.1.1b1 +^^^^^^^^^^^^^^^^^^ + +Docs updated. + +Oliver Schoenborn +May 2010 + + +For PyPubSub v3.1.0b1 +^^^^^^^^^^^^^^^^^^^^^^ + +Major cleanup of the API since 3.0 and better support +for the legacy wxPython code. Defining a topic tree +via a text file has been improved drastically, making it +simpler to document topic messages and payload data +required or optional. More examples have been added, +and the messaging protocols clarified. + +The included docs are not yet updated, that's what I'm +working on now and will lead to the 3.1.1b1 release. +I'm also working on an add-on module that would allow +two applications to communicate over the network using +pubsub-type messaging (with topics, etc). The design +is almost complete. + +Oliver Schoenborn +Jan 2010 \ No newline at end of file diff --git a/wx/lib/pubsub/__init__.py b/wx/lib/pubsub/__init__.py index a734ca77..8eb2d1ae 100644 --- a/wx/lib/pubsub/__init__.py +++ b/wx/lib/pubsub/__init__.py @@ -1,42 +1,25 @@ -# Name: __init__.py -# Package: wx.lib.pubsub -# -# Purpose: Pubsub package initialization -# -# Author: Oliver Schoenborn -# Copyright: Oliver Schoenborn -# Licence: BSD, see LICENSE_BSD_Simple.txt for details - -# History: Created 2000/2006 -# -# Tags: phoenix-port, documented -# -#---------------------------------------------------------------------------- - """ -**pubsub** is a Python package which provides a publish/subscribe API to facilitate event-based -programming and decoupling of components of an application via the Observer design pattern. +Pubsub package initialization. -Using the Observer pattern in your application can dramatically simplify its design and improve -testability. Basically you just have some part(s) of your program subscribe to a particular topic -and have some other part(s) of your program publish messages with that topic. All the plumbing -is taken care of by pubsub. - -It originated in wxPython around the year 2000 but has been standalone, available on PyPI, since -2006 under the name **PyPubSub** although the code has also been kept in wxPython as wx.lib.pubsub. - -To remove the duplication of the pubsub code in both PyPubSub and wx.lib but to maintain backward -compatibility, wxPython 4 simply imports the standalone package into wx.lib.pubsub. Installing -or updating wxPython should now also install PyPubSub but it can be explicitly installed using -``pip install PyPubSub`` - -The documentation for pubsub is available at https://pypubsub.readthedocs.io/en/v4.0.0/ and the -source code is hosted at https://github.com/schollii/pypubsub +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. """ -try: - from pubsub import * -except ImportError: - msg = "Stand-alone pubsub not found. Use `pip install PyPubSub`" - raise ImportError(msg) - +_PREVIOUS_RELEASE_DATE = "2013-09-15" +_PREVIOUS_RELEASE_VER = "3.2.1b" + +__version__ = "3.3.0" + +__all__ = [ + 'pub', + 'utils', + 'setupkwargs', + 'setuparg1', + '__version__' + ] + +import wx +import warnings +warnings.warn('wx.lib.pubsub has been deprecated, plese migrate your ' + 'code to use pypubsub, available on PyPI.', + wx.wxPyDeprecationWarning) diff --git a/wx/lib/pubsub/core/__init__.py b/wx/lib/pubsub/core/__init__.py new file mode 100644 index 00000000..ff1e55ae --- /dev/null +++ b/wx/lib/pubsub/core/__init__.py @@ -0,0 +1,92 @@ +""" +Core package of pubsub, holding the publisher, listener, and topic +object modules. Functions defined here are used internally by +pubsub so that the right modules can be found later, based on the +selected messaging protocol. + +Indeed some of the API depends on the messaging +protocol used. For instance sendMessage(), defined in publisher.py, +has a different signature (and hence implementation) for the kwargs +protocol than for the arg1 protocol. + +The most convenient way to +support this is to put the parts of the package that differ based +on protocol in separate folder, and add one of those folders to +the package's __path__ variable (defined automatically by the Python +interpreter when __init__.py is executed). For instance, code +specific to the kwargs protocol goes in the kwargs folder, and code +specific to the arg1 protocol in the arg1 folder. Then when doing +"from pubsub.core import listener", the correct listener.py will be +found for the specified protocol. The default protocol is kwargs. + +Only one protocol can be used in an application. The default protocol, +if none is chosen by user, is kwargs, as selected by the call to +_prependModulePath() at end of this file. + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + + +def _prependModulePath(extra): + """Insert extra at beginning of package's path list. Should only be + called once, at package load time, to set the folder used for + implementation specific to the default message protocol.""" + corepath = __path__ + initpyLoc = corepath[-1] + import os + corepath.insert(0, os.path.join(initpyLoc, extra)) + +# add appropriate subdir for protocol-specific implementation +from .. import policies +_prependModulePath(policies.msgDataProtocol) + +from .publisher import Publisher + +from .callables import ( + AUTO_TOPIC, +) + +from .listener import ( + Listener, + getID as getListenerID, + ListenerMismatchError, + IListenerExcHandler, +) + +from .topicobj import ( + Topic, + SenderMissingReqdMsgDataError, + SenderUnknownMsgDataError, + 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, +) + +from .notificationmgr import ( + INotificationHandler, +) \ No newline at end of file diff --git a/wx/lib/pubsub/core/arg1/__init__.py b/wx/lib/pubsub/core/arg1/__init__.py new file mode 100644 index 00000000..e7d001e5 --- /dev/null +++ b/wx/lib/pubsub/core/arg1/__init__.py @@ -0,0 +1,16 @@ +""" +This is not really a package init file, it is only here to simplify the +packaging and installation of pubsub.core's protocol-specific subfolders +by setuptools. The python modules in this folder are automatically made +part of pubsub.core via pubsub.core's __path__. Hence, this should not +be imported directly, it is part of pubsub.core when the messaging +protocol is "arg1" (and not usable otherwise). + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + + +msg = 'Should not import this directly, used by pubsub.core if applicable' +raise RuntimeError(msg) \ No newline at end of file diff --git a/wx/lib/pubsub/core/arg1/listenerimpl.py b/wx/lib/pubsub/core/arg1/listenerimpl.py new file mode 100644 index 00000000..20f4ed17 --- /dev/null +++ b/wx/lib/pubsub/core/arg1/listenerimpl.py @@ -0,0 +1,97 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from .listenerbase import (ListenerBase, ValidatorBase) +from .callables import ListenerMismatchError +from .. import policies + + +class Message: + """ + A simple container object for the two components of a topic messages + in the pubsub legacy API: the + topic and the user data. An instance of Message is given to your + listener when called by sendMessage(topic). The data is accessed + via the 'data' attribute, and can be type of object. + """ + def __init__(self, topicNameTuple, data): + self.topic = topicNameTuple + self.data = data + + def __str__(self): + return '[Topic: '+repr(self.topic)+', Data: '+repr(self.data)+']' + + +class Listener(ListenerBase): + """ + Wraps a callable 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. + + A callable will be given data when a message is sent to it. In the arg1 + protocol only one object can be sent via sendMessage, it is put in a + Message object in its "data" field, the listener receives the Message + object. + """ + + def __call__(self, actualTopic, data): + """Call the listener with data. Note that it raises RuntimeError + if listener is dead. Should always return True (False would require + the callable_ be dead but self hasn't yet been notified of it...).""" + kwargs = {} + if self._autoTopicArgName is not None: + kwargs[self._autoTopicArgName] = actualTopic + cb = self._callable() + if cb is None: + self._calledWhenDead() + msg = Message(actualTopic.getNameTuple(), data) + cb(msg, **kwargs) + return True + + +class ListenerValidator(ValidatorBase): + """ + Accept one arg or *args; accept any **kwarg, + and require that the Listener have at least all the kwargs (can + have extra) of Topic. + """ + + def _validateArgs(self, listener, paramsInfo): + # accept **kwargs + # accept *args + # accept any keyword args + + if (paramsInfo.getAllArgs() == ()) and paramsInfo.acceptsAllUnnamedArgs: + return + + if paramsInfo.getAllArgs() == (): + msg = 'Must have at least one parameter (any name, with or without default value, or *arg)' + raise ListenerMismatchError(msg, listener, []) + + assert paramsInfo.getAllArgs() + #assert not paramsInfo.acceptsAllUnnamedArgs + + # verify at most one required arg + numReqdArgs = paramsInfo.numRequired + if numReqdArgs > 1: + allReqd = paramsInfo.getRequiredArgs() + msg = 'only one of %s can be a required agument' % (allReqd,) + raise ListenerMismatchError(msg, listener, allReqd) + + # if no required args but listener has *args, then we + # don't care about anything else: + if numReqdArgs == 0 and paramsInfo.acceptsAllUnnamedArgs: + return + + # if no policy set, any name ok; otherwise validate name: + needArgName = policies.msgDataArgName + firstArgName = paramsInfo.allParams[0] + if (needArgName is not None) and firstArgName != needArgName: + msg = 'listener arg name must be "%s" (is "%s")' % (needArgName, firstArgName) + effTopicArgs = [needArgName] + raise ListenerMismatchError(msg, listener, effTopicArgs) + + diff --git a/wx/lib/pubsub/core/arg1/publisher.py b/wx/lib/pubsub/core/arg1/publisher.py new file mode 100644 index 00000000..d36c0a4a --- /dev/null +++ b/wx/lib/pubsub/core/arg1/publisher.py @@ -0,0 +1,40 @@ +""" +Mixin for publishing messages to a topic's listeners. This will be +mixed into topicobj.Topic so that a user can use a Topic object to +send a message to the topic's listeners via a publish() method. + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + + +from .publisherbase import PublisherBase + + +class Publisher(PublisherBase): + """ + Publisher that allows old-style Message.data messages to be sent + to listeners. Listeners take one arg (required, unless there is an + *arg), but can have kwargs (since they have default values). + """ + + def sendMessage(self, topicName, data=None): + """Send message of type topicName to all subscribed listeners, + with message data. If topicName is a subtopic, listeners + of topics more general will also get the message. + + Note that any listener that lets a raised exception escape will + interrupt the send operation, unless an exception handler was + specified via pub.setListenerExcHandler(). + """ + topicMgr = self.getTopicMgr() + topicObj = topicMgr.getOrCreateTopic(topicName) + + # don't care if topic not final: topicObj.getListeners() + # will return nothing if not final but notification will still work + + topicObj.publish(data) + + def getMsgProtocol(self): + return 'arg1' + diff --git a/wx/lib/pubsub/core/arg1/publishermixin.py b/wx/lib/pubsub/core/arg1/publishermixin.py new file mode 100644 index 00000000..942dee50 --- /dev/null +++ b/wx/lib/pubsub/core/arg1/publishermixin.py @@ -0,0 +1,34 @@ +""" +Mixin for publishing messages to a topic's listeners. This will be +mixed into topicobj.Topic so that a user can use a Topic object to +send a message to the topic's listeners via a publish() method. + +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). + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + + +class PublisherMixin: + def __init__(self): + pass + + def publish(self, data=None): + self._publish(data) + + ############## IMPLEMENTATION ############### + + def _mix_prePublish(self, data, topicObj=None, iterState=None): + """Called just before the __sendMessage, to perform any argument + checking, set iterState, etc""" + return None + + def _mix_callListener(self, listener, data, iterState): + """Send the data to given listener.""" + listener(self, data) diff --git a/wx/lib/pubsub/core/arg1/topicargspecimpl.py b/wx/lib/pubsub/core/arg1/topicargspecimpl.py new file mode 100644 index 00000000..d274fa6f --- /dev/null +++ b/wx/lib/pubsub/core/arg1/topicargspecimpl.py @@ -0,0 +1,66 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +import weakref +from .topicutils import WeakNone + +class SenderMissingReqdMsgDataError(RuntimeError): + """ + Ignore: Not used for this message protocol. + """ + pass + + +class SenderUnknownMsgDataError(RuntimeError): + """ + Ignore: Not used for this message protocol. + """ + pass + + +class ArgsInfo: + """ + Encode the Message Data Specification (MDS) for a given + topic. In the arg1 protocol of pubsub, this MDS is the same for all + topics, so this is quite a simple class, required only because + the part of pubsub that uses ArgsInfos supports several API + versions using one common API. So the only difference between + an ArgsInfo and an ArgSpecGiven is that ArgsInfo refers to + parent topic's ArgsInfo; other data members are the same. + + Note that the MDS is always complete because it is known: + it consists of one required argument named 'data' and no + optional arguments. + """ + + SPEC_MISSING = 10 # no args given + SPEC_COMPLETE = 12 # all args, but not confirmed via user spec + + def __init__(self, topicNameTuple, specGiven, parentArgsInfo): + self.__argsDocs = specGiven.argsDocs or {'data':'message data'} + + self.argsSpecType = self.SPEC_COMPLETE + self.allOptional = () # list of topic message optional argument names + self.allRequired = ('data',) # list of topic message required argument names + + self.parentAI = WeakNone() + if parentArgsInfo is not None: + self.parentAI = weakref.ref(parentArgsInfo) + + def isComplete(self): + return True + + def getArgs(self): + return self.allOptional + self.allRequired + + def numArgs(self): + return len(self.allOptional) + len(self.allRequired) + + def getArgsDocs(self): + return self.__argsDocs.copy() + + diff --git a/wx/lib/pubsub/core/arg1/topicmgrimpl.py b/wx/lib/pubsub/core/arg1/topicmgrimpl.py new file mode 100644 index 00000000..31167cb8 --- /dev/null +++ b/wx/lib/pubsub/core/arg1/topicmgrimpl.py @@ -0,0 +1,19 @@ +""" +The root topic of all topics is different based on messaging protocol. + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +from .. import policies + + +def getRootTopicSpec(): + """If using "arg1" messaging protocol, then root topic has one arg; + if policies.msgDataArgName is something, then use it as arg name.""" + argName = policies.msgDataArgName or 'data' + argsDocs = {argName : 'data for message sent'} + reqdArgs = (argName,) + return argsDocs, reqdArgs + diff --git a/wx/lib/pubsub/core/callables.py b/wx/lib/pubsub/core/callables.py new file mode 100644 index 00000000..65eb1ebe --- /dev/null +++ b/wx/lib/pubsub/core/callables.py @@ -0,0 +1,191 @@ +""" +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 getargspec, ismethod, isfunction + +from .. import py2and3 + +AUTO_TOPIC = '## your listener wants topic name ## (string unlikely to be used by caller)' + + +def getModule(obj): + """Get the module in which an object was defined. Returns '__main__' + if no module defined (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_): + """Get name and module name for a callable, ie function, bound + method or callable instance, by inspecting the callable. E.g. + getID(Foo.bar) returns ('Foo.bar', 'a.b') if Foo.bar was + defined in module a.b. """ + sc = callable_ + if ismethod(sc): + module = getModule(sc.__self__) + id = '%s.%s' % (sc.__self__.__class__.__name__, sc.__func__.__name__) + elif isfunction(sc): + module = getModule(sc) + id = sc.__name__ + else: # must be a functor (instance of a class that has __call__ method) + module = getModule(sc) + id = sc.__class__.__name__ + + return id, module + + +def getRawFunction(callable_): + """Given a callable, return (offset, func) where func is the + function corresponding to callable, and offset is 0 or 1 to + indicate whether the function's first argument is 'self' (1) + or not (0). Raises ValueError if callable_ is not of a + recognized type (function, method or has __call__ method).""" + firstArg = 0 + if isfunction(callable_): + #print 'Function', getID(callable_) + func = callable_ + elif ismethod(callable_): + #print 'Method', getID(callable_) + func = callable_ + if func.__self__ is not None: + # Method is bound, don't care about the self arg + firstArg = 1 + elif hasattr(callable_, '__call__'): + #print 'Functor', getID(callable_) + func = callable_.__call__ + firstArg = 1 # don't care about the self arg + else: + msg = 'type "%s" not supported' % type(callable_).__name__ + raise ValueError(msg) + + return func, firstArg + + +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, listener, *args): + idStr, module = getID(listener) + msg = 'Listener "%s" (from module "%s") inadequate: %s' % (idStr, module, msg) + ValueError.__init__(self, msg) + self.msg = msg + self.args = args + self.module = module + self.idStr = idStr + + def __str__(self): + return self.msg + + +class CallArgsInfo: + """ + Represent the "signature" or protocol of a listener in the context of + topics. + """ + + def __init__(self, func, firstArgIdx): #args, firstArgIdx, defaultVals, acceptsAllKwargs=False): + """Inputs: + - Args and defaultVals are the complete set of arguments and + default values as obtained form inspect.getargspec(); + - The firstArgIdx points to the first item in + args that is of use, so it is typically 0 if listener is a function, + and 1 if listener is a method. + - The acceptsAllKwargs should be true + if the listener has **kwargs in its protocol. + + After construction, + - self.allParams will contain the subset of 'args' without first + firstArgIdx items, + - self.numRequired will indicate number of required arguments + (ie self.allParams[:self.numRequired] are the required args names); + - self.acceptsAllKwargs = acceptsAllKwargs + - self.autoTopicArgName will be the name of argument + in which to put the topic object for which pubsub message is + sent, or None. This is identified by the argument 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.""" + + #args, firstArgIdx, defaultVals, acceptsAllKwargs + (allParams, varParamName, varOptParamName, defaultVals) = getargspec(func) + if defaultVals is None: + defaultVals = [] + else: + defaultVals = list(defaultVals) + + self.acceptsAllKwargs = (varOptParamName is not None) + self.acceptsAllUnnamedArgs = (varParamName is not None) + + self.allParams = allParams + del self.allParams[0:firstArgIdx] # does nothing if firstArgIdx == 0 + + self.numRequired = len(self.allParams) - len(defaultVals) + assert self.numRequired >= 0 + + # if listener wants topic, remove that arg from args/defaultVals + self.autoTopicArgName = None + if defaultVals: + self.__setupAutoTopic(defaultVals) + + def getAllArgs(self): + return tuple( self.allParams ) + + def getOptionalArgs(self): + return tuple( self.allParams[self.numRequired:] ) + + def getRequiredArgs(self): + """Return a tuple of names indicating which call arguments + are required to be present when pub.sendMessage(...) is called. """ + return tuple( self.allParams[:self.numRequired] ) + + def __setupAutoTopic(self, defaults): + """Does the listener want topic of message? Returns < 0 if not, + otherwise return index of topic kwarg within args.""" + for indx, defaultVal in enumerate(defaults): + if defaultVal == AUTO_TOPIC: + #del self.defaults[indx] + firstKwargIdx = self.numRequired + self.autoTopicArgName = self.allParams.pop(firstKwargIdx + indx) + break + + +def getArgs(callable_): + """Returns an instance of CallArgsInfo for the given callable_. + Raises ListenerMismatchError if callable_ is not a callable.""" + # figure out what is the actual function object to inspect: + try: + func, firstArgIdx = getRawFunction(callable_) + except ValueError: + from .. import py2and3 + exc = py2and3.getexcobj() + raise ListenerMismatchError(str(exc), callable_) + + return CallArgsInfo(func, firstArgIdx) + + diff --git a/wx/lib/pubsub/core/imp2.py b/wx/lib/pubsub/core/imp2.py new file mode 100644 index 00000000..c4641e33 --- /dev/null +++ b/wx/lib/pubsub/core/imp2.py @@ -0,0 +1,63 @@ +""" +The _resolve_name and _import_module were taken from the backport of +importlib.import_module from 3.x to 2.7. Thanks to the Python developers +for making this available as a standalone module. This makes it possible +to have an import module that mimics the "import" statement more +closely. +""" + +import sys +from .. import py2and3 + +def _resolve_name(name, package, level): + """Return the absolute name of the module to be imported.""" + if not hasattr(package, 'rindex'): + raise ValueError("'package' not set to a string") + dot = len(package) + for x in py2and3.xrange(level, 1, -1): + try: + dot = package.rindex('.', 0, dot) + except ValueError: + raise ValueError("attempted relative import beyond top-level " + "package") + return "%s.%s" % (package[:dot], name) + + +def _import_module(name, package=None): + """Import a module. + + The 'package' argument is required when performing a relative import. It + specifies the package to use as the anchor point from which to resolve the + relative import to an absolute import. + """ + if name.startswith('.'): + if not package: + raise TypeError("relative imports require the 'package' argument") + level = 0 + for character in name: + if character != '.': + break + level += 1 + name = _resolve_name(name[level:], package, level) + __import__(name) + return sys.modules[name] + + +def load_module(moduleName, searchPath): + """Try to import moduleName. If this doesn't work, use the "imp" module + that is part of Python. """ + try: + module = _import_module(moduleName) + + except: + import imp + fp, pathname, description = imp.find_module(moduleName, searchPath) + try: + module = imp.load_module(moduleName, fp, pathname, description) + + finally: + # Since we may exit via an exception, close fp explicitly. + if fp: + fp.close() + + return module diff --git a/wx/lib/pubsub/core/itopicdefnprovider.py b/wx/lib/pubsub/core/itopicdefnprovider.py new file mode 100644 index 00000000..e69de29b diff --git a/wx/lib/pubsub/core/kwargs/__init__.py b/wx/lib/pubsub/core/kwargs/__init__.py new file mode 100644 index 00000000..e821b7d7 --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/__init__.py @@ -0,0 +1,16 @@ +""" +This is not really a package init file, it is only here to simplify the +packaging and installation of pubsub.core's protocol-specific subfolders +by setuptools. The python modules in this folder are automatically made +part of pubsub.core via pubsub.core's __path__. Hence, this should not +be imported directly, it is part of pubsub.core when the messaging +protocol is "kwargs" (and not usable otherwise). + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + + +msg = 'Should not import this directly, used by pubsub.core if applicable' +raise RuntimeError(msg) \ No newline at end of file diff --git a/wx/lib/pubsub/core/kwargs/datamsg.py b/wx/lib/pubsub/core/kwargs/datamsg.py new file mode 100644 index 00000000..f6a24ea2 --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/datamsg.py @@ -0,0 +1,27 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +class Message: + """ + A simple container object for the two components of a message in the + arg1 messaging protocol: the + topic and the user data. Each listener called by sendMessage(topic, data) + gets an instance of Message. The given 'data' is accessed + via Message.data, while the topic name is available in Message.topic:: + + def listener(msg): + print "data is ", msg.data + print "topic name is ", msg.topic + print msg + + The example also shows (last line) how a message is convertible to a string. + """ + def __init__(self, topic, data): + self.topic = topic + self.data = data + + def __str__(self): + return '[Topic: '+repr(self.topic)+', Data: '+repr(self.data)+']' + diff --git a/wx/lib/pubsub/core/kwargs/listenerimpl.py b/wx/lib/pubsub/core/kwargs/listenerimpl.py new file mode 100644 index 00000000..6a5e46ae --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/listenerimpl.py @@ -0,0 +1,93 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +from .listenerbase import ListenerBase, ValidatorBase +from .callables import ListenerMismatchError + + +class Listener(ListenerBase): + """ + Wraps a callable 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. + """ + + def __call__(self, kwargs, actualTopic, allKwargs=None): + """Call the listener with **kwargs. Note that it raises RuntimeError + if listener is dead. Should always return True (False would require + the callable_ 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 + + if self._autoTopicArgName is not None: + kwargs = kwargs.copy() + kwargs[self._autoTopicArgName] = actualTopic + + cb = self._callable() + if cb is None: + self._calledWhenDead() + cb(**kwargs) + + return True + + +class ListenerValidator(ValidatorBase): + """ + 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 _validateArgs(self, listener, paramsInfo): + # accept **kwargs + # accept *args + + # check if listener missing params (only possible if + # paramsInfo.acceptsAllKwargs is False) + allTopicMsgArgs = self._topicArgs | self._topicKwargs + allParams = set(paramsInfo.allParams) + if not paramsInfo.acceptsAllKwargs: + 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 + + # check if there are unknown parameters in listener signature: + extraArgs = allParams - allTopicMsgArgs + if extraArgs: + if allTopicMsgArgs: + msg = 'args (%s) not allowed, should be (%s)' \ + % (','.join(extraArgs), ','.join(allTopicMsgArgs)) + else: + msg = 'no args allowed, has (%s)' % ','.join(extraArgs) + raise ListenerMismatchError(msg, listener, extraArgs) + + # we accept listener that has fewer required paams than TMS + # since all args passed by name (previous showed that spec met + # for all parameters). + + # now make sure listener doesn't require params that are optional in TMS: + extraArgs = set( paramsInfo.getRequiredArgs() ) - self._topicArgs + if extraArgs: + msg = 'params (%s) missing default values' % (','.join(extraArgs),) + raise ListenerMismatchError(msg, listener, extraArgs) + diff --git a/wx/lib/pubsub/core/kwargs/publisher.py b/wx/lib/pubsub/core/kwargs/publisher.py new file mode 100644 index 00000000..37c28d8c --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/publisher.py @@ -0,0 +1,77 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + + +from .publisherbase import PublisherBase +from .datamsg import Message +from .. import (policies, py2and3) + + + +class Publisher(PublisherBase): + """ + Publisher used for kwargs protocol, ie when sending message data + via keyword arguments. + """ + + def sendMessage(self, topicName, **kwargs): + """Send a message. + + :param topicName: name of message topic (dotted or tuple format) + :param kwargs: message data (must satisfy the topic's MDS) + """ + topicMgr = self.getTopicMgr() + topicObj = topicMgr.getOrCreateTopic(topicName) + topicObj.publish(**kwargs) + + def getMsgProtocol(self): + return 'kwargs' + + +class PublisherArg1Stage2(Publisher): + """ + This is used when transitioning from arg1 to kwargs + messaging protocol. + """ + + _base = Publisher + + class SenderTooManyKwargs(RuntimeError): + def __init__(self, kwargs, commonArgName): + extra = kwargs.copy() + del extra[commonArgName] + msg = 'Sender has too many kwargs (%s)' % ( py2and3.keys(extra),) + RuntimeError.__init__(self, msg) + + class SenderWrongKwargName(RuntimeError): + def __init__(self, actualKwargName, commonArgName): + msg = 'Sender uses wrong kwarg name ("%s" instead of "%s")' \ + % (actualKwargName, commonArgName) + RuntimeError.__init__(self, msg) + + def __init__(self, treeConfig = None): + self._base.__init__(self, treeConfig) + self.Msg = Message + + def sendMessage(self, _topicName, **kwarg): + commonArgName = policies.msgDataArgName + if len(kwarg) > 1: + raise self.SenderTooManyKwargs(kwarg, commonArgName) + elif len(kwarg) == 1 and commonArgName not in kwarg: + raise self.SenderWrongKwargName( py2and3.keys(kwarg)[0], commonArgName) + + data = kwarg.get(commonArgName, None) + kwargs = { commonArgName: self.Msg( _topicName, data) } + self._base.sendMessage( self, _topicName, **kwargs ) + + def getMsgProtocol(self): + return 'kwarg1' + + +if policies.msgProtocolTransStage is not None: + Publisher = PublisherArg1Stage2 + #print 'Using protocol', Publisher + + diff --git a/wx/lib/pubsub/core/kwargs/publishermixin.py b/wx/lib/pubsub/core/kwargs/publishermixin.py new file mode 100644 index 00000000..21c5cb73 --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/publishermixin.py @@ -0,0 +1,65 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + + +class PublisherMixin: + """ + Mixin for publishing messages to a topic's listeners. This will be + mixed into topicobj.Topic so that a user can use a Topic object to + send a message to the topic's listeners via a publish() method. + + 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). + """ + + def __init__(self): + pass + + def publish(self, **msgKwargs): + self._publish(msgKwargs) + + ############## IMPLEMENTATION ############### + + class IterState: + def __init__(self, msgKwargs): + self.filteredArgs = msgKwargs + self.argsChecked = False + + def checkMsgArgs(self, spec): + spec.check(self.filteredArgs) + self.argsChecked = True + + def filterMsgArgs(self, topicObj): + if self.argsChecked: + self.filteredArgs = topicObj.filterMsgArgs(self.filteredArgs) + else: + self.filteredArgs = topicObj.filterMsgArgs(self.filteredArgs, True) + self.argsChecked = True + + def _mix_prePublish(self, msgKwargs, topicObj=None, iterState=None): + if iterState is None: + # do a first check that all args are there, costly so only do once + iterState = self.IterState(msgKwargs) + if self.hasMDS(): + iterState.checkMsgArgs( self._getListenerSpec() ) + else: + assert not self.hasListeners() + + else: + iterState.filterMsgArgs(topicObj) + + assert iterState is not None + return iterState + + def _mix_callListener(self, listener, msgKwargs, iterState): + """Send the message for given topic with data in msgKwargs. + This sends message to listeners of parent topics as well. + Note that at each level, msgKwargs is filtered so only those + args that are defined for the topic are sent to listeners. """ + listener(iterState.filteredArgs, self, msgKwargs) + diff --git a/wx/lib/pubsub/core/kwargs/topicargspecimpl.py b/wx/lib/pubsub/core/kwargs/topicargspecimpl.py new file mode 100644 index 00000000..73120d5c --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/topicargspecimpl.py @@ -0,0 +1,217 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +import weakref + +from .topicutils import (stringize, WeakNone) +from .validatedefnargs import verifySubset +from .. import py2and3 + +### Exceptions raised during check() from sendMessage() + +class SenderMissingReqdMsgDataError(RuntimeError): + """ + Raised when a sendMessage() is missing arguments tagged as + 'required' by pubsub topic of message. + """ + + def __init__(self, topicName, argNames, missing): + 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, argNames, extra): + 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) + + +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, specGiven, parentArgsInfo): + 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) + + def isComplete(self): + return self.argsSpecType == self.SPEC_COMPLETE + + def getArgs(self): + return self.allOptional + self.allRequired + + def numArgs(self): + return len(self.allOptional) + len(self.allRequired) + + def getReqdArgs(self): + return self.allRequired + + def getOptArgs(self): + return self.allOptional + + def getArgsDocs(self): + return self.allDocs.copy() + + def setArgsDocs(self, docs): + """docs is a mapping from arg names to their documentation""" + if not self.isComplete(): + raise + for arg, doc in py2and3.iteritems(docs): + self.allDocs[arg] = doc + + def check(self, msgKwargs): + """Check that the message arguments given satisfy the topic message + data specification (MDS). Raises SenderMissingReqdMsgDataError if some required + args are missing or not known, and raises SenderUnknownMsgDataError if some + optional args are unknown. """ + all = set(msgKwargs) + # check that it has all required args + needReqd = set(self.allRequired) + hasReqd = (needReqd <= all) + if not hasReqd: + raise SenderMissingReqdMsgDataError( + self.topicNameTuple, py2and3.keys(msgKwargs), 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, + py2and3.keys(msgKwargs), optional - set(self.allOptional) ) + + def filterArgs(self, msgKwargs): + """Returns a dict which contains only those items of msgKwargs + which are defined for topic. E.g. if msgKwargs is {a:1, b:'b'} + and topic arg spec is ('a',) then return {a:1}. The returned dict + is valid only if check(msgKwargs) was called (or + check(superset of msgKwargs) was called).""" + assert self.isComplete() + if len(msgKwargs) == self.numArgs(): + return msgKwargs + + # only keep the keys from msgKwargs that are also in topic's kwargs + # method 1: SLOWEST + #newKwargs = dict( (k,msgKwargs[k]) for k in self.__msgArgs.allOptional if k in msgKwargs ) + #newKwargs.update( (k,msgKwargs[k]) for k in self.__msgArgs.allRequired ) + + # method 2: FAST: + #argNames = self.__msgArgs.getArgs() + #newKwargs = dict( (key, val) for (key, val) in msgKwargs.iteritems() if key in argNames ) + + # method 3: FASTEST: + argNames = set(self.getArgs()).intersection(msgKwargs) + newKwargs = dict( (k,msgKwargs[k]) for k in argNames ) + + return newKwargs + + def hasSameArgs(self, *argNames): + """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): + """return True if self has argsInfo object as parent""" + return self.parentAI() is argsInfo + + def getCompleteAI(self): + """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): + """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): + 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() + return + + 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): + 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 + + if self.parentAI() is not None: + self.__validateArgsToParent() + + # notify our children + for childAI in self.childrenAI: + childAI.__notifyParentCompleted() + + diff --git a/wx/lib/pubsub/core/kwargs/topicmgrimpl.py b/wx/lib/pubsub/core/kwargs/topicmgrimpl.py new file mode 100644 index 00000000..716368b2 --- /dev/null +++ b/wx/lib/pubsub/core/kwargs/topicmgrimpl.py @@ -0,0 +1,13 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +def getRootTopicSpec(): + """If using kwargs protocol, then root topic takes no args.""" + argsDocs = {} + reqdArgs = () + return argsDocs, reqdArgs + diff --git a/wx/lib/pubsub/core/listener.py b/wx/lib/pubsub/core/listener.py new file mode 100644 index 00000000..a67c5516 --- /dev/null +++ b/wx/lib/pubsub/core/listener.py @@ -0,0 +1,40 @@ +""" +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 .callables import ( + getID, + getArgs, + getRawFunction, + ListenerMismatchError, + CallArgsInfo +) + +from .listenerimpl import ( + Listener, + ListenerValidator +) + +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()) + """ + def __call__(self, listenerID, topicObj): + raise NotImplementedError('%s must override __call__()' % self.__class__) + + diff --git a/wx/lib/pubsub/core/listenerbase.py b/wx/lib/pubsub/core/listenerbase.py new file mode 100644 index 00000000..85358939 --- /dev/null +++ b/wx/lib/pubsub/core/listenerbase.py @@ -0,0 +1,185 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from . import weakmethod + +from .callables import ( + getID, + getArgs, + ListenerMismatchError, + CallArgsInfo, + AUTO_TOPIC as _AUTO_ARG +) + + +class ListenerBase: + """ + Base class for listeners, ie. callables subscribed to pubsub. + """ + + AUTO_TOPIC = _AUTO_ARG + + def __init__(self, callable_, argsInfo, onDead=None): + """Use callable_ 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_ gets garbage collected (callable_ is held only by weak + reference). """ + # set call policies + self.acceptsAllKwargs = argsInfo.acceptsAllKwargs + + self._autoTopicArgName = argsInfo.autoTopicArgName + self._callable = weakmethod.getWeakRef(callable_, self.__notifyOnDead) + self.__onDead = onDead + + # save identity now in case callable dies: + name, mod = getID(callable_) # + self.__nameID = name + self.__module = mod + self.__id = str(id(callable_))[-4:] # only last four digits of id + self.__hash = hash(callable_) + + def __call__(self, args, kwargs, actualTopic, allArgs=None): + raise NotImplementedError + + def name(self): + """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): + """Get a type name for the listener. This is a class name or + function name, as appropriate. """ + return self.__nameID + + def module(self): + """Get the module in which the callable was defined.""" + return self.__module + + def getCallable(self): + """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): + """Return True if this listener died (has been garbage collected)""" + return self._callable() is None + + def wantsTopicObjOnCall(self): + """True if this listener wants topic object: it has a arg=pub.AUTO_TOPIC""" + return self._autoTopicArgName is not None + + def wantsAllMessageData(self): + """True if this listener wants all message data: it has a \**kwargs argument""" + return self.acceptsAllKwargs + + 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, ref): + """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): + """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 + + try: + c1 = self._callable() + 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): + """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 + + +class ValidatorBase: + """ + Validates listeners. It checks whether the listener given to + validate() method complies with required and optional arguments + specified for topic. + """ + + def __init__(self, topicArgs, topicKwargs): + """topicArgs is a list of argument names that will be required when sending + a message to listener. Hence order of items in topicArgs matters. The topicKwargs + is a 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): + """Validate that listener satisfies the requirements of + being a topic listener, if topic's kwargs keys are topicKwargKeys + (so only the list of keyword arg names for topic are necessary). + Raises ListenerMismatchError if listener not usable for topic. + + Otherwise, returns an 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 protocol). + E.g. def fn1(msgTopic=Listener.AUTO_TOPIC) would + cause validate(fn1) to return True, whereas any other kwarg name or value + would cause a False to be returned. + """ + paramsInfo = getArgs( listener ) + self._validateArgs(listener, paramsInfo) + return paramsInfo + + + def isValid(self, listener): + """Return true only if listener can subscribe to messages where + topic has kwargs keys topicKwargKeys. Just calls validate() in + a try-except clause.""" + try: + self.validate(listener) + return True + except ListenerMismatchError: + return False + + + def _validateArgs(self, listener, paramsInfo): + """Provide implementation in derived classes""" + raise NotImplementedError + + diff --git a/wx/lib/pubsub/core/notificationmgr.py b/wx/lib/pubsub/core/notificationmgr.py new file mode 100644 index 00000000..83ffab6c --- /dev/null +++ b/wx/lib/pubsub/core/notificationmgr.py @@ -0,0 +1,185 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" +import sys + +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 = 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): + if not self.__atExitRegistered: + self.__registerForAppExit() + self.__handlers.append(handler) + + def getHandlers(self): + 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): + """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=None, unsubscribe=None, + deadListener=None, sendMessage=None, newTopic=None, + delTopic=None, all=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 + + + +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, topicObj, newSub): + """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, topicObj): + """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, topicObj): + """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, topicObj, pubListener=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, description, required, argsDocs): + """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): + """Called whenever a topic is removed from topic tree. + :param topicName: name of topic removed.""" + raise NotImplementedError + + diff --git a/wx/lib/pubsub/core/publisherbase.py b/wx/lib/pubsub/core/publisherbase.py new file mode 100644 index 00000000..b48e3989 --- /dev/null +++ b/wx/lib/pubsub/core/publisherbase.py @@ -0,0 +1,191 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from .topicmgr import ( + TopicManager, + TreeConfig +) + +from .. import py2and3 + + +class PublisherBase: + """ + 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 = None): + """If treeConfig is None, a default one is created from an + instance of TreeConfig.""" + self.__treeConfig = treeConfig or TreeConfig() + self.__topicMgr = TopicManager(self.__treeConfig) + + def getTopicMgr(self): + """Get the topic manager created for this publisher.""" + return self.__topicMgr + + def getListenerExcHandler(self): + """Get the listener exception handler that was registered + via setListenerExcHandler(), or None of none registered.""" + return self.__treeConfig.listenerExcHandler + + def setListenerExcHandler(self, handler): + """Set the function to call when a listener raises an exception + during a sendMessage(). The handler must adhere to the + IListenerExcHandler API. """ + self.__treeConfig.listenerExcHandler = handler + + def addNotificationHandler(self, handler): + """Add a handler for tracing pubsub activity. The handler should be + a class that adheres to the API of INotificationHandler. """ + 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): + """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 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): + """Return a dictionary with the notification flag states.""" + return self.__treeConfig.notificationMgr.getFlagStates() + + def setTopicUnspecifiedFatal(self, newVal=True, checkExisting=True): + """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 sendMessage(self, topicName, *args, **kwargs): + """Send a message for topic name with given data (args and kwargs). + This will be overridden by derived classes that implement + message-sending for different messaging protocols; not all + parameters may be accepted.""" + raise NotImplementedError + + def subscribe(self, listener, topicName): + """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. + + 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) + return subscribedListener, success + + def unsubscribe(self, listener, topicName): + """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 = None, + listenerFilter = None, topicFilter = None): + """By default (no args given), unsubscribe all listeners from all + topics. A listenerFilter can be given so that only the listeners + that satisfy listenerFilter(listener) == True will be unsubscribed + (with listener being a pub.Listener wrapper instance for each listener + subscribed). A topicFilter can also be given so that only topics + that satisfy topicFilter(topic name) == True will be affected. + If only one topic should have listeners unsubscribed, then a topic + name 'topicName' can be given *instead* instead of a topic filter. + + Returns the 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 py2and3.iteritems(topicsMap): + 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 + + diff --git a/wx/lib/pubsub/core/topicargspec.py b/wx/lib/pubsub/core/topicargspec.py new file mode 100644 index 00000000..202c973d --- /dev/null +++ b/wx/lib/pubsub/core/topicargspec.py @@ -0,0 +1,77 @@ +""" +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. + +""" + + +from .listener import getArgs as getListenerArgs +from .validatedefnargs import MessageDataSpecError +from .topicargspecimpl import ( + SenderMissingReqdMsgDataError, + SenderUnknownMsgDataError, + ArgsInfo +) + + +def topicArgsFromCallable(_callable): + """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) + 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 + call protocol, 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=None, reqdArgs=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, reqdArgs = None): + self.argsDocs = allArgsDocs + self.reqdArgs = reqdArgs or () + self.argsSpecType = ArgSpecGiven.SPEC_GIVEN_ALL + + def isComplete(self): + """Returns True if the definition is usable, false otherwise.""" + return self.argsSpecType == ArgSpecGiven.SPEC_GIVEN_ALL + + def getOptional(self): + return tuple( set( self.argsDocs.keys() ).difference( self.reqdArgs ) ) + + def __str__(self): + return "%s, %s, %s" % \ + (self.argsDocs, self.reqdArgs, self.argsSpecType) + + diff --git a/wx/lib/pubsub/core/topicdefnprovider.py b/wx/lib/pubsub/core/topicdefnprovider.py new file mode 100644 index 00000000..8bce8ff0 --- /dev/null +++ b/wx/lib/pubsub/core/topicdefnprovider.py @@ -0,0 +1,636 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + + +import os, re, inspect +from textwrap import TextWrapper, dedent + +from .. import ( + policies, + py2and3 +) +from .topicargspec import ( + topicArgsFromCallable, + ArgSpecGiven +) +from .topictreetraverser import TopicTreeTraverser +from .topicexc import UnrecognizedSourceFormatError + + +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): + """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): + """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): + """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 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, description, argsDocs, required): + 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): + """Get the docstring for the topic tree.""" + raise NotImplementedError + + def getNextTopic(self): + """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=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.""" + 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): + return self.__rootDoc + + def getNextTopic(self): + self.__iterStarted = True + try: + topicNameTuple, topicClassObj = py2and3.nextiter(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): + return [nt for (nt, defn) in self.__rootTopics] + + def __addDefnFromClassObj(self, pyClassObj): + """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, parentNameTuple): + 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, parentNameTuple=()): + """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, protoDocs): + 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, searchPath=None): + """Load the given named module, searched for in searchPath or, if not + specified, in sys.path. Give it to a TopicDefnDeserialClass.""" + from . import imp2 + module = imp2.load_module(moduleName, searchPath) + self.__classDeserial = TopicDefnDeserialClass(module) + + def getTreeDoc(self): + return self.__classDeserial.getTreeDoc() + + def getNextTopic(self): + return self.__classDeserial.getNextTopic() + + def doneIter(self): + self.__classDeserial.doneIter() + + def resetIter(self): + self.__classDeserial.resetIter() + + def getDefinedTopics(self): + 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): + """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.""" + + def createTmpModule(): + moduleNamePre = 'tmp_export_topics_' + import os, tempfile + creationDir = os.getcwd() + fileID, path = tempfile.mkstemp('.py', moduleNamePre, dir=creationDir) + stringFile = os.fdopen(fileID, 'w') + stringFile.write( dedent(source) ) + stringFile.close() + return path, [creationDir] + + self.__filename, searchPath = createTmpModule() + moduleName = os.path.splitext( os.path.basename(self.__filename) )[0] + self.__modDeserial = TopicDefnDeserialModule(moduleName, searchPath) + + def getTreeDoc(self): + return self.__modDeserial.getTreeDoc() + + def getNextTopic(self): + return self.__modDeserial.getNextTopic() + + def doneIter(self): + self.__modDeserial.doneIter() + # remove the temporary module and its compiled version (*.pyc) + os.remove(self.__filename) + try: # py3.2+ uses special folder/filename for .pyc files + from imp import cache_from_source + os.remove(cache_from_source(self.__filename)) + except ImportError: + os.remove(self.__filename + 'c') + + def resetIter(self): + self.__modDeserial.resetIter() + + def getDefinedTopics(self): + return self.__modDeserial.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, format, **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): + 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): + return py2and3.iterkeys(self.__topicDefns) + + def getTreeDoc(self): + return self.__treeDocs + + @classmethod + def registerTypeForImport(cls, typeName, providerClassObj): + """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, bak): + import os, shutil + if os.path.exists(filename): + 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 = None, rootTopic=None, bak='bak', moduleDoc=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 py2and3.isstring(rootTopic): + from .. import pub + rootTopic = pub.getDefaultTopicMgr().getTopic(rootTopic) + + # create exporter + if moduleName is None: + capture = py2and3.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=None, fileObj=None, width=70, indentStep=4, + treeDoc = defaultTopicTreeSpecHeader, footer = 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 = 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=footer, fileObj=fileObj) + def fmItem(argName, argVal): + if py2and3.isstring(argVal): + 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(py2and3.iterkeys(args))] + 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): + """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): + """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): + # 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): + """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) + if policies.msgDataProtocol != 'arg1': + 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): + if not msg: + return msg + if msg.startswith("'''") or msg.startswith('"""'): + return msg + return '"""\n%s\n"""' % msg.strip() + + def __printTopicDescription(self, topicObj): + if topicObj.getDescription(): + extraIndent = self.__indentStep + self.__formatItem('"""', extraIndent) + self.__formatItem( topicObj.getDescription(), extraIndent ) + self.__formatItem('"""', extraIndent) + + def __printTopicArgSpec(self, topicObj): + 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(py2and3.iterkeys(argsDocs)): + if key not in parentMsgKeys: + argDesc = argsDocs[key] + msg = "- %s: %s" % (key, argDesc) + self.__formatItem(msg, extraIndent) + self.__formatItem('"""', extraIndent) + + def __formatItem(self, item, extraIndent=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, extraIndent=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) ) + + diff --git a/wx/lib/pubsub/core/topicexc.py b/wx/lib/pubsub/core/topicexc.py new file mode 100644 index 00000000..76d70226 --- /dev/null +++ b/wx/lib/pubsub/core/topicexc.py @@ -0,0 +1,72 @@ +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + + + +class TopicNameError(ValueError): + """Raised when the topic name is not properly formatted or + no corresponding Topic object found. """ + def __init__(self, name, msg): + 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): + 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, args): + 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, topicObj, origExc=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 currently not used. """ + 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') + + diff --git a/wx/lib/pubsub/core/topicmgr.py b/wx/lib/pubsub/core/topicmgr.py new file mode 100644 index 00000000..3d080c0f --- /dev/null +++ b/wx/lib/pubsub/core/topicmgr.py @@ -0,0 +1,456 @@ +""" +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. +""" + +__all__ = [ + 'TopicManager', + 'TopicNameError', + 'TopicDefnError', + ] + + +from .callables import getID + +from .topicutils import ( + ALL_TOPICS, + tupleize, + stringize, +) + +from .topicexc import ( + TopicNameError, + TopicDefnError, +) + +from .topicargspec import ( + ArgSpecGiven, + ArgsInfo, + topicArgsFromCallable, +) + +from .topicobj import ( + Topic, +) + +from .treeconfig import TreeConfig +from .topicdefnprovider import ITopicDefnProvider +from .topicmgrimpl import getRootTopicSpec + +from .. import py2and3 + + +# --------------------------------------------------------- + +ARGS_SPEC_ALL = ArgSpecGiven.SPEC_GIVEN_ALL +ARGS_SPEC_NONE = ArgSpecGiven.SPEC_GIVEN_NONE + + +# --------------------------------------------------------- + +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=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 = getRootTopicSpec() + desc = 'Root of all topics' + specGiven = ArgSpecGiven(argsDocs, reqdArgs) + self.__allTopics = self.__createTopic((ALL_TOPICS,), desc, specGiven=specGiven) + + def getRootAllTopics(self): + """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, format=None): + """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): + """Get how many topic definitions providers are registered.""" + return self.__defnProvider.getNumProviders() + + def getTopic(self, name, okIfNone=False): + """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 newTopic(self, _name, _desc, _required=(), **_argDocs): + """Deprecated legacy method. + If topic _name already exists, just returns it and does nothing else. + Otherwise, uses getOrCreateTopic() to create it, then sets its + description (_desc) and its message data specification (_argDocs + and _required). Replaced by getOrCreateTopic().""" + topic = self.getTopic(_name, True) + if topic is None: + topic = self.getOrCreateTopic(_name) + topic.setDescription(_desc) + topic.setMsgArgSpec(_argDocs, _required) + return topic + + def getOrCreateTopic(self, name, protoListener=None): + """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): + """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): + """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 py2and3.itervalues(self._topicsMap): + if not topic.hasMDS(): + raise TopicDefnError(topic.getNameTuple()) + + def delTopic(self, name): + """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): + """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 py2and3.itervalues(self._topicsMap): + if topicObj.hasListener(listener): + assocTopics.append(topicObj) + return assocTopics + + def __getClosestParent(self, topicNameDotted): + """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): + """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, desc, specGiven, parent=None): + """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): + """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 protocol. + """ + + def __init__(self, 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): + """Return how many providers added.""" + return len(self.__providers) + + def getDefn(self, topicNameTuple): + """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): + """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 + + diff --git a/wx/lib/pubsub/core/topicobj.py b/wx/lib/pubsub/core/topicobj.py new file mode 100644 index 00000000..c4303421 --- /dev/null +++ b/wx/lib/pubsub/core/topicobj.py @@ -0,0 +1,472 @@ +""" +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 + +from .listener import ( + Listener, + ListenerValidator, +) + +from .topicutils import ( + ALL_TOPICS, + stringize, + tupleize, + validateName, + smartDedent, +) + +from .topicexc import ( + TopicDefnError, + TopicNameError, + ExcHandlerError, +) + +from .publishermixin import PublisherMixin + +from .topicargspec import ( + ArgsInfo, + ArgSpecGiven, + topicArgsFromCallable, + SenderMissingReqdMsgDataError, + SenderUnknownMsgDataError, + MessageDataSpecError, +) + +from .. import py2and3 + + +class Topic(PublisherMixin): + """ + 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, nameTuple, description, + msgArgsInfo, parent=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 + PublisherMixin.__init__(self) + + 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._getListenerSpec() + parent.__adoptSubtopic( self ) + + def setDescription(self, desc): + """Set the 'docstring' of topic""" + self.__description = desc + + def getDescription(self): + """Return the 'docstring' of topic""" + if self.__description is None: + return None + return smartDedent(self.__description) + + def setMsgArgSpec(self, argsDocs, required=()): + """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()._getListenerSpec()) + except MessageDataSpecError: + # discard the lower part of the stack trace + exc = py2and3.getexcobj() + raise exc + self.__finalize() + + else: + raise RuntimeError('Not allowed to call this: msg spec already set!') + + def getArgs(self): + """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): + """Get a map of keyword names to docstrings: documents each MDS element. """ + return self.__msgArgs.getArgsDocs() + + def setArgDescriptions(self, **docs): + """Set the docstring for each MDS datum.""" + self.__msgArgs.setArgsDocs(docs) + + def hasMDS(self): + """Return true if this topic has a message data specification (MDS).""" + return self.__validator is not None + + def filterMsgArgs(self, msgKwargs, check=False): + """Get the MDS docstrings for each of the spedified kwargs.""" + filteredArgs = self.__msgArgs.filterArgs(msgKwargs) + # if no check of args yet, do it now: + if check: + self.__msgArgs.check(filteredArgs) + return filteredArgs + + def isAll(self): + """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): + """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): + """Return dotted form of full topic name""" + return stringize(self.__tupleName) + + def getNameTuple(self): + """Return tuple form of full topic name""" + return self.__tupleName + + def getNodeName(self): + """Return the last part of the topic name (has no dots)""" + name = self.__tupleName[-1] + return name + + def getParent(self): + """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=None): + """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): + """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): + """Get a list of Topic instances that are subtopics of self.""" + return py2and3.values(self.__subTopics) + + def getNumListeners(self): + """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): + """Return true if listener is subscribed to this topic.""" + return listener in self.__listeners + + def hasListeners(self): + """Return true if there are any listeners subscribed to + this topic, false otherwise.""" + return bool(self.__listeners) + + def getListeners(self): + """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 py2and3.keys(self.__listeners) + + def getListenersIter(self): + """Get an iterator over listeners subscribed to this topic. Do not use if listeners can be + un/subscribed while iterating. """ + return py2and3.iterkeys(self.__listeners) + + def validate(self, listener): + """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) + + def isValid(self, listener): + """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) + + def subscribe(self, listener): + """Subscribe listener to this topic. Returns a pair + (pub.Listener, success). The success is true only if listener + was not already subscribed and is now subscribed. """ + if listener in self.__listeners: + assert self.hasMDS() + subdLisnr, newSub = self.__listeners[listener], False + + else: + if self.__validator is None: + args, reqd = topicArgsFromCallable(listener) + self.setMsgArgSpec(args, reqd) + argsInfo = self.__validator.validate(listener) + weakListener = Listener( + listener, argsInfo, onDead=self.__onDeadListener) + self.__listeners[weakListener] = weakListener + subdLisnr, newSub = weakListener, True + + # notify of subscription + self._treeConfig.notificationMgr.notifySubscribe(subdLisnr, self, newSub) + + return subdLisnr, newSub + + def unsubscribe(self, 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=None): + """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 = py2and3.keys(self.__listeners) + self.__listeners = {} + else: + unsubd = [] + for listener in py2and3.keys(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 + + ############################################################# + # + # Impementation + # + ############################################################# + + def _getListenerSpec(self): + """Only to be called by pubsub package""" + return self.__msgArgs + + def _publish(self, data): + """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).""" + self._treeConfig.notificationMgr.notifySend('pre', self) + + # send to ourself + iterState = self._mix_prePublish(data) + self.__sendMessage(data, self, iterState) + + # send up the chain + topicObj = self.getParent() + while topicObj is not None: + if topicObj.hasListeners(): + iterState = self._mix_prePublish(data, topicObj, iterState) + self.__sendMessage(data, topicObj, iterState) + + # done for this topic, continue up branch to parent towards root + topicObj = topicObj.getParent() + + self._treeConfig.notificationMgr.notifySend('post', self) + + def __sendMessage(self, data, topicObj, iterState): + # 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: + for listener in topicObj.getListeners(): + try: + self._treeConfig.notificationMgr.notifySend('in', topicObj, pubListener=listener) + self._mix_callListener(listener, data, iterState) + + 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 = py2and3.getexcobj() + #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): + """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): + """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 py2and3.iteritems(self.__subTopics): + assert isinstance(subObj, Topic) + #print 'Unlinking %s from parent' % subObj.getName() + subObj.__undefineBranch(topicsMap) + + self.__subTopics = {} + del topicsMap[self.getName()] + + def __adoptSubtopic(self, topicObj): + """Add topicObj as child topic.""" + assert topicObj.__parentTopic() is self + attrName = topicObj.getNodeName() + self.__subTopics[attrName] = topicObj + + def __abandonSubtopic(self, name): + """The given subtopic becomes orphan (no parent).""" + topicObj = self.__subTopics.pop(name) + assert topicObj.__parentTopic() is self + + def __onDeadListener(self, weakListener): + """One of our subscribed listeners has died, so remove it and notify""" + pubListener = self.__listeners.pop(weakListener) + # notify: + self._treeConfig.notificationMgr.notifyDeadListener(pubListener, self) + + def __str__(self): + return "%s(%s)" % (self.getName(), self.getNumListeners()) + + diff --git a/wx/lib/pubsub/core/topictreetraverser.py b/wx/lib/pubsub/core/topictreetraverser.py new file mode 100644 index 00000000..e31732f0 --- /dev/null +++ b/wx/lib/pubsub/core/topictreetraverser.py @@ -0,0 +1,143 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. + +""" + +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. + """ + DEPTH = 'Depth first through topic tree' + BREADTH = 'Breadth first through topic tree' + MAP = 'Sequential through topic manager\'s topics map' + + def __init__(self, visitor = None): + """The visitor, if given, must adhere to API of + ITopicTreeVisitor. The visitor can be changed or + set via setVisitor(visitor) before calling traverse().""" + self.__handler = visitor + + def setVisitor(self, visitor): + """The visitor must adhere to API of ITopicTreeVisitor. """ + self.__handler = visitor + + def traverse(self, topicObj, how=DEPTH, onlyFiltered=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 == self.MAP: + raise NotImplementedError('not yet available') + + self.__handler._startTraversal() + + if how == self.BREADTH: + self.__traverseBreadth(topicObj, onlyFiltered) + else: + assert how == self.DEPTH + self.__traverseDepth(topicObj, onlyFiltered) + + self.__handler._doneTraversal() + + def __traverseBreadth(self, topicObj, onlyFiltered): + 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, onlyFiltered): + visitor = self.__handler + + def extendStack(topicTreeStack, subtopics): + topicTreeStack.insert(0, visitor._endChildren) # marker functor + # put subtopics in list in alphabetical order + subtopicsTmp = 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) + + +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): + """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 + diff --git a/wx/lib/pubsub/core/topicutils.py b/wx/lib/pubsub/core/topicutils.py new file mode 100644 index 00000000..4b3d5cec --- /dev/null +++ b/wx/lib/pubsub/core/topicutils.py @@ -0,0 +1,118 @@ +""" +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 + +from .topicexc import TopicNameError + +from .. import py2and3 + +__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): + """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): + """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): + """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 py2and3.isstring(topicName): + return topicName + + if hasattr(topicName, "msgDataSpec"): + return topicName._topicNameStr + + try: + name = '.'.join(topicName) + except Exception: + exc = py2and3.getexcobj() + raise TopicNameError(topicName, str(exc)) + + return name + + +def tupleize(topicName): + """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 py2and3.isstring(topicName): + 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 + + diff --git a/wx/lib/pubsub/core/treeconfig.py b/wx/lib/pubsub/core/treeconfig.py new file mode 100644 index 00000000..73752746 --- /dev/null +++ b/wx/lib/pubsub/core/treeconfig.py @@ -0,0 +1,21 @@ +""" + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from .notificationmgr import NotificationMgr + + +class TreeConfig: + """ + Each topic tree has its own topic manager and configuration, + such as notification and exception handling. + """ + + def __init__(self, notificationHandler=None, listenerExcHandler=None): + self.notificationMgr = NotificationMgr(notificationHandler) + self.listenerExcHandler = listenerExcHandler + self.raiseOnTopicUnspecified = False + + diff --git a/wx/lib/pubsub/core/validatedefnargs.py b/wx/lib/pubsub/core/validatedefnargs.py new file mode 100644 index 00000000..397e4527 --- /dev/null +++ b/wx/lib/pubsub/core/validatedefnargs.py @@ -0,0 +1,29 @@ +""" +Some topic definition validation functions. + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from .topicexc import MessageDataSpecError + + +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) ) + + diff --git a/wx/lib/pubsub/core/weakmethod.py b/wx/lib/pubsub/core/weakmethod.py new file mode 100644 index 00000000..5b5841c4 --- /dev/null +++ b/wx/lib/pubsub/core/weakmethod.py @@ -0,0 +1,102 @@ +""" +This module provides a basic "weak method" implementation, WeakMethod. It uses +weakref.WeakRef which, used on its own, produces weak methods that are dead on +creation, not very useful. 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. + +""" + +# for function and method parameter counting: +from inspect import ismethod +# for weakly bound methods: +from types import MethodType +from weakref import ref as WeakRef + + +class WeakMethod: + """Represent a weak bound method, i.e. a method which doesn't keep alive the + object that it is bound to. """ + + def __init__(self, method, notifyDead = None): + """The method must be bound. notifyDead will be called when + object that method is bound to dies. """ + assert ismethod(method) + if method.__self__ is None: + raise ValueError('Unbound methods cannot be weak-referenced.') + + self.notifyDead = None + if notifyDead is None: + self.objRef = WeakRef(method.__self__) + else: + self.notifyDead = notifyDead + self.objRef = WeakRef(method.__self__, self.__onNotifyDeadObj) + + self.fun = method.__func__ + self.cls = method.__self__.__class__ + + def __onNotifyDeadObj(self, ref): + if self.notifyDead: + try: + self.notifyDead(self) + except Exception: + import traceback + traceback.print_exc() + + def __call__(self): + """Returns a MethodType if object for method still alive. + Otherwise return None. Note that MethodType causes a + strong reference to object to be created, so shouldn't save + the return value of this call. Note also that this __call__ + is required only for compatibility with WeakRef.ref(), otherwise + there would be more efficient ways of providing this functionality.""" + if self.objRef() is None: + return None + else: + return MethodType(self.fun, self.objRef()) + + def __eq__(self, method2): + """Two WeakMethod objects compare equal if they refer to the same method + of the same instance. Thanks to Josiah Carlson for patch and clarifications + on how dict uses eq/cmp and hashing. """ + if not isinstance(method2, WeakMethod): + return False + + return ( self.fun is method2.fun + and self.objRef() is method2.objRef() + and self.objRef() is not None ) + + def __hash__(self): + """Hash is an optimization for dict searches, it need not + return different numbers for every different object. Some objects + are not hashable (eg objects of classes derived from dict) so no + hash(objRef()) in there, and hash(self.cls) would only be useful + in the rare case where instance method was rebound. """ + return hash(self.fun) + + def __repr__(self): + dead = '' + if self.objRef() is None: + dead = '; DEAD' + obj = '<%s at %s%s>' % (self.__class__, id(self), dead) + return obj + + def refs(self, weakRef): + """Return true if we are storing same object referred to by weakRef.""" + return self.objRef == weakRef + + +def getWeakRef(obj, notifyDead=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) + diff --git a/wx/lib/pubsub/policies.py b/wx/lib/pubsub/policies.py new file mode 100644 index 00000000..bc296ce5 --- /dev/null +++ b/wx/lib/pubsub/policies.py @@ -0,0 +1,24 @@ +""" +Aggregates policies for pubsub. Mainly, related to messaging protocol. + +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +msgProtocolTransStage = None + +msgDataProtocol = 'kwargs' +msgDataArgName = None +senderKwargNameAny = False + + +def setMsgDataArgName(stage, listenerArgName, senderArgNameAny=False): + global senderKwargNameAny + global msgDataArgName + global msgProtocolTransStage + senderKwargNameAny = senderArgNameAny + msgDataArgName = listenerArgName + msgProtocolTransStage = stage + #print `policies.msgProtocolTransStage`, `policies.msgDataProtocol`, \ + # `policies.senderKwargNameAny`, `policies.msgDataArgName` + #print 'override "arg1" protocol arg name:', argName diff --git a/wx/lib/pubsub/pub.py b/wx/lib/pubsub/pub.py new file mode 100644 index 00000000..b75f4e7c --- /dev/null +++ b/wx/lib/pubsub/pub.py @@ -0,0 +1,199 @@ +""" +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 = 3 #: major API version + +VERSION_SVN = "$Rev: 243 $".split()[1] # DO NOT CHANGE: automatically updated by VCS + +from .core import ( + Publisher as _Publisher, + + AUTO_TOPIC, + + ListenerMismatchError, + TopicDefnError, + + IListenerExcHandler, + ExcHandlerError, + + SenderMissingReqdMsgDataError, + SenderUnknownMsgDataError, + + TopicDefnError, + TopicNameError, + UnrecognizedSourceFormatError, + + ALL_TOPICS, + + 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', + 'topicTreeRoot', + 'topicsMap', + + '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', + 'SenderMissingReqdMsgDataError', + 'SenderUnknownMsgDataError', + + # 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 + +getMsgProtocol = _publisher.getMsgProtocol + +def getDefaultPublisher(): + """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): + """Return true only if listener can subscribe to messages of given topic.""" + return _topicMgr.getTopic(topicName).isValid(listener) + + +def validate(listener, topicName): + """Checks if listener can subscribe to topicName. If not, raises + ListenerMismatchError, otherwise just returns.""" + _topicMgr.getTopic(topicName).validate(listener) + + +def isSubscribed(listener, topicName): + """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(): + """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): + """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. """ + for topicName in provider: + _topicMgr.getOrCreateTopic(topicName) + +#--------------------------------------------------------------------------- diff --git a/wx/lib/pubsub/py2and3.py b/wx/lib/pubsub/py2and3.py new file mode 100644 index 00000000..e00bc680 --- /dev/null +++ b/wx/lib/pubsub/py2and3.py @@ -0,0 +1,608 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.4.1" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves") + + + +class Module_six_moves_urllib_parse(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse") +sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib.parse") + + +class Module_six_moves_urllib_error(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib_error") +sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error") + + +class Module_six_moves_urllib_request(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib_request") +sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request") + + +class Module_six_moves_urllib_response(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib_response") +sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(types.ModuleType): + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +sys.modules[__name__ + ".moves.urllib_robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib_robotparser") +sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + parse = sys.modules[__name__ + ".moves.urllib_parse"] + error = sys.modules[__name__ + ".moves.urllib_error"] + request = sys.modules[__name__ + ".moves.urllib_request"] + response = sys.modules[__name__ + ".moves.urllib_response"] + robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"] + + +sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + for slots_var in orig_vars.get('__slots__', ()): + orig_vars.pop(slots_var) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + +def getexcobj(): + return sys.exc_info()[1] + +if PY3: + xrange = range +else: + xrange = xrange + +if PY3: + def keys(dictObj): + return list(dictObj.keys()) + def values(dictObj): + return list(dictObj.values()) + def nextiter(container): + return next(container) +else: + def keys(dictObj): + return dictObj.keys() + def values(dictObj): + return dictObj.values() + def nextiter(container): + return container.next() + +if PY3: + def isstring(obj): + return isinstance(obj, str) +else: + def isstring(obj): + return isinstance(obj, (str, unicode)) + diff --git a/wx/lib/pubsub/setuparg1.py b/wx/lib/pubsub/setuparg1.py new file mode 100644 index 00000000..df85780c --- /dev/null +++ b/wx/lib/pubsub/setuparg1.py @@ -0,0 +1,47 @@ +""" +Setup pubsub for the *arg1* message protocol. In a default pubsub installation +the default protocol is *kargs*. + +This module must be imported before the first ``from pubsub import pub`` +statement in the application. Once :mod:pub has been imported, the messaging +protocol must not be changed (i.e., importing it after the first +``from pubsub import pub`` statement has undefined behavior). +:: + + from .. import setuparg1 + from .. import pub + +The *arg1* protocol is identical to the legacy messaging protocol from +first version of pubsub (when it was still part of wxPython) and +is *deprecated*. This module is therefore *deprecated*. +""" + +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from . import policies +policies.msgDataProtocol = 'arg1' + +import sys +sys.stdout.write(""" + +====================================================================== + *** ATTENTION *** +This messaging protocol is deprecated. This module, and hence arg1 +messaging protocol, will be removed in v3.4 of PyPubSub. Please make +the necessary changes to your code so that it no longer requires this +module. The pypubsub documentation provides steps that may be useful +to minimize the chance of introducing bugs in your application. +====================================================================== + +""") + + +def enforceArgName(commonName): + """This will configure pubsub to require that all listeners use + the same argument name (*commonName*) as first parameter. This + is a ueful first step in migrating an application that has been + using *arg1* protocol to the more powerful *kwargs* protocol. """ + policies.setMsgDataArgName(1, commonName) diff --git a/wx/lib/pubsub/setupkwargs.py b/wx/lib/pubsub/setupkwargs.py new file mode 100644 index 00000000..7230d34e --- /dev/null +++ b/wx/lib/pubsub/setupkwargs.py @@ -0,0 +1,29 @@ +""" +Setup pubsub for the kwargs message protocol. In a default installation +this is the default protocol so this module is only needed if setupkargs +utility functions are used, or in a custom installation where kwargs +is not the default messaging protocol (such as in some versions of +wxPython). + +This module must be imported before the first ``from pubsub import pub`` +statement in the application. Once :mod:pub has been imported, the messaging +protocol cannot be changed (i.e., importing it after the first +``from pubsub import pub`` statement has undefined behavior). +""" + +""" +:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved. +:license: BSD, see LICENSE_BSD_Simple.txt for details. +""" + +from . import policies +policies.msgDataProtocol = 'kwargs' + + +def transitionFromArg1(commonName): + """Utility function to assist migrating an application from using + the arg1 messaging protocol to using the kwargs protocol. Call this + after having run and debugged your application with ``setuparg1.enforceArgName(commonName)``. See the migration docs + for more detais. + """ + policies.setMsgDataArgName(2, commonName) diff --git a/wx/lib/pubsub/utils/__init__.py b/wx/lib/pubsub/utils/__init__.py new file mode 100644 index 00000000..fe1eb961 --- /dev/null +++ b/wx/lib/pubsub/utils/__init__.py @@ -0,0 +1,27 @@ +""" +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' + ] \ No newline at end of file diff --git a/wx/lib/pubsub/utils/exchandling.py b/wx/lib/pubsub/utils/exchandling.py new file mode 100644 index 00000000..101fb7e8 --- /dev/null +++ b/wx/lib/pubsub/utils/exchandling.py @@ -0,0 +1,100 @@ +""" +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 ..core.listener import IListenerExcHandler + + +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. + self.traceback = traceback.extract_tb(tmpInfo[2])[3:] + # help avoid circular refs + del tmpInfo + + def getFormattedList(self): + """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): + """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 + as a message of topic name given by topicUncaughtExc. + """ + + # name of the topic + topicUncaughtExc = 'uncaughtExcInListener' + + def __init__(self, topicMgr=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): + """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, 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) + + diff --git a/wx/lib/pubsub/utils/misc.py b/wx/lib/pubsub/utils/misc.py new file mode 100644 index 00000000..51299eaa --- /dev/null +++ b/wx/lib/pubsub/utils/misc.py @@ -0,0 +1,100 @@ +""" +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 +from .. import py2and3 + +__all__ = ('printImported', 'StructMsg', 'Callback', 'Enum' ) + + +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() + py2and3.print_('\n'.join(ll)) + + +class StructMsg: + """ + This *can* be used to package message data. Each of the keyword + args given at construction will be stored as a member of the 'data' + member of instance. E.g. "m=Message2(a=1, b='b')" would succeed + "assert m.data.a==1" and "assert m.data.b=='b'". However, use of + Message2 makes your messaging code less documented and harder to + debug. + """ + + def __init__(self, **kwargs): + class Data: pass + self.data = Data() + self.data.__dict__.update(kwargs) + + +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_): + self.__callable = callable_ + def __call__(self, *args, **kwargs): + return self.__callable(*args, **kwargs) + + +class Enum: + """Used only internally. Represent one value out of an enumeration + set. It is meant to be used as:: + + class YourAllowedValues: + enum1 = Enum() + # or: + enum2 = Enum(value) + # or: + enum3 = Enum(value, 'descriptionLine1') + # or: + enum3 = Enum(None, 'descriptionLine1', 'descriptionLine2', ...) + + val = YourAllowedValues.enum1 + ... + if val is YourAllowedValues.enum1: + ... + """ + nextValue = 0 + values = set() + + def __init__(self, value=None, *desc): + """Use value if given, otherwise use next integer.""" + self.desc = '\n'.join(desc) + if value is None: + assert Enum.nextValue not in Enum.values + self.value = Enum.nextValue + Enum.values.add(self.value) + + Enum.nextValue += 1 + # check that we haven't run out of integers! + if Enum.nextValue == 0: + raise RuntimeError('Ran out of enumeration values?') + + else: + try: + value + Enum.nextValue + raise ValueError('Not allowed to assign integer to enumerations') + except TypeError: + pass + self.value = value + if self.value not in Enum.values: + Enum.values.add(self.value) + + diff --git a/wx/lib/pubsub/utils/notification.py b/wx/lib/pubsub/utils/notification.py new file mode 100644 index 00000000..8f785c87 --- /dev/null +++ b/wx/lib/pubsub/utils/notification.py @@ -0,0 +1,331 @@ +""" +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 ..core import callables +from ..core.notificationmgr import INotificationHandler + + +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, topicObj, newSub): + pass + def notifyUnsubscribe(self, pubListener, topicObj): + pass + def notifyDeadListener(self, pubListener, topicObj): + pass + def notifySend(self, stage, topicObj, pubListener=None): + pass + + def notifyNewTopic(self, topicObj, description, required, argsDocs): + pass + def notifyDelTopic(self, topicName): + pass + + +class NotifyByWriteFile(INotificationHandler): + """ + Print a message to stdout when a notification is received. + """ + + defaultPrefix = 'PUBSUB:' + + def __init__(self, fileObj = None, prefix = 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, topicObj, newSub): + 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, topicObj): + msg = '%s Unsubscribed listener "%s" from topic "%s"\n' + msg = msg % (self.__pre, pubListener, topicObj.getName()) + self.__fileObj.write(msg) + + def notifyDeadListener(self, pubListener, topicObj): + 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, topicObj, pubListener=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, description, required, argsDocs): + msg = '%s New topic "%s" created\n' % (self.__pre, topicObj.getName()) + self.__fileObj.write(msg) + + def notifyDelTopic(self, topicName): + 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=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): + """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, topicObj, newSub): + 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, topicObj): + 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, topicObj): + 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, topicObj, pubListener=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, desc, required, argsDocs): + if (self._pubTopic is None) or self.__sending: + return + + pubTopic = self._pubTopic.getSubtopic('newTopic') + kwargs = dict(topic=topicObj, description=desc, required=required, args=argsDocs) + self.__doNotification(pubTopic, kwargs) + + def notifyDelTopic(self, topicName): + if (self._pubTopic is None) or self.__sending: + return + + pubTopic = self._pubTopic.getSubtopic('delTopic') + self.__doNotification(pubTopic, dict(name=topicName) ) + + def __doNotification(self, pubTopic, kwargs): + self.__sending = True + try: + pubTopic.publish( **kwargs ) + finally: + self.__sending = False + + +def _createTopics(topicMap, topicMgr): + """ + 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=None, all=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=None, prefix=None, + publisher=None, all=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)""" + notifHandler = NotifyByWriteFile(fileObj, prefix) + + if publisher is None: + from .. import pub + publisher = pub.getDefaultPublisher() + publisher.addNotificationHandler(notifHandler) + publisher.setNotificationFlags(all=all, **kwargs) + + diff --git a/wx/lib/pubsub/utils/topictreeprinter.py b/wx/lib/pubsub/utils/topictreeprinter.py new file mode 100644 index 00000000..4bd52770 --- /dev/null +++ b/wx/lib/pubsub/utils/topictreeprinter.py @@ -0,0 +1,195 @@ +""" +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 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=70, indentStep=4, + bulletTopic='\\--', bulletTopicItem='|==', bulletTopicArg='-', fileObj=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) + + diff --git a/wx/lib/pubsub/utils/xmltopicdefnprovider.py b/wx/lib/pubsub/utils/xmltopicdefnprovider.py new file mode 100644 index 00000000..b135e38c --- /dev/null +++ b/wx/lib/pubsub/utils/xmltopicdefnprovider.py @@ -0,0 +1,286 @@ +""" +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, + ) +from .. import py2and3 + +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: + py2and3.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 py2and3.iterkeys(self._topics) # 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 py2and3.isstring(rootTopic): + 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) + + + +