Import Linter

repository·main·Indexed 22 days ago

https://github.com/seddonym/import-linter

A tool for enforcing architectural constraints on Python imports to ensure module dependencies follow predefined rules. Version 2.13 includes a visual interface for exploring package architecture and supports various contract types such as forbidden, independence, and acyclic_siblings to prevent unauthorized dependencies. It features a file-based cache to accelerate graph building and provides a Python API via `importlinter.api.read_configuration()` for programmatic configuration retrieval.

Tokens
16.8K
Snippets
57
Records
83
Agent score
76%

What's inside import-linter

  1. Overview of Import Linter

    main
    Import Linter is a tool designed to lint Python architecture by imposing constraints on imports between modules. It helps maintain architectural integrity by preventing unauthorized or unintended dependencies. Additionally, it provides a browser-based user interface to explore the architecture of any Python package.
  2. Define independent or non-independent sibling modules in a layer

    main

    You can group multiple sibling modules within a single layer using specific separators. This allows you to control whether siblings within the same layer are allowed to import from each other.

    1. Independent Siblings (using |) Use the pipe character | to separate modules. Siblings in this group are considered independent and cannot import from each other.

    2. Non-independent Siblings (using :) Use the colon character : to separate modules. Siblings in this group are allowed to import from each other.

    Warning: Do not mix | and : on the same line; this will result in an invalid contract.

    # Independent siblings (cannot import each other)
    layers = 
        mypackage.high
        mypackage.blue | mypackage.green | mypackage.yellow
        mypackage.low
    
    # Non-independent siblings (can import each other)
    layers = 
        mypackage.high
        mypackage.blue : mypackage.green : mypackage.yellow
        mypackage.low
  3. Use the `forbidden` contract type

    main

    The forbidden contract type ensures that a specific set of modules (source_modules) does not import another set of modules (forbidden_modules).

    By default, import-linter treats modules as packages (as_packages = True), meaning if mypackage.one is forbidden from importing mypackage.two, then all descendants (e.g., mypackage.one.blue) are also forbidden from importing descendants of the forbidden module (e.g., mypackage.two.green). This includes indirect imports.

    To prevent a module from importing its own descendants, you must set as_packages = False. In this mode, only the explicitly listed modules are checked, not their descendants.

    [[tool.importlinter.contracts]]
    name = "Forbidden descendants contract"
    type = "forbidden"
    source_modules = ["mypackage.one"]
    forbidden_modules = ["mypackage.one.**"]
    as_packages = false
  4. How caching works in Import Linter

    main

    Import Linter uses a file-based cache to accelerate linting runs, particularly on large codebases. The caching mechanism specifically optimizes the first phase of a linting run: Building the graph.

    A full run consists of two phases:

    1. Building the graph: Scanning packages to identify module imports and storing them in a Grimp graph. This phase is cached.
    2. Contract checking: Verifying the graph against defined contracts. This phase is NOT cached.
  5. Use the `layers` contract to enforce layered architecture

    main

    The layers contract enforces a 'layered architecture' where higher layers can depend on lower layers, but not vice versa. The order of layers in the configuration is from highest to lowest level.

    Rules:

    • Higher layers (listed first) can import from any lower layer.
    • Lower layers (listed later) cannot import from any higher layer.
    • This rule applies to all modules within those layers and includes indirect imports.

    Configuration Options:

    • layers: An ordered list of layer names. If containers are used, these names must be relative to the container. Layers wrapped in parentheses (e.g., (foo)) are optional and will be ignored if not present in the file system.
    • containers: (Optional) A list of absolute module names that act as parents for the layers. When used, layers names are treated as relative to these containers.
    • exhaustive: (Optional, default False) If true, every module within a container must be explicitly declared as a layer. If a module exists in a container but isn't in the layers list, the contract fails.
    • exhaustive_ignores: (Optional) A list of layers to ignore when performing exhaustive checks.
    • ignore_imports: (Shared option) See project documentation for details.
    • unmatched_ignore_imports_alerting: (Shared option) See project documentation for details.
    [[tool.importlinter.contracts]]
    name = "My layers contract"
    type = "layers"
    layers = [
        "mypackage.high",
        "mypackage.medium",
        "mypackage.low",
    ]
  6. Use independence contracts to prevent module dependencies

    main

    An independence contract ensures that a specific set of modules or subpackages do not depend on each other. The linter checks that there are no imports in any direction between the listed modules, including indirect dependencies. If any module in the list imports another module in the same list, the contract will fail.

    [[tool.importlinter.contracts]]
    name = "My independence contract"
    type = "independence"
    modules = [
        "mypackage.foo",
        "mypackage.bar",
        "mypackage.baz",
    ]
  7. Understand the available contract types

    main

    Import Linter uses different contract types to enforce various architectural constraints. The built-in types are:

    • Forbidden: Prevents one specific set of modules from being imported by another set.
    • Protected: Prevents modules from being directly imported, except by modules explicitly listed in an allow-list.
    • Layers: Enforces a 'layered architecture' where modules can only import from specific layers below them.
    • Independence: Prevents a set of modules from depending on each other (ensuring they remain decoupled).
    • Acyclic siblings: Forbids dependency cycles between sibling modules.
  8. Use the `protected` contract type to restrict imports

    main

    The protected contract type prevents specific modules from being imported directly by any module except those explicitly listed in an allow-list.

    By default, import-linter treats the specified modules as packages (as_packages = True), meaning descendants of a protected module are also protected and can only be imported by the allowed_importers or by other descendants of the same protected module. If you set as_packages = False, the contract applies strictly to the specific modules listed.

    Key Configuration Options:

    • protected_modules: The modules that are restricted from being imported. Supports wildcards.
    • allowed_importers: The list of modules permitted to import the protected_modules. Supports wildcards.
    • as_packages: (Boolean) If True (default), descendants of protected modules are also protected. If False, only the exact modules listed are treated as the targets.
    [[tool.importlinter.contracts]]
    name = "Restrict models access"
    type = "protected"
    protected_modules = ["mypackage.**.models"]
    allowed_importers = ["mypackage.colors.*"]
  9. Navigate and explore the dependency graph in the UI

    main

    The Interactive UI visualizes dependencies between the immediate children of the package you are viewing.

    • Reading Arrows: An arrow from package A to package B indicates that A depends on B (at least one module in A imports a module in B).
    • Drilling Down: You can click on any package node to view its children. Note that only packages (modules that contain submodules) are clickable; leaf modules cannot be drilled into.
  10. How Import Linter works

    main

    Import Linter allows you to impose constraints on imports between Python modules by defining 'contracts' in a configuration file.

    To use it:

    1. Install the package.
    2. Create a .importlinter configuration file in your project root.
    3. Define your root_package and one or more contracts.
    4. Run the lint-imports command to check for violations.

    If a module violates a defined contract (e.g., a forbidden import), lint-imports will error.

    # .importlinter
    
    [importlinter]
    root_package = myproject
    
    [importlinter:contract:one]
    name = Green must not import blue
    type = forbidden
    source_modules = myproject.green
    forbidden_modules = myproject.blue
  11. Configure the `acyclic_siblings` contract

    main

    To use the acyclic_siblings contract, define it in your configuration file (INI or TOML) with the type = acyclic_siblings and specify the ancestors you want to monitor.

    Configuration Options

    OptionDescription
    ancestorsThe packages whose descendants should be checked for cycles. Supports wildcards.
    depth(Optional) How many generations of siblings to check, relative to the ancestors. 0 checks only direct children; 1 checks grandchildren as sets of siblings, etc. Default is 10.
    skip_descendants(Optional) Packages whose descendants should not be checked for cycles. Supports wildcards.
    ignore_imports(Optional) Specific import paths to ignore entirely. See shared options.
    unmatched_ignore_imports_alerting(Optional) See shared options.

    Note on skip_descendants vs ignore_imports: skip_descendants only stops the linter from drilling down into a specific package to check its own children for cycles. It does not prevent imports from that package being used to form a cycle between siblings in an earlier generation. If you want to completely ignore a specific import relationship, use ignore_imports instead.

    [[tool.importlinter.contracts]]
    name = "Acyclic siblings contract with more options"
    type = "acyclic_siblings"
    ancestors = [
        "mypackage.foo",
        "mypackage.bar.*",
    ]
    depth = 5
    skip_descendants = [
        "mypackage.foo.purple",
        "mypackage.foo.**.orange",
    ]
    ignore_imports = [
        "mypackage.foo.blue.one -> mypackage.foo.green.two",
    ]
  12. Select specific contracts by ID

    main

    When running the linter via the --contract argument, you can target specific contracts using their IDs. The method of defining these IDs depends on your configuration format:

    • INI: The ID is the suffix of the section header (e.g., [importlinter:contract:my_id] defines the ID as my_id).
    • TOML: The ID is explicitly provided using the id key within the contract table.