Skip to content

# detect_circular_references

## `xsdata.codegen.handlers.detect_circular_references`

### `DetectCircularReferences`

Bases: `RelativeHandlerInterface`

Accurately detect circular dependencies between classes.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`container` | `ContainerInterface` | The class container instance | required

Attributes:

Name | Type | Description
--- | --- | ---
`reference_types` | `dict[int, list[AttrType]]` | A map of class refs to dependency types

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    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

    class DetectCircularReferences(RelativeHandlerInterface):
        """Accurately detect circular dependencies between classes.

        Args:
            container: The class container instance

        Attributes:
            reference_types: A map of class refs to dependency types
        """

        __slots__ = "container", "reference_types"

        def __init__(self, container: ContainerInterface):
            """Initialize the class."""
            super().__init__(container)
            self.reference_types: dict[int, list[AttrType]] = {}

        def process(self, target: Class) -> None:
            """Go through all the attr types and find circular references.

            Args:
                target: The class to inspect and update
            """
            if not self.reference_types:
                self.build_reference_types()

            for attr in target.attrs:
                self.process_types(attr.types, target.ref)

                for choice in attr.choices:
                    self.process_types(choice.types, target.ref)

        def process_types(self, types: list[AttrType], class_reference: int) -> None:
            """Go through the types and find circular references.

            Args:
                types: A list attr/choice type instances
                class_reference: The parent attr/choice class reference
            """
            for tp in types:
                if not tp.forward and not tp.native and not tp.circular:
                    tp.circular = self.is_circular(tp.reference, class_reference)

        def is_circular(self, start: int, stop: int) -> bool:
            """Detect if the start reference leads to the stop reference.

            The procedure is a dfs search to avoid max recursion errors.

            Args:
                start: The attr type reference
                stop: The parent class reference

            Returns:
                Whether the start reference leads back to the stop reference.
            """
            path = set()
            stack = [start]
            while stack:
                if stop in path:
                    return True

                ref = stack.pop()
                path.add(ref)

                stack.extend(
                    tp.reference
                    for tp in self.reference_types[ref]
                    if not tp.circular and tp.reference not in path
                )

            return stop in path

        def build_reference_types(self) -> None:
            """Build the reference types mapping."""

            def generate(target: Class) -> Iterator[tuple[int, list[AttrType]]]:
                yield target.ref, [tp for tp in target.types() if tp.reference]

                for inner in target.inner:
                    yield from generate(inner)

            for item in self.container:
                for ref, types in generate(item):
                    self.reference_types[ref] = types

#### `__init__(container)`

Initialize the class.

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    22
    23
    24
    25

    def __init__(self, container: ContainerInterface):
        """Initialize the class."""
        super().__init__(container)
        self.reference_types: dict[int, list[AttrType]] = {}

#### `process(target)`

Go through all the attr types and find circular references.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`target` | `Class` | The class to inspect and update | required

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    def process(self, target: Class) -> None:
        """Go through all the attr types and find circular references.

        Args:
            target: The class to inspect and update
        """
        if not self.reference_types:
            self.build_reference_types()

        for attr in target.attrs:
            self.process_types(attr.types, target.ref)

            for choice in attr.choices:
                self.process_types(choice.types, target.ref)

#### `process_types(types, class_reference)`

Go through the types and find circular references.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`types` | `list[AttrType]` | A list attr/choice type instances | required
`class_reference` | `int` | The parent attr/choice class reference | required

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    def process_types(self, types: list[AttrType], class_reference: int) -> None:
        """Go through the types and find circular references.

        Args:
            types: A list attr/choice type instances
            class_reference: The parent attr/choice class reference
        """
        for tp in types:
            if not tp.forward and not tp.native and not tp.circular:
                tp.circular = self.is_circular(tp.reference, class_reference)

#### `is_circular(start, stop)`

Detect if the start reference leads to the stop reference.

The procedure is a dfs search to avoid max recursion errors.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`start` | `int` | The attr type reference | required
`stop` | `int` | The parent class reference | required

Returns:

Type | Description
--- | ---
`bool` | Whether the start reference leads back to the stop reference.

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    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

    def is_circular(self, start: int, stop: int) -> bool:
        """Detect if the start reference leads to the stop reference.

        The procedure is a dfs search to avoid max recursion errors.

        Args:
            start: The attr type reference
            stop: The parent class reference

        Returns:
            Whether the start reference leads back to the stop reference.
        """
        path = set()
        stack = [start]
        while stack:
            if stop in path:
                return True

            ref = stack.pop()
            path.add(ref)

            stack.extend(
                tp.reference
                for tp in self.reference_types[ref]
                if not tp.circular and tp.reference not in path
            )

        return stop in path

#### `build_reference_types()`

Build the reference types mapping.

Source code in `xsdata/codegen/handlers/detect_circular_references.py`

    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93

    def build_reference_types(self) -> None:
        """Build the reference types mapping."""

        def generate(target: Class) -> Iterator[tuple[int, list[AttrType]]]:
            yield target.ref, [tp for tp in target.types() if tp.reference]

            for inner in target.inner:
                yield from generate(inner)

        for item in self.container:
            for ref, types in generate(item):
                self.reference_types[ref] = types