import-linter icon indicating copy to clipboard operation
import-linter copied to clipboard

Support for OnlyAllowContract

Open iwanbolzern opened this issue 5 years ago • 2 comments

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!

iwanbolzern avatar Mar 27 '20 14:03 iwanbolzern

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.

seddonym avatar Mar 30 '20 15:03 seddonym

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()

tom-dudley avatar Aug 25 '23 18:08 tom-dudley