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) -> None:
            """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) -> None:
            """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) -> None:
            """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) -> None:
            """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) -> None:
            """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) -> None:
            """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: str | None) -> 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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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: str | None) -> 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))