Skip to content

create_compound_fields

xsdata.codegen.handlers.create_compound_fields

CreateCompoundFields

Bases: RelativeHandlerInterface

Process attrs that belong in the same choice.

Parameters:

Name Type Description Default
container ContainerInterface

The class container instance

required

Attributes:

Name Type Description
config

The compound fields configuration

Source code in xsdata/codegen/handlers/create_compound_fields.py
 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
class CreateCompoundFields(RelativeHandlerInterface):
    """Process attrs that belong in the same choice.

    Args:
        container: The class container instance

    Attributes:
        config: The compound fields configuration
    """

    __slots__ = "config"

    def __init__(self, container: ContainerInterface):
        super().__init__(container)
        self.config = container.config.output.compound_fields

    def process(self, target: Class):
        """Process the attrs of class that belong in the same choice.

        If the compound fields configuration is enabled replace the
        attrs with a compound attr, otherwise recalculate the min
        occurs restriction for each of them.

        Args:
            target: The target class instance
        """
        groups = group_by(target.attrs, get_restriction_choice)
        for choice, attrs in groups.items():
            if choice and len(attrs) > 1:
                if self.config.enabled:
                    self.group_fields(target, attrs)
                else:
                    self.calculate_choice_min_occurs(attrs)

    @classmethod
    def calculate_choice_min_occurs(cls, attrs: List[Attr]):
        """Calculate the min occurs restriction of the attrs.

        If that attr has a path that includes a xs:choice
        with min occurs less than 1, update the attr min
        occurs restriction to zero, effectively marking
        it as optional.

        Args:
            attrs: A list of attrs that belong in the same xs:choice.
        """
        for attr in attrs:
            for path in attr.restrictions.path:
                name, index, mi, ma = path
                if name == CHOICE and mi <= 1:
                    attr.restrictions.min_occurs = 0

    @classmethod
    def update_counters(cls, attr: Attr, counters: Dict):
        """Update the counters dictionary with the attr min/max restrictions.

        This method builds a nested counters mapping per path.

        Example:
            {
                "min": [0, 1, 2]
                "max": [3, 4, 5],
                ("c", 1, 0, 1): {
                    "min": [6, 7, 8],
                    "max": [9, 10, 11],
                    ("g", 2, 0, 1): { ... }
                }
            }

        Args:
            attr: The source attr instance
            counters: The nested counters instance to update
        """
        started = False
        choice = attr.restrictions.choice
        for path in attr.restrictions.path:
            name, index, mi, ma = path
            if not started and name != CHOICE and index != choice:
                continue

            started = True
            if path not in counters:
                counters[path] = {"min": [], "max": []}
            counters = counters[path]

            if mi <= 1:
                attr.restrictions.min_occurs = 0

        counters["min"].append(attr.restrictions.min_occurs)
        counters["max"].append(attr.restrictions.max_occurs)

    def group_fields(self, target: Class, attrs: List[Attr]):
        """Group attributes into a new compound field.

        Args:
            target: The target class instance
            attrs: A list of attrs that belong to the same choice
        """
        pos = target.attrs.index(attrs[0])
        choice = attrs[0].restrictions.choice

        assert choice is not None

        names = []
        substitutions = []
        choices = []
        counters: Dict = {"min": [], "max": []}

        for attr in attrs:
            ClassUtils.remove_attribute(target, attr)
            names.append(attr.local_name)
            substitutions.append(attr.substitution)

            choices.append(self.build_attr_choice(attr))
            self.update_counters(attr, counters)

        min_occurs, max_occurs = self.sum_counters(counters)
        name = self.choose_name(target, names, list(filter(None, substitutions)))

        compound_attr = Attr(
            name=name,
            index=0,
            types=[],
            tag=Tag.CHOICE,
            restrictions=Restrictions(
                min_occurs=sum(min_occurs),
                max_occurs=max(max_occurs) if choice > 0 else sum(max_occurs),
            ),
            choices=choices,
        )
        target.attrs.insert(pos, compound_attr)

    def sum_counters(self, counters: Dict) -> Tuple[List[int], List[int]]:
        """Sum the min/max occurrences for the compound attr.

        Args:
            counters: The counters map of all the choice attrs
        """
        min_occurs = counters.pop("min", [])
        max_occurs = counters.pop("max", [])

        for path, counter in counters.items():
            mi, ma = self.sum_counters(counter)

            if path[0] == "c":
                min_occurs.append(min(mi))
                max_occurs.append(max(ma))
            else:
                min_occurs.append(sum(mi))
                max_occurs.append(sum(ma))

        return min_occurs, max_occurs

    def choose_name(
        self,
        target: Class,
        names: List[str],
        substitutions: List[str],
    ) -> str:
        """Choose a name for the compound attr.

        If the attrs were placed in the same choice because of a single
        substitution group and the configuration `use_substitution_groups`
        is enabled, the group name will be used for the compound attr.

        Otherwise, the name will be the concatenation of the names of the
        attrs, if the length of the attrs is less than the `max_name_parts`
        config and the `force_default_name` is false,
        e.g. `hat_Or_bat_Or_bar`

        Otherwise, the name will the default from the config,
        e.g. `choice`

        If there are any name collisions with other class attrs, the system
        will add an integer suffix till the name is unique in the class
        e.g. `choice_1`, `choice_2`, `choice_3`

        Args:
            target: The target class
            names: The list of the attr names
            substitutions: The list of the substitution group names of the attrs
        """
        if self.config.use_substitution_groups and len(names) == len(substitutions):
            names = substitutions

        names = collections.unique_sequence(names)
        if self.config.force_default_name or len(names) > self.config.max_name_parts:
            name = self.config.default_name
        else:
            name = "_Or_".join(names)

        reserved = self.build_reserved_names(target, names)
        return ClassUtils.unique_name(name, reserved)

    def build_reserved_names(self, target: Class, names: List[str]) -> Set[str]:
        """Build a set of reserved attr names.

        The method will also check parent attrs.

        Args:
            target: The target class instance
            names: The potential names for the compound attr
        """
        names_counter = Counter(names)
        all_attrs = self.base_attrs(target)
        all_attrs.extend(target.attrs)

        return {
            attr.slug
            for attr in all_attrs
            if attr.xml_type != XmlType.ELEMENTS
            or Counter([x.local_name for x in attr.choices]) != names_counter
        }

    @classmethod
    def build_attr_choice(cls, attr: Attr) -> Attr:
        """Build the choice attr from a normal attr.

        Steps:
            - Clone the original attr restrictions
            - Reset the min/max occurs
            - Remove the sequence reference
            - Build the new attr and maintain the basic attributes

        Args:
            attr: The source attr instance to use

        Returns:
            The new choice attr for the compound attr.
        """
        restrictions = attr.restrictions.clone(min_occurs=0, sequence=None)

        return Attr(
            name=attr.name,
            local_name=attr.local_name,
            namespace=attr.namespace,
            types=[x.clone() for x in attr.types],
            tag=attr.tag,
            help=attr.help,
            restrictions=restrictions,
        )

