by LukasNiessen
ArchUnitPython is an architecture testing library. Specify and ensure architecture rules in your Python app. Easy setup and pipeline integration.
# Add to your Claude Code skills
git clone https://github.com/LukasNiessen/ArchUnitPythonEnforce architecture rules in Python projects. Check for dependency directions, detect circular dependencies, enforce coding standards and much more. Integrates with pytest and any other testing framework. Very simple setup and pipeline integration. Zero runtime dependencies.
Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit.
Setup • Use Cases • Features • Contributing
pip install archunitpython
Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.
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)
Next we ensure that our layered architecture is respected.
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)
# More layers ...
No comments yet. Be the first to share your thoughts!
Lastly we ensure that some code metric rules are met.
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)
These tests run automatically in your testing setup, for example in your CI pipeline, so that's basically it. This setup ensures that the architectural rules you have defined are always adhered to!
# GitHub Actions
- name: Run Architecture Tests
run: pytest tests/test_architecture.py -v
Installation:
pip install archunitpython
That's it. Works with pytest, unittest, or any Python testing framework.
Use assert_passes() for clean assertion messages:
from archunitpython import project_files, assert_passes
def test_my_architecture():
rule = project_files("src/").should().have_no_cycles()
assert_passes(rule)
Use .check() directly and assert on the violations list:
from archunitpython import project_files
rule = project_files("src/").should().have_no_cycles()
violations = rule.check()
assert len(violations) == 0
Both assert_passes() and .check() accept configuration options:
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)
Here is an overview of common use cases.
Layered Architecture:
Enforce that higher layers don't depend on lower layers and vice versa.
Clean Architecture / Hexagonal:
Validate that domain logic doesn't depend on infrastructure.
Microservices / Modular:
Ensure services/modules don't have forbidden cross-dependencies.
Here is a repository with a fully functioning example that uses ArchUnitPython to ensure architectural rules:
This is an overview of what you can do with 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)
You can define your own custom rules.
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
You can define your own metrics as well.
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)
Generate HTML reports for your metrics. Note that this feature is in beta.
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",
),
)
We offer three targeting options for pattern matching across all modules:
with_name(pattern) - Pattern is checked against the filename (e.g. service.py from src/services/service.py)in_path(pattern) - Pattern is checked against the full relative path (e.g. src/services/service.py)in_folder(pattern) - Pattern is checked against the path without filename (e.g. src/services from src/services/service.py)For the metrics module there is an additional one:
for_classes_matching(pattern) - Pattern is checked against class names. The filepath or filename does not matter hereWe support string patterns and regular expressions. String patterns support glob.
# String patterns with glob support (case sensitive)
.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
# Regular expressions
import re
.with_name(re.compile(r".*Service\.py$"))
.in_folder(re.compile(r"services$"))
# For metrics module: Class name matching
.for_classes_matching("*Service*")
.for_classes_matching(re.compile(r"^User.*"))
* - Matches any characters within a single path segment (except /)** - Matches any characters across multiple path segments? - Matches exactly one character# Filename patterns
.with_name("*.py") # All Python files
.with_name("*_service.py") # Files ending with _service.py
.with_name("test_*