mirror of
https://github.com/wxWidgets/Phoenix.git
synced 2025-12-16 01:30:07 +01:00
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:
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,4 +10,3 @@ pytest
|
||||
pytest-xdist
|
||||
pytest-timeout
|
||||
numpy
|
||||
PyPubSub
|
||||
2
setup.py
2
setup.py
@@ -91,7 +91,7 @@ Topic :: Software Development :: User Interfaces
|
||||
|
||||
DEPENDENCIES = [ 'six',
|
||||
'Pillow',
|
||||
'PyPubSub']
|
||||
]
|
||||
|
||||
isWindows = sys.platform.startswith('win')
|
||||
isDarwin = sys.platform == "darwin"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
22
wx/lib/pubsub/README_WxPython.txt
Normal file
22
wx/lib/pubsub/README_WxPython.txt
Normal 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
|
||||
71
wx/lib/pubsub/RELEASE_NOTES.txt
Normal file
71
wx/lib/pubsub/RELEASE_NOTES.txt
Normal 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
|
||||
@@ -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)
|
||||
|
||||
92
wx/lib/pubsub/core/__init__.py
Normal file
92
wx/lib/pubsub/core/__init__.py
Normal 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,
|
||||
)
|
||||
16
wx/lib/pubsub/core/arg1/__init__.py
Normal file
16
wx/lib/pubsub/core/arg1/__init__.py
Normal 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)
|
||||
97
wx/lib/pubsub/core/arg1/listenerimpl.py
Normal file
97
wx/lib/pubsub/core/arg1/listenerimpl.py
Normal 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)
|
||||
|
||||
|
||||
40
wx/lib/pubsub/core/arg1/publisher.py
Normal file
40
wx/lib/pubsub/core/arg1/publisher.py
Normal 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'
|
||||
|
||||
34
wx/lib/pubsub/core/arg1/publishermixin.py
Normal file
34
wx/lib/pubsub/core/arg1/publishermixin.py
Normal 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)
|
||||
66
wx/lib/pubsub/core/arg1/topicargspecimpl.py
Normal file
66
wx/lib/pubsub/core/arg1/topicargspecimpl.py
Normal 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()
|
||||
|
||||
|
||||
19
wx/lib/pubsub/core/arg1/topicmgrimpl.py
Normal file
19
wx/lib/pubsub/core/arg1/topicmgrimpl.py
Normal 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
|
||||
|
||||
191
wx/lib/pubsub/core/callables.py
Normal file
191
wx/lib/pubsub/core/callables.py
Normal 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)
|
||||
|
||||
|
||||
63
wx/lib/pubsub/core/imp2.py
Normal file
63
wx/lib/pubsub/core/imp2.py
Normal 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
|
||||
0
wx/lib/pubsub/core/itopicdefnprovider.py
Normal file
0
wx/lib/pubsub/core/itopicdefnprovider.py
Normal file
16
wx/lib/pubsub/core/kwargs/__init__.py
Normal file
16
wx/lib/pubsub/core/kwargs/__init__.py
Normal 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)
|
||||
27
wx/lib/pubsub/core/kwargs/datamsg.py
Normal file
27
wx/lib/pubsub/core/kwargs/datamsg.py
Normal 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)+']'
|
||||
|
||||
93
wx/lib/pubsub/core/kwargs/listenerimpl.py
Normal file
93
wx/lib/pubsub/core/kwargs/listenerimpl.py
Normal 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)
|
||||
|
||||
77
wx/lib/pubsub/core/kwargs/publisher.py
Normal file
77
wx/lib/pubsub/core/kwargs/publisher.py
Normal 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
|
||||
|
||||
|
||||
65
wx/lib/pubsub/core/kwargs/publishermixin.py
Normal file
65
wx/lib/pubsub/core/kwargs/publishermixin.py
Normal 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)
|
||||
|
||||
217
wx/lib/pubsub/core/kwargs/topicargspecimpl.py
Normal file
217
wx/lib/pubsub/core/kwargs/topicargspecimpl.py
Normal 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()
|
||||
|
||||
|
||||
13
wx/lib/pubsub/core/kwargs/topicmgrimpl.py
Normal file
13
wx/lib/pubsub/core/kwargs/topicmgrimpl.py
Normal 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
|
||||
|
||||
40
wx/lib/pubsub/core/listener.py
Normal file
40
wx/lib/pubsub/core/listener.py
Normal 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__)
|
||||
|
||||
|
||||
185
wx/lib/pubsub/core/listenerbase.py
Normal file
185
wx/lib/pubsub/core/listenerbase.py
Normal 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
|
||||
|
||||
|
||||
185
wx/lib/pubsub/core/notificationmgr.py
Normal file
185
wx/lib/pubsub/core/notificationmgr.py
Normal 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
|
||||
|
||||
|
||||
191
wx/lib/pubsub/core/publisherbase.py
Normal file
191
wx/lib/pubsub/core/publisherbase.py
Normal 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
|
||||
|
||||
|
||||
77
wx/lib/pubsub/core/topicargspec.py
Normal file
77
wx/lib/pubsub/core/topicargspec.py
Normal 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)
|
||||
|
||||
|
||||
636
wx/lib/pubsub/core/topicdefnprovider.py
Normal file
636
wx/lib/pubsub/core/topicdefnprovider.py
Normal 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) )
|
||||
|
||||
|
||||
72
wx/lib/pubsub/core/topicexc.py
Normal file
72
wx/lib/pubsub/core/topicexc.py
Normal 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')
|
||||
|
||||
|
||||
456
wx/lib/pubsub/core/topicmgr.py
Normal file
456
wx/lib/pubsub/core/topicmgr.py
Normal 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
|
||||
|
||||
|
||||
472
wx/lib/pubsub/core/topicobj.py
Normal file
472
wx/lib/pubsub/core/topicobj.py
Normal 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())
|
||||
|
||||
|
||||
143
wx/lib/pubsub/core/topictreetraverser.py
Normal file
143
wx/lib/pubsub/core/topictreetraverser.py
Normal 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
|
||||
|
||||
118
wx/lib/pubsub/core/topicutils.py
Normal file
118
wx/lib/pubsub/core/topicutils.py
Normal 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
|
||||
|
||||
|
||||
21
wx/lib/pubsub/core/treeconfig.py
Normal file
21
wx/lib/pubsub/core/treeconfig.py
Normal 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
|
||||
|
||||
|
||||
29
wx/lib/pubsub/core/validatedefnargs.py
Normal file
29
wx/lib/pubsub/core/validatedefnargs.py
Normal 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) )
|
||||
|
||||
|
||||
102
wx/lib/pubsub/core/weakmethod.py
Normal file
102
wx/lib/pubsub/core/weakmethod.py
Normal 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
24
wx/lib/pubsub/policies.py
Normal 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
199
wx/lib/pubsub/pub.py
Normal 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
608
wx/lib/pubsub/py2and3.py
Normal 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))
|
||||
|
||||
47
wx/lib/pubsub/setuparg1.py
Normal file
47
wx/lib/pubsub/setuparg1.py
Normal 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)
|
||||
29
wx/lib/pubsub/setupkwargs.py
Normal file
29
wx/lib/pubsub/setupkwargs.py
Normal 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)
|
||||
27
wx/lib/pubsub/utils/__init__.py
Normal file
27
wx/lib/pubsub/utils/__init__.py
Normal 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'
|
||||
]
|
||||
100
wx/lib/pubsub/utils/exchandling.py
Normal file
100
wx/lib/pubsub/utils/exchandling.py
Normal 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
100
wx/lib/pubsub/utils/misc.py
Normal 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)
|
||||
|
||||
|
||||
331
wx/lib/pubsub/utils/notification.py
Normal file
331
wx/lib/pubsub/utils/notification.py
Normal 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)
|
||||
|
||||
|
||||
195
wx/lib/pubsub/utils/topictreeprinter.py
Normal file
195
wx/lib/pubsub/utils/topictreeprinter.py
Normal 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)
|
||||
|
||||
|
||||
286
wx/lib/pubsub/utils/xmltopicdefnprovider.py
Normal file
286
wx/lib/pubsub/utils/xmltopicdefnprovider.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user