Skip to content

rename_duplicate_classes

xsdata.codegen.handlers.rename_duplicate_classes

RenameDuplicateClasses

Bases: ContainerHandlerInterface

Resolve class name conflicts depending on the output structure style.

Source code in xsdata/codegen/handlers/rename_duplicate_classes.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
class RenameDuplicateClasses(ContainerHandlerInterface):
    """Resolve class name conflicts depending on the output structure style."""

    __slots__ = ()

    def run(self):
        """Detect and resolve class name conflicts."""
        use_name = self.should_use_names()
        getter = get_name if use_name else get_qname
        groups = collections.group_by(self.container, lambda x: text.alnum(getter(x)))

        for classes in groups.values():
            if len(classes) < 2:
                continue

            if all(x == classes[0] for x in classes):
                self.merge_classes(classes)
            else:
                self.rename_classes(classes, use_name)

    def should_use_names(self) -> bool:
        """Determine if names or qualified names should be used for detection.

        Strict unique names:
            - Single package
            - Clustered packages
            - All classes have the same source location.
        """
        return (
            self.container.config.output.structure_style in REQUIRE_UNIQUE_NAMES
            or len(set(map(get_location, self.container))) == 1
        )

    def merge_classes(self, classes: List[Class]):
        """Remove the duplicate classes and update all references.

        Args:
            classes: A list of duplicate classes
        """
        keep = classes.pop()
        replace = keep.ref
        self.container.remove(*classes)
        search = {item.ref for item in classes}

        for item in self.container:
            self.update_class_references(item, search, replace)

    def rename_classes(self, classes: List[Class], use_name: bool):
        """Rename all the classes in the list.

        Protect classes derived from xs:element if there is only one in
        the list.

        Args:
            classes: A list of classes with duplicate names
            use_name: Whether simple or qualified names should be used
                during renaming
        """
        total_elements = sum(x.is_element for x in classes)
        for target in sorted(classes, key=get_name):
            if not target.is_element or total_elements > 1:
                self.rename_class(target, use_name)

    def rename_class(self, target: Class, use_name: bool):
        """Find the next available class name.

        Save the original name in the class metadata and update
        the class qualified name and all classes that depend on
        the target class.

        Args:
            target: The target class instance to rename
            use_name: Whether simple or qualified names should be
                used during renaming
        """
        qname = target.qname
        namespace, name = namespaces.split_qname(target.qname)
        target.qname = self.next_qname(namespace, name, use_name)
        target.meta_name = name
        self.container.reset(target, qname)

        for item in self.container:
            self.rename_class_dependencies(item, id(target), target.qname)

    def next_qname(self, namespace: str, name: str, use_name: bool) -> str:
        """Use int suffixes to get the next available qualified name.

        Args:
            namespace: The class namespace
            name: The class name
            use_name: Whether simple or qualified names should be
                used during renaming
        """
        index = 0

        if use_name:
            reserved = {text.alnum(obj.name) for obj in self.container}
        else:
            reserved = {text.alnum(obj.qname) for obj in self.container}

        while True:
            index += 1
            new_name = f"{name}_{index}"
            qname = namespaces.build_qname(namespace, new_name)
            cmp = text.alnum(new_name if use_name else qname)

            if cmp not in reserved:
                return qname

    def update_class_references(self, target: Class, search: Set[int], replace: int):
        """Go through all class types and update all references.

        Args:
            target: The target class instance to update
            search: A set of class references to find
            replace: The new class reference to replace
        """

        def update_maybe(attr_type: AttrType):
            if attr_type.reference in search:
                attr_type.reference = replace

        for attr in target.attrs:
            for tp in attr.types:
                update_maybe(tp)

            for choice in attr.choices:
                for tp in choice.types:
                    update_maybe(tp)

        for ext in target.extensions:
            update_maybe(ext.type)

        for inner in target.inner:
            self.update_class_references(inner, search, replace)

    def rename_class_dependencies(self, target: Class, reference: int, replace: str):
        """Search and replace the old qualified class name in all classes.

        Args:
            target: The target class instance to inspect
            reference: The reference id of the renamed class
            replace: The new qualified name of the renamed class
        """
        for attr in target.attrs:
            self.rename_attr_dependencies(attr, reference, replace)

        for ext in target.extensions:
            if ext.type.reference == reference:
                ext.type.qname = replace

        for inner in target.inner:
            self.rename_class_dependencies(inner, reference, replace)

    def rename_attr_dependencies(self, attr: Attr, reference: int, replace: str):
        """Search and replace the old qualified class name in the attr types.

        This also covers any choices and references to enum values.

        Args:
            attr: The target attr instance to inspect
            reference: The reference id of the renamed class
            replace: The new qualified name of the renamed class
        """
        for attr_type in attr.types:
            if attr_type.reference == reference:
                attr_type.qname = replace

                if isinstance(attr.default, str) and attr.default.startswith("@enum@"):
                    members = text.suffix(attr.default, "::")
                    attr.default = f"@enum@{replace}::{members}"

        for choice in attr.choices:
            self.rename_attr_dependencies(choice, reference, replace)

