archunitpython 1.2.1__tar.gz → 1.3.0__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.2.1 → archunitpython-1.3.0}/BACKLOG.md +1 -1
- {archunitpython-1.2.1 → archunitpython-1.3.0}/CHANGELOG.md +7 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/PKG-INFO +123 -12
- {archunitpython-1.2.1 → archunitpython-1.3.0}/README.md +122 -11
- {archunitpython-1.2.1 → archunitpython-1.3.0}/pyproject.toml +1 -1
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/__init__.py +1 -1
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/__init__.py +6 -1
- archunitpython-1.3.0/src/archunitpython/common/fluentapi/__init__.py +7 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/fluentapi/checkable.py +23 -1
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/fluentapi/files.py +6 -6
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/metrics.py +7 -7
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/fluentapi/slices.py +3 -3
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/assertion.py +11 -3
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/files/test_files_fluentapi.py +19 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/integration/test_e2e.py +25 -0
- archunitpython-1.2.1/src/archunitpython/common/fluentapi/__init__.py +0 -3
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.editorconfig +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.gitattributes +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/PAGES.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.gitignore +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/.releaserc.json +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/LICENSE +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/extraction/extract_graph.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/fluentapi/layers.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_extract_graph.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.2.1 → archunitpython-1.3.0}/tests/test_setup.py +0 -0
|
@@ -12,7 +12,7 @@ This backlog collects product and maintenance ideas from project research.
|
|
|
12
12
|
## P1 - Adoption Workflow
|
|
13
13
|
|
|
14
14
|
- Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
|
|
15
|
-
- Add a `.because(...)` API so rules can carry user-facing rationale into failure messages and generated architecture documentation.
|
|
15
|
+
- [x] Add a `.because(...)` API so rules can carry user-facing rationale into failure messages and generated architecture documentation.
|
|
16
16
|
- Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
|
|
17
17
|
- Add support for monorepo and multi-package Python projects.
|
|
18
18
|
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.2.1...v1.3.0) (2026-07-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add because rule rationales ([491c666](https://github.com/LukasNiessen/ArchUnitPython/commit/491c666f1f8a6ef2f5ccd782a4e4c79ed39321f6))
|
|
7
|
+
|
|
1
8
|
## [1.2.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.2.0...v1.2.1) (2026-06-28)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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
|
|
@@ -188,6 +188,25 @@ options = CheckOptions(
|
|
|
188
188
|
violations = rule.check(options)
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
### Explaining Rules With `.because(...)`
|
|
192
|
+
|
|
193
|
+
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
rule = (
|
|
197
|
+
project_files("src/")
|
|
198
|
+
.in_folder("**/controllers/**")
|
|
199
|
+
.should_not()
|
|
200
|
+
.depend_on_files()
|
|
201
|
+
.in_folder("**/database/**")
|
|
202
|
+
.because("controllers should stay thin and delegate persistence")
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assert_passes(rule)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
When the rule fails, the rationale is included in the assertion message.
|
|
209
|
+
|
|
191
210
|
## 🐹 Use Cases
|
|
192
211
|
|
|
193
212
|
Here is an overview of common use cases.
|
|
@@ -431,21 +450,113 @@ def test_no_forbidden_dependency():
|
|
|
431
450
|
|
|
432
451
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
433
452
|
|
|
453
|
+
**Using `requests` library repo for example**
|
|
454
|
+
|
|
434
455
|
```python
|
|
435
456
|
from archunitpython import project_graph
|
|
436
457
|
|
|
437
458
|
def test_export_dependency_graph_reports():
|
|
438
|
-
graph = project_graph("src/").titled("Application Architecture")
|
|
439
|
-
|
|
440
|
-
graph.collapse_to_folder_depth(2).export_as_mermaid(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
459
|
+
graph = project_graph("src/requests").titled("Application Architecture")
|
|
460
|
+
|
|
461
|
+
graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
test_export_dependency_graph_reports()
|
|
465
|
+
```
|
|
466
|
+
**Rendered mermain diagram**
|
|
467
|
+
``` mermaid
|
|
468
|
+
flowchart LR
|
|
469
|
+
n0["__init__.py"]
|
|
470
|
+
n1["__version__.py"]
|
|
471
|
+
n2["_internal_utils.py"]
|
|
472
|
+
n3["_types.py"]
|
|
473
|
+
n4["adapters.py"]
|
|
474
|
+
n5["api.py"]
|
|
475
|
+
n6["auth.py"]
|
|
476
|
+
n7["certs.py"]
|
|
477
|
+
n8["compat.py"]
|
|
478
|
+
n9["cookies.py"]
|
|
479
|
+
n10["exceptions.py"]
|
|
480
|
+
n11["help.py"]
|
|
481
|
+
n12["hooks.py"]
|
|
482
|
+
n13["models.py"]
|
|
483
|
+
n14["packages.py"]
|
|
484
|
+
n15["sessions.py"]
|
|
485
|
+
n16["status_codes.py"]
|
|
486
|
+
n17["structures.py"]
|
|
487
|
+
n18["utils.py"]
|
|
488
|
+
n0 --> n1
|
|
489
|
+
n0 --> n5
|
|
490
|
+
n0 --> n10
|
|
491
|
+
n0 --> n13
|
|
492
|
+
n0 --> n15
|
|
493
|
+
n0 --> n16
|
|
494
|
+
n2 --> n8
|
|
495
|
+
n3 --> n6
|
|
496
|
+
n3 --> n9
|
|
497
|
+
n3 --> n13
|
|
498
|
+
n3 --> n17
|
|
499
|
+
n4 --> n0
|
|
500
|
+
n4 --> n3
|
|
501
|
+
n4 --> n6
|
|
502
|
+
n4 --> n8
|
|
503
|
+
n4 --> n9
|
|
504
|
+
n4 --> n10
|
|
505
|
+
n4 --> n13
|
|
506
|
+
n4 --> n17
|
|
507
|
+
n4 --> n18
|
|
508
|
+
n5 --> n0
|
|
509
|
+
n5 --> n13
|
|
510
|
+
n6 --> n2
|
|
511
|
+
n6 --> n8
|
|
512
|
+
n6 --> n9
|
|
513
|
+
n6 --> n13
|
|
514
|
+
n6 --> n18
|
|
515
|
+
n9 --> n2
|
|
516
|
+
n9 --> n3
|
|
517
|
+
n9 --> n8
|
|
518
|
+
n9 --> n13
|
|
519
|
+
n10 --> n8
|
|
520
|
+
n10 --> n13
|
|
521
|
+
n11 --> n0
|
|
522
|
+
n12 --> n0
|
|
523
|
+
n12 --> n13
|
|
524
|
+
n13 --> n0
|
|
525
|
+
n13 --> n2
|
|
526
|
+
n13 --> n4
|
|
527
|
+
n13 --> n6
|
|
528
|
+
n13 --> n8
|
|
529
|
+
n13 --> n9
|
|
530
|
+
n13 --> n10
|
|
531
|
+
n13 --> n12
|
|
532
|
+
n13 --> n16
|
|
533
|
+
n13 --> n17
|
|
534
|
+
n13 --> n18
|
|
535
|
+
n14 --> n8
|
|
536
|
+
n15 --> n0
|
|
537
|
+
n15 --> n2
|
|
538
|
+
n15 --> n3
|
|
539
|
+
n15 --> n4
|
|
540
|
+
n15 --> n6
|
|
541
|
+
n15 --> n8
|
|
542
|
+
n15 --> n9
|
|
543
|
+
n15 --> n10
|
|
544
|
+
n15 --> n12
|
|
545
|
+
n15 --> n13
|
|
546
|
+
n15 --> n16
|
|
547
|
+
n15 --> n17
|
|
548
|
+
n15 --> n18
|
|
549
|
+
n16 --> n17
|
|
550
|
+
n17 --> n8
|
|
551
|
+
n18 --> n0
|
|
552
|
+
n18 --> n1
|
|
553
|
+
n18 --> n2
|
|
554
|
+
n18 --> n3
|
|
555
|
+
n18 --> n8
|
|
556
|
+
n18 --> n9
|
|
557
|
+
n18 --> n10
|
|
558
|
+
n18 --> n13
|
|
559
|
+
n18 --> n17
|
|
449
560
|
```
|
|
450
561
|
|
|
451
562
|
Supported formats:
|
|
@@ -155,6 +155,25 @@ options = CheckOptions(
|
|
|
155
155
|
violations = rule.check(options)
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Explaining Rules With `.because(...)`
|
|
159
|
+
|
|
160
|
+
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
rule = (
|
|
164
|
+
project_files("src/")
|
|
165
|
+
.in_folder("**/controllers/**")
|
|
166
|
+
.should_not()
|
|
167
|
+
.depend_on_files()
|
|
168
|
+
.in_folder("**/database/**")
|
|
169
|
+
.because("controllers should stay thin and delegate persistence")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert_passes(rule)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When the rule fails, the rationale is included in the assertion message.
|
|
176
|
+
|
|
158
177
|
## 🐹 Use Cases
|
|
159
178
|
|
|
160
179
|
Here is an overview of common use cases.
|
|
@@ -398,21 +417,113 @@ def test_no_forbidden_dependency():
|
|
|
398
417
|
|
|
399
418
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
400
419
|
|
|
420
|
+
**Using `requests` library repo for example**
|
|
421
|
+
|
|
401
422
|
```python
|
|
402
423
|
from archunitpython import project_graph
|
|
403
424
|
|
|
404
425
|
def test_export_dependency_graph_reports():
|
|
405
|
-
graph = project_graph("src/").titled("Application Architecture")
|
|
406
|
-
|
|
407
|
-
graph.collapse_to_folder_depth(2).export_as_mermaid(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
426
|
+
graph = project_graph("src/requests").titled("Application Architecture")
|
|
427
|
+
|
|
428
|
+
graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
test_export_dependency_graph_reports()
|
|
432
|
+
```
|
|
433
|
+
**Rendered mermain diagram**
|
|
434
|
+
``` mermaid
|
|
435
|
+
flowchart LR
|
|
436
|
+
n0["__init__.py"]
|
|
437
|
+
n1["__version__.py"]
|
|
438
|
+
n2["_internal_utils.py"]
|
|
439
|
+
n3["_types.py"]
|
|
440
|
+
n4["adapters.py"]
|
|
441
|
+
n5["api.py"]
|
|
442
|
+
n6["auth.py"]
|
|
443
|
+
n7["certs.py"]
|
|
444
|
+
n8["compat.py"]
|
|
445
|
+
n9["cookies.py"]
|
|
446
|
+
n10["exceptions.py"]
|
|
447
|
+
n11["help.py"]
|
|
448
|
+
n12["hooks.py"]
|
|
449
|
+
n13["models.py"]
|
|
450
|
+
n14["packages.py"]
|
|
451
|
+
n15["sessions.py"]
|
|
452
|
+
n16["status_codes.py"]
|
|
453
|
+
n17["structures.py"]
|
|
454
|
+
n18["utils.py"]
|
|
455
|
+
n0 --> n1
|
|
456
|
+
n0 --> n5
|
|
457
|
+
n0 --> n10
|
|
458
|
+
n0 --> n13
|
|
459
|
+
n0 --> n15
|
|
460
|
+
n0 --> n16
|
|
461
|
+
n2 --> n8
|
|
462
|
+
n3 --> n6
|
|
463
|
+
n3 --> n9
|
|
464
|
+
n3 --> n13
|
|
465
|
+
n3 --> n17
|
|
466
|
+
n4 --> n0
|
|
467
|
+
n4 --> n3
|
|
468
|
+
n4 --> n6
|
|
469
|
+
n4 --> n8
|
|
470
|
+
n4 --> n9
|
|
471
|
+
n4 --> n10
|
|
472
|
+
n4 --> n13
|
|
473
|
+
n4 --> n17
|
|
474
|
+
n4 --> n18
|
|
475
|
+
n5 --> n0
|
|
476
|
+
n5 --> n13
|
|
477
|
+
n6 --> n2
|
|
478
|
+
n6 --> n8
|
|
479
|
+
n6 --> n9
|
|
480
|
+
n6 --> n13
|
|
481
|
+
n6 --> n18
|
|
482
|
+
n9 --> n2
|
|
483
|
+
n9 --> n3
|
|
484
|
+
n9 --> n8
|
|
485
|
+
n9 --> n13
|
|
486
|
+
n10 --> n8
|
|
487
|
+
n10 --> n13
|
|
488
|
+
n11 --> n0
|
|
489
|
+
n12 --> n0
|
|
490
|
+
n12 --> n13
|
|
491
|
+
n13 --> n0
|
|
492
|
+
n13 --> n2
|
|
493
|
+
n13 --> n4
|
|
494
|
+
n13 --> n6
|
|
495
|
+
n13 --> n8
|
|
496
|
+
n13 --> n9
|
|
497
|
+
n13 --> n10
|
|
498
|
+
n13 --> n12
|
|
499
|
+
n13 --> n16
|
|
500
|
+
n13 --> n17
|
|
501
|
+
n13 --> n18
|
|
502
|
+
n14 --> n8
|
|
503
|
+
n15 --> n0
|
|
504
|
+
n15 --> n2
|
|
505
|
+
n15 --> n3
|
|
506
|
+
n15 --> n4
|
|
507
|
+
n15 --> n6
|
|
508
|
+
n15 --> n8
|
|
509
|
+
n15 --> n9
|
|
510
|
+
n15 --> n10
|
|
511
|
+
n15 --> n12
|
|
512
|
+
n15 --> n13
|
|
513
|
+
n15 --> n16
|
|
514
|
+
n15 --> n17
|
|
515
|
+
n15 --> n18
|
|
516
|
+
n16 --> n17
|
|
517
|
+
n17 --> n8
|
|
518
|
+
n18 --> n0
|
|
519
|
+
n18 --> n1
|
|
520
|
+
n18 --> n2
|
|
521
|
+
n18 --> n3
|
|
522
|
+
n18 --> n8
|
|
523
|
+
n18 --> n9
|
|
524
|
+
n18 --> n10
|
|
525
|
+
n18 --> n13
|
|
526
|
+
n18 --> n17
|
|
416
527
|
```
|
|
417
528
|
|
|
418
529
|
Supported formats:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
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,6 +1,10 @@
|
|
|
1
1
|
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
2
2
|
from archunitpython.common.error.errors import TechnicalError, UserError
|
|
3
|
-
from archunitpython.common.fluentapi.checkable import
|
|
3
|
+
from archunitpython.common.fluentapi.checkable import (
|
|
4
|
+
Checkable,
|
|
5
|
+
CheckOptions,
|
|
6
|
+
RuleRationaleMixin,
|
|
7
|
+
)
|
|
4
8
|
from archunitpython.common.logging.types import LoggingOptions
|
|
5
9
|
from archunitpython.common.types import Filter, Pattern, PatternMatchingOptions
|
|
6
10
|
|
|
@@ -11,6 +15,7 @@ __all__ = [
|
|
|
11
15
|
"UserError",
|
|
12
16
|
"Checkable",
|
|
13
17
|
"CheckOptions",
|
|
18
|
+
"RuleRationaleMixin",
|
|
14
19
|
"LoggingOptions",
|
|
15
20
|
"Pattern",
|
|
16
21
|
"Filter",
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/fluentapi/checkable.py
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Protocol
|
|
6
|
+
from typing import Protocol, TypeVar
|
|
7
7
|
|
|
8
8
|
from archunitpython.common.assertion.violation import Violation
|
|
9
9
|
from archunitpython.common.logging.types import LoggingOptions
|
|
@@ -19,6 +19,28 @@ class CheckOptions:
|
|
|
19
19
|
ignore_type_checking_imports: bool = False
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
T = TypeVar("T", bound="RuleRationaleMixin")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RuleRationaleMixin:
|
|
26
|
+
"""Mixin for checkable rules that can carry a human-readable rationale."""
|
|
27
|
+
|
|
28
|
+
_because_reason: str | None = None
|
|
29
|
+
|
|
30
|
+
def because(self: T, reason: str) -> T:
|
|
31
|
+
"""Attach a rationale explaining why the rule exists."""
|
|
32
|
+
reason = reason.strip()
|
|
33
|
+
if not reason:
|
|
34
|
+
raise ValueError("Rule rationale must not be empty.")
|
|
35
|
+
self._because_reason = reason
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def because_reason(self) -> str | None:
|
|
40
|
+
"""Return the rationale attached with because(), if any."""
|
|
41
|
+
return self._because_reason
|
|
42
|
+
|
|
43
|
+
|
|
22
44
|
class Checkable(Protocol):
|
|
23
45
|
"""Protocol for any architecture rule that can be checked.
|
|
24
46
|
|
|
@@ -14,7 +14,7 @@ from collections.abc import Sequence
|
|
|
14
14
|
|
|
15
15
|
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
16
16
|
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
17
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
18
18
|
from archunitpython.common.pattern_matching import matches_all_patterns
|
|
19
19
|
from archunitpython.common.projection.edge_projections import (
|
|
20
20
|
per_external_edge,
|
|
@@ -322,7 +322,7 @@ def _check_empty_test(
|
|
|
322
322
|
return None
|
|
323
323
|
|
|
324
324
|
|
|
325
|
-
class CycleFreeFileCondition:
|
|
325
|
+
class CycleFreeFileCondition(RuleRationaleMixin):
|
|
326
326
|
"""Checkable that verifies no cycles exist among filtered files."""
|
|
327
327
|
|
|
328
328
|
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
@@ -350,7 +350,7 @@ class CycleFreeFileCondition:
|
|
|
350
350
|
return gather_cycle_violations(cycles)
|
|
351
351
|
|
|
352
352
|
|
|
353
|
-
class DependOnFileCondition:
|
|
353
|
+
class DependOnFileCondition(RuleRationaleMixin):
|
|
354
354
|
"""Checkable that verifies file dependency rules."""
|
|
355
355
|
|
|
356
356
|
def __init__(
|
|
@@ -377,7 +377,7 @@ class DependOnFileCondition:
|
|
|
377
377
|
)
|
|
378
378
|
|
|
379
379
|
|
|
380
|
-
class DependOnExternalModuleCondition:
|
|
380
|
+
class DependOnExternalModuleCondition(RuleRationaleMixin):
|
|
381
381
|
"""Checkable that verifies external module dependency rules."""
|
|
382
382
|
|
|
383
383
|
def __init__(
|
|
@@ -409,7 +409,7 @@ class DependOnExternalModuleCondition:
|
|
|
409
409
|
)
|
|
410
410
|
|
|
411
411
|
|
|
412
|
-
class MatchPatternFileCondition:
|
|
412
|
+
class MatchPatternFileCondition(RuleRationaleMixin):
|
|
413
413
|
"""Checkable that verifies files match/don't match patterns."""
|
|
414
414
|
|
|
415
415
|
def __init__(
|
|
@@ -434,7 +434,7 @@ class MatchPatternFileCondition:
|
|
|
434
434
|
return gather_regex_matching_violations(nodes, self._check_filters, self._is_negated)
|
|
435
435
|
|
|
436
436
|
|
|
437
|
-
class CustomFileCheckableCondition:
|
|
437
|
+
class CustomFileCheckableCondition(RuleRationaleMixin):
|
|
438
438
|
"""Checkable that evaluates a custom condition on files."""
|
|
439
439
|
|
|
440
440
|
def __init__(
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/metrics.py
RENAMED
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import Any, Callable
|
|
12
12
|
|
|
13
13
|
from archunitpython.common.assertion.violation import Violation
|
|
14
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
14
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
15
15
|
from archunitpython.common.pattern_matching import matches_pattern_classname
|
|
16
16
|
from archunitpython.common.regex_factory import RegexFactory
|
|
17
17
|
from archunitpython.common.types import Filter, Pattern
|
|
@@ -170,7 +170,7 @@ class ClassMetricThresholdBuilder:
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
class ClassMetricCondition:
|
|
173
|
+
class ClassMetricCondition(RuleRationaleMixin):
|
|
174
174
|
"""Checkable that verifies a class-level metric threshold."""
|
|
175
175
|
|
|
176
176
|
def __init__(
|
|
@@ -230,7 +230,7 @@ class FileMetricThresholdBuilder:
|
|
|
230
230
|
)
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
class FileMetricCondition:
|
|
233
|
+
class FileMetricCondition(RuleRationaleMixin):
|
|
234
234
|
"""Checkable that verifies a file-level metric threshold."""
|
|
235
235
|
|
|
236
236
|
def __init__(
|
|
@@ -364,7 +364,7 @@ class DistanceThresholdBuilder:
|
|
|
364
364
|
)
|
|
365
365
|
|
|
366
366
|
|
|
367
|
-
class DistanceCondition:
|
|
367
|
+
class DistanceCondition(RuleRationaleMixin):
|
|
368
368
|
"""Checkable for distance metric thresholds."""
|
|
369
369
|
|
|
370
370
|
def __init__(
|
|
@@ -404,7 +404,7 @@ class DistanceCondition:
|
|
|
404
404
|
return violations
|
|
405
405
|
|
|
406
406
|
|
|
407
|
-
class ZoneCondition:
|
|
407
|
+
class ZoneCondition(RuleRationaleMixin):
|
|
408
408
|
"""Checkable for zone detection (pain/uselessness)."""
|
|
409
409
|
|
|
410
410
|
def __init__(self, project_path: str | None, filters: list[Filter], zone_type: str) -> None:
|
|
@@ -485,7 +485,7 @@ class CustomMetricsBuilder:
|
|
|
485
485
|
)
|
|
486
486
|
|
|
487
487
|
|
|
488
|
-
class CustomMetricCondition:
|
|
488
|
+
class CustomMetricCondition(RuleRationaleMixin):
|
|
489
489
|
"""Checkable for custom metric thresholds."""
|
|
490
490
|
|
|
491
491
|
def __init__(
|
|
@@ -525,7 +525,7 @@ class CustomMetricCondition:
|
|
|
525
525
|
return violations
|
|
526
526
|
|
|
527
527
|
|
|
528
|
-
class CustomAssertionCondition:
|
|
528
|
+
class CustomAssertionCondition(RuleRationaleMixin):
|
|
529
529
|
"""Checkable for custom metric assertions."""
|
|
530
530
|
|
|
531
531
|
def __init__(
|
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
|
|
15
15
|
from archunitpython.common.assertion.violation import Violation
|
|
16
16
|
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
17
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
18
18
|
from archunitpython.common.projection.project_edges import project_edges
|
|
19
19
|
from archunitpython.common.projection.types import MapFunction
|
|
20
20
|
from archunitpython.slices.assertion.admissible_edges import (
|
|
@@ -140,7 +140,7 @@ class NegativeConditionBuilder:
|
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
class PositiveSliceCondition:
|
|
143
|
+
class PositiveSliceCondition(RuleRationaleMixin):
|
|
144
144
|
"""Checkable that verifies slices adhere to a diagram."""
|
|
145
145
|
|
|
146
146
|
def __init__(
|
|
@@ -176,7 +176,7 @@ class PositiveSliceCondition:
|
|
|
176
176
|
return identity()
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
class NegativeSliceCondition:
|
|
179
|
+
class NegativeSliceCondition(RuleRationaleMixin):
|
|
180
180
|
"""Checkable that verifies a specific dependency does NOT exist."""
|
|
181
181
|
|
|
182
182
|
def __init__(
|
|
@@ -7,7 +7,11 @@ from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
|
|
|
7
7
|
from archunitpython.testing.common.violation_factory import ViolationFactory
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def format_violations(
|
|
10
|
+
def format_violations(
|
|
11
|
+
violations: list[Violation],
|
|
12
|
+
*,
|
|
13
|
+
because: str | None = None,
|
|
14
|
+
) -> str:
|
|
11
15
|
"""Format violations into a human-readable string.
|
|
12
16
|
|
|
13
17
|
Args:
|
|
@@ -19,7 +23,10 @@ def format_violations(violations: list[Violation]) -> str:
|
|
|
19
23
|
if not violations:
|
|
20
24
|
return "No violations found."
|
|
21
25
|
|
|
22
|
-
lines = [f"Found {len(violations)} architecture violation(s):"
|
|
26
|
+
lines = [f"Found {len(violations)} architecture violation(s):"]
|
|
27
|
+
if because:
|
|
28
|
+
lines.extend(["", f"Because: {because}"])
|
|
29
|
+
lines.append("")
|
|
23
30
|
for i, violation in enumerate(violations, 1):
|
|
24
31
|
tv = ViolationFactory.from_violation(violation)
|
|
25
32
|
lines.append(f" {i}. {tv.message}")
|
|
@@ -44,4 +51,5 @@ def assert_passes(
|
|
|
44
51
|
"""
|
|
45
52
|
violations = checkable.check(options)
|
|
46
53
|
if violations:
|
|
47
|
-
|
|
54
|
+
because = getattr(checkable, "because_reason", None)
|
|
55
|
+
raise AssertionError(format_violations(violations, because=because))
|
|
@@ -5,6 +5,8 @@ import shutil
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
8
10
|
from archunitpython.common.assertion.violation import EmptyTestViolation
|
|
9
11
|
from archunitpython.common.extraction.extract_graph import clear_graph_cache
|
|
10
12
|
from archunitpython.files.assertion.custom_file_logic import CustomFileViolation
|
|
@@ -223,6 +225,23 @@ class TestMethodChaining:
|
|
|
223
225
|
cycle_violations = [v for v in violations if isinstance(v, ViolatingCycle)]
|
|
224
226
|
assert len(cycle_violations) == 0
|
|
225
227
|
|
|
228
|
+
def test_because_adds_rule_rationale(self):
|
|
229
|
+
rule = (
|
|
230
|
+
project_files(FIXTURES_DIR)
|
|
231
|
+
.in_folder("**/services*")
|
|
232
|
+
.should()
|
|
233
|
+
.have_no_cycles()
|
|
234
|
+
.because("service cycles are hard to refactor")
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
assert rule.because_reason == "service cycles are hard to refactor"
|
|
238
|
+
|
|
239
|
+
def test_because_rejects_empty_rationale(self):
|
|
240
|
+
rule = project_files(FIXTURES_DIR).should().have_no_cycles()
|
|
241
|
+
|
|
242
|
+
with pytest.raises(ValueError, match="must not be empty"):
|
|
243
|
+
rule.because(" ")
|
|
244
|
+
|
|
226
245
|
|
|
227
246
|
class TestTypeCheckingImports:
|
|
228
247
|
def setup_method(self):
|
|
@@ -83,6 +83,19 @@ class TestAssertPasses:
|
|
|
83
83
|
with pytest.raises(AssertionError, match="architecture violation"):
|
|
84
84
|
assert_passes(rule)
|
|
85
85
|
|
|
86
|
+
def test_failing_rule_includes_because_rationale(self):
|
|
87
|
+
rule = (
|
|
88
|
+
project_files(FIXTURES_DIR)
|
|
89
|
+
.in_folder("**/controllers*")
|
|
90
|
+
.should_not()
|
|
91
|
+
.depend_on_files()
|
|
92
|
+
.in_folder("**/services*")
|
|
93
|
+
.because("controllers should stay thin")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
with pytest.raises(AssertionError, match="Because: controllers should stay thin"):
|
|
97
|
+
assert_passes(rule)
|
|
98
|
+
|
|
86
99
|
|
|
87
100
|
class TestFormatViolations:
|
|
88
101
|
def test_no_violations(self):
|
|
@@ -102,6 +115,18 @@ class TestFormatViolations:
|
|
|
102
115
|
assert "1 architecture violation" in result
|
|
103
116
|
assert "Circular dependency" in result
|
|
104
117
|
|
|
118
|
+
def test_with_because_rationale(self):
|
|
119
|
+
from archunitpython.common.projection.types import ProjectedEdge
|
|
120
|
+
|
|
121
|
+
violation = ViolatingCycle(
|
|
122
|
+
cycle=[
|
|
123
|
+
ProjectedEdge(source_label="a.py", target_label="b.py"),
|
|
124
|
+
ProjectedEdge(source_label="b.py", target_label="a.py"),
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
result = format_violations([violation], because="cycles make changes risky")
|
|
128
|
+
assert "Because: cycles make changes risky" in result
|
|
129
|
+
|
|
105
130
|
|
|
106
131
|
class TestSelfTesting:
|
|
107
132
|
"""ArchUnitPython tests its own architecture."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/assertion/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/assertion/violation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/extraction/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/cycles.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/cycles/model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_cycles.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_edges.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/projection/project_nodes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/common/util/declaration_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/cycle_free.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/depend_on_files.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/assertion/matching_files.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/files/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/layers/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/count.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/distance.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/calculation/lcom.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/extraction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/fluentapi/export_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/metrics/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/uml/export_diagram.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/slices/uml/generate_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/common/color_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/common/violation_factory.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/src/archunitpython/testing/pytest_plugin/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/architecture.puml
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/controllers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service_a.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/services/service_b.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/utils/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.2.1 → archunitpython-1.3.0}/tests/fixtures/sample_project/utils/helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|