Merge pull request #938 from RobinD42/fix-issue932-b

Restore wx.lib.pubsub, and officially deprecate
(cherry picked from commit 8fad2231a0)
This commit is contained in:
Robin Dunn
2018-07-20 16:44:25 -07:00
parent 543fc796ab
commit 4b6ee40a14
51 changed files with 5948 additions and 43 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -10,4 +10,3 @@ pytest
pytest-xdist
pytest-timeout
numpy
PyPubSub

View File

@@ -91,7 +91,7 @@ Topic :: Software Development :: User Interfaces
DEPENDENCIES = [ 'six',
'Pillow',
'PyPubSub']
]
isWindows = sys.platform.startswith('win')
isDarwin = sys.platform == "darwin"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

View File

@@ -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)

View File

@@ -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)+']'

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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__)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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<argName>\w*)'
PAT_DOC_STR = r'(?P<doc1>.*)'
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) )

View File

@@ -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')

View File

@@ -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

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) )

View File

@@ -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)

24
wx/lib/pubsub/policies.py Normal file
View File

@@ -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

199
wx/lib/pubsub/pub.py Normal file
View File

@@ -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)
#---------------------------------------------------------------------------

608
wx/lib/pubsub/py2and3.py Normal file
View File

@@ -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 <benjamin@python.org>"
__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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'
]

View File

@@ -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)

100
wx/lib/pubsub/utils/misc.py Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 = '''
<topicdefntree>
<description>Test showing topic hierarchy and inheritance</description>
<topic id="parent">
<description>Parent with a parameter and subtopics</description>
<listenerspec>
<arg id="name" optional="true">given name</arg>
<arg id="lastname">surname</arg>
</listenerspec>
<topic id="child">
<description>This is the first child</description>
<listenerspec>
<arg id="nick">A nickname</arg>
</listenerspec>
</topic>
</topic>
</topicdefntree>
'''
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)