-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #48 from seddonym/forbidden-imports
Add ForbiddenContract
- Loading branch information
Showing
13 changed files
with
488 additions
and
25 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,3 +68,5 @@ docs/_build | |
.mypy_cache/ | ||
|
||
.python-version | ||
|
||
pip-wheel-metadata |
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
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
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,127 @@ | ||
from importlinter.application import output | ||
from importlinter.domain import fields, helpers | ||
from importlinter.domain.contract import Contract, ContractCheck | ||
from importlinter.domain.imports import Module | ||
from importlinter.domain.ports.graph import ImportGraph | ||
|
||
|
||
class ForbiddenContract(Contract): | ||
""" | ||
Forbidden contracts check that one set of modules are not imported by another set of modules. | ||
Indirect imports will also be checked. | ||
Configuration options: | ||
- source_modules: A list of Modules that should not import the forbidden modules. | ||
- forbidden_modules: A list of Modules that should not be imported by the source modules. | ||
- ignore_imports: A list of DirectImports. These imports will be ignored: if the import | ||
would cause a contract to be broken, adding it to the list will cause | ||
the contract be kept instead. (Optional.) | ||
""" | ||
|
||
type_name = "forbidden" | ||
|
||
source_modules = fields.ListField(subfield=fields.ModuleField()) | ||
forbidden_modules = fields.ListField(subfield=fields.ModuleField()) | ||
ignore_imports = fields.ListField(subfield=fields.DirectImportField(), required=False) | ||
|
||
def check(self, graph: ImportGraph) -> ContractCheck: | ||
is_kept = True | ||
invalid_chains = [] | ||
|
||
removed_imports = helpers.pop_imports( # type: ignore | ||
graph, self.ignore_imports if self.ignore_imports else [] | ||
) | ||
|
||
self._check_all_modules_exist_in_graph(graph) | ||
self._check_external_forbidden_modules(graph) | ||
|
||
# We only need to check for illegal imports for forbidden modules that are in the graph. | ||
forbidden_modules_in_graph = [ | ||
m for m in self.forbidden_modules if m.name in graph.modules # type: ignore | ||
] | ||
|
||
for source_module in self.source_modules: # type: ignore | ||
for forbidden_module in forbidden_modules_in_graph: | ||
subpackage_chain_data = { | ||
"upstream_module": forbidden_module.name, | ||
"downstream_module": source_module.name, | ||
"chains": [], | ||
} | ||
|
||
chains = graph.find_shortest_chains( | ||
importer=source_module.name, imported=forbidden_module.name | ||
) | ||
if chains: | ||
is_kept = False | ||
for chain in chains: | ||
chain_data = [] | ||
for importer, imported in [ | ||
(chain[i], chain[i + 1]) for i in range(len(chain) - 1) | ||
]: | ||
import_details = graph.get_import_details( | ||
importer=importer, imported=imported | ||
) | ||
line_numbers = tuple(j["line_number"] for j in import_details) | ||
chain_data.append( | ||
{ | ||
"importer": importer, | ||
"imported": imported, | ||
"line_numbers": line_numbers, | ||
} | ||
) | ||
subpackage_chain_data["chains"].append(chain_data) | ||
if subpackage_chain_data["chains"]: | ||
invalid_chains.append(subpackage_chain_data) | ||
|
||
helpers.add_imports(graph, removed_imports) | ||
|
||
return ContractCheck(kept=is_kept, metadata={"invalid_chains": invalid_chains}) | ||
|
||
def render_broken_contract(self, check: "ContractCheck") -> None: | ||
count = 0 | ||
for chains_data in check.metadata["invalid_chains"]: | ||
downstream, upstream = chains_data["downstream_module"], chains_data["upstream_module"] | ||
output.print_error(f"{downstream} is not allowed to import {upstream}:") | ||
output.new_line() | ||
count += len(chains_data["chains"]) | ||
for chain in chains_data["chains"]: | ||
first_line = True | ||
for direct_import in chain: | ||
importer, imported = direct_import["importer"], direct_import["imported"] | ||
line_numbers = ", ".join(f"l.{n}" for n in direct_import["line_numbers"]) | ||
import_string = f"{importer} -> {imported} ({line_numbers})" | ||
if first_line: | ||
output.print_error(f"- {import_string}", bold=False) | ||
first_line = False | ||
else: | ||
output.indent_cursor() | ||
output.print_error(import_string, bold=False) | ||
output.new_line() | ||
|
||
output.new_line() | ||
|
||
def _check_all_modules_exist_in_graph(self, graph: ImportGraph) -> None: | ||
for module in self.source_modules: # type: ignore | ||
if module.name not in graph.modules: | ||
raise ValueError(f"Module '{module.name}' does not exist.") | ||
|
||
def _check_external_forbidden_modules(self, graph: ImportGraph) -> None: | ||
if ( | ||
self._contains_external_forbidden_modules(graph) | ||
and not self._graph_was_built_with_externals() | ||
): | ||
raise ValueError( | ||
"The top level configuration must have include_external_packages=True " | ||
"when there are external forbidden modules." | ||
) | ||
|
||
def _contains_external_forbidden_modules(self, graph: ImportGraph) -> bool: | ||
root_package = Module(self.session_options["root_package"]) | ||
return not all( | ||
m.is_descendant_of(root_package) for m in self.forbidden_modules # type: ignore | ||
) | ||
|
||
def _graph_was_built_with_externals(self) -> bool: | ||
return self.session_options.get("include_external_packages") in ("True", "true") |
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,11 @@ | ||
[importlinter] | ||
root_package = testpackage | ||
contract_types = | ||
forbidden_import: tests.helpers.contracts.ForbiddenImportContract | ||
|
||
|
||
[importlinter:contract:one] | ||
name=Custom kept contract | ||
type=forbidden_import | ||
importer=testpackage.utils | ||
imported=testpackage.low |
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 |
---|---|---|
@@ -1,12 +1,10 @@ | ||
[importlinter] | ||
root_package = testpackage | ||
include_external_packages = True | ||
contract_types = | ||
forbidden: tests.helpers.contracts.ForbiddenImportContract | ||
|
||
|
||
[importlinter:contract:one] | ||
name=External broken contract | ||
name=External kept contract | ||
type=forbidden | ||
importer=testpackage.utils | ||
imported=pytest | ||
source_modules=testpackage.high.blue | ||
forbidden_modules=pytest |
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 |
---|---|---|
@@ -1,12 +1,10 @@ | ||
[importlinter] | ||
root_package = testpackage | ||
include_external_packages = True | ||
contract_types = | ||
forbidden: tests.helpers.contracts.ForbiddenImportContract | ||
|
||
|
||
[importlinter:contract:one] | ||
name=External kept contract | ||
type=forbidden | ||
importer=testpackage.high.blue | ||
imported=pytest | ||
source_modules=testpackage.high.blue | ||
forbidden_modules=sqlalchemy |
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
Oops, something went wrong.