From 51fba8f7f8a32acc4e87875f79218a6c0e7253e8 Mon Sep 17 00:00:00 2001 From: Nicholas Bunn Date: Fri, 2 Aug 2024 11:34:24 +0100 Subject: [PATCH] Implement as_packages for forbidden contracts Implement the funcitonality for treating the forbidden modules as a module rather than a package. If this is specified, we set the source_moduels and forbidden_modules as the package name rather than finding all downstream modules of that package. --- docs/contract_types.rst | 9 +++++++-- src/importlinter/contracts/forbidden.py | 24 ++++++++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/contract_types.rst b/docs/contract_types.rst index a11d7fe..999ad00 100644 --- a/docs/contract_types.rst +++ b/docs/contract_types.rst @@ -11,8 +11,10 @@ Forbidden modules Forbidden contracts check that one set of modules are not imported by another set of modules. -Descendants of each module will be checked - so if ``mypackage.one`` is forbidden from importing ``mypackage.two``, then -``mypackage.one.blue`` will be forbidden from importing ``mypackage.two.green``. Indirect imports will also be checked. +By default, descendants of each module will be checked - so if ``mypackage.one`` is forbidden from importing ``mypackage.two``, then +``mypackage.one.blue`` will be forbidden from importing ``mypackage.two.green``. Indirect imports will also be checked. This +descendant behaviour can be changed by setting ``as_packages`` to ``False``: in that case, only explicitly listed modules will be +checked, not their descendants. External packages may also be forbidden. @@ -66,6 +68,9 @@ External packages may also be forbidden. - ``unmatched_ignore_imports_alerting``: See :ref:`Shared options`. - ``allow_indirect_imports``: If ``True``, allow indirect imports to forbidden modules without interpreting them as a reason to mark the contract broken. (Optional.) + - ``as_packages``: Whether to treat the source and forbidden modules as packages. If ``False``, each of the modules + passed in will be treated as a module rather than a package. Default behaviour is ``True`` (treat modules as + packages). Independence ------------ diff --git a/src/importlinter/contracts/forbidden.py b/src/importlinter/contracts/forbidden.py index d251469..799ab81 100644 --- a/src/importlinter/contracts/forbidden.py +++ b/src/importlinter/contracts/forbidden.py @@ -30,6 +30,10 @@ class ForbiddenContract(Contract): - unmatched_ignore_imports_alerting: Decides how to report when the expression in the `ignore_imports` set is not found in the graph. Valid values are "none", "warn", "error". Default value is "error". + - as_packages: Whether to treat the source and forbidden modules as packages. If + False, each of the modules passed in will be treated as a module + rather than a package. Default behaviour is True (treat modules as + packages). """ type_name = "forbidden" @@ -88,10 +92,14 @@ def sort_key(module): } if str(self.allow_indirect_imports).lower() == "true": - chains = self._get_direct_chains(source_module, forbidden_module, graph, self.as_packages) + chains = self._get_direct_chains( + source_module, forbidden_module, graph, self.as_packages # type:ignore + ) else: chains = graph.find_shortest_chains( - importer=source_module.name, imported=forbidden_module.name + importer=source_module.name, + imported=forbidden_module.name, + as_packages=self.as_packages, # type:ignore ) if chains: is_kept = False @@ -200,8 +208,16 @@ def _get_direct_chains( as_packages: bool, ) -> set[tuple[str, ...]]: chains: set[tuple[str, ...]] = set() - source_modules = self._get_all_modules_in_package(source_package, graph) - forbidden_modules = self._get_all_modules_in_package(forbidden_package, graph) + source_modules = ( + self._get_all_modules_in_package(source_package, graph) + if as_packages + else {source_package} + ) + forbidden_modules = ( + self._get_all_modules_in_package(forbidden_package, graph) + if as_packages + else {forbidden_package} + ) for source_module in source_modules: imported_module_names = graph.find_modules_directly_imported_by(source_module.name) for imported_module_name in imported_module_names: