Skip to content

disambiguate_choices

xsdata.codegen.handlers.disambiguate_choices

DisambiguateChoices

Bases: RelativeHandlerInterface

Process choices with the same types and disambiguate them.

Essentially, this handler creates intermediate simple and complex types to ensure not two elements in a compound field can have the same type.

Parameters:

Name Type Description Default
container ContainerInterface

The class container instance

required

Attributes:

Name Type Description
unnest_classes

Specifies whether to create intermediate inner or outer classes.

Source code in xsdata/codegen/handlers/disambiguate_choices.py
 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
class DisambiguateChoices(RelativeHandlerInterface):
    """Process choices with the same types and disambiguate them.

    Essentially, this handler creates intermediate simple and complex
    types to ensure not two elements in a compound field can have the
    same type.

    Args:
        container: The class container instance

    Attributes:
        unnest_classes: Specifies whether to create intermediate
            inner or outer classes.
    """

    __slots__ = "unnest_classes"

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

    def process(self, target: Class):
        """Process the given class attrs if they contain choices.

        Args:
            target: The target class instance
        """
        for attr in target.attrs:
            if attr.choices:
                self.process_compound_field(target, attr)

    def process_compound_field(self, target: Class, attr: Attr):
        """Process a compound field.

        A compound field can be created by a mixed wildcard with
        explicit children, or because we enabled the configuration
        to group repeatable choices.

        Steps:
            1. Merge choices derived from xs:any elements
            2. Find ambiguous choices and create intermediate classes
            3. Reset the attr types if it's not a mixed wildcard.


        Args:
            target: The target class instance
            attr: An attr instance that contains choices
        """
        self.merge_wildcard_choices(attr)

        for choice in self.find_ambiguous_choices(attr):
            self.disambiguate_choice(target, choice)

    @classmethod
    def merge_wildcard_choices(cls, attr: Attr):
        """Merge choices derived from xs:any elements.

        It's a compound field it doesn't make sense
        to have multiple wildcard choices. Merge them
        together.

        Args:
            attr: The attr instance that contains choices
        """
        choices = []
        namespaces = []
        min_occurs = 0
        max_occurs = 0
        has_wildcard = False
        for choice in attr.choices:
            if choice.is_wildcard:
                min_occurs += choice.restrictions.min_occurs or 0
                max_occurs += choice.restrictions.max_occurs or 0
                namespaces.append(choice.namespace)
                has_wildcard = True
            else:
                choices.append(choice)

        attr.choices = choices

        if has_wildcard:
            attr.choices.append(
                Attr(
                    name="content",
                    types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)],
                    tag=Tag.ANY,
                    namespace=" ".join(
                        collections.unique_sequence(filter(None, namespaces))
                    ),
                    restrictions=Restrictions(
                        min_occurs=min_occurs, max_occurs=max_occurs
                    ),
                )
            )

    @classmethod
    def find_ambiguous_choices(cls, attr: Attr) -> Iterator[Attr]:
        """Find choices with the same types.

        Args:
            attr: The attr instance with the choices.

        Yields:
            An iterator of the ambiguous choices, except wildcards.
        """
        groups = defaultdict(list)
        for index, choice in enumerate(attr.choices):
            for tp in choice.types:
                dt = tp.datatype
                if dt:
                    groups[dt.type.__name__].append(index)
                else:
                    groups[tp.qname].append(index)

        ambiguous = set()
        for indexes in groups.values():
            if len(indexes) > 1:
                ambiguous.update(indexes)

        for index in ambiguous:
            choice = attr.choices[index]
            if not choice.is_wildcard:
                yield choice

    def disambiguate_choice(self, target: Class, choice: Attr):
        """Create intermediate class for the given choice.

        Scenarios:
            1. Choice is derived from xs:anyType
            2. Choice is derived from a xs:anySimpleType
            3. Choice is a reference to xs:complexType or element

        Args:
            target: The target class instance
            choice: The ambiguous choice attr instance
        """
        is_circular = choice.is_circular_ref
        inner = not self.unnest_classes and not is_circular
        ref_class = self.create_ref_class(target, choice, inner=inner)

        if choice.is_any_type:
            self.add_any_type_value(ref_class, choice)
        elif self.is_simple_type(choice):
            self.add_simple_type_value(ref_class, choice)
        else:
            self.add_extension(ref_class, choice)

        choice.restrictions = Restrictions(
            min_occurs=choice.restrictions.min_occurs,
            max_occurs=choice.restrictions.max_occurs,
        )

        ref_type = AttrType(
            qname=ref_class.qname,
            reference=id(ref_class),
            forward=inner,
            circular=is_circular,
        )
        choice.types = [ref_type]
        if not inner:
            self.container.add(ref_class)
        else:
            ref_class.parent = target
            target.inner.append(ref_class)

    def is_simple_type(self, choice: Attr) -> bool:
        """Return whether the choice attr is a simple type reference."""
        if any(tp.native for tp in choice.types):
            return True

        source = self.container.find(choice.types[0].qname)
        if source and source.is_enumeration:
            return True

        return False

    def create_ref_class(self, source: Class, choice: Attr, inner: bool) -> Class:
        """Create an intermediate class  for the given choice.

        If the reference class is going to be inner, ensure the class name is
        unique, otherwise we will still end-up with ambiguous choices.

        Args:
            source: The source class instance
            choice: The ambiguous choice attr instance
            inner: Specifies if the reference class will be inner
        """
        name = choice.name
        if inner:
            name = self.next_available_name(source, name)

        return Class(
            qname=build_qname(choice.namespace, name),
            status=source.status,
            tag=Tag.ELEMENT,
            local_type=True,
            location=source.location,
            ns_map=source.ns_map,
            nillable=choice.restrictions.nillable or False,
        )

    @classmethod
    def next_available_name(cls, parent: Class, name: str) -> str:
        """Find the next available name for an inner class.

        Args:
            parent: The parent class instance
            name: The name of the inner class

        Returns:
            The next available class name by adding a integer suffix.
        """
        reserved = {text.alnum(inner.name) for inner in parent.inner}
        index = 0
        new_name = name
        while True:
            cmp = text.alnum(new_name)

            if cmp not in reserved:
                return new_name

            index += 1
            new_name = f"{name}_{index}"

    @classmethod
    def add_any_type_value(cls, reference: Class, choice: Attr):
        """Add a simple any type content value attr to the reference class.

        Args:
            reference: The reference class instance
            choice: The source choice attr instance
        """
        attr = Attr(
            name="content",
            types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)],
            tag=Tag.ANY,
            namespace=choice.namespace,
            restrictions=Restrictions(min_occurs=1, max_occurs=1),
        )
        reference.attrs.append(attr)

    @classmethod
    def add_simple_type_value(cls, reference: Class, choice: Attr):
        """Add a simple type content value attr to the reference class.

        Args:
            reference: The reference class instance
            choice: The source choice attr instance
        """
        new_attr = Attr(
            tag=Tag.EXTENSION,
            name=DEFAULT_ATTR_NAME,
            namespace=None,
            restrictions=choice.restrictions.clone(
                min_occurs=1,
                max_occurs=1,
                path=[],
                nillable=False,
            ),
            types=[tp.clone() for tp in choice.types],
        )
        reference.attrs.append(new_attr)

    @classmethod
    def add_extension(cls, reference: Class, choice: Attr):
        """Add an extension to the reference class from the choice type.

        Args:
            reference: The reference class instance
            choice: The source choice attr instance
        """
        extension = Extension(
            tag=Tag.EXTENSION,
            type=choice.types[0].clone(forward=False, circular=False),
            restrictions=Restrictions(),
        )
        reference.extensions.append(extension)

