Source code for aioxmpp.forms.form
########################################################################
# 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
[docs]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
[docs] 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
[docs] 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
"""