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.

Parameters:

Name Type Description Default
container ContainerInterface

The class container instance

required

Attributes:

Name Type Description
use_names

Whether names or qualified names should be used to group classes

merges dict[int, int]

The renamed instructions applied at the end

merges dict[int, int]

The merge instructions applied at the end

reserved set[str]

The reserved class names or qualified names

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

    Args:
        container: The class container instance

    Attributes:
        use_names: Whether names or qualified names should be used to group classes
        merges: The renamed instructions applied at the end
        merges: The merge instructions applied at the end
        reserved: The reserved class names or qualified names
    """

    __slots__ = ("merges", "renames", "reserved", "use_names")

    def __init__(self, container: ContainerInterface):
        super().__init__(container)

        self.use_names = self.should_use_names()
        self.renames: dict[int, str] = {}
        self.merges: dict[int, int] = {}
        self.reserved: set[str] = set()

    def run(self):
        """Detect and resolve class name conflicts."""
        getter = get_name if self.use_names 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)

        if self.renames or self.merges:
            for target in self.container:
                self.update_references(target)

    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)

        for item in classes:
            self.merges[item.ref] = replace

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

        Cases:
            - Two classes, one abstract append the abstract suffix
            - One element, add numeric suffixes to the rest of the classes
            - Add numeric suffixes to all classes.

        Args:
            classes: A list of classes with duplicate names
        """
        abstract = [x for x in classes if x.abstract]
        if len(classes) == 2 and len(abstract) == 1:
            self.add_abstract_suffix(abstract[0])
        else:
            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.add_numeric_suffix(target)

    def add_abstract_suffix(self, target: Class):
        """Add the abstract suffix to class name."""
        new_qname = f"{target.qname}_abstract"
        self.rename_class(target, new_qname)

    def add_numeric_suffix(self, target: Class):
        """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
        """
        namespace, name = namespaces.split_qname(target.qname)
        new_qname = self.next_qname(namespace, name)
        self.rename_class(target, new_qname)

    def rename_class(self, target: Class, new_qname: str):
        """Update the class qualified name and update references.

        Args:
            target: The target class to update
            new_qname: The new class qualified name
        """
        qname = target.qname
        target.qname = new_qname
        target.meta_name = namespaces.local_name(qname)

        self.container.reset(target, qname)
        self.renames[target.ref] = new_qname

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

        Args:
            namespace: The class namespace
            name: The class name
        """
        index = 0
        reserved = self.get_reserved()

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

            if cmp not in reserved:
                reserved.add(cmp)
                return qname

    def get_reserved(self) -> set[str]:
        """Build the reserved names or qualified names of the container."""
        if not self.reserved:
            getter = get_name if self.use_names else get_qname
            self.reserved = {text.alnum(getter(obj)) for obj in self.container}

        return self.reserved

    def update_references(self, target: Class):
        """Search and update the target class for renamed and merged references.

        Args:
            target: The target class instance to inspect and update
        """
        for parent, tp in target.types_with_parents():
            if tp.reference in self.renames:
                tp.qname = self.renames[tp.reference]

                if (
                    isinstance(parent, Attr)
                    and isinstance(parent.default, str)
                    and parent.default.startswith("@enum@")
                ):
                    members = text.suffix(parent.default, "::")
                    parent.default = f"@enum@{tp.qname}::{members}"

            elif tp.reference in self.merges:
                tp.reference = self.merges[tp.reference]

run()

Detect and resolve class name conflicts.

Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def run(self):
    """Detect and resolve class name conflicts."""
    getter = get_name if self.use_names 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)

    if self.renames or self.merges:
        for target in self.container:
            self.update_references(target)

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
56
57
58
59
60
61
62
63
64
65
66
67
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
69
70
71
72
73
74
75
76
77
78
79
80
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)

    for item in classes:
        self.merges[item.ref] = replace

rename_classes(classes)

Rename the classes in the list.

Cases
  • Two classes, one abstract append the abstract suffix
  • One element, add numeric suffixes to the rest of the classes
  • Add numeric suffixes to all classes.

Parameters:

Name Type Description Default
classes list[Class]

A list of classes with duplicate names

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def rename_classes(self, classes: list[Class]):
    """Rename the classes in the list.

    Cases:
        - Two classes, one abstract append the abstract suffix
        - One element, add numeric suffixes to the rest of the classes
        - Add numeric suffixes to all classes.

    Args:
        classes: A list of classes with duplicate names
    """
    abstract = [x for x in classes if x.abstract]
    if len(classes) == 2 and len(abstract) == 1:
        self.add_abstract_suffix(abstract[0])
    else:
        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.add_numeric_suffix(target)

add_abstract_suffix(target)

Add the abstract suffix to class name.

Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
102
103
104
105
def add_abstract_suffix(self, target: Class):
    """Add the abstract suffix to class name."""
    new_qname = f"{target.qname}_abstract"
    self.rename_class(target, new_qname)

add_numeric_suffix(target)

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
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
107
108
109
110
111
112
113
114
115
116
117
118
119
def add_numeric_suffix(self, target: Class):
    """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
    """
    namespace, name = namespaces.split_qname(target.qname)
    new_qname = self.next_qname(namespace, name)
    self.rename_class(target, new_qname)

rename_class(target, new_qname)

Update the class qualified name and update references.

Parameters:

Name Type Description Default
target Class

The target class to update

required
new_qname str

The new class qualified name

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
121
122
123
124
125
126
127
128
129
130
131
132
133
def rename_class(self, target: Class, new_qname: str):
    """Update the class qualified name and update references.

    Args:
        target: The target class to update
        new_qname: The new class qualified name
    """
    qname = target.qname
    target.qname = new_qname
    target.meta_name = namespaces.local_name(qname)

    self.container.reset(target, qname)
    self.renames[target.ref] = new_qname

next_qname(namespace, 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
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def next_qname(self, namespace: str, name: str) -> str:
    """Use int suffixes to get the next available qualified name.

    Args:
        namespace: The class namespace
        name: The class name
    """
    index = 0
    reserved = self.get_reserved()

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

        if cmp not in reserved:
            reserved.add(cmp)
            return qname

get_reserved()

Build the reserved names or qualified names of the container.

Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
155
156
157
158
159
160
161
def get_reserved(self) -> set[str]:
    """Build the reserved names or qualified names of the container."""
    if not self.reserved:
        getter = get_name if self.use_names else get_qname
        self.reserved = {text.alnum(getter(obj)) for obj in self.container}

    return self.reserved

update_references(target)

Search and update the target class for renamed and merged references.

Parameters:

Name Type Description Default
target Class

The target class instance to inspect and update

required
Source code in xsdata/codegen/handlers/rename_duplicate_classes.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def update_references(self, target: Class):
    """Search and update the target class for renamed and merged references.

    Args:
        target: The target class instance to inspect and update
    """
    for parent, tp in target.types_with_parents():
        if tp.reference in self.renames:
            tp.qname = self.renames[tp.reference]

            if (
                isinstance(parent, Attr)
                and isinstance(parent.default, str)
                and parent.default.startswith("@enum@")
            ):
                members = text.suffix(parent.default, "::")
                parent.default = f"@enum@{tp.qname}::{members}"

        elif tp.reference in self.merges:
            tp.reference = self.merges[tp.reference]