process(target)

Process the given class attrs if they contain choices.

Parameters:

Name Type Description Default
target Class

The target class instance

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
33
34
35
36
37
38
39
40
41
def process(self, target: Class):
    """Process the given class attrs if they contain choices.

    Args:
        target: The target class instance
    """
    for attr in target.attrs:
        if attr.choices:
            self.process_compound_field(target, attr)

process_compound_field(target, attr)

Process a compound field.

A compound field can be created by a mixed wildcard with explicit children, or because we enabled the configuration to group repeatable choices.

Steps
  1. Merge choices derived from xs:any elements
  2. Find ambiguous choices and create intermediate classes
  3. Reset the attr types if it's not a mixed wildcard.

Parameters:

Name Type Description Default
target Class

The target class instance

required
attr Attr

An attr instance that contains choices

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def process_compound_field(self, target: Class, attr: Attr):
    """Process a compound field.

    A compound field can be created by a mixed wildcard with
    explicit children, or because we enabled the configuration
    to group repeatable choices.

    Steps:
        1. Merge choices derived from xs:any elements
        2. Find ambiguous choices and create intermediate classes
        3. Reset the attr types if it's not a mixed wildcard.


    Args:
        target: The target class instance
        attr: An attr instance that contains choices
    """
    self.merge_wildcard_choices(attr)

    for choice in self.find_ambiguous_choices(attr):
        self.disambiguate_choice(target, choice)

