File: elements.py

package info (click to toggle)
python-xsdata 24.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,936 kB
  • sloc: python: 29,257; xml: 404; makefile: 27; sh: 6
file content (443 lines) | stat: -rw-r--r-- 13,183 bytes parent folder | download
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
import itertools
import operator
import sys
from typing import (
    Any,
    Callable,
    Dict,
    Iterator,
    List,
    Mapping,
    Optional,
    Sequence,
    Set,
    Tuple,
    Type,
)

from xsdata.formats.converter import converter
from xsdata.models.enums import NamespaceType
from xsdata.utils import collections
from xsdata.utils.namespaces import local_name, target_uri

NoneType = type(None)


class XmlType:
    """Xml node types."""

    TEXT = sys.intern("Text")
    ELEMENT = sys.intern("Element")
    ELEMENTS = sys.intern("Elements")
    WILDCARD = sys.intern("Wildcard")
    ATTRIBUTE = sys.intern("Attribute")
    ATTRIBUTES = sys.intern("Attributes")
    IGNORE = sys.intern("Ignore")


class MetaMixin:
    """Use this mixin for unit tests only!!!"""

    __slots__: Tuple[str, ...] = ()

    def __eq__(self, other: Any) -> bool:
        return tuple(self) == tuple(other)

    def __iter__(self) -> Iterator:
        for name in self.__slots__:
            yield getattr(self, name)

    def __repr__(self) -> str:
        params = (f"{name}={getattr(self, name)!r}" for name in self.__slots__)
        return f"{self.__class__.__qualname__}({', '.join(params)})"


