About Topics
===============

.. _anchor-message-topics:

Message Topics
------------------

The topic of a pubsub message can be understood in several ways:

* in OOP parlance: 

  * as its "type", or "class"
  * the message data correspond to attributes of a type or class instance
    (but are not represented as such)
  
* in radio broadcasting parlance:

  * as a "channel", or "frequency"
  * all "receivers" "tuned" to the right channel or frequency will receive
    the message


.. _anchor-topic-names:

Topic Names
------------------
 
A topic is uniquely identified by its full name, represented as a 
character string. Example of some topic names are

* "sports", 
* "sports.hockey", 
* "app.options.editor", etc. 

As explained in another section, topics form a hierarchy. Every topic
therefore has a "node name". Some of the node names from the previous 
examples are

* "sports"
* "hockey"
* "options" and "editor"

The "full name" consists of the node-names from the root of the 
hierarchy, separated by dots (as the first examples showed). 

For this reason, topic names can also be given to pubsub functions as 
tuples of strings rather than a dotted string. The tuple 
equivalent of the topic names from the previous examples are 
("sports",), ("sports", "hockey"), ("app","options","editor").

Most :mod:`pub` functions and methods that require a topic name
accept either forms of topic name. 


.. _anchor-topic-mds-basics:

Topic MDS: Basics
------------------

A topic specifies what data can be transported as part of messages of 
that topic. This specification is called *Message Data Specification*, or 
**MDS** for short. The specification covers the data name and whether 
the data is required or optional. 

Assume the following MDS for "topic_A":

    | messages carry the data:
    |   arg1: required
    |   arg2: optional
    
Then the following messages could be sent::

    pub.sendMessage("topic_A", arg1=123)
    pub.sendMessage("topic_A", arg1=123, arg2=456)

but the following would raise an exception::

    pub.sendMessage("topic_A", arg2=456)

By default, the MDS is created for you at runtime by introspection of 
the *first* listener subscribed to a topic. This makes it 
simple to use pubsub "out of the box" (since you don't have to write the 
MDS yourself), but has the disadvantage of making your MDS specification 
dependant on the order of subscription of listeners. If this is problematic,
the MDS can be imported, as explained next. 

Pubsub allows the MDS to be imported from a Python file and referenced 
at runtime. This will make the MDS independent of the order of subscription
of listeners and even allow you to document your topics and MDS. 

You can output the MDS of each pubsub topic defined in an application via the 
:func:`pub.exportTopicTree` function::

    # init:
    from pubsub import pb
    
    # main: 
    ... subscribe listeners ...
    ... send messages ...
    
    # upon exit:
    exportModuleDoc = '''...'''
    pub.exportTopicTree(
        'filename.py', moduleDoc=exportModuleDoc)


Gradual Lockdown of MDS
^^^^^^^^^^^^^^^^^^^^^^^^

A technique that you may find useful when developing a new application is to 
start by using the runtime introspection-based MDS of pubsub;
when your application core is sufficiently developed, use 
:func:`pub.exportTopicTree` to export it, clean it up,  
save it, and add a :func:`pub.importTopicTree` early in the 
startup of you application. Also call 
:func:`pub.setTopicUnspecifiedFatal(True)` so that new topics 
can't be created inadvertently. 

.. _anchor-topic-hierarchy:

Topic Hierarchy
------------------

Topics form a hierarchy:

- Root of topic tree:

  * The root of the topic hierarchy is the :data:`pub.ALL_TOPICS` topic. 
  * This topic is automatically created by pubsub upon first import. It 
    cannot be created or deleted. 
  * This topic's node name is not included in the full name of topics
    since it is implicit. Hence topic "topic_A" is in fact a child 
    of the ALL_TOPICS topic. 
  * The MDS of this topic is empty, ie no data should be sent directly.
    However, listeners can get any data sent by having ``**kwargs``
    in their signature, ie such listeners receive all data
    sent by all topics, regardless of actual message topic. 
  * The root topic is mostly useful for logging pubsub messages. 
    
- Subtopics: a subtopic is to its parent topic as a derived class is 
  to its parent class in Python, ie something more specific, involving 
  more data. A subtopic "inherits" its parent's MDS and may "extend" 
  it by specifying additional message data permitted for the subtopic.