run()

Detect and resolve class name conflicts.

Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def run(self):
    """Detect and resolve class name conflicts."""
    use_name = self.should_use_names()
    getter = get_name if use_name else get_qname
    groups = collections.group_by(self.container, lambda x: text.alnum(getter(x)))

    for classes in groups.values():
        if len(classes) < 2:
            continue

        if all(x == classes[0] for x in classes):
            self.merge_classes(classes)
        else:
            self.rename_classes(classes, use_name)

should_use_names()

Determine if names or qualified names should be used for detection.

Strict unique names
  • Single package
  • Clustered packages
  • All classes have the same source location.
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
38
39
40
41
42
43
44
45
46
47
48
49
def should_use_names(self) -> bool:
    """Determine if names or qualified names should be used for detection.

    Strict unique names:
        - Single package
        - Clustered packages
        - All classes have the same source location.
    """
    return (
        self.container.config.output.structure_style in REQUIRE_UNIQUE_NAMES
        or len(set(map(get_location, self.container))) == 1
    )

merge_classes(classes)

Remove the duplicate classes and update all references.

Parameters:

Name Type Description Default
classes List[Class]

A list of duplicate classes

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def merge_classes(self, classes: List[Class]):
    """Remove the duplicate classes and update all references.

    Args:
        classes: A list of duplicate classes
    """
    keep = classes.pop()
    replace = keep.ref
    self.container.remove(*classes)
    search = {item.ref for item in classes}

    for item in self.container:
        self.update_class_references(item, search, replace)

rename_classes(classes, use_name)

Rename all the classes in the list.

Protect classes derived from xs:element if there is only one in the list.

Parameters:

Name Type Description Default
classes List[Class]

A list of classes with duplicate names

required
use_name bool

Whether simple or qualified names should be used during renaming

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def rename_classes(self, classes: List[Class], use_name: bool):
    """Rename all the classes in the list.

    Protect classes derived from xs:element if there is only one in
    the list.

    Args:
        classes: A list of classes with duplicate names
        use_name: Whether simple or qualified names should be used
            during renaming
    """
    total_elements = sum(x.is_element for x in classes)
    for target in sorted(classes, key=get_name):
        if not target.is_element or total_elements > 1:
            self.rename_class(target, use_name)

rename_class(target, use_name)

Find the next available class name.

Save the original name in the class metadata and update the class qualified name and all classes that depend on the target class.

Parameters:

Name Type Description Default
target Class

The target class instance to rename

required
use_name bool

