Skip to content

designate_class_packages

xsdata.codegen.handlers.designate_class_packages

DesignateClassPackages

Bases: ContainerHandlerInterface

Designate classes to packages and modules based on the output structure style.

Source code in xsdata/codegen/handlers/designate_class_packages.py
 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
class DesignateClassPackages(ContainerHandlerInterface):
    """Designate classes to packages and modules based on the output structure style."""

    __slots__ = ()

    def run(self):
        """Group classes to packages and modules based on the output structure style.

        Structure Styles:
            - Namespaces: classes with the same namespace are grouped together
            - Single Package: all classes are grouped together
            - Clusters: strong connected classes are grouped together
            - Namespace clusters: A combination of the namespaces and clusters
            - Filenames: classes are grouped together by the schema file location
        """
        structure_style = self.container.config.output.structure_style
        if structure_style == StructureStyle.NAMESPACES:
            self.group_by_namespace()
        elif structure_style == StructureStyle.SINGLE_PACKAGE:
            self.group_all_together()
        elif structure_style == StructureStyle.CLUSTERS:
            self.group_by_strong_components()
        elif structure_style == StructureStyle.NAMESPACE_CLUSTERS:
            self.group_by_namespace_clusters()
        else:
            self.group_by_filenames()

    def group_by_filenames(self):
        """Group classes by their schema file location.

        The classes are organized by the common paths of the
        file locations.

        Example:
            http://xsdata/foo/bar/schema.xsd -> foo.bar.schema.py
        """
        package = self.container.config.output.package
        class_map = collections.group_by(self.container, key=get_location)
        groups = self.group_common_paths(class_map.keys())
        modules_map = defaultdict(list)

        for keys in groups:
            if len(keys) == 1:
                common_path = os.path.dirname(keys[0])
            else:
                common_path = os.path.commonpath(keys)

            for key in keys:
                items = class_map[key]
                suffix = ".".join(Path(key).parent.relative_to(common_path).parts)
                package_name = f"{package}.{suffix}" if suffix else package
                mod_name = module_name(key)
                modules_map[(package_name, mod_name)].append(key)

                self.assign(items, package_name, mod_name)

        for locations in modules_map.values():
            if len(locations) > 1:
                logger.warning(
                    "Classes from different locations are designated to same module.\n"
                    "You can resolve this if you switch to another `--structure-style`."
                    "\n%s",
                    "\n".join(locations),
                )

    def group_by_namespace(self):
        """Group classes by their target namespace.

        Example:
            {myNS.tempuri.org}Root -> org.tempuri.myNS.py
        """
        groups = collections.group_by(self.container, key=get_target_namespace)
        for namespace, classes in groups.items():
            parts = self.combine_ns_package(namespace)
            module = parts.pop()
            package = ".".join(parts)
            self.assign(classes, package, module)

    def group_all_together(self):
        """Group all classes together in the same module."""
        package_parts = self.container.config.output.package.split(".")
        module = package_parts.pop()
        package = ".".join(package_parts)

        self.assign(self.container, package, module)

    def group_by_strong_components(self):
        """Find circular imports and cluster their classes together.

        This grouping ideally creates a class per file, if there
        are circular imports, the classes will be grouped together.
        """
        package = self.container.config.output.package
        for group in self.strongly_connected_classes():
            classes = self.sort_classes(group)
            module = classes[0].name
            self.assign(classes, package, module)

    def group_by_namespace_clusters(self):
        """Group strongly connected classes together by namespaces."""
        for group in self.strongly_connected_classes():
            classes = self.sort_classes(group)
            namespaces = set(map(get_target_namespace, classes))
            if len(namespaces) > 1:
                raise CodegenError(
                    "Found strongly connected types from different namespaces",
                    namespaces=namespaces,
                )

            parts = self.combine_ns_package(classes[0].target_namespace)
            module = classes[0].name
            self.assign(classes, ".".join(parts), module)

    def sort_classes(self, qnames: Set[str]) -> List[Class]:
        """Sort classes by their dependencies graph.

        Args:
            qnames: A set of qualified class names

        Returns:
            A class list in a safe to generate order.

        """
        edges = {
            qname: set(self.container.first(qname).dependencies()).intersection(qnames)
            for qname in qnames
        }
        return [self.container.first(qname) for qname in toposort_flatten(edges)]

    def strongly_connected_classes(self) -> Iterator[Set[str]]:
        """Compute strongly connected classes of a directed graph.

        Returns:
            A list of sets of qualified class names.
        """
        edges = {obj.qname: list(set(obj.dependencies(True))) for obj in self.container}
        return strongly_connected_components(edges)

    @classmethod
    def assign(cls, classes: Iterable[Class], package: str, module: str):
        """Assign package and model to classes.

        It's important to assign the same for any inner/nested
        classes as well.
        """
        for obj in classes:
            obj.package = package
            obj.module = module
            cls.assign(obj.inner, package, module)

    @classmethod
    def group_common_paths(cls, paths: Iterable[str]) -> List[List[str]]:
        """Group a list of file paths by their common paths.

        Args:
            paths: A list of file paths

        Returns:
            A list of file lists that belong to the same common path.
        """
        prev = ""
        index = 0
        groups = defaultdict(list)
        common_schemas_dir = COMMON_SCHEMA_DIR.as_uri()

        for path in sorted(paths):
            if path.startswith(common_schemas_dir):
                groups[0].append(path)
            else:
                path_parsed = urlparse(path)
                common_path = os.path.commonpath((prev, path))
                if not common_path or common_path == path_parsed.scheme:
                    index += 1

                prev = path
                groups[index].append(path)

        return list(groups.values())

    def combine_ns_package(self, namespace: Optional[str]) -> List[str]:
        """Combine the output package with a namespace.

        You can add aliases to namespace uri with the
        substitutions configuration.

        Without Alias:
            urn:foo-bar:add -> ["generated", "bar", "foo", "add"]

        With Package Alias:  urn:foo-bar:add -> add.again
            urn:foo-bar:add -> ["generated", "add", "again"]

        Returns:
            The package path as a list of strings.
        """
        result = self.container.config.output.package.split(".")

        if namespace:
            substitution = collections.first(
                re.sub(sub.search, sub.replace, namespace)
                for sub in self.container.config.substitutions.substitution
                if sub.type == ObjectType.PACKAGE
                and re.fullmatch(sub.search, namespace) is not None
            )
        else:
            substitution = None

        if substitution:
            result.extend(substitution.split("."))
        else:
            result.extend(to_package_name(namespace).split("."))

        return list(filter(None, result))