process(target)

Process the attrs of class that belong in the same choice.

If the compound fields configuration is enabled replace the attrs with a compound attr, otherwise recalculate the min occurs restriction for each of them.

Parameters:

Name Type Description Default
target Class

The target class instance

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def process(self, target: Class):
    """Process the attrs of class that belong in the same choice.

    If the compound fields configuration is enabled replace the
    attrs with a compound attr, otherwise recalculate the min
    occurs restriction for each of them.

    Args:
        target: The target class instance
    """
    groups = group_by(target.attrs, get_restriction_choice)
    for choice, attrs in groups.items():
        if choice and len(attrs) > 1:
            if self.config.enabled:
                self.group_fields(target, attrs)
            else:
                self.calculate_choice_min_occurs(attrs)

calculate_choice_min_occurs(attrs) classmethod

Calculate the min occurs restriction of the attrs.

If that attr has a path that includes a xs:choice with min occurs less than 1, update the attr min occurs restriction to zero, effectively marking it as optional.

Parameters:

Name Type Description Default
attrs List[Attr]

A list of attrs that belong in the same xs:choice.

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@classmethod
def calculate_choice_min_occurs(cls, attrs: List[Attr]):
    """Calculate the min occurs restriction of the attrs.

    If that attr has a path that includes a xs:choice
    with min occurs less than 1, update the attr min
    occurs restriction to zero, effectively marking
    it as optional.

    Args:
        attrs: A list of attrs that belong in the same xs:choice.
    """
    for attr in attrs:
        for path in attr.restrictions.path:
            name, index, mi, ma = path
            if name == CHOICE and mi <= 1:
                attr.restrictions.min_occurs = 0

