Skip to content

Commit 43b7c31

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

3 files changed

Lines changed: 80 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)