File: rename_duplicate_classes.py

package info (click to toggle)
python-xsdata 24.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,936 kB
  • sloc: python: 29,257; xml: 404; makefile: 27; sh: 6
file content (115 lines) | stat: -rw-r--r-- 4,471 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
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
from typing import List

from xsdata.codegen.mixins import ContainerHandlerInterface
from xsdata.codegen.models import Attr, Class, get_location, get_name, get_qname
from xsdata.models.config import StructureStyle
from xsdata.utils import collections, namespaces, text

REQUIRE_UNIQUE_NAMES = (StructureStyle.SINGLE_PACKAGE, StructureStyle.CLUSTERS)


class RenameDuplicateClasses(ContainerHandlerInterface):
    """Resolve class name conflicts depending on the output structure style."""

    __slots__ = ()

    def run(self):
        """Search for conflicts either by qualified name or local name
        depending on the configuration and start renaming classes and
        dependencies."""

        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) > 1:
                self.rename_classes(classes, use_name)

    def should_use_names(self) -> bool:
        """
        Determine if we should be using names or qualified names to detect
        collisions.

        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 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.
        """
        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 identifier, save the original name in
        the class metadata and update the class qualified name and all classes
        that depend on the target class."""

        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:
        """Append the next available index number for the given namespace and
        local name."""
        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 rename_class_dependencies(self, target: Class, reference: int, replace: str):
        """Search and replace the old qualified attribute type name with the
        new one if it exists in the target class attributes, extensions and
        inner classes."""
        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 attribute type name with the
        new one in the attr types, choices and default value."""
        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)