update_counters(attr, counters) classmethod

Update the counters dictionary with the attr min/max restrictions.

This method builds a nested counters mapping per path.

Example

{ "min": [0, 1, 2] "max": [3, 4, 5], ("c", 1, 0, 1): { "min": [6, 7, 8], "max": [9, 10, 11], ("g", 2, 0, 1): { ... } } }

Parameters:

Name Type Description Default
attr Attr

The source attr instance

required
counters Dict

The nested counters instance to update

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
 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
@classmethod
def update_counters(cls, attr: Attr, counters: Dict):
    """Update the counters dictionary with the attr min/max restrictions.

    This method builds a nested counters mapping per path.

    Example:
        {
            "min": [0, 1, 2]
            "max": [3, 4, 5],
            ("c", 1, 0, 1): {
                "min": [6, 7, 8],
                "max": [9, 10, 11],
                ("g", 2, 0, 1): { ... }
            }
        }

    Args:
        attr: The source attr instance
        counters: The nested counters instance to update
    """
    started = False
    choice = attr.restrictions.choice
    for path in attr.restrictions.path:
        name, index, mi, ma = path
        if not started and name != CHOICE and index != choice:
            continue

        started = True
        if path not in counters:
            counters[path] = {"min": [], "max": []}
        counters = counters[path]

        if mi <= 1:
            attr.restrictions.min_occurs = 0

    counters["min"].append(attr.restrictions.min_occurs)
    counters["max"].append(attr.restrictions.max_occurs)

group_fields(target, attrs)

Group attributes into a new compound field.

Parameters:

Name Type Description Default
target Class

The target class instance

required
attrs List[Attr]

A list of attrs that belong to the same choice

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
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
def group_fields(self, target: Class, attrs: List[Attr]):
    """Group attributes into a new compound field.

    Args:
        target: The target class instance
        attrs: A list of attrs that belong to the same choice
    """
    pos = target.attrs.index(attrs[0])
    choice = attrs[0].restrictions.choice

    assert choice is not None

    names = []
    substitutions = []
    choices = []
    counters: Dict = {"min": [], "max": []}

    for attr in attrs:
        ClassUtils.remove_attribute(target, attr)
        names.append(attr.local_name)
        substitutions.append(attr.substitution)

        choices.append(self.build_attr_choice(attr))
        self.update_counters(attr, counters)

    min_occurs, max_occurs = self.sum_counters(counters)
    name = self.choose_name(target, names, list(filter(None, substitutions)))

    compound_attr = Attr(
        name=name,
        index=0,
        types=[],
        tag=Tag.CHOICE,
        restrictions=Restrictions(
            min_occurs=sum(min_occurs),
            max_occurs=max(max_occurs) if choice > 0 else sum(max_occurs),
        ),
        choices=choices,
    )
    target.attrs.insert(pos, compound_attr)

sum_counters(counters)

Sum the min/max occurrences for the compound attr.

Parameters:

Name Type Description Default
counters Dict

The counters map of all the choice attrs

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def sum_counters(self, counters: Dict) -> Tuple[List[int], List[int]]:
    """Sum the min/max occurrences for the compound attr.

    Args:
        counters: The counters map of all the choice attrs
    """
    min_occurs = counters.pop("min", [])
    max_occurs = counters.pop("max", [])

    for path, counter in counters.items():
        mi, ma = self.sum_counters(counter)

        if path[0] == "c":
            min_occurs.append(min(mi))
            max_occurs.append(max(ma))
        else:
            min_occurs.append(sum(mi))
            max_occurs.append(sum(ma))

    return min_occurs, max_occurs

