File: form.py

package info (click to toggle)
python-aioxmpp 0.12.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 6,152 kB
  • sloc: python: 96,969; xml: 215; makefile: 155; sh: 72
file content (476 lines) | stat: -rw-r--r-- 16,275 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
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
        """