LukasNiessen/ArchUnitPython
GitHub: LukasNiessen/ArchUnitPython
一款零依赖的 Python 架构测试库,可在 CI 流水线中自动检测循环依赖、层级违规和代码度量问题,防止架构随迭代逐渐腐化。
Stars: 2 | Forks: 0
# ArchUnitPython - 架构测试
[](https://opensource.org/licenses/MIT)
[](https://pypi.org/project/archunitpython/)
[](https://pypi.org/project/archunitpython/)
[](https://github.com/LukasNiessen/ArchUnitPython)
在 Python 项目中强制执行架构规则。检查依赖方向、检测循环依赖、强制执行编码规范等。可与 pytest 及任何其他测试框架集成。设置极其简单,且易于集成到流水线中。零运行时依赖。
_灵感来自出色的 ArchUnit 库,但我们与 ArchUnit 没有附属关系。_
[安装设置](#-setup) • [用例](#-use-cases) • [功能](#-features) • [贡献](CONTRIBUTING.md)
## ⚡ 5 分钟快速入门
### 安装
```
pip install archunitpython
```
### 添加测试
只需将测试添加到你现有的测试套件中。以下是一个使用 pytest 的示例。首先,我们确保没有循环依赖。
```
from archunitpython import project_files, metrics, assert_passes
def test_no_circular_dependencies():
rule = project_files("src/").in_folder("src/**").should().have_no_cycles()
assert_passes(rule)
```
接下来,我们确保分层架构得到遵守。
```
def test_presentation_should_not_depend_on_database():
rule = (
project_files("src/")
.in_folder("**/presentation/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
def test_business_should_not_depend_on_database():
rule = (
project_files("src/")
.in_folder("**/business/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
# 更多层 ...
```
最后,我们确保满足一些代码度量规则。
```
def test_no_large_files():
rule = metrics("src/").count().lines_of_code().should_be_below(1000)
assert_passes(rule)
def test_high_cohesion():
# LCOM metric (lack of cohesion of methods), low = high cohesion
rule = metrics("src/").lcom().lcom96b().should_be_below(0.3)
assert_passes(rule)
```
### CI 集成
这些测试会在你的测试设置(例如你的 CI 流水线)中自动运行,基本上就是这样。此设置可确保你定义的架构规则始终得到遵守!
```
# GitHub Actions
- name: Run Architecture Tests
run: pytest tests/test_architecture.py -v
```
## 🚐 安装设置
安装:
```
pip install archunitpython
```
就是这样。可与 **pytest**、**unittest** 或任何 Python 测试框架配合使用。
### pytest(推荐)
使用 `assert_passes()` 获取清晰的断言信息:
```
from archunitpython import project_files, assert_passes
def test_my_architecture():
rule = project_files("src/").should().have_no_cycles()
assert_passes(rule)
```
### 其他框架
直接使用 `.check()` 并对违规列表进行断言:
```
from archunitpython import project_files
rule = project_files("src/").should().have_no_cycles()
violations = rule.check()
assert len(violations) == 0
```
### 配置选项
`assert_passes()` 和 `.check()` 都接受配置选项:
```
from archunitpython import CheckOptions
options = CheckOptions(
allow_empty_tests=True, # Don't fail when no files match
clear_cache=True, # Clear the graph cache
)
violations = rule.check(options)
```
## 🐹 用例
以下是常见用例的概述。
**分层架构:**
强制规定高层不依赖低层,反之亦然。
**整洁架构 / 六边形架构:**
验证领域逻辑不依赖于基础设施。
**微服务 / 模块化:**
确保服务/模块之间不存在被禁止的交叉依赖。
## 🐲 示例仓库
这是一个具有完整功能的示例仓库,它使用 ArchUnitPython 来确保架构规则得到遵守:
- **[RAG Pipeline 示例](https://github.com/LukasNiessen/ArchUnitPython-Example-RAG)**:一个模拟的 AI/RAG 流水线,采用分层架构并包含故意设置的违规行为,演示了 ArchUnitPython 如何发现真实问题
## 🐣 功能
这是你可以使用 ArchUnitPython 做什么的概述。
### 循环依赖
```
def test_services_cycle_free():
rule = project_files("src/").in_folder("**/services/**").should().have_no_cycles()
assert_passes(rule)
```
### 层依赖
```
def test_clean_architecture_layers():
rule = (
project_files("src/")
.in_folder("**/presentation/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
def test_business_not_depend_on_presentation():
rule = (
project_files("src/")
.in_folder("**/business/**")
.should_not()
.depend_on_files()
.in_folder("**/presentation/**")
)
assert_passes(rule)
```
### 命名规范
```
def test_naming_patterns():
rule = (
project_files("src/")
.in_folder("**/services/**")
.should()
.have_name("*_service.py")
)
assert_passes(rule)
```
### 代码度量
```
def test_no_large_files():
rule = metrics("src/").count().lines_of_code().should_be_below(1000)
assert_passes(rule)
def test_high_class_cohesion():
rule = metrics("src/").lcom().lcom96b().should_be_below(0.3)
assert_passes(rule)
def test_method_count():
rule = metrics("src/").count().method_count().should_be_below(20)
assert_passes(rule)
def test_field_count_for_data_classes():
rule = (
metrics("src/")
.for_classes_matching("*Data*")
.count()
.field_count()
.should_be(3)
)
assert_passes(rule)
```
### 距离度量
```
def test_proper_coupling():
rule = metrics("src/").distance().distance_from_main_sequence().should_be_below(0.3)
assert_passes(rule)
def test_not_in_zone_of_pain():
rule = metrics("src/").distance().not_in_zone_of_pain()
assert_passes(rule)
```
### 自定义规则
你可以定义自己的自定义规则。
```
rule_desc = "Python files should have docstrings"
def has_docstring(file):
return '"""' in file.content or "'''" in file.content
violations = (
project_files("src/")
.with_name("*.py")
.should()
.adhere_to(has_docstring, rule_desc)
.check()
)
assert len(violations) == 0
```
### 自定义度量
你也可以定义自己的度量。
```
def test_method_field_ratio():
rule = (
metrics("src/")
.custom_metric(
"methodFieldRatio",
"Ratio of methods to fields",
lambda ci: len(ci.methods) / max(len(ci.fields), 1),
)
.should_be_below(10)
)
assert_passes(rule)
```
### 架构切片
```
import re
from archunitpython import project_slices
def test_adhere_to_diagram():
diagram = """
@startuml
component [controllers]
component [services]
[controllers] --> [services]
@enduml"""
rule = (
project_slices("src/")
.defined_by_regex(re.compile(r"/([^/]+)/[^/]+\.py$"))
.should()
.adhere_to_diagram(diagram)
)
assert_passes(rule)
def test_no_forbidden_dependency():
rule = (
project_slices("src/")
.defined_by("src/(**)/**")
.should_not()
.contain_dependency("services", "controllers")
)
assert_passes(rule)
```
### 报告
为你的度量生成 HTML 报告。_请注意,此功能目前处于测试阶段。_
```
from archunitpython.metrics.fluentapi.export_utils import MetricsExporter, ExportOptions
MetricsExporter.export_as_html(
{"MethodCount": 5, "FieldCount": 3, "LinesOfCode": 150},
ExportOptions(
output_path="reports/metrics.html",
title="Architecture Metrics Dashboard",
),
)
```
## 🔎 模式匹配系统
我们为所有模块提供了三种模式匹配的定位选项:
- **`with_name(pattern)`** - 根据文件名检查模式(例如,从 `src/services/service.py` 中提取的 `service.py`)
- **`in_path(pattern)`** - 根据完整的相对路径检查模式(例如,`src/services/service.py`)
- **`in_folder(pattern)`** - 根据不包含文件名的路径检查模式(例如,从 `src/services/service.py` 中提取的 `src/services`)
对于度量模块,还有一个额外的选项:
- **`for_classes_matching(pattern)`** - 根据类名检查模式。此处的文件路径或文件名无关紧要
### 模式类型
我们支持字符串模式和正则表达式。字符串模式支持 glob。
```
# 支持 glob 的字符串模式(区分大小写)
.with_name("*_service.py") # All files ending with _service.py
.in_folder("**/services") # All files in any services folder
.in_path("src/api/**/*.py") # All Python files under src/api
# 正则表达式
import re
.with_name(re.compile(r".*Service\.py$"))
.in_folder(re.compile(r"services$"))
# 对于 metrics 模块:Class 名称匹配
.for_classes_matching("*Service*")
.for_classes_matching(re.compile(r"^User.*"))
```
### Glob 模式指南
#### 基本通配符
- `*` - 匹配单个路径段内的任意字符(除了 `/`)
- `**` - 匹配跨多个路径段的任意字符
- `?` - 恰好匹配一个字符
#### 常见 Glob 示例
```
# 文件名模式
.with_name("*.py") # All Python files
.with_name("*_service.py") # Files ending with _service.py
.with_name("test_*.py") # Files starting with test_
# 文件夹模式
.in_folder("**/services") # Any services folder at any depth
.in_folder("src/services") # Exact src/services folder
.in_folder("**/test/**") # Any folder containing test in path
# 路径模式
.in_path("src/**/*.py") # Python files anywhere under src
.in_path("**/test/**/*_test.py") # Test files in any test folder
```
### 建议
除非你需要非常特殊的情况,否则我们通常建议使用支持 glob 的字符串模式。正则表达式增加了大多数情况下不需要的额外复杂性。
### 支持的度量类型
#### LCOM(方法缺乏内聚性)
LCOM 度量衡量类的方法和字段连接的紧密程度。值越低表示内聚性越好。
```
# LCOM96a (Henderson et al.)
metrics("src/").lcom().lcom96a().should_be_below(0.8)
# LCOM96b (Henderson et al.) - 最常用
metrics("src/").lcom().lcom96b().should_be_below(0.7)
```
所有 8 种 LCOM 变体均可用:`lcom96a()`、`lcom96b()`、`lcom1()` 到 `lcom5()`,以及 `lcomstar()`。
LCOM96b 度量的计算公式为:
```
LCOM96b = (1/a) * sum((1/m) * (m - mu(Ai)))
```
其中:
- `m` 是类中的方法数量
- `a` 是类中属性(字段)的数量
- `mu(Ai)` 是访问属性 Ai 的方法数量
结果是一个介于 0 和 1 之间的值:
- 0:完美的内聚(所有方法都访问所有属性)
- 1:完全缺乏内聚(每个方法都只访问自己的属性)
#### 计数度量
```
metrics("src/").count().method_count().should_be_below(20)
metrics("src/").count().field_count().should_be_below(15)
metrics("src/").count().lines_of_code().should_be_below(200)
metrics("src/").count().statements().should_be_below(100)
metrics("src/").count().imports().should_be_below(20)
```
#### 距离度量
```
metrics("src/").distance().abstractness().should_be_above(0.3)
metrics("src/").distance().instability().should_be_below(0.8)
metrics("src/").distance().distance_from_main_sequence().should_be_below(0.5)
```
#### 自定义度量
```
metrics("src/").custom_metric(
"complexityRatio",
"Ratio of methods to fields",
lambda ci: len(ci.methods) / max(len(ci.fields), 1),
).should_be_below(3.0)
```
## 📐 UML 图支持
ArchUnitPython 可以根据 PlantUML 图验证你的架构,确保你的代码与你的架构设计相匹配。
### 组件图
```
def test_component_architecture():
diagram = """
@startuml
component [UserInterface]
component [BusinessLogic]
component [DataAccess]
[UserInterface] --> [BusinessLogic]
[BusinessLogic] --> [DataAccess]
@enduml"""
rule = (
project_slices("src/")
.defined_by("src/(**)/**")
.should()
.adhere_to_diagram(diagram)
)
assert_passes(rule)
```
### 从文件生成图
```
def test_from_file():
rule = (
project_slices("src/")
.defined_by("src/(**)/**")
.should()
.adhere_to_diagram_in_file("docs/architecture.puml")
)
assert_passes(rule)
```
## 📢 详细的错误信息
当测试失败时,你会得到包含文件路径和违规详情的有用输出:
```
Found 2 architecture violation(s):
1. File dependency violation
'src/api/bad_shortcut.py' depends on 'src/retrieval/vector_store.py'
2. File dependency violation
'src/api/bad_shortcut.py' depends on 'src/retrieval/embedder.py'
```
## 📝 调试日志与配置
我们支持日志记录,以帮助你了解正在分析的文件并排查测试失败的原因。默认情况下禁用日志记录,以保持测试输出的整洁。
### 启用调试日志
```
from archunitpython import CheckOptions
from archunitpython.common.logging.types import LoggingOptions
options = CheckOptions(
logging=LoggingOptions(
enabled=True,
level="debug", # "error" | "warn" | "info" | "debug"
log_file=True, # Creates logs/archunit-YYYY-MM-DD_HH-MM-SS.log
),
)
violations = rule.check(options)
```
### CI 流水线集成
```
# GitHub Actions
- name: Run Architecture Tests
run: pytest tests/test_architecture.py -v
- name: Upload Test Logs
if: always()
uses: actions/upload-artifact@v3
with:
name: architecture-test-logs
path: logs/
```
## 🏈 架构适应度函数
ArchUnitPython 的功能非常适合用作架构适应度函数。有关该主题的更多信息,请参见[此处](https://www.thoughtworks.com/en-de/insights/articles/fitness-function-driven-development)。
## 🔲 核心模块
| 模块 | 描述 | 状态 |
| ----------- | ------------------------------ | ------------ |
| **Files** | 基于文件和文件夹的规则 | 稳定 |
| **Metrics** | 代码质量度量 | 稳定 |
| **Slices** | 架构切片 | 稳定 |
| **Testing** | 测试框架集成 | 稳定 |
| **Common** | 共享工具 | 稳定 |
| **Reports** | 生成 HTML 报告 | 实验性 |
### ArchUnitPython 使用 ArchUnitPython
我们使用自己来确保本仓库的架构规则。
## 🦊 贡献
我们非常感谢各种贡献。我们使用 GitHub Flow,这意味着我们使用特性分支。一旦有内容合并或推送到 `main` 分支,它就会被部署。版本控制通过 [Conventional Commits](https://www.conventionalcommits.org/) 自动化。更多信息请参阅[贡献](CONTRIBUTING.md)。
## ℹ️ FAQ
**问:支持哪些 Python 测试框架?**
ArchUnitPython 适用于 pytest、unittest 和任何其他测试框架。我们推荐将 pytest 与 `assert_passes()` 结合使用。
**问:支持哪些 Python 版本?**
Python 3.10 及更高版本。
**问:ArchUnitPython 有任何运行时依赖吗?**
没有。ArchUnitPython 仅使用 Python 标准库。开发依赖(pytest、mypy、ruff)是可选的。
**问:它如何分析 Python 导入?**
ArchUnitPython 使用 Python 内置的 `ast` 模块来解析源文件并解析导入。它处理绝对导入、相对导入和包导入。
**问:如何处理架构规则中的误报?**
使用过滤和定位功能排除特定文件或模式。你可以按文件路径、类名或自定义谓词进行过滤,以微调你的规则。
## 📅 计划
ArchUnitPython 是 ArchUnitTS 的 Python 移植版。我们计划使其与 TypeScript 版本的功能保持同步,并使用 Python 特有的功能对其进行扩展。
## 🐣 起源故事
ArchUnitPython 最初是作为 [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS) 的 Python 移植版开始的。随着 LLM 和 AI 集成的兴起,强制执行架构边界和整体 QA 变得比以往任何时候都更加关键——尤其是在作为 AI/ML 生态系统中主导语言的 Python 中。
## 💟 社区
### 维护者
- **[LukasNiessen](https://github.com/LukasNiessen)** - 创建者和主要维护者
### 贡献者
### 问题
发现了 Bug?想要讨论功能?
- 在 GitHub 上提交 [issue](https://github.com/LukasNiessen/ArchUnitPython/issues/new/choose)
- 加入我们的 [GitHub Discussions](https://github.com/LukasNiessen/ArchUnitPython/discussions)
如果 ArchUnitPython 对你的项目有帮助,请考虑:
- 给仓库点个 Star 💚
- 提出新功能建议 💭
- 贡献代码或文档 ⌨️
### Star 历史
[](https://www.star-history.com/#LukasNiessen/ArchUnitPython&Date)
## 📄 许可证
本项目基于 **MIT** 许可证。
[](https://opensource.org/licenses/MIT)
[](https://pypi.org/project/archunitpython/)
[](https://pypi.org/project/archunitpython/)
[](https://github.com/LukasNiessen/ArchUnitPython)
标签:AST解析, LNA, pytest, Python, Redis利用, 代码规范, 依赖方向, 分层架构, 单元测试, 安全规则引擎, 循环依赖, 无后门, 架构测试, 架构规则, 模块解耦, 流水线集成, 测试框架, 编码标准, 自动化payload嵌入, 软件架构, 逆向工具, 错误基检测, 零依赖, 静态代码分析