Several essential tasks supported by pubsub
In this section:
Every message that can be sent via pubsub is of a specific topic, just as every object in Python is of a specific type.
Use pub.subscribe(callable, 'topic-path') to subscribe callable to all messages of given topic or any of its subtopics.
The callable can be:
Hence given the following definitions:
def function(): pass
class Foo:
def method(self): pass
@staticmethod
def staticMeth(): pass
@classmethod
def classMeth(cls): pass
def __call__(self): pass
foo = Foo()
the following callables could be subscribed to a pubsub message topic:
function
foo.method
foo
Foo.staticMeth
Foo.classMeth
Pubsub holds listeners by weak reference so that the lifetime of the callable is not affected by pubsub: once the application no longer references the callable, it can be garbage collected and pubsub can clean up so it is no longer registered (this happens thanks to the weakref module). Without this, it would be imperative to remember to unsubscribe certain listeners, which is error prone; they would end up living until the application exited.
A nice example of this is a user control (widget) in a GUI: if a method of the user control is registered as listener in pubsub, and the control is discarded, the application need not explicitly unregister the callable: the weak referencing will allow the widget to be garbage collected; otherwise, it would remain visible until explicit unsubscription.
Warning
One caveat that results from this useful feature is that all callables that subscribe to topics must be referenced from outside pubsub. For instance, the following will silently unsubscribe on return from pub.subscribe():
def listener(): pass
def wrap(fn):
def wrappedListener():
fn()
return wrappedListener
pub.subscribe(wrap(listener), 'topic')
since wrap() returns an object which only pubsub references: the wrappedListener gets garbage collected upon return from subscribe(). It is possible to verify that the stored listener is indeed dead:
ll,ok = pub.subscribe(wrap(listener), 'topic')
print ll.isDead() # prints True
Compare without wrapping:
ll,ok = pub.subscribe(listener, 'topic')
print ll.isDead() # prints False
Fix by storing a strong reference to wrappedListener:
ww = wrap(listener) # creates strong reference
ll,ok = pub.subscribe(ww, 'topic')
print ll.isDead() # prints False
Every topic has a name and a path. The name can contain any character a-z, A-Z, 0-9 and _&%$#@ and the hyphen. Valid examples are:
'asdfasdf'
'aS-fds0-123'
'_&%$#@-abc-ABC123'
Other characters will lead to an exception or undefined behavior.
Topics form a hierarchy:
The fully qualified topic name is therefore the path through the topic hierarchy. The path separator is ‘.’. Hence given the following topic hierarchy:
root-topic-1
sub-topic-2
sub-sub-topic-3
the following subscriptions could be valid:
pub.subscribe(callable, 'root-topic-1')
pub.subscribe(callable, 'root-topic-1.sub-topic-2')
pub.subscribe(callable, 'root-topic-1.sub-topic-2.sub-sub-topic-3')
Messages of a given topic can carry data. Which data is required and which is optional is known as the Topic Data Specification, or TDS for short. Unless your application explicitly defines the TDS for every topic in the hierarchy, Pubsub infers the TDS of each topic based on the first pub.subscribe() or the first pub.sendMessage() for the topic, whichever occurs first during an application run. Once defined, a topic’s TDS never changes (during a run).
Examples of TDS inferred from a call to pub.subscribe():
Callable signature | TDS (inferred) |
---|---|
callable(arg1) |
|
callable(arg3=1) |
|
callable(arg1, arg2, arg3=1, arg4=None) |
|
All subsequent calls to pub.subscribe() for the same topic or any subtopic must be consistent with the topic’s TDS. If a subscription specifies a callable that does not match the given topic’s TDS, pubsub raises an exception. Therefore, the pub.subscribe() calls above could be valid; they will be valid if the given callable satisfies the given topic’s TDS.
Examples of subscriptions: assume TDS of topic ‘root’ is required=arg1, optional=arg2, then pub.subscribe(callable, ‘root’) for the following callable signatures are ok:
Callable | OK | Why |
---|---|---|
callable(arg1, arg3=1) | Yes | matches TDS |
callable(arg1=None, arg3=None) | Yes | signature is less restrictive than TDS, and default value are not part of TDS |
callable(arg1) | No | arg2 could be in message, yet callable does not accept it |
callable(arg1, arg2) | No | callable requires arg2, but TDS says it won’t always be given in message |
A callable subscribed to a topic is a listener.
Note that the default value for an optional message data is not part of the TDS. Each listener can therefore decide what default value to use if the data is not provided in the message.
Use pub.sendMessage('topic-path-name', **data) to send a message with the given data. The topic path name is a dot-separated sequence of topic names from root to topic (see Topic Name).
The message is sent to all registered listeners of given topic, parent topic, and so forth up the “topic tree”, by calling each listener, in turn, until all listeners have been sent the message and data. A listener must return before the next listener can be called. The order of listeners (within a topic or up the tree) is not specified. The sender should not make any assumptions about the order in which listeners will be called, or even which ones will be called. If a listener leaks an exception, pubsub catches it and interrupts the send operation, unless an exception handler has been defined. This is discussed in Naughty Listeners: Trap Exceptions.
The data must satisfy the topic’s TDS, and all arguments must be named. So for a topic ‘root’ with TDS of arg1, arg2 required and arg3 optional, the send command would have the form:
pub.sendMessage('root', arg1=obj1, arg2=obj2, arg3=obj3)
One consequence of this is that the order of arguments does not matter:
pub.sendMessage('root', arg3=obj3, arg2=obj2, arg1=obj1)
is equally valid. But
pub.sendMessage('root', obj1, obj2, arg3=obj3)
is not allowed.
Only the message data relevant to a topic is sent to the listeners of the topic. For example if topic ‘root.sub.subsub’ has a TDS involving data arg1, arg2 and arg3, and topic ‘root’ has only arg1, then listeners of ‘root.sub.subsub’ topic will get called with arg1, arg2, and arg3, but listeners of ‘root’ will get called with the arg1 parameter only. The less specific topics have less data.
Since messages of a given topic are sent not only to listeners of the topic but also to listeners of topic up the topic tree, pubsub requires that subtopic TDS be the same or more restrictive as that of its parent: optional arguments can become required, but required arguments cannot become optional. Indeed if ‘root’ messages require arg1, then ‘root.sub’ must also require it; otherwise a message of type ‘root.sub’ could be sent without an object for arg1, and once the ‘root’ listeners received the message, they could find the required parameter missing. If ‘root’ messages have arg2 as optional data, then ‘root.sub’ can be more restrictive and require it.
Examples of subtopic TDS: assume topic ‘root’ has TDS required arg1 and optional arg2. Then following ‘root.sub’ TDS would be
Case | TDS extended by | OK | Why |
---|---|---|---|
1 |
|
Yes | Extends TDS of ‘root’ |
2 |
|
No | Less restrictive than ‘root’: arg3 could be missing from ‘root.sub’ message |
If a listener requires to know the topic of the message, a specially named default value pub.AUTO_TOPIC_OBJ can be used for one of its call parameters: at call time, pubsub will replace the value by the pub.TopicObj object for the topic. It can be queried to find the topic name via Topic.getName():
def listener(topic=pub.AUTO_TOPIC_OBJ):
print "real topic is", topic.getName()
pub.subscribe(listener, "some_topic")
pub.sendMessage("some_topic") # no data
This allows each listener to define whether it needs the topic information (rarely the case). Therefore, it is not part of the TDS. In the above example, the TDS for ‘some_topic’ is empty.
The pub.sendMessage() shares some similarities and differences with “broadcasting”. Some similarities:
Some differences:
A callable subscribed to a topic receives a message by being called. Assuming that the send command is:
pub.sendMessage('topic-path-name', **data)
then all listeners subscribed to the named topic will get called with the given **data dictionary, as well as all listeners of the topic’s parent topic, and so forth until the root topic is reached.
Warning
A listener should not make any assumptions about:
Only the portion of data that is relevant to the topic is given to each listener. Assume the following topic branch of the hierarchy:
tt: listeners a and b; TDS is r=arg1, o=arg4
uu: listeners c and d; TDS is r=(arg1, arg2), o=(arg4, arg5)
vv: listeners e and f; TDS is r=(arg1, arg2, arg3), o=(arg4, arg5, arg6)
then pub.sendMessage('root-topic', arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg6=6) will call
As stated in the ‘Sending Messages’ section, the order in which the listeners are called is not specified; your application should not make any assumptions about this order.