diff --git a/src/grimp/adaptors/_layers.py b/src/grimp/adaptors/_layers.py index f49daad4..b1cc630d 100644 --- a/src/grimp/adaptors/_layers.py +++ b/src/grimp/adaptors/_layers.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from grimp.adaptors.graph import ImportGraph + from grimp.application.ports.graph import Level from grimp.domain.analysis import PackageDependency from grimp.exceptions import NoSuchContainer @@ -16,7 +17,7 @@ def find_illegal_dependencies( graph: ImportGraph, - layers: Sequence[Union[str, set[str]]], + layers: Sequence[Union[str, set[str], Level]], containers: set[str], ) -> set[PackageDependency]: """ @@ -52,11 +53,22 @@ class _RustPackageDependency(TypedDict): routes: tuple[_RustRoute, ...] -def _layers_to_levels(layers: Sequence[Union[str, set[str]]]) -> tuple[set[str], ...]: +def _layers_to_levels(layers: Sequence[Union[str, set[str], Level]]) -> tuple[Level, ...]: """ Convert any standalone layers to a one-element level. """ - return tuple({layer} if isinstance(layer, str) else set(layer) for layer in layers) + out_layers = [] + for layer in layers: + if isinstance(layer, dict): + out_layers.append(dict( + independent=layer["independent"], + layers=set(layer["layers"]), + )) + if isinstance(layer, str): + out_layers.append(dict(independent=True, layers={layer})) + else: + out_layers.append(dict(independent=True, layers=set(layer))) + return tuple(out_layers) def _dependencies_from_tuple( diff --git a/src/grimp/adaptors/graph.py b/src/grimp/adaptors/graph.py index 7bc77743..0288a323 100644 --- a/src/grimp/adaptors/graph.py +++ b/src/grimp/adaptors/graph.py @@ -365,7 +365,7 @@ def chain_exists(self, importer: str, imported: str, as_packages: bool = False) def find_illegal_dependencies_for_layers( self, - layers: Sequence[str | set[str]], + layers: Sequence[str | set[str] | graph.Level], containers: Optional[set[str]] = None, ) -> set[PackageDependency]: return _layers.find_illegal_dependencies( diff --git a/src/grimp/application/ports/graph.py b/src/grimp/application/ports/graph.py index 68473516..272d77cd 100644 --- a/src/grimp/application/ports/graph.py +++ b/src/grimp/application/ports/graph.py @@ -15,6 +15,11 @@ class DetailedImport(TypedDict): line_contents: str +class Level(TypedDict): + independent: bool + layers: set[str] + + class ImportGraph(abc.ABC): """ A Directed Graph of imports between Python modules. @@ -271,7 +276,7 @@ def chain_exists(self, importer: str, imported: str, as_packages: bool = False) def find_illegal_dependencies_for_layers( self, - layers: Sequence[Union[str, set[str]]], + layers: Sequence[Union[str, set[str], Level]], containers: Optional[set[str]] = None, ) -> set[PackageDependency]: """ diff --git a/tests/unit/adaptors/graph/test_layers.py b/tests/unit/adaptors/graph/test_layers.py index 672af37b..4efd69a3 100644 --- a/tests/unit/adaptors/graph/test_layers.py +++ b/tests/unit/adaptors/graph/test_layers.py @@ -384,7 +384,6 @@ def _analyze( layers=("mypackage.high", "mypackage.medium", "mypackage.low"), ) - class TestIndependentLayers: @pytest.mark.parametrize("specify_container", (True, False)) def test_no_illegal_imports(self, specify_container: bool): @@ -401,6 +400,9 @@ def test_no_illegal_imports(self, specify_container: bool): frozenset, tuple, list, + lambda layers: dict(independent=True, layers=set(layers)), + lambda layers: dict(independent=True, layers=tuple(layers)), + lambda layers: dict(independent=True, layers=list(layers)), ), ) @pytest.mark.parametrize( @@ -435,6 +437,24 @@ def test_direct_illegal_between_sibling_layers( ), } + def test_imports_between_sibling_layers_illegal_if_and_only_if_level_is_independent(self): + graph = self._build_legal_graph() + graph.add_import(importer="mypackage.foo", imported="mypackage.bar") + + # Layer is independent => import is forbidden. + results = graph.find_illegal_dependencies_for_layers( + layers=("high", dict(independent=True, layers={"foo", "bar"}), "low"), + containers={"mypackage"}, + ) + assert results + + # Layer is not independent => import is allowed. + results = graph.find_illegal_dependencies_for_layers( + layers=("high", dict(independent=False, layers={"foo", "bar"}), "low"), + containers={"mypackage"}, + ) + assert not results + @pytest.mark.parametrize("specify_container", (True, False)) @pytest.mark.parametrize( "start",