python-import-linter-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePython Import-Linter Architecture Setup
Python Import-Linter 架构配置
This is a one-time setup skill. It adds an contract set to
that makes the layered architecture from a thing
the build checks, not a thing people remember. A boundary written only in a
doc rots — someone does 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 and blocks the merge.
import-linterpyproject.tomlpython-dddfrom myapp.infrastructure.db import sessionlint-importsYou 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:
builds the real import graph with (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
aggregation that a naive would miss.
import-lintergrimp__init__grepThis is the Python sibling of (ESLint boundary
rules) and (a cargo gate) —
same goal, ecosystem-native mechanism.
frontend-vue-eslint-setuprust-architecture-test-setuptests/structure/这是一项一次性配置技能。它会将一套规则添加到中,让的分层架构成为构建过程会检查的约束,而非依赖开发者记忆的规范。仅写在文档里的边界规则会逐渐失效——比如有人情急之下在领域模型中写下,代码评审没发现,六个月后领域层就无法脱离数据库进行测试了。而将规则编码为import-linter约束后,同样的错误会导致检查失败,从而阻止代码合并。
import-linterpyproject.tomlpython-dddfrom myapp.infrastructure.db import sessionlint-imports你不需要手动编写AST扫描器(Rust生态需要这么做,因为Rust没有等效工具)。Python有专门的工具:通过构建真实的导入依赖图(静态分析——它仅解析代码,不会执行代码,因此无需启动数据库、消息队列或其他服务),并检查声明式规则。它能追踪重导出和聚合的依赖,这是简单做不到的。
import-lintergrimp__init__grep这是(ESLint边界规则)和( cargo校验)的Python版本——目标相同,采用生态原生的实现机制。
frontend-vue-eslint-setuprust-architecture-test-setuptests/structure/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; is pass/fail with no "warn" tier, so you ratchet with
import-linter.ignore_imports
Run this once. After the contracts exist in , you do not re-run
this skill — you only edit the contract list as the architecture grows.
pyproject.toml- 启动全新的python-ddd后端项目→从首次提交就启用强制失败校验(全新项目没有任何违规,无需逐步收紧规则)。这是本技能的默认适用场景。
- 为已有的无架构约束Python服务添加校验→参见规则严格性:新项目 vs 已有代码库;只有通过/失败两种状态,没有"警告"级别,因此你需要通过
import-linter逐步收紧规则。ignore_imports
只需运行一次本技能。规则写入后,无需再次运行——仅需在架构扩展时编辑规则列表即可。
pyproject.tomlWhat it enforces
约束内容
The four-layer model from , with dependencies pointing strictly
inward:
python-dddpresentation → application → domain
▲
infrastructure ───┘ (implements the domain's ports)- depends on nothing — not on the other layers, and not on any framework (SQLAlchemy, FastAPI, the DB driver). It is plain Python.
domain - depends on
application(and may import the concrete Unit of Work / repositories fromdomainas default-argument values — the one sanctioned inward exception in the skill).infrastructure - depends on
presentationandapplication— never straight intodomain.infrastructure - implements the domain's ports — it depends on
infrastructureand external libraries, never ondomainorapplication.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-dddpresentation → application → domain
▲
infrastructure ───┘ (实现领域层的端口)- 层无任何依赖——不依赖其他层,也不依赖任何框架(SQLAlchemy、FastAPI、数据库驱动等)。它是纯Python代码。
domain - 层依赖
application层(可将基础设施层的具体工作单元/仓库作为默认参数导入——这是本技能允许的唯一向内例外)。domain - 层依赖
presentation层和application层——绝不能直接依赖domain层。infrastructure - 层实现领域层的端口——它依赖
infrastructure层和外部库,绝不依赖domain层或application层。presentation
这些规则编码了关注点分离和单一职责原则:每个层只有一个存在的理由,依赖箭头始终单向指向。
Step 1 — Confirm your package root and layers
步骤1——确认包根目录和分层
import-linter- is whatever you
root_package— often the distribution package (import), or literallymyappif the project uses asrcdirectory as the package (runsrc/from the directory that contains it).lint-imports - The four layers are sub-packages: ,
myapp.presentation,myapp.application,myapp.domain. Each needs anmyapp.infrastructure.__init__.py
If your project splits transport into separate (routers, middleware) and
(schemas, serializers) packages, that is a 5-layer variant — see
Customization. The canonical four-layer set is below.
api/presentation/import-linter- ****是你实际导入的包——通常是发布包(如
root_package),如果项目采用myapp目录作为包结构,则为src/(需从包含该目录的路径运行src)。lint-imports - 四个分层是子包:、
myapp.presentation、myapp.application、myapp.domain。每个子包都需要myapp.infrastructure文件。__init__.py
Step 2 — Install import-linter
步骤2——安装import-linter
bash
uv add --dev import-linter # or: pip install import-linterThe CLI is . With uv: (or, to try it without
committing the dependency first, ).
lint-importsuv run lint-importsuv run --with import-linter lint-importsbash
uv add --dev import-linter # 或:pip install import-linterCLI命令为。使用uv时:(或者不先提交依赖,直接运行进行测试)。
lint-importsuv run lint-importsuv run --with import-linter lint-importsStep 3 — Add the contracts to pyproject.toml
pyproject.toml步骤3——将规则添加到pyproject.toml
pyproject.tomlDrop this in verbatim, then replace with your and tune
the framework list in contract 2 (Step-by-step reasons for every line are in
Understanding the contracts).
myapproot_packagetoml
[tool.importlinter]
root_package = "myapp"
include_external_packages = true # required: contract 2 names external librariestoml
[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
undefinedStep 4 — Run it and wire the gate
步骤4——运行校验并接入CI
bash
uv run lint-imports # exits non-zero on any broken contractWire that command into whatever already runs your fast checks — a
recipe, a Makefile target, a hook — and into CI next to
and :
justpre-commitruffmypybash
undefinedbash
uv run lint-imports # 违反规则时返回非零退出码将该命令接入你的快速校验流程——比如脚本、Makefile目标、钩子——并接入CI,与和放在一起:
justpre-commitruffmypybash
undefinedjustfile
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
undefinedAdd 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:
-
Acontract is an ordered list, highest first. Higher layers may import lower ones; lower layers may not import higher ones. So
layersforbidspresentation > application > domain,domain → application, anddomain → presentationin one stroke.application → presentation -
Acontract says "modules in
forbiddenmust not import anything insource_modules" — direct and indirect, unless you setforbidden_modules.allow_indirect_imports
Why is not in the 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:
. That is wrong — a
contract lets higher layers import lower ones, so it would permit
, the exact thing you must forbid. Meanwhile
(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 contracts:
(contract 2) and (contract 3). is forbidden nowhere,
so it stays legal.
infrastructurelayerspresentation > application > domain > infrastructurelayersdomain → infrastructureapplication → infrastructureforbiddendomain ✗ infrastructureinfrastructure ✗ application, presentationapplication → infrastructureWhy . Contract 2 names external libraries
(, ) in . 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.
include_external_packages = truesqlalchemyfastapiforbidden_modulesimport-linterWhy on contract 4. A presentation router imports an
application service, and that service imports for its
default argument (the sanctioned pattern). Following indirect imports, contract 4
would flag presentation for that legitimate transitive path —
— drowning the real signal.
restricts the contract to direct
imports, which are the genuine leaks (a router
opening a DB session itself instead of going through a service).
allow_indirect_importsinfrastructure.uowpresentation → application → infrastructure.uowallow_indirect_imports = truepresentation → infrastructure两种规则类型完成所有工作,它们的分工是关键:
-
规则是有序列表,顶层在前。上层可导入下层,下层不得导入上层。因此
layers会一次性禁止presentation > application > domain、domain → application和domain → presentation三种反向依赖。application → presentation -
****规则表示
forbidden中的模块不得导入source_modules中的任何内容——包括直接和间接导入,除非设置forbidden_modules。allow_indirect_imports
为什么不在链中。这是DDD的特殊之处。从依赖流向来看,基础设施层是"最低"层(所有层最终都可能依赖它),因此直观的做法是将其追加到链中:。但这是错误的——规则允许上层导入下层,这会允许,而这正是必须禁止的。同时,(导入具体工作单元作为默认参数)必须保持允许。单一有序链无法同时满足这两个要求。因此将基础设施层排除在链外,用显式的规则约束:(规则2)和(规则3)。未被任何规则禁止,因此是合法的。
infrastructurelayerspresentation > application > domain > infrastructurelayersdomain → infrastructureapplication → infrastructureforbiddendomain ✗ infrastructureinfrastructure ✗ application, presentationapplication → infrastructure为什么设置。规则2在中引用了外部库(、)。禁止对外部模块运行forbidden规则,除非设置此顶层标志——否则会直接报错,而非静默通过。
include_external_packages = trueforbidden_modulessqlalchemyfastapiimport-linter为什么规则4设置。表现层路由导入应用层服务,而该服务导入作为默认参数(这是认可的模式)。如果追踪间接导入,规则4会将这种合法的传递路径标记为违规——淹没真实的违规信号。将规则限制为直接的导入,这些才是真正的违规(比如路由直接打开数据库会话,而非通过服务)。
allow_indirect_importsinfrastructure.uowpresentation → application → infrastructure.uowallow_indirect_imports = truepresentation → infrastructureSeverity: 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 — andis greenfield-only by design.
python-ddd -
Existing project with violations:has no "warn" tier, so you ratchet differently. Turn on the
import-lintercontract first (usually closest to clean), and baseline the others with documented per-contract exceptions:Domain is framework-freetomlignore_imports = [ "myapp.presentation.routers.auth -> myapp.infrastructure.db", # TODO: route via auth service ]Run, fix violations, delete eachline as you clear it, until the list is empty. Add a note toignore_imports: "architecture is import-linted; do not add newCLAUDE.md." Never leave an unexplainedignore_importspermanently — a frozen exception is a boundary everyone has learned to ignore.ignore_imports
-
新项目:所有规则从首次提交就强制失败。全新的python-ddd项目没有任何违规,无需放宽规则——且本质上是为新项目设计的。
python-ddd -
存在违规的已有项目:没有"警告"级别,因此需要逐步收紧规则。先启用
import-linter规则(通常最接近合规),然后为其他规则添加带文档说明的逐规则例外:Domain is framework-freetomlignore_imports = [ "myapp.presentation.routers.auth -> myapp.infrastructure.db", # TODO:通过认证服务路由 ]运行校验,修复违规,每修复一个就删除对应的条目,直到列表为空。在ignore_imports中添加说明:"架构已启用import-linter校验;请勿添加新的CLAUDE.md条目。"永远不要保留未解释的ignore_imports条目——固化的例外会让所有人都忽视边界规则。ignore_imports
Customization knobs
自定义选项
-
— set to the package you actually import (
root_package, ormyappfor asrc-as-package layout).srcmust run from the directory that makes it importable.lint-imports -
The framework list (contract 2) — list the project's real infrastructure and transport libraries: the ORM (), web framework (
sqlalchemy), DB driver (fastapi,aiosqlite), migration tool (asyncpg), message-broker client (alembic,nats), HTTP client (aio_pika), cloud SDKs. Do not listhttpx— the skill uses Pydantic for domain commands and value objects. Add it only if your project keeps the domain strictly dataclass-only.pydantic -
5-layer variant (separate+
api/). If transport is split —presentation/(routers, middleware, app) aboveapi/(schemas, serializers) — makepresentation/the top layer and forbid it from infrastructure too:apitomllayers = ["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, which sits outside the layer packages and is unaffected.)main.py -
Bounded contexts. When the project splits into contexts (,
myapp.billing), add anmyapp.identitycontract so they don't import each other, and/or repeat the layer contract per context.independence
-
——设置为你实际导入的包(如
root_package,或采用myapp作为包结构时的src)。src必须从能导入该包的路径运行。lint-imports -
规则2中的框架列表——列出项目实际使用的基础设施和传输层库:ORM()、Web框架(
sqlalchemy)、数据库驱动(fastapi、aiosqlite)、迁移工具(asyncpg)、消息队列客户端(alembic、nats)、HTTP客户端(aio_pika)、云SDK等。不要添加httpx——本技能允许领域层命令和值对象使用Pydantic。仅当你的项目严格要求领域层仅使用dataclass时才添加它。pydantic -
五层变体(独立的+
api/)。如果传输层被拆分——presentation/(路由、中间件、应用实例)在api/(Schema、序列化器)之上——将presentation/设为顶层,并禁止它直接依赖基础设施层:apitomllayers = ["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 §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.
python-ddd - Pair it with (run
python-code-style+ruffalongsidemypy).lint-imports - only sees imports. The judgment-residue invariants — domain models are dataclasses not Pydantic/
import-linter, repository method naming (DeclarativeBase/get/find_by_), no god-methods, correct UoW usage, thin routers — are not import-shaped and need a review pass (the analog of thelist_/rust-structure-and-style-guard/vue-structure-and-style-guardsubagents), not this gate.python-structure-and-style-guard
- 本规则约束第1条(仅向内的分层依赖)和无框架依赖的领域层。它不会重构任何代码——仅当导入违反你选择的模型时,构建才会失败。
python-ddd - 搭配使用(在
python-code-style旁边运行lint-imports+ruff)。mypy - 仅检查导入。需要人工判断的不变式——领域模型是dataclass而非Pydantic/
import-linter、仓库方法命名(DeclarativeBase/get/find_by_)、无上帝方法、正确使用工作单元、轻量路由——不属于导入范畴,需要代码评审(对应list_/rust-structure-and-style-guard/vue-structure-and-style-guard子代理),而非本规则。python-structure-and-style-guard