stanzabase: types

This commit is contained in:
mathieui 2021-04-24 22:44:41 +02:00
parent 79f71ec0c1
commit 62e66e7d03

View file

@ -1,4 +1,3 @@
# slixmpp.xmlstream.stanzabase # slixmpp.xmlstream.stanzabase
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# module implements a wrapper layer for XML objects # module implements a wrapper layer for XML objects
@ -11,13 +10,34 @@ from __future__ import annotations
import copy import copy
import logging import logging
import weakref import weakref
from typing import Optional from typing import (
cast,
Any,
Callable,
ClassVar,
Coroutine,
Dict,
List,
Iterable,
Optional,
Set,
Tuple,
Type,
TYPE_CHECKING,
Union,
)
from weakref import ReferenceType
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from slixmpp.types import JidStr
from slixmpp.xmlstream import JID from slixmpp.xmlstream import JID
from slixmpp.xmlstream.tostring import tostring from slixmpp.xmlstream.tostring import tostring
if TYPE_CHECKING:
from slixmpp.xmlstream import XMLStream
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -28,7 +48,8 @@ XML_TYPE = type(ET.Element('xml'))
XML_NS = 'http://www.w3.org/XML/1998/namespace' XML_NS = 'http://www.w3.org/XML/1998/namespace'
def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): def register_stanza_plugin(stanza: Type[ElementBase], plugin: Type[ElementBase],
iterable: bool = False, overrides: bool = False) -> None:
""" """
Associate a stanza object as a plugin for another stanza. Associate a stanza object as a plugin for another stanza.
@ -85,15 +106,15 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
stanza.plugin_overrides[interface] = plugin.plugin_attrib stanza.plugin_overrides[interface] = plugin.plugin_attrib
def multifactory(stanza, plugin_attrib): def multifactory(stanza: Type[ElementBase], plugin_attrib: str) -> Type[ElementBase]:
""" """
Returns a ElementBase class for handling reoccuring child stanzas Returns a ElementBase class for handling reoccuring child stanzas
""" """
def plugin_filter(self): def plugin_filter(self: Multi) -> Callable[..., bool]:
return lambda x: isinstance(x, self._multistanza) return lambda x: isinstance(x, self._multistanza)
def plugin_lang_filter(self, lang): def plugin_lang_filter(self: Multi, lang: Optional[str]) -> Callable[..., bool]:
return lambda x: isinstance(x, self._multistanza) and \ return lambda x: isinstance(x, self._multistanza) and \
x['lang'] == lang x['lang'] == lang
@ -101,31 +122,41 @@ def multifactory(stanza, plugin_attrib):
""" """
Template class for multifactory Template class for multifactory
""" """
def setup(self, xml=None): _multistanza: Type[ElementBase]
self.xml = ET.Element('')
def get_multi(self, lang=None): def setup(self, xml: Optional[ET.Element] = None) -> bool:
parent = self.parent() self.xml = ET.Element('')
return False
def get_multi(self: Multi, lang: Optional[str] = None) -> List[ElementBase]:
parent = fail_without_parent(self)
if not lang or lang == '*': if not lang or lang == '*':
res = filter(plugin_filter(self), parent) res = filter(plugin_filter(self), parent)
else: else:
res = filter(plugin_filter(self, lang), parent) res = filter(plugin_lang_filter(self, lang), parent)
return list(res) return list(res)
def set_multi(self, val, lang=None): def set_multi(self: Multi, val: Iterable[ElementBase], lang: Optional[str] = None) -> None:
parent = self.parent() parent = fail_without_parent(self)
del_multi = getattr(self, 'del_%s' % plugin_attrib) del_multi = getattr(self, 'del_%s' % plugin_attrib)
del_multi(lang) del_multi(lang)
for sub in val: for sub in val:
parent.append(sub) parent.append(sub)
def del_multi(self, lang=None): def fail_without_parent(self: Multi) -> ElementBase:
parent = self.parent() parent = None
if self.parent:
parent = self.parent()
if not parent:
raise ValueError('No stanza parent for multifactory')
return parent
def del_multi(self: Multi, lang: Optional[str] = None) -> None:
parent = fail_without_parent(self)
if not lang or lang == '*': if not lang or lang == '*':
res = filter(plugin_filter(self), parent) res = list(filter(plugin_filter(self), parent))
else: else:
res = filter(plugin_filter(self, lang), parent) res = list(filter(plugin_lang_filter(self, lang), parent))
res = list(res)
if not res: if not res:
del parent.plugins[(plugin_attrib, None)] del parent.plugins[(plugin_attrib, None)]
parent.loaded_plugins.remove(plugin_attrib) parent.loaded_plugins.remove(plugin_attrib)
@ -149,7 +180,8 @@ def multifactory(stanza, plugin_attrib):
return Multi return Multi
def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''): def fix_ns(xpath: str, split: bool = False, propagate_ns: bool = True,
default_ns: str = '') -> Union[str, List[str]]:
"""Apply the stanza's namespace to elements in an XPath expression. """Apply the stanza's namespace to elements in an XPath expression.
:param string xpath: The XPath expression to fix with namespaces. :param string xpath: The XPath expression to fix with namespaces.
@ -275,12 +307,12 @@ class ElementBase(object):
#: The XML tag name of the element, not including any namespace #: The XML tag name of the element, not including any namespace
#: prefixes. For example, an :class:`ElementBase` object for #: prefixes. For example, an :class:`ElementBase` object for
#: ``<message />`` would use ``name = 'message'``. #: ``<message />`` would use ``name = 'message'``.
name = 'stanza' name: ClassVar[str] = 'stanza'
#: The XML namespace for the element. Given ``<foo xmlns="bar" />``, #: The XML namespace for the element. Given ``<foo xmlns="bar" />``,
#: then ``namespace = "bar"`` should be used. The default namespace #: then ``namespace = "bar"`` should be used. The default namespace
#: is ``jabber:client`` since this is being used in an XMPP library. #: is ``jabber:client`` since this is being used in an XMPP library.
namespace = 'jabber:client' namespace: str = 'jabber:client'
#: For :class:`ElementBase` subclasses which are intended to be used #: For :class:`ElementBase` subclasses which are intended to be used
#: as plugins, the ``plugin_attrib`` value defines the plugin name. #: as plugins, the ``plugin_attrib`` value defines the plugin name.
@ -290,7 +322,7 @@ class ElementBase(object):
#: register_stanza_plugin(Message, FooPlugin) #: register_stanza_plugin(Message, FooPlugin)
#: msg = Message() #: msg = Message()
#: msg['foo']['an_interface_from_the_foo_plugin'] #: msg['foo']['an_interface_from_the_foo_plugin']
plugin_attrib = 'plugin' plugin_attrib: ClassVar[str] = 'plugin'
#: For :class:`ElementBase` subclasses that are intended to be an #: For :class:`ElementBase` subclasses that are intended to be an
#: iterable group of items, the ``plugin_multi_attrib`` value defines #: iterable group of items, the ``plugin_multi_attrib`` value defines
@ -300,29 +332,29 @@ class ElementBase(object):
#: # Given stanza class Foo, with plugin_multi_attrib = 'foos' #: # Given stanza class Foo, with plugin_multi_attrib = 'foos'
#: parent['foos'] #: parent['foos']
#: filter(isinstance(item, Foo), parent['substanzas']) #: filter(isinstance(item, Foo), parent['substanzas'])
plugin_multi_attrib = '' plugin_multi_attrib: ClassVar[str] = ''
#: The set of keys that the stanza provides for accessing and #: The set of keys that the stanza provides for accessing and
#: manipulating the underlying XML object. This set may be augmented #: manipulating the underlying XML object. This set may be augmented
#: with the :attr:`plugin_attrib` value of any registered #: with the :attr:`plugin_attrib` value of any registered
#: stanza plugins. #: stanza plugins.
interfaces = {'type', 'to', 'from', 'id', 'payload'} interfaces: ClassVar[Set[str]] = {'type', 'to', 'from', 'id', 'payload'}
#: A subset of :attr:`interfaces` which maps interfaces to direct #: A subset of :attr:`interfaces` which maps interfaces to direct
#: subelements of the underlying XML object. Using this set, the text #: subelements of the underlying XML object. Using this set, the text
#: of these subelements may be set, retrieved, or removed without #: of these subelements may be set, retrieved, or removed without
#: needing to define custom methods. #: needing to define custom methods.
sub_interfaces = set() sub_interfaces: ClassVar[Set[str]] = set()
#: A subset of :attr:`interfaces` which maps the presence of #: A subset of :attr:`interfaces` which maps the presence of
#: subelements to boolean values. Using this set allows for quickly #: subelements to boolean values. Using this set allows for quickly
#: checking for the existence of empty subelements like ``<required />``. #: checking for the existence of empty subelements like ``<required />``.
#: #:
#: .. versionadded:: 1.1 #: .. versionadded:: 1.1
bool_interfaces = set() bool_interfaces: ClassVar[Set[str]] = set()
#: .. versionadded:: 1.1.2 #: .. versionadded:: 1.1.2
lang_interfaces = set() lang_interfaces: ClassVar[Set[str]] = set()
#: In some cases you may wish to override the behaviour of one of the #: In some cases you may wish to override the behaviour of one of the
#: parent stanza's interfaces. The ``overrides`` list specifies the #: parent stanza's interfaces. The ``overrides`` list specifies the
@ -336,7 +368,7 @@ class ElementBase(object):
#: be affected. #: be affected.
#: #:
#: .. versionadded:: 1.0-Beta5 #: .. versionadded:: 1.0-Beta5
overrides = [] overrides: ClassVar[List[str]] = []
#: If you need to add a new interface to an existing stanza, you #: If you need to add a new interface to an existing stanza, you
#: can create a plugin and set ``is_extension = True``. Be sure #: can create a plugin and set ``is_extension = True``. Be sure
@ -346,7 +378,7 @@ class ElementBase(object):
#: parent stanza will be passed to the plugin directly. #: parent stanza will be passed to the plugin directly.
#: #:
#: .. versionadded:: 1.0-Beta5 #: .. versionadded:: 1.0-Beta5
is_extension = False is_extension: ClassVar[bool] = False
#: A map of interface operations to the overriding functions. #: A map of interface operations to the overriding functions.
#: For example, after overriding the ``set`` operation for #: For example, after overriding the ``set`` operation for
@ -355,15 +387,15 @@ class ElementBase(object):
#: {'set_body': <some function>} #: {'set_body': <some function>}
#: #:
#: .. versionadded: 1.0-Beta5 #: .. versionadded: 1.0-Beta5
plugin_overrides = {} plugin_overrides: ClassVar[Dict[str, str]] = {}
#: A mapping of the :attr:`plugin_attrib` values of registered #: A mapping of the :attr:`plugin_attrib` values of registered
#: plugins to their respective classes. #: plugins to their respective classes.
plugin_attrib_map = {} plugin_attrib_map: ClassVar[Dict[str, Type[ElementBase]]] = {}
#: A mapping of root element tag names (in ``'{namespace}elementname'`` #: A mapping of root element tag names (in ``'{namespace}elementname'``
#: format) to the plugin classes responsible for them. #: format) to the plugin classes responsible for them.
plugin_tag_map = {} plugin_tag_map: ClassVar[Dict[str, Type[ElementBase]]] = {}
#: The set of stanza classes that can be iterated over using #: The set of stanza classes that can be iterated over using
#: the 'substanzas' interface. Classes are added to this set #: the 'substanzas' interface. Classes are added to this set
@ -372,17 +404,26 @@ class ElementBase(object):
#: register_stanza_plugin(DiscoInfo, DiscoItem, iterable=True) #: register_stanza_plugin(DiscoInfo, DiscoItem, iterable=True)
#: #:
#: .. versionadded:: 1.0-Beta5 #: .. versionadded:: 1.0-Beta5
plugin_iterables = set() plugin_iterables: ClassVar[Set[Type[ElementBase]]] = set()
#: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``. #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``.
xml_ns = XML_NS xml_ns: ClassVar[str] = XML_NS
def __init__(self, xml=None, parent=None): plugins: Dict[Tuple[str, Optional[str]], ElementBase]
#: The underlying XML object for the stanza. It is a standard
#: :class:`xml.etree.ElementTree` object.
xml: ET.Element
_index: int
loaded_plugins: Set[str]
iterables: List[ElementBase]
tag: str
parent: Optional[ReferenceType[ElementBase]]
def __init__(self, xml: Optional[ET.Element] = None, parent: Union[Optional[ElementBase], ReferenceType[ElementBase]] = None):
self._index = 0 self._index = 0
#: The underlying XML object for the stanza. It is a standard if xml is not None:
#: :class:`xml.etree.ElementTree` object. self.xml = xml
self.xml = xml
#: An ordered dictionary of plugin stanzas, mapped by their #: An ordered dictionary of plugin stanzas, mapped by their
#: :attr:`plugin_attrib` value. #: :attr:`plugin_attrib` value.
@ -419,7 +460,7 @@ class ElementBase(object):
existing_xml=child, existing_xml=child,
reuse=False) reuse=False)
def setup(self, xml=None): def setup(self, xml: Optional[ET.Element] = None) -> bool:
"""Initialize the stanza's XML contents. """Initialize the stanza's XML contents.
Will return ``True`` if XML was generated according to the stanza's Will return ``True`` if XML was generated according to the stanza's
@ -429,29 +470,31 @@ class ElementBase(object):
:param xml: An existing XML object to use for the stanza's content :param xml: An existing XML object to use for the stanza's content
instead of generating new XML. instead of generating new XML.
""" """
if self.xml is None: if hasattr(self, 'xml'):
return False
if not hasattr(self, 'xml') and xml is not None:
self.xml = xml self.xml = xml
last_xml = self.xml
if self.xml is None:
# Generate XML from the stanza definition
for ename in self.name.split('/'):
new = ET.Element("{%s}%s" % (self.namespace, ename))
if self.xml is None:
self.xml = new
else:
last_xml.append(new)
last_xml = new
if self.parent is not None:
self.parent().xml.append(self.xml)
# We had to generate XML
return True
else:
# We did not generate XML
return False return False
def enable(self, attrib, lang=None):
# Generate XML from the stanza definition
last_xml = ET.Element('')
for ename in self.name.split('/'):
new = ET.Element("{%s}%s" % (self.namespace, ename))
if not hasattr(self, 'xml'):
self.xml = new
else:
last_xml.append(new)
last_xml = new
if self.parent is not None:
parent = self.parent()
if parent:
parent.xml.append(self.xml)
# We had to generate XML
return True
def enable(self, attrib: str, lang: Optional[str] = None) -> ElementBase:
"""Enable and initialize a stanza plugin. """Enable and initialize a stanza plugin.
Alias for :meth:`init_plugin`. Alias for :meth:`init_plugin`.
@ -487,7 +530,10 @@ class ElementBase(object):
else: else:
return None if check else self.init_plugin(name, lang) return None if check else self.init_plugin(name, lang)
def init_plugin(self, attrib, lang=None, existing_xml=None, element=None, reuse=True): def init_plugin(self, attrib: str, lang: Optional[str] = None,
existing_xml: Optional[ET.Element] = None,
reuse: bool = True,
element: Optional[ElementBase] = None) -> ElementBase:
"""Enable and initialize a stanza plugin. """Enable and initialize a stanza plugin.
:param string attrib: The :attr:`plugin_attrib` value of the :param string attrib: The :attr:`plugin_attrib` value of the
@ -525,7 +571,7 @@ class ElementBase(object):
return plugin return plugin
def _get_stanza_values(self): def _get_stanza_values(self) -> Dict[str, Any]:
"""Return A JSON/dictionary version of the XML content """Return A JSON/dictionary version of the XML content
exposed through the stanza's interfaces:: exposed through the stanza's interfaces::
@ -567,7 +613,7 @@ class ElementBase(object):
values['substanzas'] = iterables values['substanzas'] = iterables
return values return values
def _set_stanza_values(self, values): def _set_stanza_values(self, values: Dict[str, Any]) -> ElementBase:
"""Set multiple stanza interface values using a dictionary. """Set multiple stanza interface values using a dictionary.
Stanza plugin values may be set using nested dictionaries. Stanza plugin values may be set using nested dictionaries.
@ -623,7 +669,7 @@ class ElementBase(object):
plugin.values = value plugin.values = value
return self return self
def __getitem__(self, full_attrib): def __getitem__(self, full_attrib: str) -> Any:
"""Return the value of a stanza interface using dict-like syntax. """Return the value of a stanza interface using dict-like syntax.
Example:: Example::
@ -688,7 +734,7 @@ class ElementBase(object):
else: else:
return '' return ''
def __setitem__(self, attrib, value): def __setitem__(self, attrib: str, value: Any) -> Any:
"""Set the value of a stanza interface using dictionary-like syntax. """Set the value of a stanza interface using dictionary-like syntax.
Example:: Example::
@ -773,7 +819,7 @@ class ElementBase(object):
plugin[full_attrib] = value plugin[full_attrib] = value
return self return self
def __delitem__(self, attrib): def __delitem__(self, attrib: str) -> Any:
"""Delete the value of a stanza interface using dict-like syntax. """Delete the value of a stanza interface using dict-like syntax.
Example:: Example::
@ -851,7 +897,7 @@ class ElementBase(object):
pass pass
return self return self
def _set_attr(self, name, value): def _set_attr(self, name: str, value: Optional[JidStr]) -> None:
"""Set the value of a top level attribute of the XML object. """Set the value of a top level attribute of the XML object.
If the new value is None or an empty string, then the attribute will If the new value is None or an empty string, then the attribute will
@ -868,7 +914,7 @@ class ElementBase(object):
value = str(value) value = str(value)
self.xml.attrib[name] = value self.xml.attrib[name] = value
def _del_attr(self, name): def _del_attr(self, name: str) -> None:
"""Remove a top level attribute of the XML object. """Remove a top level attribute of the XML object.
:param name: The name of the attribute. :param name: The name of the attribute.
@ -876,7 +922,7 @@ class ElementBase(object):
if name in self.xml.attrib: if name in self.xml.attrib:
del self.xml.attrib[name] del self.xml.attrib[name]
def _get_attr(self, name, default=''): def _get_attr(self, name: str, default: str = '') -> str:
"""Return the value of a top level attribute of the XML object. """Return the value of a top level attribute of the XML object.
In case the attribute has not been set, a default value can be In case the attribute has not been set, a default value can be
@ -889,7 +935,8 @@ class ElementBase(object):
""" """
return self.xml.attrib.get(name, default) return self.xml.attrib.get(name, default)
def _get_sub_text(self, name, default='', lang=None): def _get_sub_text(self, name: str, default: str = '',
lang: Optional[str] = None) -> Union[str, Dict[str, str]]:
"""Return the text contents of a sub element. """Return the text contents of a sub element.
In case the element does not exist, or it has no textual content, In case the element does not exist, or it has no textual content,
@ -900,7 +947,7 @@ class ElementBase(object):
:param default: Optional default to return if the element does :param default: Optional default to return if the element does
not exists. An empty string is returned otherwise. not exists. An empty string is returned otherwise.
""" """
name = self._fix_ns(name) name = cast(str, self._fix_ns(name))
if lang == '*': if lang == '*':
return self._get_all_sub_text(name, default, None) return self._get_all_sub_text(name, default, None)
@ -924,8 +971,9 @@ class ElementBase(object):
return result return result
return default return default
def _get_all_sub_text(self, name, default='', lang=None): def _get_all_sub_text(self, name: str, default: str = '',
name = self._fix_ns(name) lang: Optional[str] = None) -> Dict[str, str]:
name = cast(str, self._fix_ns(name))
default_lang = self.get_lang() default_lang = self.get_lang()
results = {} results = {}
@ -935,10 +983,16 @@ class ElementBase(object):
stanza_lang = stanza.attrib.get('{%s}lang' % XML_NS, stanza_lang = stanza.attrib.get('{%s}lang' % XML_NS,
default_lang) default_lang)
if not lang or lang == '*' or stanza_lang == lang: if not lang or lang == '*' or stanza_lang == lang:
results[stanza_lang] = stanza.text if stanza.text is None:
text = default
else:
text = stanza.text
results[stanza_lang] = text
return results return results
def _set_sub_text(self, name, text=None, keep=False, lang=None): def _set_sub_text(self, name: str, text: Optional[str] = None,
keep: bool = False,
lang: Optional[str] = None) -> Optional[ET.Element]:
"""Set the text contents of a sub element. """Set the text contents of a sub element.
In case the element does not exist, a element will be created, In case the element does not exist, a element will be created,
@ -959,15 +1013,16 @@ class ElementBase(object):
lang = default_lang lang = default_lang
if not text and not keep: if not text and not keep:
return self._del_sub(name, lang=lang) self._del_sub(name, lang=lang)
return None
path = self._fix_ns(name, split=True) path = cast(List[str], self._fix_ns(name, split=True))
name = path[-1] name = path[-1]
parent = self.xml parent: Optional[ET.Element] = self.xml
# The first goal is to find the parent of the subelement, or, if # The first goal is to find the parent of the subelement, or, if
# we can't find that, the closest grandparent element. # we can't find that, the closest grandparent element.
missing_path = [] missing_path: List[str] = []
search_order = path[:-1] search_order = path[:-1]
while search_order: while search_order:
parent = self.xml.find('/'.join(search_order)) parent = self.xml.find('/'.join(search_order))
@ -1008,15 +1063,17 @@ class ElementBase(object):
parent.append(element) parent.append(element)
return element return element
def _set_all_sub_text(self, name, values, keep=False, lang=None): def _set_all_sub_text(self, name: str, values: Dict[str, str],
self._del_sub(name, lang) keep: bool = False,
lang: Optional[str] = None) -> None:
self._del_sub(name, lang=lang)
for value_lang, value in values.items(): for value_lang, value in values.items():
if not lang or lang == '*' or value_lang == lang: if not lang or lang == '*' or value_lang == lang:
self._set_sub_text(name, text=value, self._set_sub_text(name, text=value,
keep=keep, keep=keep,
lang=value_lang) lang=value_lang)
def _del_sub(self, name, all=False, lang=None): def _del_sub(self, name: str, all: bool = False, lang: Optional[str] = None) -> None:
"""Remove sub elements that match the given name or XPath. """Remove sub elements that match the given name or XPath.
If the element is in a path, then any parent elements that become If the element is in a path, then any parent elements that become
@ -1034,11 +1091,11 @@ class ElementBase(object):
if not lang: if not lang:
lang = default_lang lang = default_lang
parent = self.xml parent: Optional[ET.Element] = self.xml
for level, _ in enumerate(path): for level, _ in enumerate(path):
# Generate the paths to the target elements and their parent. # Generate the paths to the target elements and their parent.
element_path = "/".join(path[:len(path) - level]) element_path = "/".join(path[:len(path) - level])
parent_path = "/".join(path[:len(path) - level - 1]) parent_path: Optional[str] = "/".join(path[:len(path) - level - 1])
elements = self.xml.findall(element_path) elements = self.xml.findall(element_path)
if parent_path == '': if parent_path == '':
@ -1061,7 +1118,7 @@ class ElementBase(object):
# after deleting the first level of elements. # after deleting the first level of elements.
return return
def match(self, xpath): def match(self, xpath: Union[str, List[str]]) -> bool:
"""Compare a stanza object with an XPath-like expression. """Compare a stanza object with an XPath-like expression.
If the XPath matches the contents of the stanza object, the match If the XPath matches the contents of the stanza object, the match
@ -1127,7 +1184,7 @@ class ElementBase(object):
# Everything matched. # Everything matched.
return True return True
def get(self, key, default=None): def get(self, key: str, default: Optional[Any] = None) -> Any:
"""Return the value of a stanza interface. """Return the value of a stanza interface.
If the found value is None or an empty string, return the supplied If the found value is None or an empty string, return the supplied
@ -1144,7 +1201,7 @@ class ElementBase(object):
return default return default
return value return value
def keys(self): def keys(self) -> List[str]:
"""Return the names of all stanza interfaces provided by the """Return the names of all stanza interfaces provided by the
stanza object. stanza object.
@ -1158,7 +1215,7 @@ class ElementBase(object):
out.append('substanzas') out.append('substanzas')
return out return out
def append(self, item): def append(self, item: Union[ET.Element, ElementBase]) -> ElementBase:
"""Append either an XML object or a substanza to this stanza object. """Append either an XML object or a substanza to this stanza object.
If a substanza object is appended, it will be added to the list If a substanza object is appended, it will be added to the list
@ -1189,7 +1246,7 @@ class ElementBase(object):
return self return self
def appendxml(self, xml): def appendxml(self, xml: ET.Element) -> ElementBase:
"""Append an XML object to the stanza's XML. """Append an XML object to the stanza's XML.
The added XML will not be included in the list of The added XML will not be included in the list of
@ -1200,7 +1257,7 @@ class ElementBase(object):
self.xml.append(xml) self.xml.append(xml)
return self return self
def pop(self, index=0): def pop(self, index: int = 0) -> ElementBase:
"""Remove and return the last substanza in the list of """Remove and return the last substanza in the list of
iterable substanzas. iterable substanzas.
@ -1212,11 +1269,11 @@ class ElementBase(object):
self.xml.remove(substanza.xml) self.xml.remove(substanza.xml)
return substanza return substanza
def next(self): def next(self) -> ElementBase:
"""Return the next iterable substanza.""" """Return the next iterable substanza."""
return self.__next__() return self.__next__()
def clear(self): def clear(self) -> ElementBase:
"""Remove all XML element contents and plugins. """Remove all XML element contents and plugins.
Any attribute values will be preserved. Any attribute values will be preserved.
@ -1229,7 +1286,7 @@ class ElementBase(object):
return self return self
@classmethod @classmethod
def tag_name(cls): def tag_name(cls) -> str:
"""Return the namespaced name of the stanza's root element. """Return the namespaced name of the stanza's root element.
The format for the tag name is:: The format for the tag name is::
@ -1241,29 +1298,32 @@ class ElementBase(object):
""" """
return "{%s}%s" % (cls.namespace, cls.name) return "{%s}%s" % (cls.namespace, cls.name)
def get_lang(self, lang=None): def get_lang(self, lang: Optional[str] = None) -> str:
result = self.xml.attrib.get('{%s}lang' % XML_NS, '') result = self.xml.attrib.get('{%s}lang' % XML_NS, '')
if not result and self.parent and self.parent(): if not result and self.parent:
return self.parent()['lang'] parent = self.parent()
if parent:
return cast(str, parent['lang'])
return result return result
def set_lang(self, lang): def set_lang(self, lang: Optional[str]) -> None:
self.del_lang() self.del_lang()
attr = '{%s}lang' % XML_NS attr = '{%s}lang' % XML_NS
if lang: if lang:
self.xml.attrib[attr] = lang self.xml.attrib[attr] = lang
def del_lang(self): def del_lang(self) -> None:
attr = '{%s}lang' % XML_NS attr = '{%s}lang' % XML_NS
if attr in self.xml.attrib: if attr in self.xml.attrib:
del self.xml.attrib[attr] del self.xml.attrib[attr]
def _fix_ns(self, xpath, split=False, propagate_ns=True): def _fix_ns(self, xpath: str, split: bool = False,
propagate_ns: bool = True) -> Union[str, List[str]]:
return fix_ns(xpath, split=split, return fix_ns(xpath, split=split,
propagate_ns=propagate_ns, propagate_ns=propagate_ns,
default_ns=self.namespace) default_ns=self.namespace)
def __eq__(self, other): def __eq__(self, other: Any) -> bool:
"""Compare the stanza object with another to test for equality. """Compare the stanza object with another to test for equality.
Stanzas are equal if their interfaces return the same values, Stanzas are equal if their interfaces return the same values,
@ -1290,7 +1350,7 @@ class ElementBase(object):
# must be equal. # must be equal.
return True return True
def __ne__(self, other): def __ne__(self, other: Any) -> bool:
"""Compare the stanza object with another to test for inequality. """Compare the stanza object with another to test for inequality.
Stanzas are not equal if their interfaces return different values, Stanzas are not equal if their interfaces return different values,
@ -1300,16 +1360,16 @@ class ElementBase(object):
""" """
return not self.__eq__(other) return not self.__eq__(other)
def __bool__(self): def __bool__(self) -> bool:
"""Stanza objects should be treated as True in boolean contexts. """Stanza objects should be treated as True in boolean contexts.
""" """
return True return True
def __len__(self): def __len__(self) -> int:
"""Return the number of iterable substanzas in this stanza.""" """Return the number of iterable substanzas in this stanza."""
return len(self.iterables) return len(self.iterables)
def __iter__(self): def __iter__(self) -> ElementBase:
"""Return an iterator object for the stanza's substanzas. """Return an iterator object for the stanza's substanzas.
The iterator is the stanza object itself. Attempting to use two The iterator is the stanza object itself. Attempting to use two
@ -1318,7 +1378,7 @@ class ElementBase(object):
self._index = 0 self._index = 0
return self return self
def __next__(self): def __next__(self) -> ElementBase:
"""Return the next iterable substanza.""" """Return the next iterable substanza."""
self._index += 1 self._index += 1
if self._index > len(self.iterables): if self._index > len(self.iterables):
@ -1326,13 +1386,16 @@ class ElementBase(object):
raise StopIteration raise StopIteration
return self.iterables[self._index - 1] return self.iterables[self._index - 1]
def __copy__(self): def __copy__(self) -> ElementBase:
"""Return a copy of the stanza object that does not share the same """Return a copy of the stanza object that does not share the same
underlying XML object. underlying XML object.
""" """
return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) return self.__class__(
xml=copy.deepcopy(self.xml),
parent=self.parent,
)
def __str__(self, top_level_ns=True): def __str__(self, top_level_ns: bool = True) -> str:
"""Return a string serialization of the underlying XML object. """Return a string serialization of the underlying XML object.
.. seealso:: :ref:`tostring` .. seealso:: :ref:`tostring`
@ -1343,12 +1406,33 @@ class ElementBase(object):
return tostring(self.xml, xmlns='', return tostring(self.xml, xmlns='',
top_level=True) top_level=True)
def __repr__(self): def __repr__(self) -> str:
"""Use the stanza's serialized XML as its representation.""" """Use the stanza's serialized XML as its representation."""
return self.__str__() return self.__str__()
# Compatibility. # Compatibility.
_get_plugin = get_plugin _get_plugin = get_plugin
get_stanza_values = _get_stanza_values
set_stanza_values = _set_stanza_values
#: A JSON/dictionary version of the XML content exposed through
#: the stanza interfaces::
#:
#: >>> msg = Message()
#: >>> msg.values
#: {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
#: 'to': , 'type': 'normal', 'id': '', 'subject': ''}
#:
#: Likewise, assigning to the :attr:`values` will change the XML
#: content::
#:
#: >>> msg = Message()
#: >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
#: >>> msg
#: '<message to="user@example.com"><body>Hi!</body></message>'
#:
#: Child stanzas are exposed as nested dictionaries.
values = property(_get_stanza_values, _set_stanza_values) # type: ignore
class StanzaBase(ElementBase): class StanzaBase(ElementBase):
@ -1386,9 +1470,14 @@ class StanzaBase(ElementBase):
#: The default XMPP client namespace #: The default XMPP client namespace
namespace = 'jabber:client' namespace = 'jabber:client'
types: ClassVar[Set[str]] = set()
def __init__(self, stream=None, xml=None, stype=None, def __init__(self, stream: Optional[XMLStream] = None,
sto=None, sfrom=None, sid=None, parent=None, recv=False): xml: Optional[ET.Element] = None,
stype: Optional[str] = None,
sto: Optional[JidStr] = None, sfrom: Optional[JidStr] = None,
sid: Optional[str] = None,
parent: Optional[ElementBase] = None, recv: bool = False):
self.stream = stream self.stream = stream
if stream is not None: if stream is not None:
self.namespace = stream.default_ns self.namespace = stream.default_ns
@ -1403,7 +1492,7 @@ class StanzaBase(ElementBase):
self['id'] = sid self['id'] = sid
self.tag = "{%s}%s" % (self.namespace, self.name) self.tag = "{%s}%s" % (self.namespace, self.name)
def set_type(self, value): def set_type(self, value: str) -> StanzaBase:
"""Set the stanza's ``'type'`` attribute. """Set the stanza's ``'type'`` attribute.
Only type values contained in :attr:`types` are accepted. Only type values contained in :attr:`types` are accepted.
@ -1414,11 +1503,11 @@ class StanzaBase(ElementBase):
self.xml.attrib['type'] = value self.xml.attrib['type'] = value
return self return self
def get_to(self): def get_to(self) -> JID:
"""Return the value of the stanza's ``'to'`` attribute.""" """Return the value of the stanza's ``'to'`` attribute."""
return JID(self._get_attr('to')) return JID(self._get_attr('to'))
def set_to(self, value): def set_to(self, value: JidStr) -> None:
"""Set the ``'to'`` attribute of the stanza. """Set the ``'to'`` attribute of the stanza.
:param value: A string or :class:`slixmpp.xmlstream.JID` object :param value: A string or :class:`slixmpp.xmlstream.JID` object
@ -1426,11 +1515,11 @@ class StanzaBase(ElementBase):
""" """
return self._set_attr('to', str(value)) return self._set_attr('to', str(value))
def get_from(self): def get_from(self) -> JID:
"""Return the value of the stanza's ``'from'`` attribute.""" """Return the value of the stanza's ``'from'`` attribute."""
return JID(self._get_attr('from')) return JID(self._get_attr('from'))
def set_from(self, value): def set_from(self, value: JidStr) -> None:
"""Set the 'from' attribute of the stanza. """Set the 'from' attribute of the stanza.
:param from: A string or JID object representing the sender's JID. :param from: A string or JID object representing the sender's JID.
@ -1438,11 +1527,11 @@ class StanzaBase(ElementBase):
""" """
return self._set_attr('from', str(value)) return self._set_attr('from', str(value))
def get_payload(self): def get_payload(self) -> List[ET.Element]:
"""Return a list of XML objects contained in the stanza.""" """Return a list of XML objects contained in the stanza."""
return list(self.xml) return list(self.xml)
def set_payload(self, value): def set_payload(self, value: Union[List[ElementBase], ElementBase]) -> StanzaBase:
"""Add XML content to the stanza. """Add XML content to the stanza.
:param value: Either an XML or a stanza object, or a list :param value: Either an XML or a stanza object, or a list
@ -1454,12 +1543,12 @@ class StanzaBase(ElementBase):
self.append(val) self.append(val)
return self return self
def del_payload(self): def del_payload(self) -> StanzaBase:
"""Remove the XML contents of the stanza.""" """Remove the XML contents of the stanza."""
self.clear() self.clear()
return self return self
def reply(self, clear=True): def reply(self, clear: bool = True) -> StanzaBase:
"""Prepare the stanza for sending a reply. """Prepare the stanza for sending a reply.
Swaps the ``'from'`` and ``'to'`` attributes. Swaps the ``'from'`` and ``'to'`` attributes.
@ -1475,7 +1564,7 @@ class StanzaBase(ElementBase):
new_stanza = copy.copy(self) new_stanza = copy.copy(self)
# if it's a component, use from # if it's a component, use from
if self.stream and hasattr(self.stream, "is_component") and \ if self.stream and hasattr(self.stream, "is_component") and \
self.stream.is_component: getattr(self.stream, 'is_component'):
new_stanza['from'], new_stanza['to'] = self['to'], self['from'] new_stanza['from'], new_stanza['to'] = self['to'], self['from']
else: else:
new_stanza['to'] = self['from'] new_stanza['to'] = self['from']
@ -1484,19 +1573,19 @@ class StanzaBase(ElementBase):
new_stanza.clear() new_stanza.clear()
return new_stanza return new_stanza
def error(self): def error(self) -> StanzaBase:
"""Set the stanza's type to ``'error'``.""" """Set the stanza's type to ``'error'``."""
self['type'] = 'error' self['type'] = 'error'
return self return self
def unhandled(self): def unhandled(self) -> None:
"""Called if no handlers have been registered to process this stanza. """Called if no handlers have been registered to process this stanza.
Meant to be overridden. Meant to be overridden.
""" """
pass pass
def exception(self, e): def exception(self, e: Exception) -> None:
"""Handle exceptions raised during stanza processing. """Handle exceptions raised during stanza processing.
Meant to be overridden. Meant to be overridden.
@ -1504,18 +1593,21 @@ class StanzaBase(ElementBase):
log.exception('Error handling {%s}%s stanza', self.namespace, log.exception('Error handling {%s}%s stanza', self.namespace,
self.name) self.name)
def send(self): def send(self) -> None:
"""Queue the stanza to be sent on the XML stream.""" """Queue the stanza to be sent on the XML stream."""
self.stream.send(self) if self.stream is not None:
self.stream.send(self)
else:
log.error("Tried to send stanza without a stream: %s", self)
def __copy__(self): def __copy__(self) -> StanzaBase:
"""Return a copy of the stanza object that does not share the """Return a copy of the stanza object that does not share the
same underlying XML object, but does share the same XML stream. same underlying XML object, but does share the same XML stream.
""" """
return self.__class__(xml=copy.deepcopy(self.xml), return self.__class__(xml=copy.deepcopy(self.xml),
stream=self.stream) stream=self.stream)
def __str__(self, top_level_ns=False): def __str__(self, top_level_ns: bool = False) -> str:
"""Serialize the stanza's XML to a string. """Serialize the stanza's XML to a string.
:param bool top_level_ns: Display the top-most namespace. :param bool top_level_ns: Display the top-most namespace.
@ -1525,27 +1617,3 @@ class StanzaBase(ElementBase):
return tostring(self.xml, xmlns=xmlns, return tostring(self.xml, xmlns=xmlns,
stream=self.stream, stream=self.stream,
top_level=(self.stream is None)) top_level=(self.stream is None))
#: A JSON/dictionary version of the XML content exposed through
#: the stanza interfaces::
#:
#: >>> msg = Message()
#: >>> msg.values
#: {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
#: 'to': , 'type': 'normal', 'id': '', 'subject': ''}
#:
#: Likewise, assigning to the :attr:`values` will change the XML
#: content::
#:
#: >>> msg = Message()
#: >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
#: >>> msg
#: '<message to="user@example.com"><body>Hi!</body></message>'
#:
#: Child stanzas are exposed as nested dictionaries.
ElementBase.values = property(ElementBase._get_stanza_values,
ElementBase._set_stanza_values)
ElementBase.get_stanza_values = ElementBase._get_stanza_values
ElementBase.set_stanza_values = ElementBase._set_stanza_values