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 """