Skip to content

Commit e9db163

Browse files
authored
feat(deps): identify circular brick dependencies (#395)
* feat(deps): identify circular dependencies * bump Poetry plugin to 1.45.0 * bump CLI to 1.38.0
1 parent 5bb76d2 commit e9db163

File tree

8 files changed

+149
-39
lines changed

8 files changed

+149
-39
lines changed

components/polylith/commands/deps.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,23 @@ def run(root: Path, ns: str, options: dict):
4444

4545
imports = get_imports(root, ns, bricks)
4646

47+
bricks_deps = {
48+
b: deps.calculate_brick_deps(b, bricks, imports)
49+
for b in set().union(*bricks.values())
50+
}
51+
52+
circular_bricks = deps.find_bricks_with_circular_dependencies(bricks_deps)
53+
4754
if brick and imports.get(brick):
48-
deps.print_brick_deps(brick, bricks, imports, options)
55+
brick_deps = bricks_deps[brick]
56+
circular_deps = circular_bricks.get(brick)
57+
58+
deps.print_brick_deps(brick, bricks, brick_deps, options)
59+
60+
if circular_deps:
61+
deps.print_brick_with_circular_deps(brick, circular_deps, bricks)
62+
4963
return
5064

5165
deps.print_deps(bricks, imports, options)
66+
deps.print_bricks_with_circular_deps(circular_bricks, bricks)

components/polylith/commands/diff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def flatten_dependent_bricks(
2828
changed_bricks: Set[str], bases: Set[str], components: Set[str], import_data: dict
2929
) -> Set[str]:
3030
matrix = [
31-
deps.report.sorted_used_by(brick, bases, components, import_data)
31+
deps.core.sorted_used_by(brick, bases, components, import_data)
3232
for brick in changed_bricks
3333
]
3434

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
from polylith.deps.core import get_brick_imports
2-
from polylith.deps.report import print_brick_deps, print_deps
1+
from polylith.deps.core import (
2+
calculate_brick_deps,
3+
find_bricks_with_circular_dependencies,
4+
get_brick_imports,
5+
)
6+
from polylith.deps.report import (
7+
print_brick_deps,
8+
print_brick_with_circular_deps,
9+
print_bricks_with_circular_deps,
10+
print_deps,
11+
)
312

4-
__all__ = ["get_brick_imports", "print_brick_deps", "print_deps"]
13+
__all__ = [
14+
"calculate_brick_deps",
15+
"find_bricks_with_circular_dependencies",
16+
"get_brick_imports",
17+
"print_brick_deps",
18+
"print_brick_with_circular_deps",
19+
"print_bricks_with_circular_deps",
20+
"print_deps",
21+
]

components/polylith/deps/core.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Set
2+
from typing import List, Set
33

44
from polylith import check, workspace
55

@@ -21,3 +21,53 @@ def get_brick_imports(
2121
root, ns, brick_imports_in_components
2222
),
2323
}
24+
25+
26+
def without(key: str, bricks: Set[str]) -> Set[str]:
27+
return {b for b in bricks if b != key}
28+
29+
30+
def sorted_usings(usings: Set[str], bases: Set[str], components: Set[str]) -> List[str]:
31+
usings_bases = sorted({b for b in usings if b in bases})
32+
usings_components = sorted({c for c in usings if c in components})
33+
34+
return usings_components + usings_bases
35+
36+
37+
def sorted_used_by(
38+
brick: str, bases: Set[str], components: Set[str], import_data: dict
39+
) -> List[str]:
40+
brick_used_by = without(brick, {k for k, v in import_data.items() if brick in v})
41+
42+
return sorted_usings(brick_used_by, bases, components)
43+
44+
45+
def sorted_uses(
46+
brick: str, bases: Set[str], components: Set[str], import_data: dict
47+
) -> List[str]:
48+
brick_uses = without(brick, import_data.get(brick, set()))
49+
50+
return sorted_usings(brick_uses, bases, components)
51+
52+
53+
def calculate_brick_deps(brick: str, bricks: dict, import_data: dict) -> dict:
54+
bases = bricks["bases"]
55+
components = bricks["components"]
56+
57+
brick_used_by = sorted_used_by(brick, bases, components, import_data)
58+
brick_uses = sorted_uses(brick, bases, components, import_data)
59+
60+
return {"used_by": brick_used_by, "uses": brick_uses}
61+
62+
63+
def find_intersection_for_usings(usings: dict) -> Set[str]:
64+
uses = set(usings["uses"])
65+
used_by = set(usings["used_by"])
66+
67+
return uses.intersection(used_by)
68+
69+
70+
def find_bricks_with_circular_dependencies(bricks_deps: dict) -> dict:
71+
res = {k: find_intersection_for_usings(v) for k, v in bricks_deps.items()}
72+
73+
return {k: v for k, v in sorted(res.items()) if v}

components/polylith/deps/report.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -102,45 +102,17 @@ def print_deps(bricks: dict, import_data: dict, options: dict):
102102
output.save(table, options, "deps")
103103

