LukasNiessen/ArchUnitPython

GitHub: LukasNiessen/ArchUnitPython

一款零依赖的 Python 架构测试库,可在 CI 流水线中自动检测循环依赖、层级违规和代码度量问题,防止架构随迭代逐渐腐化。

Stars: 2 | Forks: 0

# ArchUnitPython - 架构测试
ArchUnitPython Logo

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI version](https://img.shields.io/pypi/v/archunitpython.svg)](https://pypi.org/project/archunitpython/) [![Python versions](https://img.shields.io/pypi/pyversions/archunitpython.svg)](https://pypi.org/project/archunitpython/) [![GitHub stars](https://img.shields.io/github/stars/LukasNiessen/ArchUnitPython.svg)](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 历史 [![Star History Chart](https://api.star-history.com/svg?repos=LukasNiessen/ArchUnitPython&type=Date)](https://www.star-history.com/#LukasNiessen/ArchUnitPython&Date) ## 📄 许可证 本项目基于 **MIT** 许可证。

返回顶部

标签:AST解析, LNA, pytest, Python, Redis利用, 代码规范, 依赖方向, 分层架构, 单元测试, 安全规则引擎, 循环依赖, 无后门, 架构测试, 架构规则, 模块解耦, 流水线集成, 测试框架, 编码标准, 自动化payload嵌入, 软件架构, 逆向工具, 错误基检测, 零依赖, 静态代码分析