choose_name(target, names, substitutions)

Choose a name for the compound attr.

If the attrs were placed in the same choice because of a single substitution group and the configuration use_substitution_groups is enabled, the group name will be used for the compound attr.

Otherwise, the name will be the concatenation of the names of the attrs, if the length of the attrs is less than the max_name_parts config and the force_default_name is false, e.g. hat_Or_bat_Or_bar

Otherwise, the name will the default from the config, e.g. choice

If there are any name collisions with other class attrs, the system will add an integer suffix till the name is unique in the class e.g. choice_1, choice_2, choice_3

Parameters:

Name Type Description Default
target Class

The target class

required
names List[str]

The list of the attr names

required
substitutions List[str]

The list of the substitution group names of the attrs

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
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
def choose_name(
    self,
    target: Class,
    names: List[str],
    substitutions: List[str],
) -> str:
    """Choose a name for the compound attr.

    If the attrs were placed in the same choice because of a single
    substitution group and the configuration `use_substitution_groups`
    is enabled, the group name will be used for the compound attr.

    Otherwise, the name will be the concatenation of the names of the
    attrs, if the length of the attrs is less than the `max_name_parts`
    config and the `force_default_name` is false,
    e.g. `hat_Or_bat_Or_bar`

    Otherwise, the name will the default from the config,
    e.g. `choice`

    If there are any name collisions with other class attrs, the system
    will add an integer suffix till the name is unique in the class
    e.g. `choice_1`, `choice_2`, `choice_3`

    Args:
        target: The target class
        names: The list of the attr names
        substitutions: The list of the substitution group names of the attrs
    """
    if self.config.use_substitution_groups and len(names) == len(substitutions):
        names = substitutions

    names = collections.unique_sequence(names)
    if self.config.force_default_name or len(names) > self.config.max_name_parts:
        name = self.config.default_name
    else:
        name = "_Or_".join(names)

    reserved = self.build_reserved_names(target, names)
    return ClassUtils.unique_name(name, reserved)

build_reserved_names(target, names)

Build a set of reserved attr names.

The method will also check parent attrs.

Parameters:

Name Type Description Default
target Class

The target class instance

required
names List[str]

The potential names for the compound attr

required
Source code in xsdata/codegen/handlers/create_compound_fields.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def build_reserved_names(self, target: Class, names: List[str]) -> Set[str]:
    """Build a set of reserved attr names.

    The method will also check parent attrs.

    Args:
        target: The target class instance
        names: The potential names for the compound attr
    """
    names_counter = Counter(names)
    all_attrs = self.base_attrs(target)
    all_attrs.extend(target.attrs)

    return {
        attr.slug
        for attr in all_attrs
        if attr.xml_type != XmlType.ELEMENTS
        or Counter([x.local_name for x in attr.choices]) != names_counter
    }

build_attr_choice(attr) classmethod

Build the choice attr from a normal attr.

Steps
  • Clone the original attr restrictions
  • Reset the min/max occurs
  • Remove the sequence reference
  • Build the new attr and maintain the basic attributes

Parameters:

Name Type Description Default
attr Attr

The source attr instance to use

required

Returns:

Type Description
Attr

The new choice attr for the compound attr.

Source code in xsdata/codegen/handlers/create_compound_fields.py
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
@classmethod
def build_attr_choice(cls, attr: Attr) -> Attr:
    """Build the choice attr from a normal attr.

    Steps:
        - Clone the original attr restrictions
        - Reset the min/max occurs
        - Remove the sequence reference
        - Build the new attr and maintain the basic attributes

    Args:
        attr: The source attr instance to use

    Returns:
        The new choice attr for the compound attr.
    """
    restrictions = attr.restrictions.clone(min_occurs=0, sequence=None)

    return Attr(
        name=attr.name,
        local_name=attr.local_name,
        namespace=attr.namespace,
        types=[x.clone() for x in attr.types],
        tag=attr.tag,
        help=attr.help,
        restrictions=restrictions,
    )