python-import-linter-setup

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Python Import-Linter Architecture Setup

Python Import-Linter 架构配置

This is a one-time setup skill. It adds an
import-linter
contract set to
pyproject.toml
that makes the layered architecture from
python-ddd
a thing the build checks, not a thing people remember. A boundary written only in a doc rots — someone does
from myapp.infrastructure.db import session
inside a domain model in a hurry, review misses it, and six months later the domain can't be tested without a database. Encoded as an import-linter contract, the same mistake fails
lint-imports
and blocks the merge.
You do not hand-roll an AST scanner (that is what the Rust gate does, because Rust has no equivalent tool). Python has the purpose-built tool:
import-linter
builds the real import graph with
grimp
(static analysis — it parses, it does not execute your code, so no database, broker, or services need to be running) and checks declarative contracts. It follows re-exports and
__init__
aggregation that a naive
grep
would miss.
This is the Python sibling of
frontend-vue-eslint-setup
(ESLint boundary rules) and
rust-architecture-test-setup
(a
tests/structure/
cargo gate) — same goal, ecosystem-native mechanism.
这是一项一次性配置技能。它会将一套
import-linter
规则添加到
pyproject.toml
中,让
python-ddd
的分层架构成为构建过程会检查的约束,而非依赖开发者记忆的规范。仅写在文档里的边界规则会逐渐失效——比如有人情急之下在领域模型中写下
from myapp.infrastructure.db import session
,代码评审没发现,六个月后领域层就无法脱离数据库进行测试了。而将规则编码为import-linter约束后,同样的错误会导致
lint-imports
检查失败,从而阻止代码合并。
你不需要手动编写AST扫描器(Rust生态需要这么做,因为Rust没有等效工具)。Python有专门的工具:
import-linter
通过
grimp
构建真实的导入依赖图(静态分析——它仅解析代码,不会执行代码,因此无需启动数据库、消息队列或其他服务),并检查声明式规则。它能追踪重导出和
__init__
聚合的依赖,这是简单
grep
做不到的。
这是
frontend-vue-eslint-setup
(ESLint边界规则)和
rust-architecture-test-setup
tests/structure/
cargo校验)的Python版本——目标相同,采用生态原生的实现机制。

When to use

适用场景

  • Bootstrapping a new python-ddd backend → enforce hard-fail from the first commit (a greenfield project has zero violations, so there is nothing to ratchet). This is the skill's default.
  • Adding enforcement to an existing Python service that has none → see Severity;
    import-linter
    is pass/fail with no "warn" tier, so you ratchet with
    ignore_imports
    .
Run this once. After the contracts exist in
pyproject.toml
, you do not re-run this skill — you only edit the contract list as the architecture grows.
  • 启动全新的python-ddd后端项目→从首次提交就启用强制失败校验(全新项目没有任何违规,无需逐步收紧规则)。这是本技能的默认适用场景。
  • 已有的无架构约束Python服务添加校验→参见规则严格性:新项目 vs 已有代码库
    import-linter
    只有通过/失败两种状态,没有"警告"级别,因此你需要通过
    ignore_imports
    逐步收紧规则。
只需运行一次本技能。规则写入
pyproject.toml
后,无需再次运行——仅需在架构扩展时编辑规则列表即可。

What it enforces

约束内容

