archunitpython 1.7.0__tar.gz → 1.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {archunitpython-1.7.0 → archunitpython-1.8.1}/CHANGELOG.md +23 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/PKG-INFO +147 -6
- {archunitpython-1.7.0 → archunitpython-1.8.1}/README.md +146 -5
- {archunitpython-1.7.0 → archunitpython-1.8.1}/pyproject.toml +1 -1
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/__init__.py +3 -1
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/extract_graph.py +199 -81
- archunitpython-1.8.1/src/archunitpython/common/fluentapi/inspection.py +79 -0
- archunitpython-1.8.1/src/archunitpython/common/logging/inspection.py +99 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/logging/types.py +2 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/pattern_matching.py +9 -2
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_edges.py +12 -1
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/regex_factory.py +14 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/types.py +2 -1
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_files.py +14 -3
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/files.py +10 -2
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/layers.py +2 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/extract_class_info.py +17 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/metrics.py +47 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/slices.py +3 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/assertion.py +16 -6
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/color_utils.py +12 -1
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_core_types.py +13 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_extract_graph.py +179 -58
- archunitpython-1.8.1/tests/common/test_inspection.py +315 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_pattern_matching.py +31 -0
- archunitpython-1.8.1/tests/common/test_pretty_output.py +51 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/test_file_assertions.py +28 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.editorconfig +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.gitattributes +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/CODEOWNERS +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/FUNDING.yml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/PAGES.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/dependabot.yml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/pull_request_template.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.gitignore +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/.releaserc.json +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/BACKLOG.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/CONTRIBUTING.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/LICENSE +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/assets/logo-rounded.png +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/config/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/config/loader.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_logger.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_projection.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/config/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/config/test_config_loader.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/integration/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/slices/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/test_setup.py +0 -0
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [1.8.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.8.0...v1.8.1) (2026-10-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* preserve diagnostics in selective extraction when merging main ([72fb440](https://github.com/LukasNiessen/ArchUnitPython/commit/72fb440cd5038ec091e5846d1042fae46b6a809b))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Performance Improvements
|
|
10
|
+
|
|
11
|
+
* cache dependency filter matches per check ([a0800fa](https://github.com/LukasNiessen/ArchUnitPython/commit/a0800fa40a00630f0eff7b525cd5d6d07eec185e))
|
|
12
|
+
* parse only selected sources for file dependency rules ([0e3f261](https://github.com/LukasNiessen/ArchUnitPython/commit/0e3f261f4dfdb2ef1655412cc79fe3e157e2d9b1))
|
|
13
|
+
* skip directory-only excludes for Python files ([1a6d5ed](https://github.com/LukasNiessen/ArchUnitPython/commit/1a6d5ed14f01a0ca2b4bb81e9055dce46e36dd21))
|
|
14
|
+
* skip redundant leading glob stars during search ([27f14e8](https://github.com/LukasNiessen/ArchUnitPython/commit/27f14e8215217f0091c5239f29cde13e07b8470e))
|
|
15
|
+
* walk parsed syntax trees once per file ([2f7efbf](https://github.com/LukasNiessen/ArchUnitPython/commit/2f7efbfbbdbdb115639bad58d1581e5e55961ba1))
|
|
16
|
+
|
|
17
|
+
# [1.8.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.7.0...v1.8.0) (2026-09-30)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add detailed per-check inspection and styled reports ([#65](https://github.com/LukasNiessen/ArchUnitPython/issues/65)) ([ab0f9ef](https://github.com/LukasNiessen/ArchUnitPython/commit/ab0f9efb71c90712366490121e82c09b112a5d06))
|
|
23
|
+
|
|
1
24
|
# [1.7.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.1...v1.7.0) (2026-09-13)
|
|
2
25
|
|
|
3
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics.
|
|
5
5
|
Project-URL: Homepage, https://github.com/LukasNiessen/ArchUnitPython
|
|
6
6
|
Project-URL: Repository, https://github.com/LukasNiessen/ArchUnitPython.git
|
|
@@ -60,6 +60,13 @@ _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUni
|
|
|
60
60
|
pip install archunitpython
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
> **Choose the import root:** Pass the directory containing your top-level Python
|
|
64
|
+
> packages to `project_files(...)` and `metrics(...)`. If `import taskboard` refers
|
|
65
|
+
> to `src/taskboard/`, use `"src"`; if `taskboard/` is at the repository root, use
|
|
66
|
+
> `"."`. This tells ArchUnitPython where to scan and how to resolve your imports.
|
|
67
|
+
> Omitting the argument uses the current working directory; it does not
|
|
68
|
+
> automatically discover a `src/` layout. [Details and examples](#choosing-the-import-root).
|
|
69
|
+
|
|
63
70
|
### Add tests
|
|
64
71
|
|
|
65
72
|
Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.
|
|
@@ -68,7 +75,7 @@ Simply add tests to your existing test suites. The following is an example using
|
|
|
68
75
|
from archunitpython import project_files, metrics, assert_passes
|
|
69
76
|
|
|
70
77
|
def test_no_circular_dependencies():
|
|
71
|
-
rule = project_files("src/").
|
|
78
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
72
79
|
assert_passes(rule)
|
|
73
80
|
```
|
|
74
81
|
|
|
@@ -147,6 +154,70 @@ pip install archunitpython
|
|
|
147
154
|
|
|
148
155
|
That's it. Works with **pytest**, **unittest**, or any Python testing framework.
|
|
149
156
|
|
|
157
|
+
### Choosing the import root
|
|
158
|
+
|
|
159
|
+
The optional `project_path` argument to `project_files(...)`, `metrics(...)`,
|
|
160
|
+
`project_layers(...)`, `project_slices(...)`, and `project_graph(...)` selects the
|
|
161
|
+
directory to analyze. Choose the directory containing your top-level Python
|
|
162
|
+
packages so absolute imports can be resolved within that directory. Relative
|
|
163
|
+
paths are interpreted from the current working directory; omitting the argument
|
|
164
|
+
uses that working directory.
|
|
165
|
+
|
|
166
|
+
For example, suppose your repository contains:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
my_repo/
|
|
170
|
+
src/
|
|
171
|
+
taskboard/
|
|
172
|
+
__init__.py
|
|
173
|
+
application/board_service.py
|
|
174
|
+
domain/task.py
|
|
175
|
+
tests/test_architecture.py
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
An import such as `from taskboard.domain.task import Task` names
|
|
179
|
+
`taskboard/domain/task.py` underneath `src/`. It does not include `src` in the
|
|
180
|
+
import name. Run your tests from `my_repo/` and use:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from archunitpython import assert_passes, metrics, project_files
|
|
184
|
+
|
|
185
|
+
def test_no_cycles():
|
|
186
|
+
rule = project_files("src").should().have_no_cycles()
|
|
187
|
+
assert_passes(rule)
|
|
188
|
+
|
|
189
|
+
def test_no_large_files():
|
|
190
|
+
rule = metrics("src").count().lines_of_code().should_be_below(1000)
|
|
191
|
+
assert_passes(rule)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The directory name itself has no special meaning:
|
|
195
|
+
|
|
196
|
+
| Package location, relative to your working directory | Import name | Argument |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| `src/taskboard/` | `taskboard` | `"src"` |
|
|
199
|
+
| `taskboard/` | `taskboard` | `"."` |
|
|
200
|
+
| `backend/taskboard/` | `taskboard` | `"backend"` |
|
|
201
|
+
|
|
202
|
+
Selectors such as `.in_path("**/domain/**")` and `.in_folder(...)` narrow which
|
|
203
|
+
files a rule checks. They do not change the import root. For example,
|
|
204
|
+
`project_files().in_path("src/**")` can select files under `src/` while still
|
|
205
|
+
resolving absolute imports from the repository root. Set `project_files("src")`
|
|
206
|
+
first, then add selectors if you need to narrow the rule.
|
|
207
|
+
|
|
208
|
+
With the wrong root, imports between your own modules can be classified as
|
|
209
|
+
external, causing cycle and internal-dependency rules to miss violations and
|
|
210
|
+
dependency-based metrics to report incorrect values. The root also determines
|
|
211
|
+
which files are included in metric measurements. ArchUnitPython does not infer a
|
|
212
|
+
`src/` import root from `pyproject.toml` or your Python environment; pass it
|
|
213
|
+
explicitly for this layout.
|
|
214
|
+
|
|
215
|
+
## 🎬 Demo
|
|
216
|
+
|
|
217
|
+
https://github.com/user-attachments/assets/56f2ad65-a2e1-4965-b84d-445cf7bf4cd2
|
|
218
|
+
|
|
219
|
+
## Testing Framework
|
|
220
|
+
|
|
150
221
|
### pytest (Recommended)
|
|
151
222
|
|
|
152
223
|
Use `assert_passes()` for clean assertion messages:
|
|
@@ -926,6 +997,10 @@ The most important differences:
|
|
|
926
997
|
|
|
927
998
|
When tests fail, you get helpful output with file paths and violation details:
|
|
928
999
|
|
|
1000
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
1001
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
1002
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
1003
|
+
|
|
929
1004
|
```
|
|
930
1005
|
Found 2 architecture violation(s):
|
|
931
1006
|
|
|
@@ -938,25 +1013,91 @@ Found 2 architecture violation(s):
|
|
|
938
1013
|
|
|
939
1014
|
## 📝 Debug Logging & Configuration
|
|
940
1015
|
|
|
941
|
-
|
|
1016
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
1017
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
1018
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
1019
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
1020
|
+
|
|
1021
|
+
| Level | What you see |
|
|
1022
|
+
| --- | --- |
|
|
1023
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
1024
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
1025
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
1026
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
942
1027
|
|
|
943
1028
|
### Enabling Debug Logging
|
|
944
1029
|
|
|
945
1030
|
```python
|
|
946
|
-
from archunitpython import CheckOptions
|
|
947
|
-
|
|
1031
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
1032
|
+
|
|
1033
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
948
1034
|
|
|
949
1035
|
options = CheckOptions(
|
|
950
1036
|
logging=LoggingOptions(
|
|
951
1037
|
enabled=True,
|
|
952
1038
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
953
|
-
log_file=True, # Creates logs/
|
|
1039
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
954
1040
|
),
|
|
955
1041
|
)
|
|
956
1042
|
|
|
957
1043
|
violations = rule.check(options)
|
|
958
1044
|
```
|
|
959
1045
|
|
|
1046
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
1047
|
+
on your project):
|
|
1048
|
+
|
|
1049
|
+
```text
|
|
1050
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
1051
|
+
[DEBUG] Graph cache miss
|
|
1052
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
1053
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
1054
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
1055
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
1056
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
1060
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
1061
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
1062
|
+
Python's standard `logging` module in your application.
|
|
1063
|
+
|
|
1064
|
+
### Save detailed inspection without console noise
|
|
1065
|
+
|
|
1066
|
+
```python
|
|
1067
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
1068
|
+
enabled=True,
|
|
1069
|
+
level="debug",
|
|
1070
|
+
console=False,
|
|
1071
|
+
log_file=True,
|
|
1072
|
+
log_path="logs/architecture.log",
|
|
1073
|
+
append_to_log_file=True,
|
|
1074
|
+
))
|
|
1075
|
+
violations = rule.check(options)
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
1079
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
1080
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
1081
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
1082
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
1083
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
1084
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
1085
|
+
not re-run custom predicates or metric calculations.
|
|
1086
|
+
|
|
1087
|
+
### Format results independently of logging
|
|
1088
|
+
|
|
1089
|
+
```python
|
|
1090
|
+
from archunitpython import assert_passes, format_violations
|
|
1091
|
+
|
|
1092
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
1093
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
1094
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
1098
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
1099
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
1100
|
+
|
|
960
1101
|
### CI Pipeline Integration
|
|
961
1102
|
|
|
962
1103
|
```yaml
|
|
@@ -27,6 +27,13 @@ _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUni
|
|
|
27
27
|
pip install archunitpython
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
> **Choose the import root:** Pass the directory containing your top-level Python
|
|
31
|
+
> packages to `project_files(...)` and `metrics(...)`. If `import taskboard` refers
|
|
32
|
+
> to `src/taskboard/`, use `"src"`; if `taskboard/` is at the repository root, use
|
|
33
|
+
> `"."`. This tells ArchUnitPython where to scan and how to resolve your imports.
|
|
34
|
+
> Omitting the argument uses the current working directory; it does not
|
|
35
|
+
> automatically discover a `src/` layout. [Details and examples](#choosing-the-import-root).
|
|
36
|
+
|
|
30
37
|
### Add tests
|
|
31
38
|
|
|
32
39
|
Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.
|
|
@@ -35,7 +42,7 @@ Simply add tests to your existing test suites. The following is an example using
|
|
|
35
42
|
from archunitpython import project_files, metrics, assert_passes
|
|
36
43
|
|
|
37
44
|
def test_no_circular_dependencies():
|
|
38
|
-
rule = project_files("src/").
|
|
45
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
39
46
|
assert_passes(rule)
|
|
40
47
|
```
|
|
41
48
|
|
|
@@ -114,6 +121,70 @@ pip install archunitpython
|
|
|
114
121
|
|
|
115
122
|
That's it. Works with **pytest**, **unittest**, or any Python testing framework.
|
|
116
123
|
|
|
124
|
+
### Choosing the import root
|
|
125
|
+
|
|
126
|
+
The optional `project_path` argument to `project_files(...)`, `metrics(...)`,
|
|
127
|
+
`project_layers(...)`, `project_slices(...)`, and `project_graph(...)` selects the
|
|
128
|
+
directory to analyze. Choose the directory containing your top-level Python
|
|
129
|
+
packages so absolute imports can be resolved within that directory. Relative
|
|
130
|
+
paths are interpreted from the current working directory; omitting the argument
|
|
131
|
+
uses that working directory.
|
|
132
|
+
|
|
133
|
+
For example, suppose your repository contains:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
my_repo/
|
|
137
|
+
src/
|
|
138
|
+
taskboard/
|
|
139
|
+
__init__.py
|
|
140
|
+
application/board_service.py
|
|
141
|
+
domain/task.py
|
|
142
|
+
tests/test_architecture.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
An import such as `from taskboard.domain.task import Task` names
|
|
146
|
+
`taskboard/domain/task.py` underneath `src/`. It does not include `src` in the
|
|
147
|
+
import name. Run your tests from `my_repo/` and use:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from archunitpython import assert_passes, metrics, project_files
|
|
151
|
+
|
|
152
|
+
def test_no_cycles():
|
|
153
|
+
rule = project_files("src").should().have_no_cycles()
|
|
154
|
+
assert_passes(rule)
|
|
155
|
+
|
|
156
|
+
def test_no_large_files():
|
|
157
|
+
rule = metrics("src").count().lines_of_code().should_be_below(1000)
|
|
158
|
+
assert_passes(rule)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The directory name itself has no special meaning:
|
|
162
|
+
|
|
163
|
+
| Package location, relative to your working directory | Import name | Argument |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `src/taskboard/` | `taskboard` | `"src"` |
|
|
166
|
+
| `taskboard/` | `taskboard` | `"."` |
|
|
167
|
+
| `backend/taskboard/` | `taskboard` | `"backend"` |
|
|
168
|
+
|
|
169
|
+
Selectors such as `.in_path("**/domain/**")` and `.in_folder(...)` narrow which
|
|
170
|
+
files a rule checks. They do not change the import root. For example,
|
|
171
|
+
`project_files().in_path("src/**")` can select files under `src/` while still
|
|
172
|
+
resolving absolute imports from the repository root. Set `project_files("src")`
|
|
173
|
+
first, then add selectors if you need to narrow the rule.
|
|
174
|
+
|
|
175
|
+
With the wrong root, imports between your own modules can be classified as
|
|
176
|
+
external, causing cycle and internal-dependency rules to miss violations and
|
|
177
|
+
dependency-based metrics to report incorrect values. The root also determines
|
|
178
|
+
which files are included in metric measurements. ArchUnitPython does not infer a
|
|
179
|
+
`src/` import root from `pyproject.toml` or your Python environment; pass it
|
|
180
|
+
explicitly for this layout.
|
|
181
|
+
|
|
182
|
+
## 🎬 Demo
|
|
183
|
+
|
|
184
|
+
https://github.com/user-attachments/assets/56f2ad65-a2e1-4965-b84d-445cf7bf4cd2
|
|
185
|
+
|
|
186
|
+
## Testing Framework
|
|
187
|
+
|
|
117
188
|
### pytest (Recommended)
|
|
118
189
|
|
|
119
190
|
Use `assert_passes()` for clean assertion messages:
|
|
@@ -893,6 +964,10 @@ The most important differences:
|
|
|
893
964
|
|
|
894
965
|
When tests fail, you get helpful output with file paths and violation details:
|
|
895
966
|
|
|
967
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
968
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
969
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
970
|
+
|
|
896
971
|
```
|
|
897
972
|
Found 2 architecture violation(s):
|
|
898
973
|
|
|
@@ -905,25 +980,91 @@ Found 2 architecture violation(s):
|
|
|
905
980
|
|
|
906
981
|
## 📝 Debug Logging & Configuration
|
|
907
982
|
|
|
908
|
-
|
|
983
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
984
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
985
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
986
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
987
|
+
|
|
988
|
+
| Level | What you see |
|
|
989
|
+
| --- | --- |
|
|
990
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
991
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
992
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
993
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
909
994
|
|
|
910
995
|
### Enabling Debug Logging
|
|
911
996
|
|
|
912
997
|
```python
|
|
913
|
-
from archunitpython import CheckOptions
|
|
914
|
-
|
|
998
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
999
|
+
|
|
1000
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
915
1001
|
|
|
916
1002
|
options = CheckOptions(
|
|
917
1003
|
logging=LoggingOptions(
|
|
918
1004
|
enabled=True,
|
|
919
1005
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
920
|
-
log_file=True, # Creates logs/
|
|
1006
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
921
1007
|
),
|
|
922
1008
|
)
|
|
923
1009
|
|
|
924
1010
|
violations = rule.check(options)
|
|
925
1011
|
```
|
|
926
1012
|
|
|
1013
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
1014
|
+
on your project):
|
|
1015
|
+
|
|
1016
|
+
```text
|
|
1017
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
1018
|
+
[DEBUG] Graph cache miss
|
|
1019
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
1020
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
1021
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
1022
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
1023
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
1027
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
1028
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
1029
|
+
Python's standard `logging` module in your application.
|
|
1030
|
+
|
|
1031
|
+
### Save detailed inspection without console noise
|
|
1032
|
+
|
|
1033
|
+
```python
|
|
1034
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
1035
|
+
enabled=True,
|
|
1036
|
+
level="debug",
|
|
1037
|
+
console=False,
|
|
1038
|
+
log_file=True,
|
|
1039
|
+
log_path="logs/architecture.log",
|
|
1040
|
+
append_to_log_file=True,
|
|
1041
|
+
))
|
|
1042
|
+
violations = rule.check(options)
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
1046
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
1047
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
1048
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
1049
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
1050
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
1051
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
1052
|
+
not re-run custom predicates or metric calculations.
|
|
1053
|
+
|
|
1054
|
+
### Format results independently of logging
|
|
1055
|
+
|
|
1056
|
+
```python
|
|
1057
|
+
from archunitpython import assert_passes, format_violations
|
|
1058
|
+
|
|
1059
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
1060
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
1061
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
1065
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
1066
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
1067
|
+
|
|
927
1068
|
### CI Pipeline Integration
|
|
928
1069
|
|
|
929
1070
|
```yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.1"
|
|
8
8
|
description = "Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.8.1"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
7
7
|
from archunitpython.common import (
|
|
8
8
|
CheckOptions,
|
|
9
9
|
EmptyTestViolation,
|
|
10
|
+
LoggingOptions,
|
|
10
11
|
TechnicalError,
|
|
11
12
|
UserError,
|
|
12
13
|
Violation,
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"Violation",
|
|
51
52
|
"EmptyTestViolation",
|
|
52
53
|
"CheckOptions",
|
|
54
|
+
"LoggingOptions",
|
|
53
55
|
"TechnicalError",
|
|
54
56
|
"UserError",
|
|
55
57
|
"extract_graph",
|