class XmlVar(MetaMixin):
    """
    Class field binding metadata.

    :param index: Field ordering
    :param name: Field name
    :param qname: Qualified name
    :param types: List of all the supported data types
    :param init: Include field in the constructor
    :param mixed: Field supports mixed content type values
    :param tokens: Field is derived from xs:list
    :param format: Value format information
    :param derived: Wrap parsed values with a generic type
    :param any_type: Field supports dynamic value types
    :param required: Field is mandatory
    :param nillable: Field supports nillable content
    :param sequence: Render values in sequential mode
    :param list_element: Field is a list of elements
    :param default: Field default value or factory
    :param xml_Type: Field xml type
    :param namespaces: List of the supported namespaces
    :param elements: Mapping of qname-repeatable elements
    :param wildcards: List of repeatable wildcards
    :param wrapper: A name for the wrapper. Applies for list types only.
    """

    __slots__ = (
        "index",
        "name",
        "qname",
        "types",
        "clazz",
        "init",
        "mixed",
        "factory",
        "tokens_factory",
        "format",
        "derived",
        "any_type",
        "process_contents",
        "required",
        "nillable",
        "sequence",
        "default",
        "namespaces",
        "elements",
        "wildcards",
        "wrapper",
        # Calculated
        "tokens",
        "list_element",
        "is_text",
        "is_element",
        "is_elements",
        "is_wildcard",
        "is_attribute",
        "is_attributes",
        "namespace_matches",
        "is_clazz_union",
        "local_name",
    )

    def __init__(
        self,
        index: int,
        name: str,
        qname: str,
        types: Sequence[Type],
        clazz: Optional[Type],
        init: bool,
        mixed: bool,
        factory: Optional[Callable],
        tokens_factory: Optional[Callable],
        format: Optional[str],
        derived: bool,
        any_type: bool,
        process_contents: str,
        required: bool,
        nillable: bool,
        sequence: Optional[int],
        default: Any,
        xml_type: str,
        namespaces: Sequence[str],
        elements: Mapping[str, "XmlVar"],
        wildcards: Sequence["XmlVar"],
        wrapper: Optional[str] = None,
        **kwargs: Any,
    ):
        self.index = index
        self.name = name
        self.qname = qname
        self.types = types
        self.clazz = clazz
        self.init = init
        self.mixed = mixed
        self.tokens = tokens_factory is not None
        self.format = format
        self.derived = derived
        self.any_type = any_type
        self.process_contents = process_contents
        self.required = required
        self.nillable = nillable
        self.sequence = sequence
        self.list_element = factory in (list, tuple)
        self.default = default
        self.namespaces = namespaces
        self.elements = elements
        self.wildcards = wildcards
        self.wrapper = wrapper

        self.factory = factory
        self.tokens_factory = tokens_factory

        self.namespace_matches: Optional[Dict[str, bool]] = None

        self.is_clazz_union = self.clazz and len(types) > 1
        self.local_name = local_name(qname)

        self.is_text = False
        self.is_element = False
        self.is_elements = False
        self.is_wildcard = False
        self.is_attribute = False
        self.is_attributes = False

        if xml_type == XmlType.ELEMENTS:
            self.is_elements = True
        elif xml_type == XmlType.ELEMENT or self.clazz:
            self.is_element = True
        elif xml_type == XmlType.ATTRIBUTE:
            self.is_attribute = True
        elif xml_type == XmlType.ATTRIBUTES:
            self.is_attributes = True
        elif xml_type == XmlType.WILDCARD:
            self.is_wildcard = True
        else:
            self.is_text = True

    @property
    def element_types(self) -> Set[Type]:
        return {tp for element in self.elements.values() for tp in element.types}

    def find_choice(self, qname: str) -> Optional["XmlVar"]:
        """Match and return a choice field by its qualified name."""
        match = self.elements.get(qname)
        return match or find_by_namespace(self.wildcards, qname)

    def find_value_choice(self, value: Any, is_class: bool) -> Optional["XmlVar"]:
        """
        Match and return a choice field that matches the given value.

        Cases:
            - value is none or empty tokens list: look for a nillable choice
            - value is a dataclass: look for exact type or a subclass
            - value is primitive: test value against the converter
        """
        is_tokens = collections.is_array(value)
        if value is None or (not value and is_tokens):
            return self.find_nillable_choice(is_tokens)

        if is_class:
            return self.find_clazz_choice(type(value))

        return self.find_primitive_choice(value, is_tokens)

    def find_nillable_choice(self, is_tokens: bool) -> Optional["XmlVar"]:
        return collections.first(
            element
            for element in self.elements.values()
            if element.nillable and is_tokens == element.tokens
        )

    def find_clazz_choice(self, tp: Type) -> Optional["XmlVar"]:
        derived = None
        for element in self.elements.values():
            if element.clazz:
                if tp in element.types:
                    return element

                if derived is None and any(issubclass(tp, t) for t in element.types):
                    derived = element

        return derived

    def find_primitive_choice(self, value: Any, is_tokens: bool) -> Optional["XmlVar"]:
        tp = type(value) if not is_tokens else type(value[0])
        for element in self.elements.values():
            if (element.any_type or element.clazz) or element.tokens != is_tokens:
                continue

            if tp in element.types:
                return element

            if is_tokens and all(converter.test(val, element.types) for val in value):
                return element

            if converter.test(value, element.types):
                return element

        return None

    def is_optional(self, value: Any) -> bool:
        """Return whether this var instance is not required and the given value
        matches the default one."""
        if self.required:
            return False

        if callable(self.default):
            return self.default() == value
        return self.default == value

    def match_namespace(self, qname: str) -> bool:
        """Match the given qname to the wildcard allowed namespaces."""
        if self.namespace_matches is None:
            self.namespace_matches = {}

        matches = self.namespace_matches.get(qname)
        if matches is None:
            matches = self._match_namespace(qname)
            self.namespace_matches[qname] = matches

        return matches

    def _match_namespace(self, qname: str) -> bool:
        uri = target_uri(qname)
        if not self.namespaces and uri is None:
            return True

        for check in self.namespaces:
            if (
                (not check and uri is None)
                or check == uri
                or check == NamespaceType.ANY_NS
                or (check and check[0] == "!" and check[1:] != uri)
            ):
                return True

        return False