Whether simple or qualified names should be used during renaming

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def rename_class(self, target: Class, use_name: bool):
    """Find the next available class name.

    Save the original name in the class metadata and update
    the class qualified name and all classes that depend on
    the target class.

    Args:
        target: The target class instance to rename
        use_name: Whether simple or qualified names should be
            used during renaming
    """
    qname = target.qname
    namespace, name = namespaces.split_qname(target.qname)
    target.qname = self.next_qname(namespace, name, use_name)
    target.meta_name = name
    self.container.reset(target, qname)

    for item in self.container:
        self.rename_class_dependencies(item, id(target), target.qname)

next_qname(namespace, name, use_name)

Use int suffixes to get the next available qualified name.

Parameters:

Name Type Description Default
namespace str

The class namespace

required
name str

The class name

required
use_name bool

Whether simple or qualified names should be used during renaming

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def next_qname(self, namespace: str, name: str, use_name: bool) -> str:
    """Use int suffixes to get the next available qualified name.

    Args:
        namespace: The class namespace
        name: The class name
        use_name: Whether simple or qualified names should be
            used during renaming
    """
    index = 0

    if use_name:
        reserved = {text.alnum(obj.name) for obj in self.container}
    else:
        reserved = {text.alnum(obj.qname) for obj in self.container}

    while True:
        index += 1
        new_name = f"{name}_{index}"
        qname = namespaces.build_qname(namespace, new_name)
        cmp = text.alnum(new_name if use_name else qname)

        if cmp not in reserved:
            return qname

update_class_references(target, search, replace)

Go through all class types and update all references.

Parameters:

Name Type Description Default
target Class

The target class instance to update

required
search Set[int]

A set of class references to find

required
replace int

The new class reference to replace

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
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
def update_class_references(self, target: Class, search: Set[int], replace: int):
    """Go through all class types and update all references.

    Args:
        target: The target class instance to update
        search: A set of class references to find
        replace: The new class reference to replace
    """

    def update_maybe(attr_type: AttrType):
        if attr_type.reference in search:
            attr_type.reference = replace

    for attr in target.attrs:
        for tp in attr.types:
            update_maybe(tp)

        for choice in attr.choices:
            for tp in choice.types:
                update_maybe(tp)

    for ext in target.extensions:
        update_maybe(ext.type)

    for inner in target.inner:
        self.update_class_references(inner, search, replace)

rename_class_dependencies(target, reference, replace)

Search and replace the old qualified class name in all classes.

Parameters:

Name Type Description Default
target Class

The target class instance to inspect

required
reference int

The reference id of the renamed class

required
replace str

The new qualified name of the renamed class

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def rename_class_dependencies(self, target: Class, reference: int, replace: str):
    """Search and replace the old qualified class name in all classes.

    Args:
        target: The target class instance to inspect
        reference: The reference id of the renamed class
        replace: The new qualified name of the renamed class
    """
    for attr in target.attrs:
        self.rename_attr_dependencies(attr, reference, replace)

    for ext in target.extensions:
        if ext.type.reference == reference:
            ext.type.qname = replace

    for inner in target.inner:
        self.rename_class_dependencies(inner, reference, replace)

rename_attr_dependencies(attr, reference, replace)

Search and replace the old qualified class name in the attr types.

This also covers any choices and references to enum values.

Parameters:

Name Type Description Default
attr Attr

The target attr instance to inspect

required
reference int

The reference id of the renamed class

required
replace str

The new qualified name of the renamed class

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def rename_attr_dependencies(self, attr: Attr, reference: int, replace: str):
    """Search and replace the old qualified class name in the attr types.

    This also covers any choices and references to enum values.

    Args:
        attr: The target attr instance to inspect
        reference: The reference id of the renamed class
        replace: The new qualified name of the renamed class
    """
    for attr_type in attr.types:
        if attr_type.reference == reference:
            attr_type.qname = replace

            if isinstance(attr.default, str) and attr.default.startswith("@enum@"):
                members = text.suffix(attr.default, "::")
                attr.default = f"@enum@{replace}::{members}"

    for choice in attr.choices:
        self.rename_attr_dependencies(choice, reference, replace)