Support for OnlyAllowContract
Thank you for your great linter. This is exactly what I was looking for.
It would be great if it would support an OnlyAllow Contract, which should only allow the defined imports and no others. This would especially be handy with include_external_packages=False. This way you could simple code your architecture and don't have to define the inverse of your architecture every time.
Thanks in advance!
Hi Iwan,
Thanks for your message - great idea!
If you'd find this useful, you may be interested that you can create custom contract types - so you don't need to wait around for me to write one! https://import-linter.readthedocs.io/en/stable/custom_contract_types.html
Let me know if you have any questions.
I needed the same functionality so leaving this here in case it's of use to anyone. Note the use of find_modules_that_directly_import.
from grimp import ImportGraph
from importlinter.application import output
from importlinter.domain import fields
from importlinter.domain.contract import Contract, ContractCheck
class OnlyAllowContract(Contract):
"""
OnlyAllow contract checks that only a set of allowed modules can import another set of target modules.
Configuration options:
- allowed_modules: A list of Modules that are allowed to import the target modules.
- target_modules: A list of Modules that can be imported by the allowed modules.
"""
type_name = "only_allow"
allowed_module = fields.StringField
allowed_modules = fields.ListField(subfield=fields.ModuleField())
target_modules = fields.ListField(subfield=fields.ModuleField())
def check(self, graph: ImportGraph, verbose: bool) -> ContractCheck:
is_kept = True
invalid_chains = []
for target_module in self.target_modules:
importing_modules = graph.find_modules_that_directly_import(target_module.name)
forbidden_importers = importing_modules - {allowed_module.name for allowed_module in self.allowed_modules}
if forbidden_importers:
is_kept = False
invalid_chains.append({
"target_module": target_module.name,
"forbidden_importers": forbidden_importers,
})
return ContractCheck(
kept=is_kept, metadata={"invalid_chains": invalid_chains}
)
def render_broken_contract(self, check: "ContractCheck") -> None:
for chain_data in check.metadata["invalid_chains"]:
target_module = chain_data["target_module"]
output.print_error(f"{target_module} is not allowed to be imported by:", bold=True)
output.new_line()
for forbidden_importer in chain_data["forbidden_importers"]:
output.indent_cursor()
output.print_error(forbidden_importer, bold=False)
output.new_line()