run()

Group classes to packages and modules based on the output structure style.

Structure Styles
  • Namespaces: classes with the same namespace are grouped together
  • Single Package: all classes are grouped together
  • Clusters: strong connected classes are grouped together
  • Namespace clusters: A combination of the namespaces and clusters
  • Filenames: classes are grouped together by the schema file location
Source code in xsdata/codegen/handlers/designate_class_packages.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def run(self):
    """Group classes to packages and modules based on the output structure style.

    Structure Styles:
        - Namespaces: classes with the same namespace are grouped together
        - Single Package: all classes are grouped together
        - Clusters: strong connected classes are grouped together
        - Namespace clusters: A combination of the namespaces and clusters
        - Filenames: classes are grouped together by the schema file location
    """
    structure_style = self.container.config.output.structure_style
    if structure_style == StructureStyle.NAMESPACES:
        self.group_by_namespace()
    elif structure_style == StructureStyle.SINGLE_PACKAGE:
        self.group_all_together()
    elif structure_style == StructureStyle.CLUSTERS:
        self.group_by_strong_components()
    elif structure_style == StructureStyle.NAMESPACE_CLUSTERS:
        self.group_by_namespace_clusters()
    else:
        self.group_by_filenames()

group_by_filenames()

Group classes by their schema file location.