merge_wildcard_choices(attr) classmethod

Merge choices derived from xs:any elements.

It's a compound field it doesn't make sense to have multiple wildcard choices. Merge them together.

Parameters:

Name Type Description Default
attr Attr

The attr instance that contains choices

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
 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
@classmethod
def merge_wildcard_choices(cls, attr: Attr):
    """Merge choices derived from xs:any elements.

    It's a compound field it doesn't make sense
    to have multiple wildcard choices. Merge them
    together.

    Args:
        attr: The attr instance that contains choices
    """
    choices = []
    namespaces = []
    min_occurs = 0
    max_occurs = 0
    has_wildcard = False
    for choice in attr.choices:
        if choice.is_wildcard:
            min_occurs += choice.restrictions.min_occurs or 0
            max_occurs += choice.restrictions.max_occurs or 0
            namespaces.append(choice.namespace)
            has_wildcard = True
        else:
            choices.append(choice)

    attr.choices = choices

    if has_wildcard:
        attr.choices.append(
            Attr(
                name="content",
                types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)],
                tag=Tag.ANY,
                namespace=" ".join(
                    collections.unique_sequence(filter(None, namespaces))
                ),
                restrictions=Restrictions(
                    min_occurs=min_occurs, max_occurs=max_occurs
                ),
            )
        )

find_ambiguous_choices(attr) classmethod

Find choices with the same types.

Parameters:

Name Type Description Default
attr Attr

The attr instance with the choices.

required

Yields:

Type Description
Attr

An iterator of the ambiguous choices, except wildcards.

Source code in xsdata/codegen/handlers/disambiguate_choices.py
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
@classmethod
def find_ambiguous_choices(cls, attr: Attr) -> Iterator[Attr]:
    """Find choices with the same types.

    Args:
        attr: The attr instance with the choices.

    Yields:
        An iterator of the ambiguous choices, except wildcards.
    """
    groups = defaultdict(list)
    for index, choice in enumerate(attr.choices):
        for tp in choice.types:
            dt = tp.datatype
            if dt:
                groups[dt.type.__name__].append(index)
            else:
                groups[tp.qname].append(index)

    ambiguous = set()
    for indexes in groups.values():
        if len(indexes) > 1:
            ambiguous.update(indexes)

    for index in ambiguous:
        choice = attr.choices[index]
        if not choice.is_wildcard:
            yield choice

disambiguate_choice(target, choice)

Create intermediate class for the given choice.

Scenarios
  1. Choice is derived from xs:anyType
  2. Choice is derived from a xs:anySimpleType
  3. Choice is a reference to xs:complexType or element

Parameters:

Name Type Description Default
target Class

The target class instance

required
choice Attr

The ambiguous choice attr instance

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
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
def disambiguate_choice(self, target: Class, choice: Attr):
    """Create intermediate class for the given choice.

    Scenarios:
        1. Choice is derived from xs:anyType
        2. Choice is derived from a xs:anySimpleType
        3. Choice is a reference to xs:complexType or element

    Args:
        target: The target class instance
        choice: The ambiguous choice attr instance
    """
    is_circular = choice.is_circular_ref
    inner = not self.unnest_classes and not is_circular
    ref_class = self.create_ref_class(target, choice, inner=inner)

    if choice.is_any_type:
        self.add_any_type_value(ref_class, choice)
    elif self.is_simple_type(choice):
        self.add_simple_type_value(ref_class, choice)
    else:
        self.add_extension(ref_class, choice)

    choice.restrictions = Restrictions(
        min_occurs=choice.restrictions.min_occurs,
        max_occurs=choice.restrictions.max_occurs,
    )

    ref_type = AttrType(
        qname=ref_class.qname,
        reference=id(ref_class),
        forward=inner,
        circular=is_circular,
    )
    choice.types = [ref_type]
    if not inner:
        self.container.add(ref_class)
    else:
        ref_class.parent = target
        target.inner.append(ref_class)

is_simple_type(choice)

Return whether the choice attr is a simple type reference.