- Messages are delivered to the listeners of the specified topic, as 
  well as listeners of topics up the topic branch::
  
    pub.sendMessage('a.b.c', argA=1, argB=2, argC=3)
    
  The message, of topic "a.b.c", will be received by listeners of "a.b.c". 
  Parts of the message will be received by listeners of "a.b", by 
  listeners of "a", and by listeners of ALL_TOPICS. 
  
  The part of the message received by a parent topic is determined by 
  the parent topic's MDS.  IE only the message data that pertains to a 
  topic's parent is sent to the listeners of the parent topic. 
  In the above example, if topic "a.b" specifies ``argA`` and ``argB`` 
  only, then only those two items will be sent to listeners of "a.b". 
  Similarly, if topic "a" specifies ``argA`` only as MDS, then only 
  ``argA`` will be sent to listeners of "a". 
  
- The order of delivery of a message to listeners is unspecified. Your 
  application must not depend on it. 
  

.. _anchor-topic-mds-hierarchy:

Topic MDS: Hierarchy
---------------------

A subtopic "inherits" its parent's MDS and may "extend" 
it by specifying additional message data permitted for the subtopic.
For instance if topic "a" has message data arg1 only, then its 
subtopics (such as "a.b") must support at least arg1::

    def listen(    arg1): pass
    def listenSubB(arg1): pass
    def listenSubC(arg1, arg2): pass

    pub.subscribe(listen,     "a")
    pub.subscribe(listenSubB, "a.b")
    pub.subscribe(listenSubC, "a.c")

Assuming the above, the following would cause pubsub to raise 
an exception::

    def listenSubD(arg2): pass
    pub.subscribe(listenSubD, "a.d") // raises: arg1 missing

.. note:: Because the root topic has an empty MDS, no data is 
    transmitted to listeners of the root topic, except 
    if a listener uses the catch-all ``**kwargs`` in its signature.
  
.. note:: A subtopic can make one or more of its parent data items 
    required for the subtopic if it was optional for the topic, 
    but cannot do the reverse. In ::

            def listen(   arg1=1): pass
            def listenSub(arg1): pass

            pub.subscribe(listen,    "a")
            pub.subscribe(listenSub, "a.b")

    the subtopic "a.b" is valid: arg1 is required, so a sender 
    will always provide it, so eventhough it is optional for 
    topic 1 listeners, those listeners will always receive it too. 


Topic Definition Tree and Providers
-------------------------------------

topic definition tree and its syntax, topic tree printing




Topics (OLD)
-------------

The topic hierarchy can be created on the fly by the application (default), 
or can be specified and documented via one or more *Topic Definition Providers* 
(TDP). TDP's are objects that state 

- what are valid topic names in the application
- what is the topic hierarchy
- what data is allowed or required when sending messages for each topic
- describe what is the purpose of each topic and message data

Pubsub can be setup to deny (via an exception) any attempt to use a topic 
that is not specified by a TDP. This is very useful in larger applications.

Every topic has a message arguments specification, i.e. a prescribed 
set of argument names that are allowed when sending a message of that
topic. This is the *TMAS*, for *topic message arguments specification*.
For instance, topic "sports.baseball" might have following TMAS:

- playersA: list of players in team A
- playersB: list of players in team B
- location: where the game is taking place
- dateTime: when is the game

but only location and playersA are required: playersB and dateTime are 
only used during a match (vs a practice). Then every message of topic 
"sports.baseball" must be given at least a location and list for playersA,
and every recipient of the message must accept all four parameters (since
some messages will have all four parameters). I.e.::

  pub.sendMessage("sports.baseball", playersA=[], location="Montreal")
  pub.sendMessage("sports.baseball", playersA=[], playersB=[], location="Montreal")
  pub.sendMessage("sports.baseball", playersA=[], location="Montreal", dateTime=today() )
  pub.sendMessage("sports.baseball", playersA=[], playersB=[], location="Montreal", dateTime=today() )

  pub.sendMessage("sports.baseball", playersA=[]) # ERROR: required location missing


The TMAS of each topic:

- Is obtained from the first TDP that provides it. If the application 
  does not use TDP's, the TMAS is inferred from the first listener 
  subscribed to the topic.
- Extends the TMAS of the parent topic. This is almost identical to 
  inheritence of attributes in a class hierarchy. For instance if topic 
  "a.b" has TMAS (arg1 (required), arg2 (optional)), then topic "a.b.c" 
  has at least that TMAS, and topic "a" has at most that TMAS.