Import Linter
repository·main·Indexed 22 days ago
https://github.com/seddonym/import-linterA 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.
What's inside import-linter
- 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.
Define independent or non-independent sibling modules in a layer
mainYou 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.lowUse the `forbidden` contract type
mainThe
forbiddencontract type ensures that a specific set of modules (source_modules) does not import another set of modules (forbidden_modules).By default,
import-lintertreats modules as packages (as_packages = True), meaning ifmypackage.oneis forbidden from importingmypackage.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 = falseHow caching works in Import Linter
mainImport 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:
- Building the graph: Scanning packages to identify module imports and storing them in a Grimp graph. This phase is cached.
- Contract checking: Verifying the graph against defined contracts. This phase is NOT cached.
Use the `layers` contract to enforce layered architecture
mainThe
layerscontract 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. Ifcontainersare 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,layersnames are treated as relative to these containers.exhaustive: (Optional, defaultFalse) Iftrue, every module within a container must be explicitly declared as a layer. If a module exists in a container but isn't in thelayerslist, 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", ]Use independence contracts to prevent module dependencies
mainAn
independencecontract 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", ]Understand the available contract types
mainImport 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.
Use the `protected` contract type to restrict imports
mainThe
protectedcontract type prevents specific modules from being imported directly by any module except those explicitly listed in an allow-list.By default,
import-lintertreats the specified modules as packages (as_packages = True), meaning descendants of a protected module are also protected and can only be imported by theallowed_importersor by other descendants of the same protected module. If you setas_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 theprotected_modules. Supports wildcards.as_packages: (Boolean) IfTrue(default), descendants of protected modules are also protected. IfFalse, 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.*"]Navigate and explore the dependency graph in the UI
mainThe Interactive UI visualizes dependencies between the immediate children of the package you are viewing.
- Reading Arrows: An arrow from package
Ato packageBindicates thatAdepends onB(at least one module inAimports a module inB). - 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.
- Reading Arrows: An arrow from package
How Import Linter works
mainImport Linter allows you to impose constraints on imports between Python modules by defining 'contracts' in a configuration file.
To use it:
- Install the package.
- Create a
.importlinterconfiguration file in your project root. - Define your
root_packageand one or more contracts. - Run the
lint-importscommand to check for violations.
If a module violates a defined contract (e.g., a forbidden import),
lint-importswill error.# .importlinter [importlinter] root_package = myproject [importlinter:contract:one] name = Green must not import blue type = forbidden source_modules = myproject.green forbidden_modules = myproject.blueConfigure the `acyclic_siblings` contract
mainTo use the
acyclic_siblingscontract, define it in your configuration file (INI or TOML) with thetype = acyclic_siblingsand specify theancestorsyou want to monitor.Configuration Options
Option Description ancestorsThe packages whose descendants should be checked for cycles. Supports wildcards. depth(Optional) How many generations of siblings to check, relative to the ancestors. 0checks only direct children;1checks grandchildren as sets of siblings, etc. Default is10.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_descendantsvsignore_imports:skip_descendantsonly 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, useignore_importsinstead.[[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", ]Select specific contracts by ID
mainWhen running the linter via the
--contractargument, 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 asmy_id). - TOML: The ID is explicitly provided using the
idkey within the contract table.
- INI: The ID is the suffix of the section header (e.g.,