class ForbiddenImportContract(Contract): """ Contract that defines a single forbidden import between two modules. """ importer = fields.ModuleField() imported = fields.ModuleField() def check(self, graph: ImportGraph) -> ContractCheck: forbidden_import_details = graph.get_import_details( importer=self.importer.name, imported=self.imported.name # type: ignore ) import_exists = bool(forbidden_import_details) return ContractCheck( kept=not import_exists, metadata={"forbidden_import_details": forbidden_import_details}) def render_broken_contract(self, check: "ContractCheck") -> None: output.print( f"{self.importer} is not allowed to import {self.imported}:") output.print() for details in check.metadata["forbidden_import_details"]: line_number = details["line_number"] line_contents = details["line_contents"] output.indent_cursor() output.print(f"{self.importer}:{line_number}: {line_contents}")
class IndependenceContract(Contract): """ Independence contracts check that a set of modules do not depend on each other. They do this by checking that there are no imports in any direction between the modules, even indirectly. Configuration options: - modules: A list of Modules that should be independent from each other. - 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 = "independence" 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( graph, self.ignore_imports if self.ignore_imports else [] # type: ignore ) self._check_all_modules_exist_in_graph(graph) for subpackage_1, subpackage_2 in permutations(self.modules, r=2): # type: ignore subpackage_chain_data = { "upstream_module": subpackage_2.name, "downstream_module": subpackage_1.name, "chains": [], } assert isinstance(subpackage_chain_data["chains"], list) # For type checker. chains = graph.find_shortest_chains(importer=subpackage_1.name, imported=subpackage_2.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.modules: # type: ignore if module.name not in graph.modules: raise ValueError(f"Module '{module.name}' does not exist.")
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 set of DirectImports. These imports will be ignored: if the import would cause a contract to be broken, adding it to the set 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.SetField(subfield=fields.DirectImportField(), required=False) def check(self, graph: ImportGraph) -> ContractCheck: is_kept = True invalid_chains = [] helpers.pop_imports( graph, self.ignore_imports if self.ignore_imports else [] # type: ignore ) 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) 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_packages = self.session_options["root_packages"] return not all(m.root_package_name in root_packages 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")