orchestrata_tools.dependency.cycle_detector
Reference › orchestrata_tools › dependency › cycle_detector
This module finds circular imports in the graph built by import_graph. CycleDetector.find_cycles runs a depth-first search from each module, following dependencies_of, and records a cycle whenever a dependency already sits on the current path. Paths longer than max_length (default 6) are abandoned. Each cycle is rotated so it starts at its smallest module name and ends by repeating it, which prevents the same cycle being reported from several starting points. has_cycles returns true if any cycle of length up to 3 exists.
At a glance
| Source | src/orchestrata_tools/dependency/cycle_detector.py |
| Lines of code | 45 |
| Classes | 1 |
| Functions | 0 |
| Imports (internal) | orchestrata_tools.dependency.import_graph |
| Imported by | orchestrata_tools.dependency |
| Tests that import it | none |
Class diagram
Dependency diagram
API
Classes
CycleDetector
Line 11
Detect circular imports in the dependency graph.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(graph: ImportGraph) |
|
find_cycles |
find_cycles(max_length: int = 6) -> list[list[str]] |
Find all import cycles up to a maximum length. |
has_cycles |
has_cycles() -> bool |
Return True if any cycles exist. |
Source
"""Circular import detection using DFS."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from orchestrata_tools.dependency.import_graph import ImportGraph
class CycleDetector:
"""Detect circular imports in the dependency graph."""
def __init__(self, graph: ImportGraph):
self.graph = graph
def find_cycles(self, max_length: int = 6) -> list[list[str]]:
"""Find all import cycles up to a maximum length."""
cycles: list[list[str]] = []
visited: set[str] = set()
def _dfs(node: str, path: list[str]) -> None:
if len(path) > max_length:
return
for dep in self.graph.dependencies_of(node):
if dep in path:
cycle = path[path.index(dep) :] + [dep]
# Normalize: start from alphabetically smallest node
start = min(range(len(cycle) - 1), key=lambda i: cycle[i])
normalized = cycle[start:-1] + cycle[:start] + [cycle[start]]
if normalized not in cycles:
cycles.append(normalized)
elif dep not in visited:
_dfs(dep, path + [dep])
for module in self.graph.all_modules():
if module not in visited:
_dfs(module, [module])
visited.add(module)
return cycles
def has_cycles(self) -> bool:
"""Return True if any cycles exist."""
return len(self.find_cycles(max_length=3)) > 0