diff options
author | mathieui <mathieui@mathieui.net> | 2021-02-06 12:29:31 +0100 |
---|---|---|
committer | mathieui <mathieui@mathieui.net> | 2021-02-06 12:29:31 +0100 |
commit | 648ca16b4ceb935e837619ecbf34385a1044f7c6 (patch) | |
tree | f8cea4425ccec644c92cac720896022269a8acf3 /docs | |
parent | 622cfd4ed73b6fada9b445772ab0805d893f7678 (diff) | |
download | slixmpp-648ca16b4ceb935e837619ecbf34385a1044f7c6.tar.gz slixmpp-648ca16b4ceb935e837619ecbf34385a1044f7c6.tar.bz2 slixmpp-648ca16b4ceb935e837619ecbf34385a1044f7c6.tar.xz slixmpp-648ca16b4ceb935e837619ecbf34385a1044f7c6.zip |
docs: fill the stanza howto
Diffstat (limited to 'docs')
-rw-r--r-- | docs/howto/stanzas.rst | 401 |
1 files changed, 392 insertions, 9 deletions
diff --git a/docs/howto/stanzas.rst b/docs/howto/stanzas.rst index d52a90d4..56dfce79 100644 --- a/docs/howto/stanzas.rst +++ b/docs/howto/stanzas.rst @@ -3,28 +3,411 @@ How to Work with Stanza Objects =============================== +Slixmpp provides a large variety of facilities for abstracting the underlying +XML payloads of XMPP. Most of the visible user interface comes in a +dict-like interface provided in a specific ``__getitem__`` implementation +for :class:`~slixmpp.xmlstream.ElementBase` objects. + + +As a very high-level example, here is how to create a stanza with +an XEP-0191 payload, assuming the :class:`xep_0191 <slixmpp.plugins.xep_0191.XEP_0191>` +plugin is loaded: + +.. code-block:: python + + from slixmpp.stanza import Iq + iq = Iq() + iq['to'] = 'toto@example.com' + iq['type'] = 'set' + iq['block']['items'] = {'a@example.com', 'b@example.com'} + +Printing the resulting :class:`~slixmpp.stanaz.Iq` object gives us the +following XML (reformatted for readability): + +.. code-block:: xml + + <iq xmlns="jabber:client" id="0" to="toto@example.com" type="set"> + <block xmlns="urn:xmpp:blocking"> + <item jid="b@example.com" /> + <item jid="a@example.com" /> + </block> + </iq> + + +Realistically, users of the Slixmpp library should make use of the shorthand +functions available in their :class:`~.ClientXMPP` or +:class:`~.ComponentXMPP` objects to create :class:`~.Iq`, :class:`~.Message` +or :class:`~.Presence` objects that are bound to a stream, and which have +a generated unique identifier. + +The most relevant functions are: + +.. autofunction:: slixmpp.BaseXMPP.make_iq_get + +.. autofunction:: slixmpp.BaseXMPP.make_iq_set + +.. autofunction:: slixmpp.BaseXMPP.make_message + +.. autofunction:: slixmpp.BaseXMPP.make_presence + +The previous example then becomes: + +.. code-block:: python + + iq = xmpp.make_iq_get(ito='toto@example.com') + iq['block']['items'] = {'a@example.com', 'b@example.com'} + + +.. note:: + + xml:lang is handled by piping the lang name after the attribute. For + example ``message['body|fr']`` will return the ``<body/>`` attribute + with ``xml:lang="fr``. + +The next sections will try to explain as clearly as possible +how the magic operates. .. _create-stanza-interfaces: Defining Stanza Interfaces -------------------------- +The stanza interface is very rich and let developers have full control +over the API they want to have to manipulate stanzas. -.. _create-stanza-plugins: +The entire interface is defined as class attributes that are redefined +when subclassing :class:`~.ElementBase` when `creating a stanza plugin <create-stanza-plugins>`_. -Creating Stanza Plugins ------------------------ +The main attributes defining a stanza interface: +- plugin_attrib_: ``str``, the name of this element on the parent +- plugin_multi_attrib_: ``str``, the name of the iterable for this element on the parent +- interfaces_: ``set``, all known interfaces for this element +- sub_interfaces_: ``set`` (subset of ``interfaces``), for sub-elements with only text nodes +- bool_interfaces_: ``set`` (subset of ``interfaces``), for empty-sub-elements +- overrides_: ``list`` (subset of ``interfaces``), for ``interfaces`` to ovverride on the parent +- is_extension_: ``bool``, if the element is only an extension of the parent stanza -.. _create-extension-plugins: +.. _plugin_attrib: -Creating a Stanza Extension ---------------------------- +plugin_attrib +~~~~~~~~~~~~~ +The ``plugin_attrib`` string is the defining element of any stanza plugin, +as it the name through which the element is accessed (except for ``overrides`` +and ``is_extension``). +The extension is then registered through the help of :func:`~.register_stanza_plugin` +which will attach the plugin to its parent. -.. _override-parent-interfaces: +.. code-block:: python -Overriding a Parent Stanza --------------------------- + from slixmpp import ElementBase, Iq + + class Payload(ElementBase): + name = 'apayload' + plugin_attrib = 'mypayload' + namespace = 'x-toto' + + register_stanza_plugin(Iq, Payload) + + iq = Iq() + iq.enable('mypayload') # Similar to iq['mypayload'] + +The :class:`~.Iq` element created now contains our custom ``<apayload/>`` element. + +.. code-block:: xml + + <iq xmlns="jabber:client" id="0"> + <apayload xmlns="x-toto"/> + </iq> + + +.. _plugin_multi_attrib: + +plugin_multi_attrib +~~~~~~~~~~~~~~~~~~~ + +The :func:`~.register_stanza_plugin` function has an ``iterable`` parameter, which +defaults to ``False``. When set to ``True``, it means that iterating over the element +is possible. + + +.. code-block:: python + + class Parent(ElementBase): + pass # does not matter + + class Sub(ElementBase): + name = 'sub' + plugin_attrib = 'sub' + + class Sub2(ElementBase): + name = 'sub2' + plugin_attrib = 'sub2' + + register_stanza_plugin(Parent, Sub, iterable=True) + register_stanza_plugin(Parent, Sub2, iterable=True) + + parent = Parent() + parent.append(Sub()) + parent.append(Sub2()) + parent.append(Sub2()) + parent.append(Sub()) + + for element in parent: + do_something # A mix of Sub and Sub2 elements + +In this situation, iterating over ``parent`` will yield each of the appended elements, +one after the other. + +Sometimes you only want one specific type of sub-element, which is the use of +the ``plugin_multi_attrib`` string interface. This name will be mapped on the +parent, just like ``plugin_attrib``, but will return a list of all elements +of the same type only. + +Re-using our previous example: + +.. code-block:: python + + class Parent(ElementBase): + pass # does not matter + + class Sub(ElementBase): + name = 'sub' + plugin_attrib = 'sub' + plugin_multi_attrib = 'subs' + + class Sub2(ElementBase): + name = 'sub2' + plugin_attrib = 'sub2' + plugin_multi_attrib = 'subs2' + + register_stanza_plugin(Parent, Sub, iterable=True) + register_stanza_plugin(Parent, Sub2, iterable=True) + + parent = Parent() + parent.append(Sub()) + parent.append(Sub2()) + parent.append(Sub2()) + parent.append(Sub()) + + for sub in parent['subs']: + do_something # ony Sub objects here + + for sub2 in parent['subs2']: + do_something # ony Sub2 objects here + + +.. _interfaces: + +interfaces +~~~~~~~~~~ + +The ``interfaces`` set **must** contain all the known ways to interact with +this element. It does not include plugins (registered to the element through +:func:`~.register_stanza_plugin`), which are dynamic. + +By default, a name present in ``interfaces`` will be mapped to an attribute +of the element with the same name. + +.. code-block:: python + + class Example(Element): + name = 'example' + interfaces = {'toto'} + + example = Example() + example['toto'] = 'titi' + +In this case, ``example`` contains ``<example toto="titi"/>``. + +For empty and text_only sub-elements, there are sub_interfaces_ and +bool_interfaces_ (the keys **must** still be in ``interfaces``. + +You can however define any getter, setter, and delete custom method for any of +those interfaces. Keep in mind that if one of the three is not custom, +Slixmpp will use the default one, so you have to make sure that either you +redefine all get/set/del custom methods, or that your custom methods are +compatible with the default ones. + +In the following example, we want the ``toto`` attribute to be an integer. + +.. code-block:: python + + class Example(Element): + interfaces = {'toto', 'titi', 'tata'} + + def get_toto(self) -> Optional[int]: + try: + return int(self.xml.attrib.get('toto', '')) + except ValueError: + return None + + def set_toto(self, value: int): + int(value) # make sure the value is an int + self.xml.attrib['toto'] = str(value) + + example = Example() + example['tata'] = "Test" # works + example['toto'] = 1 # works + print(type(example['toto'])) # the value is an int + example['toto'] = "Test 2" # ValueError + + +One important thing to keep in mind is that the ``get_`` methods must be resilient +(when having a default value makes sense) because they are called on objects +received from the network. + +.. _sub_interfaces: + +sub_interfaces +~~~~~~~~~~~~~~ + +The ``bool_interfaces`` set allows mapping an interface to the text node of +sub-element of the current payload, with the same namespace + +Here is a simple example: + +.. code-block:: python + + class FirstLevel(ElementBase): + name = 'first' + namespace = 'ns' + interfaces = {'second'} + sub_interfaces = {'second'} + + parent = FirstLevel() + parent['second'] = 'Content of second node' + + +Which will produces the following: + +.. code-block:: xml + + <first xmlns="ns"> + <second>Content of second node</second> + </first> + +We can see that ``sub_interfaces`` allows to quickly create a sub-element and +manipulate its text node without requiring a custom element, getter or setter. + +.. _bool_interfaces: + +bool_interfaces +~~~~~~~~~~~~~~~ + +The ``bool_interfaces`` set allows mapping an interface to a direct sub-element of the +current payload, with the same namespace. + + +Here is a simple example: + +.. code-block:: python + + class FirstLevel(ElementBase): + name = 'first' + namespace = 'ns' + interfaces = {'second'} + bool_interfaces = {'second'} + + parent = FirstLevel() + parent['second'] = True + + +Which will produces the following: + +.. code-block:: xml + + <first xmlns="ns"> + <second/> + </first> + +We can see that ``bool_interfaces`` allows to quickly create sub-elements with no +content, without the need to create a custom class or getter/setter. + +overrides +~~~~~~~~~ + +List of ``interfaces`` on the present element that should override the +parent ``interfaces`` with the same name. + +.. code-block:: python + + class Parent(ElementBase): + name = 'parent' + interfaces = {'toto', 'titi'} + + class Sub(ElementBase): + name = 'sub' + plugin_attrib = name + interfaces = {'toto', 'titi'} + overrides = ['toto'] + + register_stanza_plugin(Parent, Sub) + + parent = Parent() + parent['toto'] = 'test' # equivalent to parent['sub']['toto'] = "test" + +is_extension +~~~~~~~~~~~~ + +Stanza extensions are a specific kind of stanza plugin which have +the ``is_extension`` class attribute set to ``True``. + +The following code will directly plug the extension into the +:class:`~.Message` element, allowing direct access +to the interface: + +.. code-block:: python + + class MyCustomExtension(ElementBase): + is_extension = True + name = 'mycustom' + namespace = 'custom-ns' + plugin_attrib = 'mycustom' + interfaces = {'mycustom'} + + register_stanza_plugin(Message, MyCustomExtension) + +With this extension, we can do the folliowing: + +.. code-block:: python + + message = Message() + message['mycustom'] = 'toto' + +Without the extension, obtaining the same results would be: + +.. code-block:: python + + message = Message() + message['mycustom']['mycustom'] = 'toto' + + +The extension is therefore named extension because it extends the +parent element transparently. + + +.. _create-stanza-plugins: + +Creating Stanza Plugins +----------------------- + +A stanza plugin is a class that inherits from :class:`~.ElementBase`, and +**must** contain at least the following attributes: + +- name: XML element name (e.g. ``toto`` if the element is ``<toto/>`` +- namespace: The XML namespace of the element. +- plugin_attrib_: ``str``, the name of this element on the parent +- interfaces_: ``set``, all known interfaces for this element + +It is then registered through :func:`~.register_stanza_plugin` on the parent +element. + +.. note:: + + :func:`~.register_stanza_plugin` should NOT be called at the module level, + because it executes code, and executing code at the module level can slow + down import significantly! |