v1.3.5
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
########################################################################
|
||||
# File name: __init__.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
"""
|
||||
:mod:`~aioxmpp.forms` --- Data Forms support (:xep:`4`)
|
||||
#######################################################
|
||||
|
||||
This subpackage contains tools to deal with :xep:`4` Data Forms. Data Forms is
|
||||
a pervasive and highly flexible protocol used in XMPP. It allows for
|
||||
machine-readable (and processable) forms as well as tables of data. This
|
||||
flexibility comes unfortunately at the price of complexity. This subpackage
|
||||
attempts to take some of the load of processing Data Forms off the application
|
||||
developer.
|
||||
|
||||
Cheat Sheet:
|
||||
|
||||
* The :class:`Form` class exists for use cases where automated processing of
|
||||
Data Forms is supposed to happen. The
|
||||
:ref:`api-aioxmpp.forms-declarative-style` allow convenient access to and
|
||||
manipulation of form data from within code.
|
||||
|
||||
* Direct use of the :class:`Data` XSO is advisable if you want to present forms
|
||||
or data to sentient beings: even though :class:`Form` is more convenient for
|
||||
machine-to-machine use, using the :class:`Data` sent by the peer easily
|
||||
allows showing the user *all* fields supported by the peer.
|
||||
|
||||
* For machine-processed tables, there is no tooling (yet).
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
Even though the :mod:`aioxmpp.forms` module existed pre-0.7, it has not been
|
||||
documented and was thus not part of the public API.
|
||||
|
||||
.. note::
|
||||
|
||||
The authors are not entirely happy with the API at some points.
|
||||
Specifically, at some places where mutable data structures are used, the
|
||||
mutation of these data structures may have unexpected side effects. This may
|
||||
be rectified in a future release by replacing these data structures with
|
||||
their appropriate immutable equivalents.
|
||||
|
||||
These locations are marked accordingly.
|
||||
|
||||
Attributes added to stanzas
|
||||
===========================
|
||||
|
||||
:mod:`aioxmpp.forms` adds the following attributes to stanzas:
|
||||
|
||||
.. attribute:: aioxmpp.Message.xep0004_data
|
||||
|
||||
A sequence of :class:`Data` instances. This is used for example by the
|
||||
:mod:`~.muc` implementation (:xep:`45`).
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
|
||||
.. _api-aioxmpp.forms-declarative-style:
|
||||
|
||||
Declarative-style Forms
|
||||
=======================
|
||||
|
||||
Base class
|
||||
----------
|
||||
|
||||
.. autoclass:: Form
|
||||
|
||||
Fields
|
||||
------
|
||||
|
||||
Text fields
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: TextSingle(var, type_=xso.String(), *[, default=None][, required=False][, desc=None][, label=None])
|
||||
|
||||
.. autoclass:: TextPrivate(var, type_=xso.String(), *[, default=None][, required=False][, desc=None][, label=None])
|
||||
|
||||
.. autoclass:: TextMulti(var, type_=xso.String(), *[, default=()][, required=False][, desc=None][, label=None])
|
||||
|
||||
JID fields
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoclass:: JIDSingle(var, *[, default=None][, required=False][, desc=None][, label=None])
|
||||
|
||||
.. autoclass:: JIDMulti(var, *[, default=()][, required=False][, desc=None][, label=None])
|
||||
|
||||
Selection fields
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: ListSingle(var, type_=xso.String(), *[, default=None][, options=[]][, required=False][, desc=None][, label=None])
|
||||
|
||||
.. autoclass:: ListMulti(var, type_=xso.String(), *[, default=frozenset()][, options=[]][, required=False][, desc=None][, label=None])
|
||||
|
||||
Other fields
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: Boolean(var, *[, default=False][, required=False][, desc=None][, label=None])
|
||||
|
||||
Abstract base classes
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. currentmodule:: aioxmpp.forms.fields
|
||||
|
||||
.. autoclass:: AbstractField
|
||||
|
||||
.. autoclass:: AbstractChoiceField(var, type_=xso.String(), *[, options=[]][, required=False][, desc=None][, label=None])
|
||||
|
||||
.. currentmodule:: aioxmpp.forms
|
||||
|
||||
.. _api-aioxmpp.forms-bound-fields:
|
||||
|
||||
Bound fields
|
||||
============
|
||||
|
||||
Bound fields are objects which are returned when the descriptor attribute is
|
||||
accessed on a form instance. It holds the value of the field, as well as
|
||||
overrides for the default (specified on the descriptors themselves) values for
|
||||
certain attributes (such as :attr:`~.AbstractField.desc`).
|
||||
|
||||
For the different field types, there are different classes of bound fields,
|
||||
which are documented below.
|
||||
|
||||
.. currentmodule:: aioxmpp.forms.fields
|
||||
|
||||
.. autoclass:: BoundField
|
||||
|
||||
.. autoclass:: BoundSingleValueField
|
||||
|
||||
.. autoclass:: BoundMultiValueField
|
||||
|
||||
.. autoclass:: BoundOptionsField
|
||||
|
||||
.. autoclass:: BoundSelectField
|
||||
|
||||
.. autoclass:: BoundMultiSelectField
|
||||
|
||||
.. currentmodule:: aioxmpp.forms
|
||||
|
||||
XSOs
|
||||
====
|
||||
|
||||
.. autoclass:: Data
|
||||
|
||||
.. autoclass:: DataType
|
||||
|
||||
.. autoclass:: Field
|
||||
|
||||
.. autoclass:: FieldType
|
||||
|
||||
Report and table support
|
||||
------------------------
|
||||
|
||||
.. autoclass:: Reported
|
||||
|
||||
.. autoclass:: Item
|
||||
|
||||
""" # NOQA: E501
|
||||
|
||||
|
||||
from . import xso # NOQA: F401
|
||||
|
||||
from .xso import ( # NOQA: F401
|
||||
Data,
|
||||
DataType,
|
||||
Field,
|
||||
FieldType,
|
||||
Reported,
|
||||
Item,
|
||||
)
|
||||
|
||||
from .fields import ( # NOQA: F401
|
||||
Boolean,
|
||||
ListSingle,
|
||||
ListMulti,
|
||||
JIDSingle,
|
||||
JIDMulti,
|
||||
TextSingle,
|
||||
TextMulti,
|
||||
TextPrivate,
|
||||
)
|
||||
|
||||
from .form import ( # NOQA: F401
|
||||
Form,
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,476 @@
|
||||
########################################################################
|
||||
# File name: form.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from . import xso as forms_xso
|
||||
from . import fields as fields
|
||||
|
||||
|
||||
def descriptor_attr_name(descriptor):
|
||||
return "_descriptor_{:x}".format(id(descriptor))
|
||||
|
||||
|
||||
class DescriptorClass(abc.ABCMeta):
|
||||
@classmethod
|
||||
def _merge_descriptors(mcls, dest_map, source):
|
||||
for key, (descriptor, from_class) in source:
|
||||
try:
|
||||
existing_descriptor, exists_at_class = dest_map[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if descriptor is not existing_descriptor:
|
||||
raise TypeError(
|
||||
"descriptor with key {!r} already "
|
||||
"declared at {}".format(
|
||||
key,
|
||||
exists_at_class,
|
||||
)
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
||||
dest_map[key] = descriptor, from_class
|
||||
|
||||
@classmethod
|
||||
def _upcast_descriptor_map(mcls, descriptor_map, from_class):
|
||||
return {
|
||||
key: (descriptor, from_class)
|
||||
for key, descriptor in descriptor_map.items()
|
||||
}
|
||||
|
||||
def __new__(mcls, name, bases, namespace, *, protect=True):
|
||||
descriptor_info = {}
|
||||
|
||||
for base in bases:
|
||||
if not isinstance(base, DescriptorClass):
|
||||
continue
|
||||
|
||||
base_descriptor_info = mcls._upcast_descriptor_map(
|
||||
base.DESCRIPTOR_MAP,
|
||||
"{}.{}".format(
|
||||
base.__module__,
|
||||
base.__qualname__,
|
||||
)
|
||||
)
|
||||
mcls._merge_descriptors(
|
||||
descriptor_info,
|
||||
base_descriptor_info.items(),
|
||||
)
|
||||
|
||||
fqcn = "{}.{}".format(
|
||||
namespace["__module__"],
|
||||
namespace["__qualname__"],
|
||||
)
|
||||
|
||||
descriptors = [
|
||||
(attribute_name, descriptor)
|
||||
for attribute_name, descriptor in namespace.items()
|
||||
if isinstance(descriptor, fields.AbstractDescriptor)
|
||||
]
|
||||
|
||||
if any(descriptor.root_class is not None
|
||||
for _, descriptor in descriptors):
|
||||
raise ValueError(
|
||||
"descriptor cannot be used on multiple classes"
|
||||
)
|
||||
|
||||
mcls._merge_descriptors(
|
||||
descriptor_info,
|
||||
(
|
||||
(key, (descriptor, fqcn))
|
||||
for _, descriptor in descriptors
|
||||
for key in descriptor.descriptor_keys()
|
||||
)
|
||||
)
|
||||
|
||||
namespace["DESCRIPTOR_MAP"] = {
|
||||
key: descriptor
|
||||
for key, (descriptor, _) in descriptor_info.items()
|
||||
}
|
||||
namespace["DESCRIPTORS"] = set(namespace["DESCRIPTOR_MAP"].values())
|
||||
if "__slots__" not in namespace and protect:
|
||||
namespace["__slots__"] = ()
|
||||
|
||||
result = super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
for attribute_name, descriptor in descriptors:
|
||||
descriptor.attribute_name = attribute_name
|
||||
descriptor.root_class = result
|
||||
|
||||
return result
|
||||
|
||||
def __init__(self, name, bases, namespace, *, protect=True):
|
||||
super().__init__(name, bases, namespace)
|
||||
|
||||
def _is_descriptor_attribute(self, name):
|
||||
try:
|
||||
existing = getattr(self, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(existing, fields.AbstractDescriptor):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if self._is_descriptor_attribute(name):
|
||||
raise AttributeError("descriptor attributes cannot be set")
|
||||
|
||||
if not isinstance(value, fields.AbstractDescriptor):
|
||||
return super().__setattr__(name, value)
|
||||
|
||||
if self.__subclasses__():
|
||||
raise TypeError("cannot add descriptors to classes with "
|
||||
"subclasses")
|
||||
|
||||
meta = type(self)
|
||||
descriptor_info = meta._upcast_descriptor_map(
|
||||
self.DESCRIPTOR_MAP,
|
||||
"{}.{}".format(self.__module__, self.__qualname__),
|
||||
)
|
||||
|
||||
new_descriptor_info = [
|
||||
(key, (value, "<added via __setattr__>"))
|
||||
for key in value.descriptor_keys()
|
||||
]
|
||||
|
||||
# this would raise on conflict
|
||||
meta._merge_descriptors(
|
||||
descriptor_info,
|
||||
new_descriptor_info,
|
||||
)
|
||||
|
||||
for key, (descriptor, _) in new_descriptor_info:
|
||||
self.DESCRIPTOR_MAP[key] = descriptor
|
||||
|
||||
self.DESCRIPTORS.add(value)
|
||||
|
||||
return super().__setattr__(name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
if self._is_descriptor_attribute(name):
|
||||
raise AttributeError("removal of descriptors is not allowed")
|
||||
|
||||
return super().__delattr__(name)
|
||||
|
||||
def _register_descriptor_keys(self, descriptor, keys):
|
||||
"""
|
||||
Register the given descriptor keys for the given descriptor at the
|
||||
class.
|
||||
|
||||
:param descriptor: The descriptor for which the `keys` shall be
|
||||
registered.
|
||||
:type descriptor: :class:`AbstractDescriptor` instance
|
||||
:param keys: An iterable of descriptor keys
|
||||
:raises TypeError: if the specified keys are already handled by a
|
||||
descriptor.
|
||||
:raises TypeError: if this class has subclasses or if it is not the
|
||||
:attr:`~AbstractDescriptor.root_class` of the given
|
||||
descriptor.
|
||||
|
||||
If the method raises, the caller must assume that registration was not
|
||||
successful.
|
||||
|
||||
.. note::
|
||||
|
||||
The intended audience for this method are developers of
|
||||
:class:`AbstractDescriptor` subclasses, which are generally only
|
||||
expected to live in the :mod:`aioxmpp` package.
|
||||
|
||||
Thus, you should not expect this API to be stable. If you have a
|
||||
use-case for using this function outside of :mod:`aioxmpp`, please
|
||||
let me know through the usual issue reporting means.
|
||||
"""
|
||||
|
||||
if descriptor.root_class is not self or self.__subclasses__():
|
||||
raise TypeError(
|
||||
"descriptors cannot be modified on classes with subclasses"
|
||||
)
|
||||
|
||||
meta = type(self)
|
||||
descriptor_info = meta._upcast_descriptor_map(
|
||||
self.DESCRIPTOR_MAP,
|
||||
"{}.{}".format(self.__module__, self.__qualname__),
|
||||
)
|
||||
|
||||
# this would raise on conflict
|
||||
meta._merge_descriptors(
|
||||
descriptor_info,
|
||||
[
|
||||
(key, (descriptor, "<added via _register_descriptor_keys>"))
|
||||
for key in keys
|
||||
]
|
||||
)
|
||||
|
||||
for key in keys:
|
||||
self.DESCRIPTOR_MAP[key] = descriptor
|
||||
|
||||
|
||||
class FormClass(DescriptorClass):
|
||||
def from_xso(self, xso):
|
||||
"""
|
||||
Construct and return an instance from the given `xso`.
|
||||
|
||||
.. note::
|
||||
|
||||
This is a static method (classmethod), even though sphinx does not
|
||||
document it as such.
|
||||
|
||||
:param xso: A :xep:`4` data form
|
||||
:type xso: :class:`~.Data`
|
||||
:raises ValueError: if the ``FORM_TYPE`` mismatches
|
||||
:raises ValueError: if field types mismatch
|
||||
:return: newly created instance of this class
|
||||
|
||||
The fields from the given `xso` are matched against the fields on the
|
||||
form. Any matching field loads its data from the `xso` field. Fields
|
||||
which occur on the form template but not in the `xso` are skipped.
|
||||
Fields which occur in the `xso` but not on the form template are also
|
||||
skipped (but are re-emitted when the form is rendered as reply, see
|
||||
:meth:`~.Form.render_reply`).
|
||||
|
||||
If the form template has a ``FORM_TYPE`` attribute and the incoming
|
||||
`xso` also has a ``FORM_TYPE`` field, a mismatch between the two values
|
||||
leads to a :class:`ValueError`.
|
||||
|
||||
The field types of matching fields are checked. If the field type on
|
||||
the incoming XSO may not be upcast to the field type declared on the
|
||||
form (see :meth:`~.FieldType.allow_upcast`), a :class:`ValueError` is
|
||||
raised.
|
||||
|
||||
If the :attr:`~.Data.type_` does not indicate an actual form (but
|
||||
rather a cancellation request or tabular result), :class:`ValueError`
|
||||
is raised.
|
||||
"""
|
||||
|
||||
my_form_type = getattr(self, "FORM_TYPE", None)
|
||||
|
||||
f = self()
|
||||
for field in xso.fields:
|
||||
if field.var == "FORM_TYPE":
|
||||
if (my_form_type is not None and
|
||||
field.type_ == forms_xso.FieldType.HIDDEN and
|
||||
field.values):
|
||||
if my_form_type != field.values[0]:
|
||||
raise ValueError(
|
||||
"mismatching FORM_TYPE ({!r} != {!r})".format(
|
||||
field.values[0],
|
||||
my_form_type,
|
||||
)
|
||||
)
|
||||
continue
|
||||
if field.var is None:
|
||||
continue
|
||||
|
||||
key = fields.descriptor_ns, field.var
|
||||
try:
|
||||
descriptor = self.DESCRIPTOR_MAP[key]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if (field.type_ is not None and not
|
||||
field.type_.allow_upcast(descriptor.FIELD_TYPE)):
|
||||
raise ValueError(
|
||||
"mismatching type ({!r} != {!r}) on field var={!r}".format(
|
||||
field.type_,
|
||||
descriptor.FIELD_TYPE,
|
||||
field.var,
|
||||
)
|
||||
)
|
||||
|
||||
data = descriptor.__get__(f, self)
|
||||
data.load(field)
|
||||
|
||||
f._recv_xso = xso
|
||||
|
||||
return f
|
||||
|
||||
|
||||
class Form(metaclass=FormClass):
|
||||
"""
|
||||
A form template for :xep:`0004` Data Forms.
|
||||
|
||||
Fields are declared using the different field descriptors available in this
|
||||
module:
|
||||
|
||||
.. autosummary::
|
||||
|
||||
TextSingle
|
||||
TextMulti
|
||||
TextPrivate
|
||||
JIDSingle
|
||||
JIDMulti
|
||||
ListSingle
|
||||
ListMulti
|
||||
Boolean
|
||||
|
||||
A form template can be instantiated by two different means:
|
||||
|
||||
1. the :meth:`from_xso` method can be called on a :class:`.xso.Data`
|
||||
instance to fill in the template with the data from the XSO.
|
||||
|
||||
2. the constructor can be called.
|
||||
|
||||
With the first method, labels, descriptions, options and values are taken
|
||||
from the XSO. The descriptors declared on the form merely act as a
|
||||
convenient way to access the fields in the XSO.
|
||||
|
||||
If a field is missing from the XSO, its descriptor still works as if the
|
||||
form had been constructed using its constructor. It will not be emitted
|
||||
when re-serialising the form for a response using :meth:`render_reply`.
|
||||
|
||||
If the XSO has more fields than the form template, these fields are
|
||||
re-emitted when the form is serialised using :meth:`render_reply`.
|
||||
|
||||
.. attribute:: LAYOUT
|
||||
|
||||
A mixed list of descriptors and strings to determine form layout as
|
||||
generated by :meth:`render_request`. The semantics are the following:
|
||||
|
||||
* each :class:`str` is converted to a ``"fixed"`` field without ``var``
|
||||
attribute in the output.
|
||||
* each :class:`AbstractField` descriptor is rendered to its
|
||||
corresponding :class:`Field` XSO.
|
||||
|
||||
The elements of :attr:`LAYOUT` are processed in-order. This attribute is
|
||||
optional and can be set on either the :class:`Form` or a specific
|
||||
instance. If it is absent, it is treated as if it were set to
|
||||
``list(self.DESCRIPTORS)``.
|
||||
|
||||
.. automethod:: from_xso
|
||||
|
||||
.. automethod:: render_reply
|
||||
|
||||
.. automethod:: render_request
|
||||
"""
|
||||
|
||||
__slots__ = ("_descriptor_data", "_recv_xso")
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
result = super().__new__(cls)
|
||||
result._descriptor_data = {}
|
||||
result._recv_xso = None
|
||||
return result
|
||||
|
||||
def __copy__(self):
|
||||
result = type(self).__new__(type(self))
|
||||
result._descriptor_data.update(self._descriptor_data)
|
||||
return result
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
result = type(self).__new__(type(self))
|
||||
result._descriptor_data = {
|
||||
k: v.clone_for(self, memo=memo)
|
||||
for k, v in self._descriptor_data.items()
|
||||
}
|
||||
return result
|
||||
|
||||
def render_reply(self):
|
||||
"""
|
||||
Create a :class:`~.Data` object equal to the object from which the from
|
||||
was created through :meth:`from_xso`, except that the values of the
|
||||
fields are exchanged with the values set on the form.
|
||||
|
||||
Fields which have no corresponding form descriptor are left untouched.
|
||||
Fields which are accessible through form descriptors, but are not in
|
||||
the original :class:`~.Data` are not included in the output.
|
||||
|
||||
This method only works on forms created through :meth:`from_xso`.
|
||||
|
||||
The resulting :class:`~.Data` instance has the :attr:`~.Data.type_` set
|
||||
to :attr:`~.DataType.SUBMIT`.
|
||||
"""
|
||||
|
||||
data = copy.copy(self._recv_xso)
|
||||
data.type_ = forms_xso.DataType.SUBMIT
|
||||
data.fields = list(self._recv_xso.fields)
|
||||
|
||||
for i, field_xso in enumerate(data.fields):
|
||||
if field_xso.var is None:
|
||||
continue
|
||||
if field_xso.var == "FORM_TYPE":
|
||||
continue
|
||||
key = fields.descriptor_ns, field_xso.var
|
||||
try:
|
||||
descriptor = self.DESCRIPTOR_MAP[key]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
bound_field = descriptor.__get__(self, type(self))
|
||||
data.fields[i] = bound_field.render(
|
||||
use_local_metadata=False
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def render_request(self):
|
||||
"""
|
||||
Create a :class:`Data` object containing all fields known to the
|
||||
:class:`Form`. If the :class:`Form` has a :attr:`LAYOUT` attribute, it
|
||||
is used during generation.
|
||||
"""
|
||||
|
||||
data = forms_xso.Data(type_=forms_xso.DataType.FORM)
|
||||
|
||||
try:
|
||||
layout = self.LAYOUT
|
||||
except AttributeError:
|
||||
layout = list(self.DESCRIPTORS)
|
||||
|
||||
my_form_type = getattr(self, "FORM_TYPE", None)
|
||||
if my_form_type is not None:
|
||||
field_xso = forms_xso.Field()
|
||||
field_xso.var = "FORM_TYPE"
|
||||
field_xso.type_ = forms_xso.FieldType.HIDDEN
|
||||
field_xso.values[:] = [my_form_type]
|
||||
data.fields.append(field_xso)
|
||||
|
||||
for item in layout:
|
||||
if isinstance(item, str):
|
||||
field_xso = forms_xso.Field()
|
||||
field_xso.type_ = forms_xso.FieldType.FIXED
|
||||
field_xso.values[:] = [item]
|
||||
else:
|
||||
field_xso = item.__get__(
|
||||
self, type(self)
|
||||
).render()
|
||||
data.fields.append(field_xso)
|
||||
|
||||
return data
|
||||
|
||||
def _layout(self, usecase):
|
||||
"""
|
||||
Return an iterable of form members which are used to lay out the form.
|
||||
|
||||
:param usecase: Configure the use case of the layout. This either
|
||||
indicates transmitting the form to a peer as
|
||||
*response*, as *initial form*, or as *error form*, or
|
||||
*showing* the form to a local user.
|
||||
|
||||
Each element in the iterable must be one of the following:
|
||||
|
||||
* A string; gets converted to a ``"fixed"`` form field.
|
||||
* A field XSO; gets used verbatimly
|
||||
* A descriptor; gets converted to a field XSO
|
||||
"""
|
||||
@@ -0,0 +1,643 @@
|
||||
########################################################################
|
||||
# File name: xso.py
|
||||
# This file is part of: aioxmpp
|
||||
#
|
||||
# LICENSE
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program. If not, see
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
########################################################################
|
||||
import collections
|
||||
import enum
|
||||
|
||||
import aioxmpp
|
||||
import aioxmpp.xso as xso
|
||||
|
||||
from aioxmpp.utils import namespaces
|
||||
|
||||
namespaces.xep0004_data = "jabber:x:data"
|
||||
|
||||
|
||||
class Value(xso.XSO):
|
||||
TAG = (namespaces.xep0004_data, "value")
|
||||
|
||||
value = xso.Text(default="")
|
||||
|
||||
|
||||
class ValueElement(xso.AbstractElementType):
|
||||
def unpack(self, item):
|
||||
return item.value
|
||||
|
||||
def pack(self, value):
|
||||
v = Value()
|
||||
v.value = value
|
||||
return v
|
||||
|
||||
def get_xso_types(self):
|
||||
return [Value]
|
||||
|
||||
|
||||
class Option(xso.XSO):
|
||||
TAG = (namespaces.xep0004_data, "option")
|
||||
|
||||
label = xso.Attr(
|
||||
tag="label",
|
||||
default=None,
|
||||
)
|
||||
|
||||
value = xso.ChildText(
|
||||
(namespaces.xep0004_data, "value"),
|
||||
default=None,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if self.value is None:
|
||||
raise ValueError("option is missing a value")
|
||||
|
||||
|
||||
class OptionElement(xso.AbstractElementType):
|
||||
def unpack(self, item):
|
||||
return (item.value, item.label)
|
||||
|
||||
def pack(self, value):
|
||||
value, label = value
|
||||
o = Option()
|
||||
o.value = value
|
||||
o.label = label
|
||||
return o
|
||||
|
||||
def get_xso_types(self):
|
||||
return [Option]
|
||||
|
||||
|
||||
class FieldType(enum.Enum):
|
||||
"""
|
||||
Enumeration containing the field types defined in :xep:`4`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`Field.values`
|
||||
for important information regarding typing, restrictions, validation
|
||||
and constraints of values in that attribute.
|
||||
|
||||
Each type has the following attributes and methods:
|
||||
|
||||
.. automethod:: allow_upcast
|
||||
|
||||
.. autoattribute:: has_options
|
||||
|
||||
.. autoattribute:: is_multivalued
|
||||
|
||||
Quotations in the following attribute descriptions are from said XEP.
|
||||
|
||||
.. attribute:: BOOLEAN
|
||||
|
||||
The ``"boolean"`` field:
|
||||
|
||||
The field enables an entity to gather or provide an either-or choice
|
||||
between two options. The default value is "false".
|
||||
|
||||
The :attr:`Field.values` sequence should contain zero or one elements.
|
||||
If it contains an element, it must be ``"0"``, ``"1"``, ``"false"``, or
|
||||
``"true"``, in accordance with the XML Schema documents.
|
||||
|
||||
.. attribute:: FIXED
|
||||
|
||||
The ``"fixed"`` field:
|
||||
|
||||
The field is intended for data description (e.g., human-readable text
|
||||
such as "section" headers) rather than data gathering or provision.
|
||||
The <value/> child SHOULD NOT contain newlines (the ``\\n`` and
|
||||
``\\r`` characters); instead an application SHOULD generate multiple
|
||||
fixed fields, each with one <value/> child.
|
||||
|
||||
As such, the :attr:`Field.values` sequence should contain exactly one
|
||||
element. :attr:`Field.desc`, :attr:`Field.label`, :attr:`Field.options`
|
||||
and :attr:`Field.var` should be set to :data:`None` or empty containers.
|
||||
|
||||
.. attribute:: HIDDEN
|
||||
|
||||
The ``"hidden"`` field:
|
||||
|
||||
The field is not shown to the form-submitting entity, but instead is
|
||||
returned with the form. The form-submitting entity SHOULD NOT modify
|
||||
the value of a hidden field, but MAY do so if such behavior is
|
||||
defined for the "using protocol".
|
||||
|
||||
This type is commonly used for the ``var="FORM_TYPE"`` field, as
|
||||
specified in :xep:`68`.
|
||||
|
||||
.. attribute:: JID_MULTI
|
||||
|
||||
The ``"jid-multi"`` field:
|
||||
|
||||
The field enables an entity to gather or provide multiple Jabber IDs.
|
||||
Each provided JID SHOULD be unique (as determined by comparison that
|
||||
includes application of the Nodeprep, Nameprep, and Resourceprep
|
||||
profiles of Stringprep as specified in XMPP Core), and duplicate JIDs
|
||||
MUST be ignored.
|
||||
|
||||
As such, the :attr:`Field.values` sequence should contain zero or more
|
||||
strings representing Jabber IDs. :attr:`Field.options` should be empty.
|
||||
|
||||
.. attribute:: JID_SINGLE
|
||||
|
||||
The ``"jid-single"`` field:
|
||||
|
||||
The field enables an entity to gather or provide a single Jabber ID.
|
||||
|
||||
As such, the :attr:`Field.values` sequence should contain zero or one
|
||||
string representing a Jabber ID. :attr:`Field.options` should be empty.
|
||||
|
||||
.. attribute:: LIST_MULTI
|
||||
|
||||
The ``"list-multi"`` field:
|
||||
|
||||
The field enables an entity to gather or provide one or more options
|
||||
from among many. A form-submitting entity chooses one or more items
|
||||
from among the options presented by the form-processing entity and
|
||||
MUST NOT insert new options. The form-submitting entity MUST NOT
|
||||
modify the order of items as received from the form-processing
|
||||
entity, since the order of items MAY be significant.
|
||||
|
||||
Thus, :attr:`Field.values` should contain a subset of the keys of the
|
||||
:class:`Field.options` dictionary.
|
||||
|
||||
.. attribute:: LIST_SINGLE
|
||||
|
||||
The ``"list-single"`` field:
|
||||
|
||||
The field enables an entity to gather or provide one option from
|
||||
among many. A form-submitting entity chooses one item from among the
|
||||
options presented by the form-processing entity and MUST NOT insert
|
||||
new options.
|
||||
|
||||
Thus, :attr:`Field.values` should contain a zero or one of the keys of
|
||||
the :class:`Field.options` dictionary.
|
||||
|
||||
.. attribute:: TEXT_MULTI
|
||||
|
||||
The ``"text-multi"`` field:
|
||||
|
||||
The field enables an entity to gather or provide multiple lines of
|
||||
text.
|
||||
|
||||
Each string in the :attr:`Field.values` attribute should be a single
|
||||
line of text. Newlines are not allowed in data forms fields (due to the
|
||||
ambiguity between ``\\r`` and ``\\n`` and combinations thereof), which
|
||||
is why the text is split on the line endings.
|
||||
|
||||
.. attribute:: TEXT_PRIVATE
|
||||
|
||||
The ``"text-private"`` field:
|
||||
|
||||
The field enables an entity to gather or provide a single line or
|
||||
word of text, which shall be obscured in an interface (e.g., with
|
||||
multiple instances of the asterisk character).
|
||||
|
||||
The :attr:`Field.values` attribute should contain zero or one string
|
||||
without any newlines.
|
||||
|
||||
.. attribute:: TEXT_SINGLE
|
||||
|
||||
The ``"text-single"`` field:
|
||||
|
||||
The field enables an entity to gather or provide a single line or
|
||||
word of text, which may be shown in an interface. This field type is
|
||||
the default and MUST be assumed if a form-submitting entity receives
|
||||
a field type it does not understand.
|
||||
|
||||
The :attr:`Field.values` attribute should contain zero or one string
|
||||
without any newlines.
|
||||
|
||||
"""
|
||||
|
||||
FIXED = "fixed"
|
||||
HIDDEN = "hidden"
|
||||
BOOLEAN = "boolean"
|
||||
TEXT_SINGLE = "text-single"
|
||||
TEXT_MULTI = "text-multi"
|
||||
TEXT_PRIVATE = "text-private"
|
||||
LIST_SINGLE = "list-single"
|
||||
LIST_MULTI = "list-multi"
|
||||
JID_SINGLE = "jid-single"
|
||||
JID_MULTI = "jid-multi"
|
||||
|
||||
@property
|
||||
def has_options(self):
|
||||
"""
|
||||
true for the ``list-`` field types, false otherwise.
|
||||
"""
|
||||
return self.value.startswith("list-")
|
||||
|
||||
@property
|
||||
def is_multivalued(self):
|
||||
"""
|
||||
true for the ``-multi`` field types, false otherwise.
|
||||
"""
|
||||
return self.value.endswith("-multi")
|
||||
|
||||
def allow_upcast(self, to):
|
||||
"""
|
||||
Return true if the field type may be upcast to the other field type
|
||||
`to`.
|
||||
|
||||
This relation specifies when it is safe to transfer data from this
|
||||
field type to the given other field type `to`.
|
||||
|
||||
This is the case if any of the following holds true:
|
||||
|
||||
* `to` is equal to this type
|
||||
* this type is :attr:`TEXT_SINGLE` and `to` is :attr:`TEXT_PRIVATE`
|
||||
"""
|
||||
|
||||
if self == to:
|
||||
return True
|
||||
if self == FieldType.TEXT_SINGLE and to == FieldType.TEXT_PRIVATE:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Field(xso.XSO):
|
||||
"""
|
||||
Represent a single field in a Data Form.
|
||||
|
||||
:param type_: Field type, must be one of the valid field types specified in
|
||||
:xep:`4`.
|
||||
:type type_: :class:`FieldType`
|
||||
:param options: A mapping of values to labels defining the options in a
|
||||
``list-*`` field.
|
||||
:type options: :class:`dict` mapping :class:`str` to :class:`str`
|
||||
:param values: A sequence of values currently given for the field. Having
|
||||
more than one value is only valid in ``*-multi`` fields.
|
||||
:type values: :class:`list` of :class:`str`
|
||||
:param desc: Description which can be shown in a tool-tip or similar,
|
||||
without newlines.
|
||||
:type desc: :class:`str` or :data:`None`
|
||||
:param label: Human-readable label to be shown next to the field input
|
||||
:type label: :class:`str` or :data:`None`
|
||||
:param required: Flag to indicate that the field is required
|
||||
:type required: :class:`bool`
|
||||
:param var: "ID" identifying the field uniquely inside the form. Only
|
||||
required for fields carrying a meaning (thus, not for
|
||||
``fixed``).
|
||||
:type var: :class:`str` or :data:`None`
|
||||
|
||||
The semantics of a :class:`Field` are different depending on where it
|
||||
occurs: in a :class:`Data`, it is a form field to be filled in, in a
|
||||
:class:`Item` it is a cell of a row and in a :class:`Reported` it
|
||||
represents a column header.
|
||||
|
||||
.. attribute:: required
|
||||
|
||||
A boolean flag indicating whether the field is required.
|
||||
|
||||
If true, the XML serialisation will contain the corresponding
|
||||
``<required/>`` tag.
|
||||
|
||||
.. attribute:: desc
|
||||
|
||||
Single line of description for the field. This attribute represents the
|
||||
``<desc/>`` element from :xep:`4`.
|
||||
|
||||
.. attribute:: values
|
||||
|
||||
A sequence of strings representing the ``<value/>`` elements of the
|
||||
field, one string for each value.
|
||||
|
||||
.. note::
|
||||
|
||||
Since the requirements on the sequence of strings in :attr:`values`
|
||||
change depending on the :attr:`type_` attribute, validation and type
|
||||
conversion on assignment is very lax. The attribute accepts all
|
||||
sequences of strings, even if the field is for example a
|
||||
:attr:`FieldType.BOOLEAN` field, which allows for at most one string
|
||||
of a well-defined format (see the documentation there for the
|
||||
details).
|
||||
|
||||
This makes it easy to inadvertendly generate invalid forms, which is
|
||||
why you should be using :class:`Form` subclasses when accessing forms
|
||||
from within normal code and some other, generic mechanism taking care
|
||||
of these details when showing forms in a UI framework to users. Note
|
||||
that devising such a mechanism is out of scope for :mod:`aioxmpp`, as
|
||||
every UI framework has different requirements.
|
||||
|
||||
.. attribute:: options
|
||||
|
||||
A dictionary mapping values to human-readable labels, representing the
|
||||
``<option/>`` elements of the field.
|
||||
|
||||
.. attribute:: var
|
||||
|
||||
The uniquely identifying string of the (valued, that is,
|
||||
non-:attr:`FieldType.FIXED` field). Represents the ``var`` attribute of
|
||||
the field.
|
||||
|
||||
.. attribute:: type_
|
||||
|
||||
The type of the field. The :attr:`type_` must be a :class:`FieldType`
|
||||
enumeration value and determines restrictions and constraints on other
|
||||
attributes. See the :class:`FieldType` enumeration and :xep:`4` for
|
||||
details.
|
||||
|
||||
.. attribute:: label
|
||||
|
||||
The human-readable label for the field, representing the ``label``
|
||||
attribute of the field. May be :data:`None` if the label is omitted.
|
||||
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0004_data, "field")
|
||||
|
||||
required = xso.ChildFlag(
|
||||
(namespaces.xep0004_data, "required"),
|
||||
)
|
||||
|
||||
desc = xso.ChildText(
|
||||
(namespaces.xep0004_data, "desc"),
|
||||
default=None
|
||||
)
|
||||
|
||||
values = xso.ChildValueList(
|
||||
type_=ValueElement()
|
||||
)
|
||||
|
||||
options = xso.ChildValueMap(
|
||||
type_=OptionElement(),
|
||||
mapping_type=collections.OrderedDict,
|
||||
)
|
||||
|
||||
var = xso.Attr(
|
||||
(None, "var"),
|
||||
default=None
|
||||
)
|
||||
|
||||
type_ = xso.Attr(
|
||||
(None, "type"),
|
||||
type_=xso.EnumCDataType(
|
||||
FieldType,
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
|
||||
label = xso.Attr(
|
||||
(None, "label"),
|
||||
default=None
|
||||
)
|
||||
|
||||
def __init__(self, *,
|
||||
type_=FieldType.TEXT_SINGLE,
|
||||
options={},
|
||||
values=[],
|
||||
desc=None,
|
||||
label=None,
|
||||
required=False,
|
||||
var=None):
|
||||
super().__init__()
|
||||
self.type_ = type_
|
||||
self.options.update(options)
|
||||
self.values[:] = values
|
||||
self.desc = desc
|
||||
self.label = label
|
||||
self.required = required
|
||||
self.var = var
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
if self.type_ != FieldType.FIXED and not self.var:
|
||||
raise ValueError("missing attribute var")
|
||||
|
||||
if self.type_ is not None:
|
||||
if not self.type_.has_options and self.options:
|
||||
raise ValueError("unexpected option on non-list field")
|
||||
|
||||
if not self.type_.is_multivalued and len(self.values) > 1:
|
||||
raise ValueError("too many values on non-multi field")
|
||||
|
||||
values_list = [opt for opt in self.options.values() if opt is not None]
|
||||
values_set = set(values_list)
|
||||
|
||||
if len(values_list) != len(values_set):
|
||||
raise ValueError("duplicate option label in {}".format(
|
||||
values_list
|
||||
))
|
||||
|
||||
|
||||
class AbstractItem(xso.XSO):
|
||||
fields = xso.ChildList([Field])
|
||||
|
||||
|
||||
class Item(AbstractItem):
|
||||
"""
|
||||
A single row in a report :class:`Data` object.
|
||||
|
||||
.. attribute:: fields
|
||||
|
||||
A sequence of :class:`Field` objects representing the cells of the row.
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0004_data, "item")
|
||||
|
||||
|
||||
class Reported(AbstractItem):
|
||||
"""
|
||||
The table heading of a report :class:`Data` object.
|
||||
|
||||
.. attribute:: fields
|
||||
|
||||
A sequence of :class:`Field` objects representing the columns of the
|
||||
report or table.
|
||||
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0004_data, "reported")
|
||||
|
||||
|
||||
class Instructions(xso.XSO):
|
||||
TAG = (namespaces.xep0004_data, "instructions")
|
||||
|
||||
value = xso.Text(default="")
|
||||
|
||||
|
||||
class InstructionsElement(xso.AbstractElementType):
|
||||
def unpack(self, item):
|
||||
return item.value
|
||||
|
||||
def pack(self, value):
|
||||
v = Instructions()
|
||||
v.value = value
|
||||
return v
|
||||
|
||||
def get_xso_types(self):
|
||||
return [Instructions]
|
||||
|
||||
|
||||
class DataType(enum.Enum):
|
||||
"""
|
||||
Enumeration containing the :class:`Data` types defined in :xep:`4`.
|
||||
|
||||
Quotations in the following attribute descriptions are from :xep:`4`.
|
||||
|
||||
.. attribute:: FORM
|
||||
|
||||
The ``"form"`` type:
|
||||
|
||||
The form-processing entity is asking the form-submitting entity to
|
||||
complete a form.
|
||||
|
||||
.. attribute:: SUBMIT
|
||||
|
||||
The ``"submit"`` type:
|
||||
|
||||
The form-submitting entity is submitting data to the form-processing
|
||||
entity. The submission MAY include fields that were not provided in
|
||||
the empty form, but the form-processing entity MUST ignore any fields
|
||||
that it does not understand.
|
||||
|
||||
.. attribute:: CANCEL
|
||||
|
||||
The ``"cancel"`` type:
|
||||
|
||||
The form-submitting entity has cancelled submission of data to the
|
||||
form-processing entity.
|
||||
|
||||
.. attribute:: RESULT
|
||||
|
||||
The ``"result"`` type:
|
||||
|
||||
The form-processing entity is returning data (e.g., search results) to
|
||||
the form-submitting entity, or the data is a generic data set.
|
||||
"""
|
||||
FORM = "form"
|
||||
SUBMIT = "submit"
|
||||
RESULT = "result"
|
||||
CANCEL = "cancel"
|
||||
|
||||
|
||||
class Data(AbstractItem):
|
||||
"""
|
||||
A :xep:`4` ``x`` element, that is, a Data Form.
|
||||
|
||||
:param type_: Initial value for the :attr:`type_` attribute.
|
||||
|
||||
.. attribute:: type_
|
||||
|
||||
The ``type`` attribute of the form, represented by one of the members of
|
||||
the :class:`DataType` enumeration.
|
||||
|
||||
.. attribute:: title
|
||||
|
||||
The (optional) title of the form. Either a :class:`str` or :data:`None`.
|
||||
|
||||
.. attribute:: instructions
|
||||
|
||||
A sequence of strings which represent the instructions elements on the
|
||||
form.
|
||||
|
||||
.. attribute:: fields
|
||||
|
||||
If the :class:`Data` is a form, this is a sequence of :class:`Field`
|
||||
elements which represent the fields to be filled in.
|
||||
|
||||
This does not make sense on :attr:`.DataType.RESULT` typed objects.
|
||||
|
||||
.. attribute:: items
|
||||
|
||||
If the :class:`Data` is a table, this is a sequence of :class:`Item`
|
||||
instances which represent the table rows.
|
||||
|
||||
This only makes sense on :attr:`.DataType.RESULT` typed objects.
|
||||
|
||||
.. attribute:: reported
|
||||
|
||||
If the :class:`Data` is a table, this is a :class:`Reported` object
|
||||
representing the table header.
|
||||
|
||||
This only makes sense on :attr:`.DataType.RESULT` typed objects.
|
||||
|
||||
.. automethod:: get_form_type
|
||||
"""
|
||||
|
||||
TAG = (namespaces.xep0004_data, "x")
|
||||
|
||||
type_ = xso.Attr(
|
||||
"type",
|
||||
type_=xso.EnumCDataType(DataType)
|
||||
)
|
||||
|
||||
title = xso.ChildText(
|
||||
(namespaces.xep0004_data, "title"),
|
||||
default=None,
|
||||
)
|
||||
|
||||
instructions = xso.ChildValueList(
|
||||
type_=InstructionsElement()
|
||||
)
|
||||
|
||||
items = xso.ChildList([Item])
|
||||
|
||||
reported = xso.Child([Reported], required=False)
|
||||
|
||||
def __init__(self, type_):
|
||||
super().__init__()
|
||||
self.type_ = type_
|
||||
|
||||
def _validate_result(self):
|
||||
if self.fields:
|
||||
raise ValueError("field in report result")
|
||||
|
||||
fieldvars = {field.var for field in self.reported.fields}
|
||||
|
||||
if not fieldvars:
|
||||
raise ValueError("empty report header")
|
||||
|
||||
for item in self.items:
|
||||
itemvars = {field.var for field in item.fields}
|
||||
if itemvars != fieldvars:
|
||||
raise ValueError("field mismatch between row and header")
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
if (self.type_ != DataType.RESULT and
|
||||
(self.reported is not None or self.items)):
|
||||
raise ValueError("report in non-result")
|
||||
|
||||
if (self.type_ == DataType.RESULT and
|
||||
(self.reported is not None or self.items)):
|
||||
self._validate_result()
|
||||
|
||||
def get_form_type(self):
|
||||
"""
|
||||
Extract the ``FORM_TYPE`` from the fields.
|
||||
|
||||
:return: ``FORM_TYPE`` value or :data:`None`
|
||||
:rtype: :class:`str` or :data:`None`
|
||||
|
||||
Return :data:`None` if no well-formed ``FORM_TYPE`` field is found in
|
||||
the list of fields.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
|
||||
for field in self.fields:
|
||||
if field.var == "FORM_TYPE" and field.type_ == FieldType.HIDDEN:
|
||||
if len(field.values) != 1:
|
||||
return None
|
||||
return field.values[0]
|
||||
|
||||
|
||||
aioxmpp.Message.xep0004_data = xso.ChildList([Data])
|
||||
Reference in New Issue
Block a user