From 4bfb49f11efa3c7f76e894b6ab190a6a6e553fb2 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Wed, 5 Aug 2026 10:33:07 +0200 Subject: [PATCH 1/7] wip(poly test diff): extract related/owner bricks from changed tests --- components/polylith/commands/test.py | 10 +++++ components/polylith/test/__init__.py | 10 ++++- components/polylith/test/core.py | 45 +++++++++++++++++++++- test/components/polylith/test/test_core.py | 14 +++++++ 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 test/components/polylith/test/test_core.py diff --git a/components/polylith/commands/test.py b/components/polylith/commands/test.py index a160ef46..2abadf8a 100644 --- a/components/polylith/commands/test.py +++ b/components/polylith/commands/test.py @@ -28,6 +28,16 @@ def get_affected_bricks( return bases, components +def get_related_bricks( + root: Path, ns: str, tag_name: Union[str, None], theme: str +) -> Tuple[Set[str], Set[str]]: + files = test.get_changed_files(root, tag_name) + + bricks = test.get_related_bricks(root, ns, theme, files) + + return bricks["bases"], bricks["components"] + + def get_affected_projects( root: Path, ns: str, bases: Set[str], components: Set[str] ) -> List[dict]: diff --git a/components/polylith/test/__init__.py b/components/polylith/test/__init__.py index d0b7ec1f..b5ba479a 100644 --- a/components/polylith/test/__init__.py +++ b/components/polylith/test/__init__.py @@ -1,5 +1,11 @@ from polylith.test import report -from polylith.test.core import get_brick_imports_in_tests, get_changed_files +from polylith.test.core import get_brick_imports_in_tests, get_changed_files, get_related_bricks from polylith.test.tests import create_test -__all__ = ["report", "create_test", "get_brick_imports_in_tests", "get_changed_files"] +__all__ = [ + "report", + "create_test", + "get_brick_imports_in_tests", + "get_changed_files", + "get_related_bricks", +] diff --git a/components/polylith/test/core.py b/components/polylith/test/core.py index 41048ab3..c40dd730 100644 --- a/components/polylith/test/core.py +++ b/components/polylith/test/core.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Union +from typing import List, Set, Union from polylith import diff, imports @@ -16,6 +16,29 @@ def is_test(root: Path, ns: str, path: Path, theme: str) -> bool: return f"/{expected}/{ns}" in file_path +def extract_parts_from_test_path(root: Path, path: Path) -> List[str]: + relative_path = str.replace(path.as_posix(), root.as_posix(), "") + parts = str.split(relative_path, "/") + + return [p for p in parts if p] + + +def extract_brick_type_from_test(root: Path, path: Path, theme: str) -> str: + parts = extract_parts_from_test_path(root, path) + + return parts[1] if theme == "loose" else parts[0] + + +def extract_brick_name_from_test(root: Path, path: Path, theme: str) -> str: + parts = extract_parts_from_test_path(root, path) + + return parts[3] if theme == "loose" else parts[1] + + +def find_tests(root: Path, ns: str, theme: str, files: List[Path]) -> Set[Path]: + return {f for f in files if is_test(root, ns, f, theme)} + + def get_changed_files(root: Path, tag_name: Union[str, None]) -> List[Path]: tag = diff.collect.get_latest_tag(root, tag_name) or tag_name @@ -28,10 +51,28 @@ def get_changed_files(root: Path, tag_name: Union[str, None]) -> List[Path]: def get_brick_imports_in_tests( root: Path, ns: str, theme: str, files: List[Path] ) -> dict: - matched = {f for f in files if is_test(root, ns, f, theme)} + matched = find_tests(root, ns, theme, files) listed_imports = [imports.list_imports(m) for m in matched] all_imports = dict(enumerate(listed_imports)) return imports.extract_brick_imports(all_imports, ns) + + +def get_related_brick(root: Path, path: Path, theme: str) -> dict: + brick_name = extract_brick_name_from_test(root, path, theme) + brick_type = extract_brick_type_from_test(root, path, theme) + + return {"name": brick_name, "type": brick_type} + + +def get_related_bricks(root: Path, ns: str, theme: str, files: List[Path]) -> dict: + matched = find_tests(root, ns, theme, files) + + bricks = (get_related_brick(root, m, theme) for m in matched) + + bases = {b["name"] for b in bricks if b["type"] == "bases"} + components = {b["name"] for b in bricks if b["type"] == "components"} + + return {"bases": bases, "components": components} diff --git a/test/components/polylith/test/test_core.py b/test/components/polylith/test/test_core.py new file mode 100644 index 00000000..f910cd9a --- /dev/null +++ b/test/components/polylith/test/test_core.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from polylith.test import core + + +def test_extract_brick_name_from_test() -> None: + expected = "check" + root = Path.cwd() + + changed_test = root / f"test/components/polylith/{expected}/the_test.py" + + res = core.extract_brick_name_from_test(root, changed_test, theme="loose") + + assert res == expected From 5b2e4715bda30669dc26b46988ee6c60782ad6a1 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Thu, 6 Aug 2026 08:20:57 +0200 Subject: [PATCH 2/7] test(poly test diff): generic variable and path name --- test/components/polylith/test/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/polylith/test/test_core.py b/test/components/polylith/test/test_core.py index f910cd9a..293d2aa3 100644 --- a/test/components/polylith/test/test_core.py +++ b/test/components/polylith/test/test_core.py @@ -4,10 +4,10 @@ def test_extract_brick_name_from_test() -> None: - expected = "check" + expected = "hello_world" root = Path.cwd() - changed_test = root / f"test/components/polylith/{expected}/the_test.py" + changed_test = root / f"test/components/my_namespace/{expected}/the_test.py" res = core.extract_brick_name_from_test(root, changed_test, theme="loose") From d355755161f1a5a073f165bffaa1dec5794257b9 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Thu, 6 Aug 2026 08:29:44 +0200 Subject: [PATCH 3/7] test(poly test diff): add tests for TDD theme --- test/components/polylith/test/test_core.py | 31 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/test/components/polylith/test/test_core.py b/test/components/polylith/test/test_core.py index 293d2aa3..20099ca8 100644 --- a/test/components/polylith/test/test_core.py +++ b/test/components/polylith/test/test_core.py @@ -7,8 +7,33 @@ def test_extract_brick_name_from_test() -> None: expected = "hello_world" root = Path.cwd() - changed_test = root / f"test/components/my_namespace/{expected}/the_test.py" + first_test = root / f"test/components/my_namespace/{expected}/the_test.py" + second_test = root / f"components/{expected}/my_namespace/{expected}/test/the_test.py" - res = core.extract_brick_name_from_test(root, changed_test, theme="loose") + first = core.extract_brick_name_from_test(root, first_test, theme="loose") + second = core.extract_brick_name_from_test(root, second_test, theme="tdd") - assert res == expected + assert first == expected + assert second == expected + + +def test_extract_brick_type_from_test() -> None: + root = Path.cwd() + + loose_base_test = root / "test/bases/my_namespace/hello/the_test.py" + loose_comp_test = root / "test/components/my_namespace/world/the_test.py" + + tdd_base_test = root / "bases/hello/my_namespace/hello/test/the_test.py" + tdd_comp_test = root / "components/world/my_namespace/world/test/the_test.py" + + first = core.extract_brick_type_from_test(root, loose_base_test, theme="loose") + second = core.extract_brick_type_from_test(root, loose_comp_test, theme="loose") + + third = core.extract_brick_type_from_test(root, tdd_base_test, theme="tdd") + fourth = core.extract_brick_type_from_test(root, tdd_comp_test, theme="tdd") + + assert first == "bases" + assert second == "components" + + assert third == "bases" + assert fourth == "components" From 88c5440542cf881dc22c7baa43eb076a075204d3 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Thu, 6 Aug 2026 09:16:06 +0200 Subject: [PATCH 4/7] wip(poly test diff): get related bricks in the same format as the existing affected bricks feature --- components/polylith/commands/test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/polylith/commands/test.py b/components/polylith/commands/test.py index 2abadf8a..0fb464cf 100644 --- a/components/polylith/commands/test.py +++ b/components/polylith/commands/test.py @@ -13,8 +13,8 @@ def get_imported_bricks_in_tests( return set().union(*brick_imports.values()) -def extract_brick_names(bricks_data: List[dict], imported_bricks: Set[str]) -> Set[str]: - return {v for b in bricks_data for v in b.values() if v in imported_bricks} +def extract_brick_names(bricks_data: List[dict], possible_bricks: Set[str]) -> Set[str]: + return {v for b in bricks_data for v in b.values() if v in possible_bricks} def get_affected_bricks( @@ -32,10 +32,12 @@ def get_related_bricks( root: Path, ns: str, tag_name: Union[str, None], theme: str ) -> Tuple[Set[str], Set[str]]: files = test.get_changed_files(root, tag_name) + related = test.get_related_bricks(root, ns, theme, files) - bricks = test.get_related_bricks(root, ns, theme, files) + bases = extract_brick_names(dirs.get_bases_data(root, ns), related["bases"]) + components = extract_brick_names(dirs.get_bases_data(root, ns), related["components"]) - return bricks["bases"], bricks["components"] + return bases, components def get_affected_projects( From faed46ba094bba1649043dc428717ee9b61a687a Mon Sep 17 00:00:00 2001 From: David Vujic Date: Thu, 6 Aug 2026 10:37:02 +0200 Subject: [PATCH 5/7] feat(poly test diff): add support for strategy, brick and project lookup by imports/usage or by corresponding brick path --- bases/polylith/cli/test.py | 5 ++++- components/polylith/commands/test.py | 21 +++++++++++++++++++-- components/polylith/poetry/commands/test.py | 7 +++++++ components/polylith/test/core.py | 3 +-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/bases/polylith/cli/test.py b/bases/polylith/cli/test.py index fc39ca99..b02f281a 100644 --- a/bases/polylith/cli/test.py +++ b/bases/polylith/cli/test.py @@ -16,6 +16,9 @@ def diff_command( projects: Annotated[ bool, Option(help="Projects affected by changes in tests") ] = False, + strategy: Annotated[ + str, Option(help="By 'imports' (the bricks used in tests) or by 'path' (the corresponding bricks).") + ] = "imports", ): """Shows the Polylith projects and bricks that are affected by changes in tests.""" root = repo.get_workspace_root(Path.cwd()) @@ -27,6 +30,6 @@ def diff_command( print("No matching tags or commits found in repository.") return - options = {"short": short, "bricks": bricks, "projects": projects} + options = {"short": short, "bricks": bricks, "projects": projects, "strategy": strategy} commands.test.run(root, ns, tag, options) diff --git a/components/polylith/commands/test.py b/components/polylith/commands/test.py index 0fb464cf..60faf04f 100644 --- a/components/polylith/commands/test.py +++ b/components/polylith/commands/test.py @@ -35,7 +35,7 @@ def get_related_bricks( related = test.get_related_bricks(root, ns, theme, files) bases = extract_brick_names(dirs.get_bases_data(root, ns), related["bases"]) - components = extract_brick_names(dirs.get_bases_data(root, ns), related["components"]) + components = extract_brick_names(dirs.get_components_data(root, ns), related["components"]) return bases, components @@ -52,10 +52,27 @@ def get_affected_projects( return [p for p in projects_data if p["path"].name in names] +def parse_strategy(strategy: str) -> List[str]: + strategies = str.split(strategy, ",") + + return [str.lower(s) for s in strategies] + + def run(root: Path, ns: str, tag: str, options: dict) -> None: theme = configuration.get_theme_from_config(root) - bases, components = get_affected_bricks(root, ns, tag, theme) + strategy = parse_strategy(options["strategy"]) + + by_imports = "imports" in strategy + by_path = "path" in strategy + + fallback: Tuple[Set[str], Set[str]] = set(), set() + affected = get_affected_bricks(root, ns, tag, theme) if by_imports else fallback + related = get_related_bricks(root, ns, tag, theme) if by_path else fallback + + bases = set().union(affected[0], related[0]) + components = set().union(affected[1], related[1]) + projects_data = get_affected_projects(root, ns, bases, components) if options.get("bricks"): diff --git a/components/polylith/poetry/commands/test.py b/components/polylith/poetry/commands/test.py index 611fc0d1..15d3c006 100644 --- a/components/polylith/poetry/commands/test.py +++ b/components/polylith/poetry/commands/test.py @@ -25,6 +25,12 @@ class TestDiffCommand(Command): description="Projects affected by changes in tests", flag=True, ), + option( + long_name="strategy", + description="By 'imports' (the bricks used in tests) or by 'path' (the corresponding bricks)", + flag=False, + default="imports", + ), ] def handle(self) -> int: @@ -34,6 +40,7 @@ def handle(self) -> int: "short": self.option("short"), "bricks": self.option("bricks"), "projects": self.option("projects"), + "strategy": self.option("strategy"), } root = repo.get_workspace_root(Path.cwd()) diff --git a/components/polylith/test/core.py b/components/polylith/test/core.py index c40dd730..dabda294 100644 --- a/components/polylith/test/core.py +++ b/components/polylith/test/core.py @@ -70,8 +70,7 @@ def get_related_brick(root: Path, path: Path, theme: str) -> dict: def get_related_bricks(root: Path, ns: str, theme: str, files: List[Path]) -> dict: matched = find_tests(root, ns, theme, files) - bricks = (get_related_brick(root, m, theme) for m in matched) - + bricks = [get_related_brick(root, m, theme) for m in matched] bases = {b["name"] for b in bricks if b["type"] == "bases"} components = {b["name"] for b in bricks if b["type"] == "components"} From 76614b31646fe789163a4fff04d62495b2fecb3d Mon Sep 17 00:00:00 2001 From: David Vujic Date: Fri, 7 Aug 2026 08:33:53 +0200 Subject: [PATCH 6/7] bump Poetry plugin to 1.54.0 --- projects/poetry_polylith_plugin/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml index 742ad436..df0a4870 100644 --- a/projects/poetry_polylith_plugin/pyproject.toml +++ b/projects/poetry_polylith_plugin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-polylith-plugin" -version = "1.53.1" +version = "1.54.0" description = "A Poetry plugin that adds tooling support for the Polylith Architecture" authors = ["David Vujic"] homepage = "https://davidvujic.github.io/python-polylith-docs/" From 7239cded5d7289cc042fc664557311c0de6ae254 Mon Sep 17 00:00:00 2001 From: David Vujic Date: Fri, 7 Aug 2026 08:34:15 +0200 Subject: [PATCH 7/7] bump CLI to 1.50.0 --- projects/polylith_cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml index 8e9b096f..12bf7545 100644 --- a/projects/polylith_cli/pyproject.toml +++ b/projects/polylith_cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polylith-cli" -version = "1.49.1" +version = "1.50.0" description = "Python tooling support for the Polylith Architecture" authors = ['David Vujic'] homepage = "https://davidvujic.github.io/python-polylith-docs/"