CLI ยท commit f3c717c

importlens

Python architecture & import graph analyzer

Download importlens.zip
Quickstart
unzip importlens.zip && cd importlens
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

importlens

importlens builds the module import graph of a Python codebase from source (AST only, nothing is executed). It finds import cycles and suggests the cheapest edge to break, enforces layer and contract rules in CI, reports coupling metrics, finds modules nobody reaches, diffs the architecture against a git ref, and writes a self-contained interactive HTML report.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/importlens --help

Requires Python 3.11+. Runtime dependencies: click, rich.

Quickstart

cd examples
../.venv/bin/importlens cycles sample_project
../.venv/bin/importlens check sample_project --config importlens.toml
../.venv/bin/importlens html sample_project -o report.html

SRC is either a source root (containing top-level packages) or a package directory itself; module names are always relative to the directory above the outermost enclosing package.

Shared options for every command: --exclude-type-checking, --exclude-lazy, --cache-dir (default .importlens_cache), --no-cache.

Usage

cycles

$ importlens cycles sample_project

Cycle group 1: 3 modules, 1 representative cycle(s)
  sample_project.domain.order, sample_project.domain.policies, sample_project.domain.pricing
  cycle 1 (length 3)
    sample_project.domain.order -> sample_project.domain.pricing   sample_project/domain/order.py:3
    sample_project.domain.pricing -> sample_project.domain.policies   sample_project/domain/pricing.py:2
    sample_project.domain.policies -> sample_project.domain.order   sample_project/domain/policies.py:2
  cheapest edge to break: sample_project.domain.order -> sample_project.domain.pricing (on 1 of 1 shortest cycles, 1 import site(s))
    sample_project/domain/order.py:3

Cycle group 2: 2 modules, 1 representative cycle(s)
  sample_project.services.billing, sample_project.services.order_service
  ...

Options: --json, --fail-on-cycles (exit 1 if any cycle).

check

$ importlens check sample_project --transitive
layers (direct): layer 'sample_project.infra' must not import higher layer 'sample_project.services'
    sample_project.infra.mailer -> sample_project.services.notifications   sample_project/infra/mailer.py:4
layers (indirect): layer 'sample_project.infra' must not import higher layer 'sample_project.domain'
    sample_project.infra.mailer -> sample_project.services.notifications -> sample_project.domain.user   sample_project/infra/mailer.py:4
...
3 violation(s)

$ importlens check sample_project --format github
::error file=sample_project/infra/mailer.py,line=4,title=importlens layers::layer 'sample_project.infra' must not import higher layer 'sample_project.services'

--config defaults to ./importlens.toml. Formats: text, github, json. Exit code 1 when there is any violation. GitHub Actions step:

- run: importlens check src --config importlens.toml --format github

metrics

$ importlens metrics sample_project --level package --sort distance
 Name                      Kind     Ca  Ce     I     A     D  LOC
 sample_project.utils      package   0   0  0.00  0.00  1.00    5
 sample_project.domain     package   9   0  0.00  0.17  0.83   54
 sample_project.infra      package   7   1  0.12  0.33  0.54   53
 sample_project.services   package   5   9  0.64  0.00  0.36   37
 sample_project.api        package   1   9  0.90  0.00  0.10   23

--level module|package|both, --sort name|fan_in|fan_out|instability|abstractness|distance|loc, --asc, --top N, --json.

dead

$ importlens dead sample_project
entry points: sample_project.cli, sample_project.__main__
 6 unreachable of 42 modules
 sample_project.infra.mailer
 sample_project.infra.sql_repository
 sample_project.services.legacy_export
 sample_project.utils / .utils.dates / .utils.text

Entry points come from --entry (repeatable), [project.scripts]/[project.gui-scripts] in the nearest pyproject.toml above SRC, and __main__ modules. --json, --fail-on-dead.

diff

$ importlens diff sample_project --base main --config importlens.toml
importlens diff main -> working tree: 3 new edge(s), 0 removed, 0 new module(s), 0 removed
  + edge sample_project.domain.money -> sample_project.api.router   sample_project/domain/money.py:12
  + edge sample_project.domain.product -> sample_project.services.billing   sample_project/domain/product.py:11
  new cycle: sample_project.domain.order -> sample_project.domain.product -> sample_project.services.billing -> sample_project.domain.order
  new violation layers: sample_project.domain.money -> sample_project.api.router  sample_project/domain/money.py:12

The base graph is built from a git archive export of the ref into a temp directory, so the working tree is never touched. Exit 1 on new cycles or new violations (--config optional).

graph

$ importlens graph sample_project --focus sample_project.services.billing --radius 1 --format mermaid
graph LR
  n0["sample_project.domain.order"]
  n1["sample_project.services.billing"]
  n2["sample_project.services.order_service"]
  n1 --> n0
  n1 --> n2
  n2 --> n0
  n2 --> n1
  linkStyle 1,3 stroke:#d33,stroke-width:2px

Cycle edges are red, TYPE_CHECKING edges dashed, lazy edges dotted. -o FILE writes to a file.

html

importlens html SRC -o report.html [--collapse DEPTH]. Scroll to zoom, drag to pan or move nodes, click a module for its imports and importers with line numbers, search with Enter to jump, "Highlight cycles" lists every cycle. URL hash shortcuts: #cycles, #q=text, #select=module.name. For very large codebases use --collapse 3 to keep the picture readable.

Contract configuration

[layers]                       # highest first; a layer may import itself and lower layers
order = ["app.api", "app.services", "app.domain", "app.infra"]
allow = [{ from = "app.infra.compat", to = "app.domain.types" }]

[[forbidden]]
name = "api-not-sql"
from = "app.api"
to = "app.infra.sql_repository"
message = "go through services"      # optional

[[independence]]
modules = ["app.api.users", "app.api.orders"]

[[external]]                   # third-party packages (stdlib is always allowed)
module = "app.domain"
allow = []

Patterns are globs (fnmatch) and also cover everything below a named module (app.api matches app.api.users). examples/importlens.toml is a complete example and importlens.toml at the repo root is importlens checking itself.

Results on real code

Measured on Python 3.12, one machine, --no-cache unless noted:

Target Size Command Time
importlens itself 14 modules cycles / check 0.06 s / 0.07 s
Django 5 (pip-installed) 884 modules, 3142 edges graph 0.9 s
cycles 1.8 s
metrics 1.3 s
graph with warm cache 0.2 s

Django has 16 cycle groups; the biggest has 171 modules. Most of that is package __init__ re-exports and lazy imports, so --exclude-lazy is a useful first filter.

Architecture

Module Role
parser.py AST walk for one file: imports (level, names, line, TYPE_CHECKING, lazy), classes, __all__, LOC
cache.py mtime+size / sha1 parse cache in .importlens_cache/parsed.json
builder.py module discovery, import resolution, Graph construction
model.py Graph, Module, Edge, Site dataclasses
algorithms.py Tarjan SCC, BFS, cycle analysis, collapse, focus
contracts.py config loading and rule evaluation
metrics.py, dead.py, diff.py one per command
exporters.py, html_report.py, assets/ DOT/Mermaid/JSON and the HTML report (layout.js is the pure layout, app.js the UI)
cli.py click commands and rich output

See docs/algorithms.md for the design in depth.

Tests

.venv/bin/pytest -q      # 63 tests; the JS layout tests need node and are skipped without it

Limitations