Skip to content

Commit b1804b0

Browse files
committed
Add find_shortest_cycle to python graph
1 parent 1cbefec commit b1804b0

File tree

3 files changed

+73
-0
lines changed

3 files changed

+73
-0
lines changed

src/grimp/adaptors/graph.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ def find_shortest_chains(
126126
def chain_exists(self, importer: str, imported: str, as_packages: bool = False) -> bool:
127127
return self._rustgraph.chain_exists(importer, imported, as_packages)
128128

129+
def find_shortest_cycle(
130+
self, module: str, as_package: bool = False
131+
) -> Optional[Tuple[str, ...]]:
132+
if not self._rustgraph.contains_module(module):
133+
raise ValueError(f"Module {module} is not present in the graph.")
134+
135+
cycle = self._rustgraph.find_shortest_cycle(module, as_package)
136+
return tuple(cycle) if cycle else None
137+
129138
def find_illegal_dependencies_for_layers(
130139
self,
131140
layers: Sequence[Layer | str | set[str]],

src/grimp/application/ports/graph.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,22 @@ def chain_exists(self, importer: str, imported: str, as_packages: bool = False)
282282
"""
283283
raise NotImplementedError
284284

285+
# Cycles
286+
# ------
287+
288+
@abc.abstractmethod
289+
def find_shortest_cycle(
290+
self, module: str, as_package: bool = False
291+
) -> Optional[Tuple[str, ...]]:
292+
"""
293+
Returns the shortest import cycle from `module` to itself, or `None` if no cycle exist.
294+
295+
Optional args:
296+
as_package: Whether or not to treat the supplied module as an individual module,
297+
or as an entire subpackage (including any descendants).
298+
"""
299+
raise NotImplementedError
300+
285301
# High level analysis
286302
# -------------------
287303

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from grimp.adaptors.graph import ImportGraph
2+
3+
4+
class TestFindShortestCycle:
5+
def test_finds_shortest_cycle_when_exists(self):
6+
graph = ImportGraph()
7+
# Shortest cycle
8+
graph.add_import(importer="foo", imported="bar")
9+
graph.add_import(importer="bar", imported="baz")
10+
graph.add_import(importer="baz", imported="foo")
11+
# Longer cycle
12+
graph.add_import(importer="foo", imported="x")
13+
graph.add_import(importer="x", imported="y")
14+
graph.add_import(importer="y", imported="z")
15+
graph.add_import(importer="z", imported="foo")
16+
17+
assert graph.find_shortest_cycle("foo") == ("foo", "bar", "baz", "foo")
18+
19+
graph.remove_import(importer="baz", imported="foo")
20+
21+
assert graph.find_shortest_cycle("foo") == ("foo", "x", "y", "z", "foo")
22+
23+
def test_returns_none_if_no_cycle_exists(self):
24+
graph = ImportGraph()
25+
# Shortest cycle
26+
graph.add_import(importer="foo", imported="bar")
27+
graph.add_import(importer="bar", imported="baz")
28+
# graph.add_import(importer="baz", imported="foo") # This import is missing -> No cycle.
29+
30+
assert graph.find_shortest_cycle("foo") is None
31+
32+
def test_ignores_internal_imports_when_as_package_is_true(self):
33+
graph = ImportGraph()
34+
graph.add_module("colors")
35+
graph.add_import(importer="colors.red", imported="colors.blue")
36+
graph.add_import(importer="colors.blue", imported="colors.red")
37+
graph.add_import(importer="colors.red", imported="x")
38+
graph.add_import(importer="x", imported="y")
39+
graph.add_import(importer="y", imported="z")
40+
graph.add_import(importer="z", imported="colors.blue")
41+
42+
assert graph.find_shortest_cycle("colors", as_package=True) == (
43+
"colors.red",
44+
"x",
45+
"y",
46+
"z",
47+
"colors.blue",
48+
)

0 commit comments

Comments
 (0)