Source code in xsdata/codegen/handlers/disambiguate_choices.py
177
178
179
180
181
182
183
184
185
186
def is_simple_type(self, choice: Attr) -> bool:
    """Return whether the choice attr is a simple type reference."""
    if any(tp.native for tp in choice.types):
        return True

    source = self.container.find(choice.types[0].qname)
    if source and source.is_enumeration:
        return True

    return False

create_ref_class(source, choice, inner)

Create an intermediate class for the given choice.

If the reference class is going to be inner, ensure the class name is unique, otherwise we will still end-up with ambiguous choices.

Parameters:

Name Type Description Default
source Class

The source class instance

required
choice Attr

The ambiguous choice attr instance

required
inner bool

Specifies if the reference class will be inner

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def create_ref_class(self, source: Class, choice: Attr, inner: bool) -> Class:
    """Create an intermediate class  for the given choice.

    If the reference class is going to be inner, ensure the class name is
    unique, otherwise we will still end-up with ambiguous choices.

    Args:
        source: The source class instance
        choice: The ambiguous choice attr instance
        inner: Specifies if the reference class will be inner
    """
    name = choice.name
    if inner:
        name = self.next_available_name(source, name)

    return Class(
        qname=build_qname(choice.namespace, name),
        status=source.status,
        tag=Tag.ELEMENT,
        local_type=True,
        location=source.location,
        ns_map=source.ns_map,
        nillable=choice.restrictions.nillable or False,
    )

next_available_name(parent, name) classmethod

Find the next available name for an inner class.

Parameters:

Name Type Description Default
parent Class

The parent class instance

required
name str

The name of the inner class

required

Returns:

Type Description
str

The next available class name by adding a integer suffix.

Source code in xsdata/codegen/handlers/disambiguate_choices.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
@classmethod
def next_available_name(cls, parent: Class, name: str) -> str:
    """Find the next available name for an inner class.

    Args:
        parent: The parent class instance
        name: The name of the inner class

    Returns:
        The next available class name by adding a integer suffix.
    """
    reserved = {text.alnum(inner.name) for inner in parent.inner}
    index = 0
    new_name = name
    while True:
        cmp = text.alnum(new_name)

        if cmp not in reserved:
            return new_name

        index += 1
        new_name = f"{name}_{index}"

add_any_type_value(reference, choice) classmethod

Add a simple any type content value attr to the reference class.

Parameters:

Name Type Description Default
reference Class

The reference class instance

required
choice Attr

The source choice attr instance

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
@classmethod
def add_any_type_value(cls, reference: Class, choice: Attr):
    """Add a simple any type content value attr to the reference class.

    Args:
        reference: The reference class instance
        choice: The source choice attr instance
    """
    attr = Attr(
        name="content",
        types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)],
        tag=Tag.ANY,
        namespace=choice.namespace,
        restrictions=Restrictions(min_occurs=1, max_occurs=1),
    )
    reference.attrs.append(attr)

add_simple_type_value(reference, choice) classmethod

Add a simple type content value attr to the reference class.

Parameters:

Name Type Description Default
reference Class

The reference class instance

required
choice Attr

The source choice attr instance

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
@classmethod
def add_simple_type_value(cls, reference: Class, choice: Attr):
    """Add a simple type content value attr to the reference class.

    Args:
        reference: The reference class instance
        choice: The source choice attr instance
    """
    new_attr = Attr(
        tag=Tag.EXTENSION,
        name=DEFAULT_ATTR_NAME,
        namespace=None,
        restrictions=choice.restrictions.clone(
            min_occurs=1,
            max_occurs=1,
            path=[],
            nillable=False,
        ),
        types=[tp.clone() for tp in choice.types],
    )
    reference.attrs.append(new_attr)

add_extension(reference, choice) classmethod

Add an extension to the reference class from the choice type.

Parameters:

Name Type Description Default
reference Class

The reference class instance

required
choice Attr

The source choice attr instance

required
Source code in xsdata/codegen/handlers/disambiguate_choices.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
@classmethod
def add_extension(cls, reference: Class, choice: Attr):
    """Add an extension to the reference class from the choice type.

    Args:
        reference: The reference class instance
        choice: The source choice attr instance
    """
    extension = Extension(
        tag=Tag.EXTENSION,
        type=choice.types[0].clone(forward=False, circular=False),
        restrictions=Restrictions(),
    )
    reference.extensions.append(extension)