get_index = operator.attrgetter("index")


class XmlMeta(MetaMixin):
    """
    Class binding metadata.

    :param clazz: The dataclass type
    :param qname: The namespace qualified name.
    :param target_qname: The target namespace qualified name.
    :param nillable: Specifies whether an explicit empty value can be
        assigned.
    :param mixed_content: Has a wildcard with mixed flag enabled
    :param text: Text var
    :param choices: List of compound vars
    :param elements: Mapping of qname-element vars
    :param wildcards: List of wildcard vars
    :param attributes: Mapping of qname-attribute vars
    :param any_attributes: List of wildcard attributes vars
    """

    __slots__ = (
        "clazz",
        "qname",
        "target_qname",
        "nillable",
        "text",
        "choices",
        "elements",
        "wildcards",
        "attributes",
        "any_attributes",
        "wrappers",
        # Calculated
        "namespace",
        "mixed_content",
    )

    def __init__(
        self,
        clazz: Type,
        qname: str,
        target_qname: Optional[str],
        nillable: bool,
        text: Optional[XmlVar],
        choices: Sequence[XmlVar],
        elements: Mapping[str, Sequence[XmlVar]],
        wildcards: Sequence[XmlVar],
        attributes: Mapping[str, XmlVar],
        any_attributes: Sequence[XmlVar],
        wrappers: Mapping[str, Sequence[XmlVar]],
        **kwargs: Any,
    ):
        self.clazz = clazz
        self.qname = qname
        self.namespace = target_uri(qname)
        self.target_qname = target_qname
        self.nillable = nillable
        self.text = text
        self.choices = choices
        self.elements = elements
        self.wildcards = wildcards
        self.attributes = attributes
        self.any_attributes = any_attributes
        self.mixed_content = any(wildcard.mixed for wildcard in self.wildcards)
        self.wrappers = wrappers

    @property
    def element_types(self) -> Set[Type]:
        return {
            tp
            for elements in self.elements.values()
            for element in elements
            for tp in element.types
        }

    def get_element_vars(self) -> List[XmlVar]:
        result = list(
            itertools.chain(self.wildcards, self.choices, *self.elements.values())
        )
        if self.text:
            result.append(self.text)

        return sorted(result, key=get_index)

    def get_attribute_vars(self) -> List[XmlVar]:
        result = itertools.chain(self.any_attributes, self.attributes.values())
        return sorted(result, key=get_index)

    def get_all_vars(self) -> List[XmlVar]:
        result = list(
            itertools.chain(
                self.wildcards,
                self.choices,
                self.any_attributes,
                self.attributes.values(),
                *self.elements.values(),
            )
        )
        if self.text:
            result.append(self.text)

        return sorted(result, key=get_index)

    def find_attribute(self, qname: str) -> Optional[XmlVar]:
        return self.attributes.get(qname)

    def find_any_attributes(self, qname: str) -> Optional[XmlVar]:
        return find_by_namespace(self.any_attributes, qname)

    def find_wildcard(self, qname: str) -> Optional[XmlVar]:
        """Match the given qualified name to a wildcard and optionally to one
        of its choice elements."""
        wildcard = find_by_namespace(self.wildcards, qname)

        if wildcard and wildcard.elements:
            choice = wildcard.find_choice(qname)
            if choice:
                return choice

        return wildcard

    def find_any_wildcard(self) -> Optional[XmlVar]:
        if self.wildcards:
            return self.wildcards[0]

        return None

    def find_children(self, qname: str) -> Iterator[XmlVar]:
        elements = self.elements.get(qname)
        if elements:
            yield from elements

        for choice in self.choices:
            match = choice.find_choice(qname)
            if match:
                yield match

        chd = self.find_wildcard(qname)
        if chd:
            yield chd


def find_by_namespace(xml_vars: Sequence[XmlVar], qname: str) -> Optional[XmlVar]:
    for xml_var in xml_vars:
        if xml_var.match_namespace(qname):
            return xml_var

    return None