The classes are organized by the common paths of the file locations.

Example

http://xsdata/foo/bar/schema.xsd -> foo.bar.schema.py

Source code in xsdata/codegen/handlers/designate_class_packages.py
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
def group_by_filenames(self):
    """Group classes by their schema file location.

    The classes are organized by the common paths of the
    file locations.

    Example:
        http://xsdata/foo/bar/schema.xsd -> foo.bar.schema.py
    """
    package = self.container.config.output.package
    class_map = collections.group_by(self.container, key=get_location)
    groups = self.group_common_paths(class_map.keys())
    modules_map = defaultdict(list)

    for keys in groups:
        if len(keys) == 1:
            common_path = os.path.dirname(keys[0])
        else:
            common_path = os.path.commonpath(keys)

        for key in keys:
            items = class_map[key]
            suffix = ".".join(Path(key).parent.relative_to(common_path).parts)
            package_name = f"{package}.{suffix}" if suffix else package
            mod_name = module_name(key)
            modules_map[(package_name, mod_name)].append(key)

            self.assign(items, package_name, mod_name)

    for locations in modules_map.values():
        if len(locations) > 1:
            logger.warning(
                "Classes from different locations are designated to same module.\n"
                "You can resolve this if you switch to another `--structure-style`."
                "\n%s",
                "\n".join(locations),
            )

group_by_namespace()

Group classes by their target namespace.

Example

{myNS.tempuri.org}Root -> org.tempuri.myNS.py

Source code in xsdata/codegen/handlers/designate_class_packages.py
87
88
89
90
91
92
93
94
95
96
97
98
def group_by_namespace(self):
    """Group classes by their target namespace.

    Example:
        {myNS.tempuri.org}Root -> org.tempuri.myNS.py
    """
    groups = collections.group_by(self.container, key=get_target_namespace)
    for namespace, classes in groups.items():
        parts = self.combine_ns_package(namespace)
        module = parts.pop()
        package = ".".join(parts)
        self.assign(classes, package, module)

group_all_together()

Group all classes together in the same module.

Source code in xsdata/codegen/handlers/designate_class_packages.py
100
101
102
103
104
105
106
def group_all_together(self):
    """Group all classes together in the same module."""
    package_parts = self.container.config.output.package.split(".")
    module = package_parts.pop()
    package = ".".join(package_parts)

    self.assign(self.container, package, module)

group_by_strong_components()

Find circular imports and cluster their classes together.

This grouping ideally creates a class per file, if there are circular imports, the classes will be grouped together.

Source code in xsdata/codegen/handlers/designate_class_packages.py
108
109
110
111
112
113
114
115
116
117
118
def group_by_strong_components(self):
    """Find circular imports and cluster their classes together.

    This grouping ideally creates a class per file, if there
    are circular imports, the classes will be grouped together.
    """
    package = self.container.config.output.package
    for group in self.strongly_connected_classes():
        classes = self.sort_classes(group)
        module = classes[0].name
        self.assign(classes, package, module)

group_by_namespace_clusters()

Group strongly connected classes together by namespaces.

Source code in xsdata/codegen/handlers/designate_class_packages.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def group_by_namespace_clusters(self):
    """Group strongly connected classes together by namespaces."""
    for group in self.strongly_connected_classes():
        classes = self.sort_classes(group)
        namespaces = set(map(get_target_namespace, classes))
        if len(namespaces) > 1:
            raise CodegenError(
                "Found strongly connected types from different namespaces",
                namespaces=namespaces,
            )

        parts = self.combine_ns_package(classes[0].target_namespace)
        module = classes[0].name
        self.assign(classes, ".".join(parts), module)

sort_classes(qnames)

Sort classes by their dependencies graph.

Parameters:

Name Type Description Default
qnames Set[str]

A set of qualified class names

required

Returns:

Type Description
List[Class]

A class list in a safe to generate order.

