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`

     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

    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):
            """Initialize the class."""
            super().__init__(container)
            self.config = container.config.output.compound_fields

        def process(self, target: Class) -> None:
            """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]) -> None:
            """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,
            )

#### `__init__(container)`

Initialize the class.

Source code in `xsdata/codegen/handlers/create_compound_fields.py`

    29
    30
    31
    32

    def __init__(self, container: ContainerInterface):
        """Initialize the class."""
        super().__init__(container)
        self.config = container.config.output.compound_fields

#### `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) -> None:
        """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]) -> None:
        """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,
        )