File: sanitize_attributes_default_value.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 (196 lines) | stat: -rw-r--r-- 6,361 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
from xsdata.codegen.mixins import RelativeHandlerInterface
from xsdata.codegen.models import Attr, AttrType, Class
from xsdata.formats.converter import converter
from xsdata.logger import logger
from xsdata.models.enums import DataType


class SanitizeAttributesDefaultValue(RelativeHandlerInterface):
    """
    Sanitize attributes default values.

    Cases:
        1. Ignore enumerations.
        2. List fields can not have a default value
        3. Optional choice/sequence fields can not have a default value
        4. xsi:type fields are ignored, mark them as optional
        5. Convert string literal default value for enum fields.
    """

    __slots__ = ()

    def process(self, target: Class):
        for attr in target.attrs:
            self.process_attribute(target, attr)

            for choice in attr.choices:
                self.process_attribute(target, choice)

    def process_attribute(self, target: Class, attr: Attr):
        if self.should_reset_required(attr):
            attr.restrictions.min_occurs = 0

        if self.should_reset_default(attr):
            attr.fixed = False
            attr.default = None

        if attr.default is not None:
            self.process_types(target, attr)
        elif attr.xml_type is None and str in attr.native_types:
            # String text nodes get an empty string as default!
            attr.default = ""

    def process_types(self, target: Class, attr: Attr):
        if self.is_valid_external_value(target, attr):
            return

        if self.is_valid_native_value(target, attr):
            return

        logger.warning(
            "Failed to match %s.%s default value `%s` to one of %s",
            target.name,
            attr.local_name,
            attr.default,
            [tp.qname for tp in attr.types],
        )

        self.reset_attribute_types(attr)

    def is_valid_external_value(self, target: Class, attr: Attr) -> bool:
        """Return whether the default value of the given attr can be mapped to
        user defined type like an enumeration or an inner complex content
        class."""

        for tp in attr.user_types:
            source = self.find_type(target, tp)
            if self.is_valid_inner_type(source, attr, tp):
                return True

            if self.is_valid_enum_type(source, attr):
                return True

        return False

    def find_type(self, target: Class, attr_type: AttrType) -> Class:
        if attr_type.forward:
            return self.container.find_inner(target, attr_type.qname)

        return self.container.first(attr_type.qname)

    @classmethod
    def is_valid_inner_type(
        cls, source: Class, attr: Attr, attr_type: AttrType
    ) -> bool:
        """Return whether the inner class can inherit the attr default value
        and swap them as well."""
        if attr_type.forward:
            for src_attr in source.attrs:
                if src_attr.xml_type is None:
                    src_attr.default = attr.default
                    src_attr.fixed = attr.fixed
                    attr.default = None
                    attr.fixed = False
                    return True
        return False

    @classmethod
    def is_valid_enum_type(cls, source: Class, attr: Attr) -> bool:
        """
        Convert string literal default values to enumeration members
        placeholders and return result.

        The placeholders will be converted to proper references from the
        generator filters.

        Placeholder examples: Single -> @enum@qname::member_name
        Multiple -> @enum@qname::first_member@second_member
        """
        assert attr.default is not None

        value_members = {x.default: x.name for x in source.attrs}
        name = value_members.get(attr.default)
        if name:
            attr.default = f"@enum@{source.qname}::{name}"
            return True

        names = [
            value_members[token]
            for token in attr.default.split()
            if token in value_members
        ]
        if names:
            attr.default = f"@enum@{source.qname}::{'@'.join(names)}"
            return True

        return False

    @classmethod
    def is_valid_native_value(cls, target: Class, attr: Attr) -> bool:
        """
        Return whether the default value of the given attribute can be
        converted successfully to and from xml.

        The test process for enumerations and fixed value fields are
        strict, meaning the textual representation also needs to match
        the original.
        """
        assert attr.default is not None

        types = converter.sort_types(attr.native_types)
        if not types:
            return False

        tokens = attr.default.split() if attr.restrictions.tokens else [attr.default]

        if len(tokens) == 1 and attr.is_enumeration and attr.restrictions.tokens:
            attr.restrictions.tokens = False

        # Enumerations are also fixed!!!
        strict = attr.fixed

        return all(
            converter.test(
                token,
                types,
                strict=strict,
                ns_map=target.ns_map,
                format=attr.restrictions.format,
            )
            for token in tokens
        )

    @classmethod
    def should_reset_required(cls, attr: Attr) -> bool:
        """
        Return whether the min occurrences for the attr needs to be reset.

        @Todo figure out if wildcards are supposed to be optional!
        """
        return (
            not attr.is_attribute
            and attr.default is None
            and object in attr.native_types
            and not attr.is_list
        )

    @classmethod
    def should_reset_default(cls, attr: Attr) -> bool:
        """
        Return whether we should unset the default value of the attribute.

        - Default value is not set
        - Attribute is xsi:type (ignorable)
        - Attribute is part of a choice
        """
        return attr.default is not None and (
            attr.is_xsi_type
            or attr.is_list
            or (not attr.is_attribute and attr.is_optional)
        )

    @classmethod
    def reset_attribute_types(cls, attr: Attr):
        attr.types.clear()
        attr.types.append(AttrType(qname=str(DataType.STRING), native=True))
        attr.restrictions.format = None