Source code in xsdata/codegen/handlers/designate_class_packages.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def sort_classes(self, qnames: Set[str]) -> List[Class]:
    """Sort classes by their dependencies graph.

    Args:
        qnames: A set of qualified class names

    Returns:
        A class list in a safe to generate order.

    """
    edges = {
        qname: set(self.container.first(qname).dependencies()).intersection(qnames)
        for qname in qnames
    }
    return [self.container.first(qname) for qname in toposort_flatten(edges)]

strongly_connected_classes()

Compute strongly connected classes of a directed graph.

Returns:

Type Description
Iterator[Set[str]]

A list of sets of qualified class names.

Source code in xsdata/codegen/handlers/designate_class_packages.py
151
152
153
154
155
156
157
158
def strongly_connected_classes(self) -> Iterator[Set[str]]:
    """Compute strongly connected classes of a directed graph.

    Returns:
        A list of sets of qualified class names.
    """
    edges = {obj.qname: list(set(obj.dependencies(True))) for obj in self.container}
    return strongly_connected_components(edges)

assign(classes, package, module) classmethod

Assign package and model to classes.

It's important to assign the same for any inner/nested classes as well.

Source code in xsdata/codegen/handlers/designate_class_packages.py
160
161
162
163
164
165
166
167
168
169
170
@classmethod
def assign(cls, classes: Iterable[Class], package: str, module: str):
    """Assign package and model to classes.

    It's important to assign the same for any inner/nested
    classes as well.
    """
    for obj in classes:
        obj.package = package
        obj.module = module
        cls.assign(obj.inner, package, module)

group_common_paths(paths) classmethod

Group a list of file paths by their common paths.

Parameters:

Name Type Description Default
paths Iterable[str]

A list of file paths

required

Returns:

Type Description
List[List[str]]

A list of file lists that belong to the same common path.

Source code in xsdata/codegen/handlers/designate_class_packages.py
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
@classmethod
def group_common_paths(cls, paths: Iterable[str]) -> List[List[str]]:
    """Group a list of file paths by their common paths.

    Args:
        paths: A list of file paths

    Returns:
        A list of file lists that belong to the same common path.
    """
    prev = ""
    index = 0
    groups = defaultdict(list)
    common_schemas_dir = COMMON_SCHEMA_DIR.as_uri()

    for path in sorted(paths):
        if path.startswith(common_schemas_dir):
            groups[0].append(path)
        else:
            path_parsed = urlparse(path)
            common_path = os.path.commonpath((prev, path))
            if not common_path or common_path == path_parsed.scheme:
                index += 1

            prev = path
            groups[index].append(path)

    return list(groups.values())

combine_ns_package(namespace)

Combine the output package with a namespace.

You can add aliases to namespace uri with the substitutions configuration.

Without Alias

urn:foo-bar:add -> ["generated", "bar", "foo", "add"]

urn:foo-bar:add -> add.again

urn:foo-bar:add -> ["generated", "add", "again"]

Returns:

Type Description
List[str]

The package path as a list of strings.

Source code in xsdata/codegen/handlers/designate_class_packages.py
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
def combine_ns_package(self, namespace: Optional[str]) -> List[str]:
    """Combine the output package with a namespace.

    You can add aliases to namespace uri with the
    substitutions configuration.

    Without Alias:
        urn:foo-bar:add -> ["generated", "bar", "foo", "add"]

    With Package Alias:  urn:foo-bar:add -> add.again
        urn:foo-bar:add -> ["generated", "add", "again"]

    Returns:
        The package path as a list of strings.
    """
    result = self.container.config.output.package.split(".")

    if namespace:
        substitution = collections.first(
            re.sub(sub.search, sub.replace, namespace)
            for sub in self.container.config.substitutions.substitution
            if sub.type == ObjectType.PACKAGE
            and re.fullmatch(sub.search, namespace) is not None
        )
    else:
        substitution = None

    if substitution:
        result.extend(substitution.split("."))
    else:
        result.extend(to_package_name(namespace).split("."))

    return list(filter(None, result))