104104

105-
def without(key: str, bricks: Set[str]) -> Set[str]:
106-
return {b for b in bricks if b != key}
107-
108-
109-
def sorted_usings(usings: Set[str], bases: Set[str], components: Set[str]) -> List[str]:
110-
usings_bases = sorted({b for b in usings if b in bases})
111-
usings_components = sorted({c for c in usings if c in components})
112-
113-
return usings_components + usings_bases
114-
115-
116-
def sorted_used_by(
117-
brick: str, bases: Set[str], components: Set[str], import_data: dict
118-
) -> List[str]:
119-
brick_used_by = without(brick, {k for k, v in import_data.items() if brick in v})
120-
121-
return sorted_usings(brick_used_by, bases, components)
122-
123-
124-
def sorted_uses(
125-
brick: str, bases: Set[str], components: Set[str], import_data: dict
126-
) -> List[str]:
127-
brick_uses = without(brick, {b for b in import_data[brick]})
128-
129-
return sorted_usings(brick_uses, bases, components)
130-
131-
132105
def calculate_tag(brick: str, bases: Set[str]) -> str:
133106
return "base" if brick in bases else "comp"
134107

135108

136-
def print_brick_deps(brick: str, bricks: dict, import_data: dict, options: dict):
109+
def print_brick_deps(brick: str, bricks: dict, brick_deps: dict, options: dict):
137110
bases = bricks["bases"]
138-
components = bricks["components"]
139111

140112
save = options.get("save", False)
141113

142-
brick_used_by = sorted_used_by(brick, bases, components, import_data)
143-
brick_uses = sorted_uses(brick, bases, components, import_data)
114+
brick_used_by = brick_deps["used_by"]
115+
brick_uses = brick_deps["uses"]
144116

145117
tag = calculate_tag(brick, bases)
146118

@@ -170,3 +142,24 @@ def print_brick_deps(brick: str, bricks: dict, import_data: dict, options: dict)
170142

171143
if save:
172144
output.save(table, options, f"deps_{brick}")
145+
146+
147+
def print_brick_with_circular_deps(brick: str, deps: Set[str], bricks: dict) -> None:
148+
bases = bricks["bases"]
149+
150+
console = Console(theme=theme.poly_theme)
151+
152+
tag = calculate_tag(brick, bases)
153+
154+
with_tags = [f"[{calculate_tag(name, bases)}]{name}" for name in sorted(deps)]
155+
others = "[data],[/] ".join(with_tags)
156+
157+
prefix = ":information:"
158+
message = f"[{tag}]{brick}[/] [data]is used by[/] {others} [data]and is also uses[/] {others}[data].[/]"
159+
160+
console.print(f"{prefix} {message}", overflow="ellipsis")
161+
162+
163+
def print_bricks_with_circular_deps(circular_bricks: dict, bricks: dict) -> None:
164+
for brick, deps in circular_bricks.items():
165+
print_brick_with_circular_deps(brick, deps, bricks)

projects/poetry_polylith_plugin/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "poetry-polylith-plugin"
3-
version = "1.44.0"
3+
version = "1.45.0"
44
description = "A Poetry plugin that adds tooling support for the Polylith Architecture"
55
authors = ["David Vujic"]
66
homepage = "https://davidvujic.github.io/python-polylith-docs/"

projects/polylith_cli/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "polylith-cli"
3-
version = "1.37.0"
3+
version = "1.38.0"
44
description = "Python tooling support for the Polylith Architecture"
55
authors = ['David Vujic']
66
homepage = "https://davidvujic.github.io/python-polylith-docs/"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from polylith.deps.core import (
2+
calculate_brick_deps,
3+
find_bricks_with_circular_dependencies,
4+
)
5+
6+
7+
def test_calculate_brick_deps() -> None:
8+
bricks = {"bases": {"base"}, "components": {"one", "two", "three", "four"}}
9+
10+
imports = {
11+
"base": {"base", "one"},
12+
"one": {"one", "four"},
13+
"two": {"two"},
14+
"three": {"three", "one"},
15+
"four": {"four", "two"},
16+
}
17+
18+
res = calculate_brick_deps("one", bricks, imports)
19+
20+
assert sorted(res["used_by"]) == ["base", "three"]
21+
assert sorted(res["uses"]) == ["four"]
22+
23+
24+
def test_find_bricks_with_circular_dependencies() -> None:
25+
deps = {
26+
"base": {"used_by": [], "uses": ["one", "four"]},
27+
"one": {"used_by": ["two", "three"], "uses": ["three"]},
28+
"two": {"used_by": "three", "uses": []},
29+
"three": {"used_by": ["one"], "uses": ["one", "two"]},
30+
"four": {"used_by": ["base"], "uses": ["two"]},
31+
}
32+
33+
res = find_bricks_with_circular_dependencies(deps)
34+
35+
assert res == {"one": {"three"}, "three": {"one"}}

0 commit comments

Comments
 (0)