From 8dbdc97bc4acef27bf163245d2e88c38fd92bf42 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sun, 23 Nov 2025 11:32:22 +0100 Subject: [PATCH 01/12] wip --- components/polylith/imports/__init__.py | 2 ++ components/polylith/imports/parser.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/components/polylith/imports/__init__.py b/components/polylith/imports/__init__.py index 94143ff5..a4f6ded8 100644 --- a/components/polylith/imports/__init__.py +++ b/components/polylith/imports/__init__.py @@ -1,6 +1,7 @@ from polylith.imports.parser import ( extract_top_ns, fetch_all_imports, + fetch_api, fetch_excluded_imports, list_imports, ) @@ -8,6 +9,7 @@ __all__ = [ "extract_top_ns", "fetch_all_imports", + "fetch_api", "fetch_excluded_imports", "list_imports", ] diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 1a70a764..2979df0e 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -113,6 +113,25 @@ def fetch_all_imports(paths: Set[Path]) -> dict: return {k: v for row in rows for k, v in row.items()} +def extract_api_part(path: str) -> str: + *_parts, api = str.split(path, ".") + + return api + + +def extract_api(paths: Set[str]) -> Set[str]: + return {extract_api_part(p) for p in paths} + + +def fetch_api(paths: Set[Path]) -> dict: + interface = "__init__.py" + interfaces = [Path(p / interface) for p in paths] + + rows = [{i.parent.name: extract_api(list_imports(i))} for i in interfaces] + + return {k: v for row in rows for k, v in row.items()} + + def should_exclude(path: Path, excludes: Set[str]): return any(path.match(pattern) for pattern in excludes) From 6330685da6bcb628268ea7f03857b1963b369b7c Mon Sep 17 00:00:00 2001 From: David Vujic Date: Fri, 2 Jan 2026 14:37:40 +0100 Subject: [PATCH 02/12] refactor(check): move grouping of brick imports to the 'imports' module --- components/polylith/check/__init__.py | 4 ++-- components/polylith/check/collect.py | 4 ++-- components/polylith/check/report.py | 6 +++--- components/polylith/imports/__init__.py | 2 ++ components/polylith/{check => imports}/grouping.py | 0 components/polylith/test/core.py | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) rename components/polylith/{check => imports}/grouping.py (100%) diff --git a/components/polylith/check/__init__.py b/components/polylith/check/__init__.py index c2c6e1fb..5ce57b76 100644 --- a/components/polylith/check/__init__.py +++ b/components/polylith/check/__init__.py @@ -1,3 +1,3 @@ -from polylith.check import collect, grouping, report +from polylith.check import collect, report -__all__ = ["collect", "grouping", "report"] +__all__ = ["collect", "report"] diff --git a/components/polylith/check/collect.py b/components/polylith/check/collect.py index 38e974de..a7da8887 100644 --- a/components/polylith/check/collect.py +++ b/components/polylith/check/collect.py @@ -1,13 +1,13 @@ from pathlib import Path from typing import Set -from polylith import check, imports, workspace +from polylith import imports, workspace def extract_bricks(paths: Set[Path], ns: str) -> dict: all_imports = imports.fetch_all_imports(paths) - return check.grouping.extract_brick_imports(all_imports, ns) + return imports.extract_brick_imports(all_imports, ns) def with_unknown_components(root: Path, ns: str, brick_imports: dict) -> dict: diff --git a/components/polylith/check/report.py b/components/polylith/check/report.py index 1b9440f4..a07e76b2 100644 --- a/components/polylith/check/report.py +++ b/components/polylith/check/report.py @@ -2,7 +2,7 @@ from typing import Set from polylith import imports, libs, workspace -from polylith.check import collect, grouping +from polylith.check import collect from polylith.reporting import theme from rich.console import Console @@ -78,8 +78,8 @@ def extract_collected_imports( ns: str, imports_in_bases: dict, imports_in_components: dict ) -> dict: brick_imports = { - "bases": grouping.extract_brick_imports(imports_in_bases, ns), - "components": grouping.extract_brick_imports(imports_in_components, ns), + "bases": imports.grouping.extract_brick_imports(imports_in_bases, ns), + "components": imports.grouping.extract_brick_imports(imports_in_components, ns), } third_party_imports = { diff --git a/components/polylith/imports/__init__.py b/components/polylith/imports/__init__.py index a4f6ded8..fecae14d 100644 --- a/components/polylith/imports/__init__.py +++ b/components/polylith/imports/__init__.py @@ -1,3 +1,4 @@ +from polylith.imports.grouping import extract_brick_imports from polylith.imports.parser import ( extract_top_ns, fetch_all_imports, @@ -7,6 +8,7 @@ ) __all__ = [ + "extract_brick_imports", "extract_top_ns", "fetch_all_imports", "fetch_api", diff --git a/components/polylith/check/grouping.py b/components/polylith/imports/grouping.py similarity index 100% rename from components/polylith/check/grouping.py rename to components/polylith/imports/grouping.py diff --git a/components/polylith/test/core.py b/components/polylith/test/core.py index 3115b326..8e464461 100644 --- a/components/polylith/test/core.py +++ b/components/polylith/test/core.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import List, Union -from polylith import check, diff, imports +from polylith import diff, imports def is_test(root: Path, ns: str, path: Path, theme: str) -> bool: @@ -34,4 +34,4 @@ def get_brick_imports_in_tests( all_imports = {k: v for k, v in enumerate(listed_imports)} - return check.grouping.extract_brick_imports(all_imports, ns) + return imports.extract_brick_imports(all_imports, ns) From a63dadb3faaa8a0735d3a93f577710955b26c724 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Fri, 2 Jan 2026 17:00:25 +0100 Subject: [PATCH 03/12] wip: interface command --- bases/polylith/cli/core.py | 19 +++++++++ bases/polylith/cli/options.py | 2 + components/polylith/commands/__init__.py | 24 ++++++++++- components/polylith/commands/interfaces.py | 45 +++++++++++++++++++++ components/polylith/imports/__init__.py | 6 ++- components/polylith/imports/grouping.py | 6 +++ components/polylith/imports/parser.py | 5 ++- components/polylith/interface/__init__.py | 5 ++- components/polylith/interface/interfaces.py | 4 +- components/polylith/interface/report.py | 28 +++++++++++++ 10 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 components/polylith/commands/interfaces.py create mode 100644 components/polylith/interface/report.py diff --git a/bases/polylith/cli/core.py b/bases/polylith/cli/core.py index 6d39109f..64b31b0f 100644 --- a/bases/polylith/cli/core.py +++ b/bases/polylith/cli/core.py @@ -210,5 +210,24 @@ def deps_command( commands.deps.run(root, ns, cli_options) +@app.command("interfaces") +def interfaces_command( + brick: Annotated[str, options.brick_interface], + save: Annotated[bool, options.save] = False, +): + """Visualize the interface for a brick.""" + root = repo.get_workspace_root(Path.cwd()) + ns = configuration.get_namespace_from_config(root) + + output = configuration.get_output_dir(root, "interfaces") if save else None + + cli_options = { + "save": save, + "output": output, + } + + commands.interfaces.run(root, ns, brick, cli_options) + + if __name__ == "__main__": app() diff --git a/bases/polylith/cli/options.py b/bases/polylith/cli/options.py index b9cbcc20..5538a63d 100644 --- a/bases/polylith/cli/options.py +++ b/bases/polylith/cli/options.py @@ -19,3 +19,5 @@ brick = Option(help="Shows dependencies for selected brick.") save = Option(help="Store the contents of this command to file.") + +brick_interface = Option(help="Shows the interface for selected brick.") diff --git a/components/polylith/commands/__init__.py b/components/polylith/commands/__init__.py index 1e3b148a..e5ad94a4 100644 --- a/components/polylith/commands/__init__.py +++ b/components/polylith/commands/__init__.py @@ -1,3 +1,23 @@ -from polylith.commands import check, create, deps, diff, info, libs, sync, test +from polylith.commands import ( + check, + create, + deps, + diff, + info, + interfaces, + libs, + sync, + test, +) -__all__ = ["check", "create", "deps", "diff", "info", "libs", "sync", "test"] +__all__ = [ + "check", + "create", + "deps", + "diff", + "info", + "interfaces", + "libs", + "sync", + "test", +] diff --git a/components/polylith/commands/interfaces.py b/components/polylith/commands/interfaces.py new file mode 100644 index 00000000..a36244a9 --- /dev/null +++ b/components/polylith/commands/interfaces.py @@ -0,0 +1,45 @@ +from pathlib import Path + +from polylith import imports, info, interface, workspace + + +def get_brick_data(root: Path, ns: str, brick: str, brick_type: str) -> dict: + paths = {brick} + + if brick_type == "base": + brick_path = workspace.paths.collect_bases_paths(root, ns, paths) + else: + brick_path = workspace.paths.collect_components_paths(root, ns, paths) + + brick_api = imports.fetch_api(brick_path) + exposes = brick_api.get(brick) or set() + + return { + "name": brick, + "type": brick_type, + "exposes": exposes, + } + + +def get_brick_imports(root: Path, ns: str, bases: set, components: set) -> dict: + bases_paths = workspace.paths.collect_bases_paths(root, ns, bases) + components_paths = workspace.paths.collect_components_paths(root, ns, components) + + in_bases = imports.fetch_all_imports(bases_paths) + in_components = imports.fetch_all_imports(components_paths) + + return { + "bases": imports.extract_brick_imports_with_namespaces(in_bases, ns), + "components": imports.extract_brick_imports_with_namespaces(in_components, ns), + } + + +def run(root: Path, ns: str, brick: str, options: dict) -> None: + bases = set(info.get_bases(root, ns)) + components = set(info.get_components(root, ns)) + brick_type = "base" if brick in bases else "component" + + brick_data = get_brick_data(root, ns, brick, brick_type) + brick_imports = get_brick_imports(root, ns, bases, components) + + interface.report.print_brick_interface(brick_data, brick_imports, options) diff --git a/components/polylith/imports/__init__.py b/components/polylith/imports/__init__.py index fecae14d..051ce4a8 100644 --- a/components/polylith/imports/__init__.py +++ b/components/polylith/imports/__init__.py @@ -1,4 +1,7 @@ -from polylith.imports.grouping import extract_brick_imports +from polylith.imports.grouping import ( + extract_brick_imports, + extract_brick_imports_with_namespaces, +) from polylith.imports.parser import ( extract_top_ns, fetch_all_imports, @@ -9,6 +12,7 @@ __all__ = [ "extract_brick_imports", + "extract_brick_imports_with_namespaces", "extract_top_ns", "fetch_all_imports", "fetch_api", diff --git a/components/polylith/imports/grouping.py b/components/polylith/imports/grouping.py index 2e39a520..5dd304e7 100644 --- a/components/polylith/imports/grouping.py +++ b/components/polylith/imports/grouping.py @@ -34,3 +34,9 @@ def extract_brick_imports(all_imports: dict, top_ns) -> dict: with_only_brick_names = only_brick_names(with_only_bricks) return exclude_empty(with_only_brick_names) + + +def extract_brick_imports_with_namespaces(all_imports: dict, top_ns) -> dict: + with_only_bricks = only_bricks(all_imports, top_ns) + + return exclude_empty(with_only_bricks) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 2979df0e..c85a520a 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import List, Set, Union +from polylith import interface + typing_ns = "typing" type_checking = "TYPE_CHECKING" @@ -124,8 +126,7 @@ def extract_api(paths: Set[str]) -> Set[str]: def fetch_api(paths: Set[Path]) -> dict: - interface = "__init__.py" - interfaces = [Path(p / interface) for p in paths] + interfaces = [Path(p / interface.file_name) for p in paths] rows = [{i.parent.name: extract_api(list_imports(i))} for i in interfaces] diff --git a/components/polylith/interface/__init__.py b/components/polylith/interface/__init__.py index 3774173f..df84199c 100644 --- a/components/polylith/interface/__init__.py +++ b/components/polylith/interface/__init__.py @@ -1,3 +1,4 @@ -from polylith.interface.interfaces import create_interface +from polylith.interface.interfaces import create_interface, file_name +from polylith.interface import report -__all__ = ["create_interface"] +__all__ = ["create_interface", "file_name", "report"] diff --git a/components/polylith/interface/interfaces.py b/components/polylith/interface/interfaces.py index 358b2623..74a44082 100644 --- a/components/polylith/interface/interfaces.py +++ b/components/polylith/interface/interfaces.py @@ -15,6 +15,8 @@ __all__ = ["{modulename}"] """ +file_name = "__init__.py" + def to_namespaced_path(package: str) -> str: parts = package.split("/") @@ -23,7 +25,7 @@ def to_namespaced_path(package: str) -> str: def create_interface(path: Path, options: dict) -> None: - interface = create_file(path, "__init__.py") + interface = create_file(path, file_name) namespace = options["namespace"] package = options["package"] diff --git a/components/polylith/interface/report.py b/components/polylith/interface/report.py new file mode 100644 index 00000000..83a3d3a4 --- /dev/null +++ b/components/polylith/interface/report.py @@ -0,0 +1,28 @@ +from polylith import output +from polylith.reporting import theme +from rich import box +from rich.console import Console +from rich.table import Table +from rich.padding import Padding + + +def print_brick_interface(brick_data: dict, brick_imports: dict, options: dict) -> None: + save = options.get("save", False) + + brick = brick_data["name"] + exposes = brick_data["exposes"] + tag = "base" if brick_data["type"] == "base" else "comp" + + console = Console(theme=theme.poly_theme) + console.print(Padding(f"[data]Brick:[/] [{tag}]{brick}[/]", (1, 0, 0, 0))) + + table = Table(box=box.SIMPLE_HEAD) + table.add_column("[data]exposes[/]") + + for e in exposes: + table.add_row(e) + + console.print(table, overflow="ellipsis") + + if save: + output.save(table, options, f"interface_{brick}") From 5ed829d5e82414da3182533b7e8a0cb783abf708 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sun, 4 Jan 2026 15:14:06 +0100 Subject: [PATCH 04/12] wip: interface command --- bases/polylith/cli/core.py | 19 -------- bases/polylith/cli/options.py | 2 - components/polylith/commands/__init__.py | 13 +----- components/polylith/commands/interfaces.py | 45 ------------------- components/polylith/interface/report.py | 52 +++++++++++++++++----- 5 files changed, 41 insertions(+), 90 deletions(-) delete mode 100644 components/polylith/commands/interfaces.py diff --git a/bases/polylith/cli/core.py b/bases/polylith/cli/core.py index 64b31b0f..6d39109f 100644 --- a/bases/polylith/cli/core.py +++ b/bases/polylith/cli/core.py @@ -210,24 +210,5 @@ def deps_command( commands.deps.run(root, ns, cli_options) -@app.command("interfaces") -def interfaces_command( - brick: Annotated[str, options.brick_interface], - save: Annotated[bool, options.save] = False, -): - """Visualize the interface for a brick.""" - root = repo.get_workspace_root(Path.cwd()) - ns = configuration.get_namespace_from_config(root) - - output = configuration.get_output_dir(root, "interfaces") if save else None - - cli_options = { - "save": save, - "output": output, - } - - commands.interfaces.run(root, ns, brick, cli_options) - - if __name__ == "__main__": app() diff --git a/bases/polylith/cli/options.py b/bases/polylith/cli/options.py index 5538a63d..b9cbcc20 100644 --- a/bases/polylith/cli/options.py +++ b/bases/polylith/cli/options.py @@ -19,5 +19,3 @@ brick = Option(help="Shows dependencies for selected brick.") save = Option(help="Store the contents of this command to file.") - -brick_interface = Option(help="Shows the interface for selected brick.") diff --git a/components/polylith/commands/__init__.py b/components/polylith/commands/__init__.py index e5ad94a4..0ae43570 100644 --- a/components/polylith/commands/__init__.py +++ b/components/polylith/commands/__init__.py @@ -1,14 +1,4 @@ -from polylith.commands import ( - check, - create, - deps, - diff, - info, - interfaces, - libs, - sync, - test, -) +from polylith.commands import check, create, deps, diff, info, libs, sync, test __all__ = [ "check", @@ -16,7 +6,6 @@ "deps", "diff", "info", - "interfaces", "libs", "sync", "test", diff --git a/components/polylith/commands/interfaces.py b/components/polylith/commands/interfaces.py deleted file mode 100644 index a36244a9..00000000 --- a/components/polylith/commands/interfaces.py +++ /dev/null @@ -1,45 +0,0 @@ -from pathlib import Path - -from polylith import imports, info, interface, workspace - - -def get_brick_data(root: Path, ns: str, brick: str, brick_type: str) -> dict: - paths = {brick} - - if brick_type == "base": - brick_path = workspace.paths.collect_bases_paths(root, ns, paths) - else: - brick_path = workspace.paths.collect_components_paths(root, ns, paths) - - brick_api = imports.fetch_api(brick_path) - exposes = brick_api.get(brick) or set() - - return { - "name": brick, - "type": brick_type, - "exposes": exposes, - } - - -def get_brick_imports(root: Path, ns: str, bases: set, components: set) -> dict: - bases_paths = workspace.paths.collect_bases_paths(root, ns, bases) - components_paths = workspace.paths.collect_components_paths(root, ns, components) - - in_bases = imports.fetch_all_imports(bases_paths) - in_components = imports.fetch_all_imports(components_paths) - - return { - "bases": imports.extract_brick_imports_with_namespaces(in_bases, ns), - "components": imports.extract_brick_imports_with_namespaces(in_components, ns), - } - - -def run(root: Path, ns: str, brick: str, options: dict) -> None: - bases = set(info.get_bases(root, ns)) - components = set(info.get_components(root, ns)) - brick_type = "base" if brick in bases else "component" - - brick_data = get_brick_data(root, ns, brick, brick_type) - brick_imports = get_brick_imports(root, ns, bases, components) - - interface.report.print_brick_interface(brick_data, brick_imports, options) diff --git a/components/polylith/interface/report.py b/components/polylith/interface/report.py index 83a3d3a4..80679d9e 100644 --- a/components/polylith/interface/report.py +++ b/components/polylith/interface/report.py @@ -1,28 +1,56 @@ -from polylith import output +from pathlib import Path + +from polylith import imports, workspace from polylith.reporting import theme from rich import box from rich.console import Console from rich.table import Table -from rich.padding import Padding -def print_brick_interface(brick_data: dict, brick_imports: dict, options: dict) -> None: - save = options.get("save", False) +def get_brick_data(root: Path, ns: str, brick: str, brick_type: str) -> dict: + paths = {brick} + + if brick_type == "base": + brick_path = workspace.paths.collect_bases_paths(root, ns, paths) + else: + brick_path = workspace.paths.collect_components_paths(root, ns, paths) + + brick_api = imports.fetch_api(brick_path) + exposes = brick_api.get(brick) or set() + + return { + "name": brick, + "type": brick_type, + "exposes": sorted(exposes), + } + + +def get_brick_imports(root: Path, ns: str, bases: set, components: set) -> dict: + bases_paths = workspace.paths.collect_bases_paths(root, ns, bases) + components_paths = workspace.paths.collect_components_paths(root, ns, components) - brick = brick_data["name"] + in_bases = imports.fetch_all_imports(bases_paths) + in_components = imports.fetch_all_imports(components_paths) + + return { + "bases": imports.extract_brick_imports_with_namespaces(in_bases, ns), + "components": imports.extract_brick_imports_with_namespaces(in_components, ns), + } + + +def print_brick_interface(root: Path, ns: str, brick: str, bricks: dict) -> None: + bases = bricks["bases"] + tag = "base" if brick in bases else "comp" + + brick_data = get_brick_data(root, ns, brick, tag) exposes = brick_data["exposes"] - tag = "base" if brick_data["type"] == "base" else "comp" console = Console(theme=theme.poly_theme) - console.print(Padding(f"[data]Brick:[/] [{tag}]{brick}[/]", (1, 0, 0, 0))) table = Table(box=box.SIMPLE_HEAD) - table.add_column("[data]exposes[/]") + table.add_column(f"[{tag}]{brick}[/] [data]brick interface[/]") for e in exposes: - table.add_row(e) + table.add_row(f"[data]{e}[/]") console.print(table, overflow="ellipsis") - - if save: - output.save(table, options, f"interface_{brick}") From dc1bc151a7a12e507345fa9d982bbde87732e4db Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sun, 4 Jan 2026 15:18:36 +0100 Subject: [PATCH 05/12] wip: interface command --- components/polylith/imports/parser.py | 4 +--- components/polylith/interface/__init__.py | 4 ++-- components/polylith/interface/interfaces.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index c85a520a..6b24732a 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import List, Set, Union -from polylith import interface - typing_ns = "typing" type_checking = "TYPE_CHECKING" @@ -126,7 +124,7 @@ def extract_api(paths: Set[str]) -> Set[str]: def fetch_api(paths: Set[Path]) -> dict: - interfaces = [Path(p / interface.file_name) for p in paths] + interfaces = [Path(p / "__init__.py") for p in paths] rows = [{i.parent.name: extract_api(list_imports(i))} for i in interfaces] diff --git a/components/polylith/interface/__init__.py b/components/polylith/interface/__init__.py index df84199c..3a6799ef 100644 --- a/components/polylith/interface/__init__.py +++ b/components/polylith/interface/__init__.py @@ -1,4 +1,4 @@ -from polylith.interface.interfaces import create_interface, file_name from polylith.interface import report +from polylith.interface.interfaces import create_interface -__all__ = ["create_interface", "file_name", "report"] +__all__ = ["create_interface", "report"] diff --git a/components/polylith/interface/interfaces.py b/components/polylith/interface/interfaces.py index 74a44082..358b2623 100644 --- a/components/polylith/interface/interfaces.py +++ b/components/polylith/interface/interfaces.py @@ -15,8 +15,6 @@ __all__ = ["{modulename}"] """ -file_name = "__init__.py" - def to_namespaced_path(package: str) -> str: parts = package.split("/") @@ -25,7 +23,7 @@ def to_namespaced_path(package: str) -> str: def create_interface(path: Path, options: dict) -> None: - interface = create_file(path, file_name) + interface = create_file(path, "__init__.py") namespace = options["namespace"] package = options["package"] From 80327969ea756bd938dc667d416e75ad63073e2d Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sun, 4 Jan 2026 15:19:37 +0100 Subject: [PATCH 06/12] wip: interface command --- components/polylith/commands/__init__.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/components/polylith/commands/__init__.py b/components/polylith/commands/__init__.py index 0ae43570..1e3b148a 100644 --- a/components/polylith/commands/__init__.py +++ b/components/polylith/commands/__init__.py @@ -1,12 +1,3 @@ from polylith.commands import check, create, deps, diff, info, libs, sync, test -__all__ = [ - "check", - "create", - "deps", - "diff", - "info", - "libs", - "sync", - "test", -] +__all__ = ["check", "create", "deps", "diff", "info", "libs", "sync", "test"] From 3d88752d3ce9d7d66c6cd7dc2acdf3ec01fb252a Mon Sep 17 00:00:00 2001 From: David Vujic Date: Tue, 6 Jan 2026 21:01:57 +0100 Subject: [PATCH 07/12] wip: parse brick usages in modules --- components/polylith/imports/parser.py | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 6b24732a..e3db31c7 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import List, Set, Union +from polylith.imports import grouping + typing_ns = "typing" type_checking = "TYPE_CHECKING" @@ -68,6 +70,26 @@ def parse_node(node: ast.AST) -> Union[dict, None]: return None +def parse_import_usage(node: ast.AST, imported: Set[str]) -> Union[str, None]: + child = None + + wrapper_nodes = (ast.Await, ast.Expr, ast.NamedExpr, ast.Starred, ast.Subscript) + + if isinstance(node, ast.Attribute): + if isinstance(node.value, ast.Name) and node.value.id in imported: + return f"{node.value.id}.{node.attr}" + + child = node.value + elif isinstance(node, wrapper_nodes): + child = node.value + elif isinstance(node, ast.Call): + child = node.func + elif isinstance(node, ast.UnaryOp): + child = node.operand + + return parse_import_usage(child, imported) if child is not None else None + + def parse_module(path: Path) -> ast.AST: with open(path.as_posix(), "r", encoding="utf-8", errors="ignore") as f: tree = ast.parse(f.read(), path.name) @@ -113,6 +135,32 @@ def fetch_all_imports(paths: Set[Path]) -> dict: return {k: v for row in rows for k, v in row.items()} +def fetch_import_usages_in_module(path: Path, imported: Set[str]) -> Set[str]: + tree = parse_module(path) + + nodes = (parse_import_usage(n, imported) for n in ast.walk(tree)) + + return {n for n in nodes if n is not None} + + +@lru_cache(maxsize=None) +def fetch_brick_import_usages(path: Path, ns: str) -> Set[str]: + brick = path.name + + all_imports = fetch_all_imports({path}) + brick_imports = grouping.extract_brick_imports(all_imports, ns) + imported = brick_imports.get(brick) + + if not imported: + return set() + + py_modules = find_files(path) + + res = (fetch_import_usages_in_module(p, imported) for p in py_modules) + + return {i for n in res if n for i in n} + + def extract_api_part(path: str) -> str: *_parts, api = str.split(path, ".") From 5b84861631ec6e10be2c3441057e2000312e0050 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sat, 10 Jan 2026 14:10:56 +0100 Subject: [PATCH 08/12] wip: parse brick usages in modules --- components/polylith/imports/__init__.py | 2 ++ components/polylith/imports/parser.py | 43 ++++++++++++------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/components/polylith/imports/__init__.py b/components/polylith/imports/__init__.py index 051ce4a8..20863904 100644 --- a/components/polylith/imports/__init__.py +++ b/components/polylith/imports/__init__.py @@ -6,6 +6,7 @@ extract_top_ns, fetch_all_imports, fetch_api, + fetch_brick_import_usages, fetch_excluded_imports, list_imports, ) @@ -16,6 +17,7 @@ "extract_top_ns", "fetch_all_imports", "fetch_api", + "fetch_brick_import_usages", "fetch_excluded_imports", "list_imports", ] diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index e3db31c7..347e2d95 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -2,9 +2,7 @@ from collections.abc import Iterable from functools import lru_cache from pathlib import Path -from typing import List, Set, Union - -from polylith.imports import grouping +from typing import FrozenSet, List, Set, Union typing_ns = "typing" type_checking = "TYPE_CHECKING" @@ -70,19 +68,33 @@ def parse_node(node: ast.AST) -> Union[dict, None]: return None -def parse_import_usage(node: ast.AST, imported: Set[str]) -> Union[str, None]: +def find_imported(node_id: str, imported: Iterable[str]) -> Union[str, None]: + return next((i for i in imported if str.endswith(i, f".{node_id}")), None) + + +def extract_api_part(path: str) -> str: + *_parts, api = str.split(path, ".") + + return api + + +def parse_import_usage(node: ast.AST, imported: Iterable[str]) -> Union[str, None]: child = None + api = {extract_api_part(i) for i in imported} + wrapper_nodes = (ast.Await, ast.Expr, ast.NamedExpr, ast.Starred, ast.Subscript) if isinstance(node, ast.Attribute): - if isinstance(node.value, ast.Name) and node.value.id in imported: - return f"{node.value.id}.{node.attr}" + if isinstance(node.value, ast.Name) and node.value.id in api: + return find_imported(node.value.id, imported) child = node.value elif isinstance(node, wrapper_nodes): child = node.value elif isinstance(node, ast.Call): + if isinstance(node.func, ast.Name) and node.func.id in api: + return find_imported(node.func.id, imported) child = node.func elif isinstance(node, ast.UnaryOp): child = node.operand @@ -135,7 +147,7 @@ def fetch_all_imports(paths: Set[Path]) -> dict: return {k: v for row in rows for k, v in row.items()} -def fetch_import_usages_in_module(path: Path, imported: Set[str]) -> Set[str]: +def fetch_import_usages_in_module(path: Path, imported: Iterable[str]) -> Set[str]: tree = parse_module(path) nodes = (parse_import_usage(n, imported) for n in ast.walk(tree)) @@ -144,16 +156,7 @@ def fetch_import_usages_in_module(path: Path, imported: Set[str]) -> Set[str]: @lru_cache(maxsize=None) -def fetch_brick_import_usages(path: Path, ns: str) -> Set[str]: - brick = path.name - - all_imports = fetch_all_imports({path}) - brick_imports = grouping.extract_brick_imports(all_imports, ns) - imported = brick_imports.get(brick) - - if not imported: - return set() - +def fetch_brick_import_usages(path: Path, imported: FrozenSet[str]) -> Set[str]: py_modules = find_files(path) res = (fetch_import_usages_in_module(p, imported) for p in py_modules) @@ -161,12 +164,6 @@ def fetch_brick_import_usages(path: Path, ns: str) -> Set[str]: return {i for n in res if n for i in n} -def extract_api_part(path: str) -> str: - *_parts, api = str.split(path, ".") - - return api - - def extract_api(paths: Set[str]) -> Set[str]: return {extract_api_part(p) for p in paths} From 50ce9022eb4261cacd5f832d0da6f7c35c8bd2b5 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sat, 10 Jan 2026 14:11:42 +0100 Subject: [PATCH 09/12] wip: parse brick usages in modules --- components/polylith/imports/parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 347e2d95..44cf7d0e 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -88,7 +88,6 @@ def parse_import_usage(node: ast.AST, imported: Iterable[str]) -> Union[str, Non if isinstance(node, ast.Attribute): if isinstance(node.value, ast.Name) and node.value.id in api: return find_imported(node.value.id, imported) - child = node.value elif isinstance(node, wrapper_nodes): child = node.value From 2e36684f18f4d22978472fb1f66dd48f4b0b0fa9 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sat, 10 Jan 2026 15:37:55 +0100 Subject: [PATCH 10/12] wip: parse brick usages in modules --- components/polylith/imports/parser.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 44cf7d0e..c1ef7413 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -68,7 +68,7 @@ def parse_node(node: ast.AST) -> Union[dict, None]: return None -def find_imported(node_id: str, imported: Iterable[str]) -> Union[str, None]: +def find_imported(node_id: str, imported: FrozenSet[str]) -> Union[str, None]: return next((i for i in imported if str.endswith(i, f".{node_id}")), None) @@ -78,21 +78,25 @@ def extract_api_part(path: str) -> str: return api -def parse_import_usage(node: ast.AST, imported: Iterable[str]) -> Union[str, None]: - child = None - +def is_matching_node(expr: ast.expr, imported: FrozenSet[str]) -> bool: api = {extract_api_part(i) for i in imported} + return isinstance(expr, ast.Name) and expr.id in api + + +def parse_import_usage(node: ast.AST, imported: FrozenSet[str]) -> Union[str, None]: + child = None + wrapper_nodes = (ast.Await, ast.Expr, ast.NamedExpr, ast.Starred, ast.Subscript) if isinstance(node, ast.Attribute): - if isinstance(node.value, ast.Name) and node.value.id in api: + if is_matching_node(node.value, imported): return find_imported(node.value.id, imported) child = node.value elif isinstance(node, wrapper_nodes): child = node.value elif isinstance(node, ast.Call): - if isinstance(node.func, ast.Name) and node.func.id in api: + if is_matching_node(node.func, imported): return find_imported(node.func.id, imported) child = node.func elif isinstance(node, ast.UnaryOp): @@ -146,7 +150,7 @@ def fetch_all_imports(paths: Set[Path]) -> dict: return {k: v for row in rows for k, v in row.items()} -def fetch_import_usages_in_module(path: Path, imported: Iterable[str]) -> Set[str]: +def fetch_import_usages_in_module(path: Path, imported: FrozenSet[str]) -> Set[str]: tree = parse_module(path) nodes = (parse_import_usage(n, imported) for n in ast.walk(tree)) From 6a62beac94ac23df5c2530ab81d51fc261a50af3 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sat, 10 Jan 2026 15:43:53 +0100 Subject: [PATCH 11/12] wip: parse brick usages in modules --- components/polylith/imports/parser.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index c1ef7413..6bc38dd3 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -78,10 +78,13 @@ def extract_api_part(path: str) -> str: return api -def is_matching_node(expr: ast.expr, imported: FrozenSet[str]) -> bool: +def find_matching_node(expr: ast.expr, imported: FrozenSet[str]) -> Union[str, None]: api = {extract_api_part(i) for i in imported} - return isinstance(expr, ast.Name) and expr.id in api + if isinstance(expr, ast.Name) and expr.id in api: + return find_imported(expr.id, imported) + + return None def parse_import_usage(node: ast.AST, imported: FrozenSet[str]) -> Union[str, None]: @@ -90,15 +93,21 @@ def parse_import_usage(node: ast.AST, imported: FrozenSet[str]) -> Union[str, No wrapper_nodes = (ast.Await, ast.Expr, ast.NamedExpr, ast.Starred, ast.Subscript) if isinstance(node, ast.Attribute): - if is_matching_node(node.value, imported): - return find_imported(node.value.id, imported) - child = node.value + found = find_matching_node(node.value, imported) + + if found: + return found + else: + child = node.value elif isinstance(node, wrapper_nodes): child = node.value elif isinstance(node, ast.Call): - if is_matching_node(node.func, imported): - return find_imported(node.func.id, imported) - child = node.func + found = find_matching_node(node.func, imported) + + if found: + return found + else: + child = node.func elif isinstance(node, ast.UnaryOp): child = node.operand From 59046a62561e8a6ee39490cfd90a2114cdb1749f Mon Sep 17 00:00:00 2001 From: David Vujic Date: Sat, 10 Jan 2026 15:48:53 +0100 Subject: [PATCH 12/12] wip: parse brick usages in modules --- components/polylith/imports/parser.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/components/polylith/imports/parser.py b/components/polylith/imports/parser.py index 6bc38dd3..9790184e 100644 --- a/components/polylith/imports/parser.py +++ b/components/polylith/imports/parser.py @@ -88,29 +88,25 @@ def find_matching_node(expr: ast.expr, imported: FrozenSet[str]) -> Union[str, N def parse_import_usage(node: ast.AST, imported: FrozenSet[str]) -> Union[str, None]: + found = None child = None wrapper_nodes = (ast.Await, ast.Expr, ast.NamedExpr, ast.Starred, ast.Subscript) if isinstance(node, ast.Attribute): found = find_matching_node(node.value, imported) - - if found: - return found - else: - child = node.value + child = node.value elif isinstance(node, wrapper_nodes): child = node.value elif isinstance(node, ast.Call): found = find_matching_node(node.func, imported) - - if found: - return found - else: - child = node.func + child = node.func elif isinstance(node, ast.UnaryOp): child = node.operand + if found: + return found + return parse_import_usage(child, imported) if child is not None else None