-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from __future__ import annotations | ||
|
||
from grimp import ImportGraph | ||
|
||
from importlinter.application import output | ||
from importlinter.domain import fields | ||
from importlinter.domain.contract import Contract, ContractCheck | ||
from importlinter.domain.helpers import _to_pattern | ||
from importlinter.domain.imports import Module | ||
|
||
|
||
# TODO: import from helpers once https://github.com/seddonym/import-linter/pull/220 is merged | ||
def _resolve_wildcards(value: str, graph: ImportGraph) -> set[Module]: | ||
pattern = _to_pattern(value) | ||
return {Module(module) for module in graph.modules if pattern.match(module)} | ||
|
||
|
||
class ModularContract(Contract): | ||
""" | ||
Modular contracts check that one set of modules has no children with circular dependencies. | ||
Indirect imports will also be checked. | ||
Configuration options: | ||
- modules: A list of Modules that should be modular. | ||
""" | ||
|
||
type_name = "modular" | ||
|
||
modules = fields.ListField(subfield=fields.ModuleField()) | ||
|
||
def check(self, graph: ImportGraph, verbose: bool) -> ContractCheck: | ||
violations = {} | ||
for module in self.modules: # type: ignore | ||
direct_submodules = _resolve_wildcards(f"{module.name}.*", graph) | ||
dependencies = graph.find_illegal_dependencies_for_layers( | ||
layers=({y.name for y in direct_submodules},), | ||
) | ||
violations[module.name] = sorted( | ||
{ | ||
f"{dependency.imported} <-> {dependency.importer}" | ||
for dependency in dependencies | ||
if graph.find_shortest_chains(dependency.imported, dependency.importer) | ||
} | ||
) | ||
|
||
kept = all(len(violation) == 0 for violation in violations.values()) | ||
return ContractCheck(kept=kept, warnings=None, metadata={"violations": violations}) | ||
|
||
def render_broken_contract(self, check: "ContractCheck") -> None: | ||
for module_name, violations in check.metadata["violations"].items(): | ||
output.print( | ||
f"child modules of {module_name} must be modular and thus circular dependencies " | ||
"are not allowed:" | ||
) | ||
output.new_line() | ||
for violation in violations: | ||
output.print_error(f"- {violation}") | ||
output.new_line() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
from __future__ import annotations | ||
|
||
import pytest | ||
from grimp.adaptors.graph import ImportGraph | ||
|
||
from importlinter.application.app_config import settings | ||
from importlinter.contracts.modular import ModularContract | ||
from importlinter.domain.contract import ContractCheck | ||
from tests.adapters.printing import FakePrinter | ||
from tests.adapters.timing import FakeTimer | ||
|
||
|
||
@pytest.fixture(scope="module", autouse=True) | ||
def configure(): | ||
settings.configure(TIMER=FakeTimer()) | ||
|
||
|
||
class TestModularContract: | ||
def _build_default_graph(self): | ||
graph = ImportGraph() | ||
for module in ( | ||
"mypackage", | ||
"mypackage.blue", | ||
"mypackage.blue.alpha", | ||
"mypackage.blue.beta", | ||
"mypackage.blue.beta.foo", | ||
"mypackage.green", | ||
"mypackage.yellow", | ||
"mypackage.yellow.gamma", | ||
"mypackage.yellow.delta", | ||
"mypackage.other", | ||
): | ||
graph.add_module(module) | ||
return graph | ||
|
||
def _check_default_contract(self, graph): | ||
contract = ModularContract( | ||
name="Modular contract", | ||
session_options={"root_packages": ["mypackage"]}, | ||
contract_options={"modules": ("mypackage",)}, | ||
) | ||
return contract.check(graph=graph, verbose=False) | ||
|
||
def test_when_modules_are_modular(self): | ||
graph = self._build_default_graph() | ||
graph.add_import( | ||
importer="mypackage.blue", | ||
imported="mypackage.other", | ||
line_number=10, | ||
line_contents="-", | ||
) | ||
graph.add_import( | ||
importer="mypackage.other", | ||
imported="mypackage.green", | ||
line_number=11, | ||
line_contents="-", | ||
) | ||
|
||
contract_check = self._check_default_contract(graph) | ||
|
||
assert contract_check.kept, contract_check.metadata | ||
|
||
def test_non_modular_bidirectional(self): | ||
graph = self._build_default_graph() | ||
graph.add_import( | ||
importer="mypackage.blue", | ||
imported="mypackage.other", | ||
line_number=10, | ||
line_contents="-", | ||
) | ||
graph.add_import( | ||
importer="mypackage.other", | ||
imported="mypackage.green", | ||
line_number=11, | ||
line_contents="-", | ||
) | ||
graph.add_import( | ||
importer="mypackage.other", | ||
imported="mypackage.blue", | ||
line_number=10, | ||
line_contents="-", | ||
) | ||
|
||
contract_check = self._check_default_contract(graph) | ||
|
||
assert not contract_check.kept | ||
|
||
expected_metadata = { | ||
"violations": { | ||
"mypackage": [ | ||
"mypackage.blue <-> mypackage.other", | ||
"mypackage.other <-> mypackage.blue", | ||
] | ||
} | ||
} | ||
|
||
assert expected_metadata == contract_check.metadata | ||
|
||
def test_non_modular_circular(self): | ||
graph = self._build_default_graph() | ||
graph.add_import( | ||
importer="mypackage.blue", | ||
imported="mypackage.other", | ||
line_number=10, | ||
line_contents="-", | ||
) | ||
graph.add_import( | ||
importer="mypackage.other", | ||
imported="mypackage.green", | ||
line_number=11, | ||
line_contents="-", | ||
) | ||
graph.add_import( | ||
importer="mypackage.green", | ||
imported="mypackage.blue", | ||
line_number=10, | ||
line_contents="-", | ||
) | ||
|
||
contract_check = self._check_default_contract(graph) | ||
|
||
assert not contract_check.kept | ||
|
||
expected_metadata = { | ||
"violations": { | ||
"mypackage": [ | ||
"mypackage.blue <-> mypackage.green", | ||
"mypackage.green <-> mypackage.other", | ||
"mypackage.other <-> mypackage.blue", | ||
] | ||
} | ||
} | ||
assert expected_metadata == contract_check.metadata | ||
|
||
|
||
def test_render_broken_contract(): | ||
settings.configure(PRINTER=FakePrinter()) | ||
contract = ModularContract( | ||
name="Modular contract", | ||
session_options={"root_packages": ["mypackage"]}, | ||
contract_options={"modules": ["mypackage", "mypackage.green"]}, | ||
) | ||
check = ContractCheck( | ||
kept=False, | ||
metadata={ | ||
"violations": { | ||
"mypackage": [ | ||
"mypackage.blue.foo <-> mypackage.utils.red", | ||
"mypackage.blue.red <-> mypackage.utils.yellow", | ||
], | ||
"mypackage.green": [ | ||
"mypackage.green.a.b <-> mypackage.green.b.a", | ||
], | ||
} | ||
}, | ||
) | ||
|
||
contract.render_broken_contract(check) | ||
|
||
settings.PRINTER.pop_and_assert( | ||
""" | ||
child modules of mypackage must be modular and thus circular dependencies are not allowed: | ||
- mypackage.blue.foo <-> mypackage.utils.red | ||
- mypackage.blue.red <-> mypackage.utils.yellow | ||
child modules of mypackage.green must be modular and thus circular dependencies are not allowed: | ||
- mypackage.green.a.b <-> mypackage.green.b.a | ||
""" # noqa | ||
) |