The four-layer model from
python-ddd
, with dependencies pointing strictly inward:
presentation  →  application  →  domain
                  infrastructure ───┘   (implements the domain's ports)
  • domain
    depends on nothing
    — not on the other layers, and not on any framework (SQLAlchemy, FastAPI, the DB driver). It is plain Python.
  • application
    depends on
    domain
    (and may import the concrete Unit of Work / repositories from
    infrastructure
    as default-argument values — the one sanctioned inward exception in the skill).
  • presentation
    depends on
    application
    and
    domain
    — never straight into
    infrastructure
    .
  • infrastructure
    implements the domain's ports
    — it depends on
    domain
    and external libraries, never on
    application
    or
    presentation
    .
These encode separation of concerns and single responsibility: each layer has one reason to exist, and the dependency arrows only ever point one way.
python-ddd
的四层模型,依赖严格向内指向:
presentation  →  application  →  domain
                  infrastructure ───┘   (实现领域层的端口)
  • domain
    层无任何依赖
    ——不依赖其他层,也不依赖任何框架(SQLAlchemy、FastAPI、数据库驱动等)。它是纯Python代码。
  • application
    层依赖
    domain
    (可将基础设施层的具体工作单元/仓库作为默认参数导入——这是本技能允许的唯一向内例外)。
  • presentation
    层依赖
    application
    层和
    domain
    ——绝不能直接依赖
    infrastructure
    层。
  • infrastructure
    层实现领域层的端口
    ——它依赖
    domain
    层和外部库,绝不依赖
    application
    层或
    presentation
    层。
这些规则编码了关注点分离和单一职责原则:每个层只有一个存在的理由,依赖箭头始终单向指向。

Step 1 — Confirm your package root and layers

步骤1——确认包根目录和分层

import-linter
needs the importable root package and the layer sub-packages to exist and be discoverable.
  • root_package
    is whatever you
    import
    — often the distribution package (
    myapp
    ), or literally
    src
    if the project uses a
    src/
    directory as the package (run
    lint-imports
    from the directory that contains it).
  • The four layers are sub-packages:
    myapp.presentation
    ,
    myapp.application
    ,
    myapp.domain
    ,
    myapp.infrastructure
    . Each needs an
    __init__.py
    .
If your project splits transport into separate
api/
(routers, middleware) and
presentation/
(schemas, serializers) packages, that is a 5-layer variant — see Customization. The canonical four-layer set is below.
import-linter
需要可导入的根包和分层子包已存在且可被发现。
  • **
    root_package
    **是你实际导入的包——通常是发布包(如
    myapp
    ),如果项目采用
    src/
    目录作为包结构,则为
    src
    (需从包含该目录的路径运行
    lint-imports
    )。
  • 四个分层是子包:
    myapp.presentation
    myapp.application
    myapp.domain
    myapp.infrastructure
    。每个子包都需要
    __init__.py
    文件。
如果你的项目将传输层拆分为独立的
api/
(路由、中间件)和
presentation/
(Schema、序列化器)包,这是五层变体——参见自定义选项。标准四层结构如下。

Step 2 — Install import-linter

步骤2——安装import-linter

bash
uv add --dev import-linter      # or: pip install import-linter
The CLI is
lint-imports
. With uv:
uv run lint-imports
(or, to try it without committing the dependency first,
uv run --with import-linter lint-imports
).
bash
uv add --dev import-linter      # 或:pip install import-linter
CLI命令为
lint-imports
。使用uv时:
uv run lint-imports
(或者不先提交依赖,直接运行
uv run --with import-linter lint-imports
进行测试)。

Step 3 — Add the contracts to
pyproject.toml

步骤3——将规则添加到
pyproject.toml

Drop this in verbatim, then replace
myapp
with your
root_package
and tune the framework list in contract 2 (Step-by-step reasons for every line are in Understanding the contracts).
toml
[tool.importlinter]
root_package = "myapp"
include_external_packages = true   # required: contract 2 names external libraries
直接复制以下内容,然后将
myapp
替换为你的
root_package
,并调整规则2中的框架列表(每一行的详细说明见理解规则(以便自定义))。
toml
[tool.importlinter]
root_package = "myapp"
include_external_packages = true   # 必填:规则2引用了外部库

1. Layered architecture — dependencies point strictly inward.

1. 分层架构——依赖严格向内指向。

Infrastructure is DELIBERATELY not in this chain (see "Understanding").

故意将Infrastructure排除在该链之外(参见「理解规则」)。

[[tool.importlinter.contracts]] name = "Layered architecture (presentation -> application -> domain)" type = "layers" layers = [ "myapp.presentation", "myapp.application", "myapp.domain", ]
[[tool.importlinter.contracts]] name = "Layered architecture (presentation -> application -> domain)" type = "layers" layers = [ "myapp.presentation", "myapp.application", "myapp.domain", ]

2. The domain stays framework-free and never reaches into infrastructure.

2. 领域层无框架依赖,绝不直接依赖基础设施层。

[[tool.importlinter.contracts]] name = "Domain is framework-free" type = "forbidden" source_modules = ["myapp.domain"] forbidden_modules = [ "myapp.infrastructure", # The project's infrastructure & transport libraries — TUNE THIS LIST. # NOT pydantic: domain commands / value objects may legitimately use it. "sqlalchemy", "fastapi", "httpx", ]
[[tool.importlinter.contracts]] name = "Domain is framework-free" type = "forbidden" source_modules = ["myapp.domain"] forbidden_modules = [ "myapp.infrastructure", # 项目的基础设施和传输层库——调整此列表。 # 不要添加pydantic:领域命令/值对象可合法使用它。 "sqlalchemy", "fastapi", "httpx", ]

3. Infrastructure implements the domain's ports — it must not depend on the

3. 基础设施层仅向内依赖——不得依赖上层。(允许infrastructure -> domain)

layers above it. (infrastructure -> domain stays allowed.)

[[tool.importlinter.contracts]] name = "Infrastructure depends only inward" type = "forbidden" source_modules = ["myapp.infrastructure"] forbidden_modules = [ "myapp.application", "myapp.presentation", ]
[[tool.importlinter.contracts]] name = "Infrastructure depends only inward" type = "forbidden" source_modules = ["myapp.infrastructure"] forbidden_modules = [ "myapp.application", "myapp.presentation", ]

4. Presentation talks to application/domain — never straight into infrastructure.

4. 表现层仅与应用层/领域层交互——绝不直接依赖基础设施层。

allow_indirect_imports: presentation -> application -> infrastructure.uow is

allow_indirect_imports:表现层→应用层→infrastructure.uow是本技能认可的工作单元默认参数链,因此仅标记直接违规。

the skill-sanctioned UoW default-arg chain, so only DIRECT leaks are flagged.

[[tool.importlinter.contracts]] name = "Presentation never imports infrastructure directly" type = "forbidden" source_modules = ["myapp.presentation"] forbidden_modules = ["myapp.infrastructure"] allow_indirect_imports = true
undefined
[[tool.importlinter.contracts]] name = "Presentation never imports infrastructure directly" type = "forbidden" source_modules = ["myapp.presentation"] forbidden_modules = ["myapp.infrastructure"] allow_indirect_imports = true
undefined

Step 4 — Run it and wire the gate

步骤4——运行校验并接入CI

bash
uv run lint-imports        # exits non-zero on any broken contract
Wire that command into whatever already runs your fast checks — a
just
recipe, a Makefile target, a
pre-commit
hook — and into CI next to
ruff
and
mypy
:
bash
undefined
bash
uv run lint-imports        # 违反规则时返回非零退出码
将该命令接入你的快速校验流程——比如
just
脚本、Makefile目标、
pre-commit
钩子——并接入CI,
ruff
mypy
放在一起
bash
undefined

justfile

justfile

lint-backend: uv run ruff check . uv run lint-imports # <- the architecture gate

`lint-imports` is hermetic (static graph, no services), so it is a cheap CI job:
checkout → `uv sync` → `uv run lint-imports`. Without that CI step the contracts
only report locally and the boundary is not actually gated.
lint-backend: uv run ruff check . uv run lint-imports # <- 架构校验 gate

`lint-imports`是封闭环境的静态分析(仅分析依赖图,无需启动服务),因此是轻量的CI任务:拉取代码→`uv sync`→`uv run lint-imports`。如果没有CI步骤,规则仅能在本地生效,无法真正实现架构约束。

Step 5 — Verify a contract actually fires

步骤5——验证规则确实会触发

Do not trust a contract you have not seen break. Temporarily plant a violation, run, confirm, revert:
bash
undefined
不要信任未验证过的规则。临时添加一个违规导入,运行校验,确认触发后再撤销:
bash
undefined

Add a forbidden import to any domain module, then lint:

在任意领域层模块中添加禁止的导入,然后运行校验:

echo "import sqlalchemy" >> myapp/domain/<some_module>.py uv run lint-imports # expect: "Domain is framework-free" BROKEN git checkout -- myapp/domain/<some_module>.py

You should see contract 2 report the illegal `myapp.domain... -> sqlalchemy`
import. If it stays green, `root_package` is wrong or the layer package path
does not match — fix that before trusting the gate.
echo "import sqlalchemy" >> myapp/domain/<some_module>.py uv run lint-imports # 预期结果:"Domain is framework-free" 规则失效 git checkout -- myapp/domain/<some_module>.py

你应该会看到规则2报告非法的`myapp.domain... -> sqlalchemy`导入。如果校验仍通过,说明`root_package`配置错误或分层路径不匹配——在信任该规则前请修复此问题。

Understanding the contracts (so you can adapt them)

理解规则(以便自定义)

Two contract types do all the work, and the split between them is the whole trick:
  • A
    layers
    contract is an ordered list, highest first. Higher layers may import lower ones; lower layers may not import higher ones. So
    presentation > application > domain
    forbids
    domain → application
    ,
    domain → presentation
    , and
    application → presentation
    in one stroke.
  • A
    forbidden
    contract says "modules in
    source_modules
    must not import anything in
    forbidden_modules
    " — direct and indirect, unless you set
    allow_indirect_imports
    .
Why
infrastructure
is not in the
layers
chain.
This is the DDD wrinkle. In dependency-flow terms infrastructure is the "lowest" layer (everything can end up depending on it), so the naive move is to append it:
presentation > application > domain > infrastructure
. That is wrong — a
layers
contract lets higher layers import lower ones, so it would permit
domain → infrastructure
, the exact thing you must forbid. Meanwhile
application → infrastructure
(importing the concrete UoW as a default arg) must stay allowed. You can't get both from one ordered chain. So infrastructure is kept out of the chain and pinned with explicit
forbidden
contracts:
domain ✗ infrastructure
(contract 2) and
infrastructure ✗ application, presentation
(contract 3).
application → infrastructure
is forbidden nowhere, so it stays legal.
Why
include_external_packages = true
.
Contract 2 names external libraries (
sqlalchemy
,
fastapi
) in
forbidden_modules
.
import-linter
refuses to run a forbidden contract against external modules unless this top-level flag is set — without it you get a hard error, not a silent pass.
Why
allow_indirect_imports
on contract 4.
A presentation router imports an application service, and that service imports
infrastructure.uow
for its default argument (the sanctioned pattern). Following indirect imports, contract 4 would flag presentation for that legitimate transitive path —
presentation → application → infrastructure.uow
— drowning the real signal.
allow_indirect_imports = true
restricts the contract to direct
presentation → infrastructure
imports, which are the genuine leaks (a router opening a DB session itself instead of going through a service).
两种规则类型完成所有工作,它们的分工是关键:
  • layers
    规则是有序列表,顶层在前。上层可导入下层,下层不得
    导入上层。因此
    presentation > application > domain
    会一次性禁止
    domain → application
    domain → presentation
    application → presentation
    三种反向依赖。
  • **
    forbidden
    **规则表示
    source_modules
    中的模块不得导入
    forbidden_modules
    中的任何内容——包括直接和间接导入,除非设置
    allow_indirect_imports
为什么
infrastructure
不在
layers
链中
。这是DDD的特殊之处。从依赖流向来看,基础设施层是"最低"层(所有层最终都可能依赖它),因此直观的做法是将其追加到链中:
presentation > application > domain > infrastructure
。但这是错误的——
layers
规则允许上层导入下层,这会允许
domain → infrastructure
,而这正是必须禁止的。同时,
application → infrastructure
(导入具体工作单元作为默认参数)必须保持允许。单一有序链无法同时满足这两个要求。因此将基础设施层排除在链外,用显式的
forbidden
规则约束:
domain ✗ infrastructure
(规则2)和
infrastructure ✗ application, presentation
(规则3)。
application → infrastructure
未被任何规则禁止,因此是合法的。
为什么设置
include_external_packages = true
。规则2在
forbidden_modules
中引用了外部库(
sqlalchemy
fastapi
)。
import-linter
禁止对外部模块运行forbidden规则,除非设置此顶层标志——否则会直接报错,而非静默通过。
为什么规则4设置
allow_indirect_imports
。表现层路由导入应用层服务,而该服务导入
infrastructure.uow
作为默认参数(这是认可的模式)。如果追踪间接导入,规则4会将这种合法的传递路径
presentation → application → infrastructure.uow
标记为违规——淹没真实的违规信号。
allow_indirect_imports = true
将规则限制为直接
presentation → infrastructure
导入,这些才是真正的违规(比如路由直接打开数据库会话,而非通过服务)。

Severity: new project vs. existing codebase

规则严格性:新项目 vs 已有代码库

  • New project: every contract hard-fails from commit one. A greenfield python-ddd project has zero violations, so there is nothing to soften — and
    python-ddd
    is greenfield-only by design.
  • Existing project with violations:
    import-linter
    has no "warn" tier, so you ratchet differently. Turn on the
    Domain is framework-free
    contract first (usually closest to clean), and baseline the others with documented per-contract exceptions:
    toml
    ignore_imports = [
        "myapp.presentation.routers.auth -> myapp.infrastructure.db",  # TODO: route via auth service
    ]
    Run, fix violations, delete each
    ignore_imports
    line as you clear it, until the list is empty. Add a note to
    CLAUDE.md
    : "architecture is import-linted; do not add new
    ignore_imports
    ." Never leave an unexplained
    ignore_imports
    permanently — a frozen exception is a boundary everyone has learned to ignore.
  • 新项目:所有规则从首次提交就强制失败。全新的python-ddd项目没有任何违规,无需放宽规则——且
    python-ddd
    本质上是为新项目设计的。
  • 存在违规的已有项目
    import-linter
    没有"警告"级别,因此需要逐步收紧规则。先启用
    Domain is framework-free
    规则(通常最接近合规),然后为其他规则添加带文档说明的逐规则例外:
    toml
    ignore_imports = [
        "myapp.presentation.routers.auth -> myapp.infrastructure.db",  # TODO:通过认证服务路由
    ]
    运行校验,修复违规,每修复一个就删除对应的
    ignore_imports
    条目,直到列表为空。在
    CLAUDE.md
    中添加说明:"架构已启用import-linter校验;请勿添加新的
    ignore_imports
    条目。"永远不要保留未解释的
    ignore_imports
    条目——固化的例外会让所有人都忽视边界规则。

Customization knobs

自定义选项

  • root_package
    — set to the package you actually import (
    myapp
    , or
    src
    for a
    src
    -as-package layout).
    lint-imports
    must run from the directory that makes it importable.
  • The framework list (contract 2) — list the project's real infrastructure and transport libraries: the ORM (
    sqlalchemy
    ), web framework (
    fastapi
    ), DB driver (
    aiosqlite
    ,
    asyncpg
    ), migration tool (
    alembic
    ), message-broker client (
    nats
    ,
    aio_pika
    ), HTTP client (
    httpx
    ), cloud SDKs. Do not list
    pydantic
    — the skill uses Pydantic for domain commands and value objects. Add it only if your project keeps the domain strictly dataclass-only.
  • 5-layer variant (separate
    api/
    +
    presentation/
    ).
    If transport is split —
    api/
    (routers, middleware, app) above
    presentation/
    (schemas, serializers) — make
    api
    the top layer and forbid it from infrastructure too:
    toml
    layers = ["myapp.api", "myapp.presentation", "myapp.application", "myapp.domain"]
    # ...plus a 5th contract mirroring contract 4 for myapp.api:
    [[tool.importlinter.contracts]]
    name = "API never imports infrastructure directly"
    type = "forbidden"
    source_modules = ["myapp.api"]
    forbidden_modules = ["myapp.infrastructure"]
    allow_indirect_imports = true
    (Genuine composition wiring belongs in
    main.py
    , which sits outside the layer packages and is unaffected.)
  • Bounded contexts. When the project splits into contexts (
    myapp.billing
    ,
    myapp.identity
    ), add an
    independence
    contract so they don't import each other, and/or repeat the layer contract per context.
  • root_package
    ——设置为你实际导入的包(如
    myapp
    ,或采用
    src
    作为包结构时的
    src
    )。
    lint-imports
    必须从能导入该包的路径运行。
  • 规则2中的框架列表——列出项目实际使用的基础设施和传输层库:ORM(
    sqlalchemy
    )、Web框架(
    fastapi
    )、数据库驱动(
    aiosqlite
    asyncpg
    )、迁移工具(
    alembic
    )、消息队列客户端(
    nats
    aio_pika
    )、HTTP客户端(
    httpx
    )、云SDK等。不要添加
    pydantic
    ——本技能允许领域层命令和值对象使用Pydantic。仅当你的项目严格要求领域层仅使用dataclass时才添加它。
  • 五层变体(独立的
    api/
    +
    presentation/
    。如果传输层被拆分——
    api/
    (路由、中间件、应用实例)在
    presentation/
    (Schema、序列化器)之上——将
    api
    设为顶层,并禁止它直接依赖基础设施层:
    toml
    layers = ["myapp.api", "myapp.presentation", "myapp.application", "myapp.domain"]
    # ...添加第5条规则,镜像规则4用于myapp.api:
    [[tool.importlinter.contracts]]
    name = "API never imports infrastructure directly"
    type = "forbidden"
    source_modules = ["myapp.api"]
    forbidden_modules = ["myapp.infrastructure"]
    allow_indirect_imports = true
    (真正的组合配置应放在
    main.py
    中,该文件不属于分层包,不受规则约束。)
  • 限界上下文。当项目拆分为多个上下文(如
    myapp.billing
    myapp.identity
    )时,添加
    independence
    规则以禁止它们互相导入,或为每个上下文重复分层规则。

Compliance with the python-ddd & code-style skills

与python-ddd和代码风格技能的兼容性

  • This gate enforces
    python-ddd
    §1 (inward-only layer dependencies) and the framework-free domain. It does not restructure anything — it fails the build when an import violates the model you already chose.
  • Pair it with
    python-code-style
    (run
    ruff
    +
    mypy
    alongside
    lint-imports
    ).
  • import-linter
    only sees imports. The judgment-residue invariants — domain models are dataclasses not Pydantic/
    DeclarativeBase
    , repository method naming (
    get
    /
    find_by_
    /
    list_
    ), no god-methods, correct UoW usage, thin routers — are not import-shaped and need a review pass (the analog of the
    rust-structure-and-style-guard
    /
    vue-structure-and-style-guard
    /
    python-structure-and-style-guard
    subagents), not this gate.
  • 本规则约束
    python-ddd
    第1条(仅向内的分层依赖)和无框架依赖的领域层。它不会重构任何代码——仅当导入违反你选择的模型时,构建才会失败。
  • 搭配
    python-code-style
    使用(在
    lint-imports
    旁边运行
    ruff
    +
    mypy
    )。
  • import-linter
    仅检查导入。需要人工判断的不变式——领域模型是dataclass而非Pydantic/
    DeclarativeBase
    、仓库方法命名(
    get
    /
    find_by_
    /
    list_
    )、无上帝方法、正确使用工作单元、轻量路由——不属于导入范畴,需要代码评审(对应
    rust-structure-and-style-guard
    /
    vue-structure-and-style-guard
    /
    python-structure-and-style-guard
    子代理),而非本规则。