archunitpython 1.4.0__tar.gz → 1.6.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.
Files changed (146) hide show
  1. {archunitpython-1.4.0 → archunitpython-1.6.0}/BACKLOG.md +6 -6
  2. {archunitpython-1.4.0 → archunitpython-1.6.0}/CHANGELOG.md +20 -0
  3. {archunitpython-1.4.0 → archunitpython-1.6.0}/PKG-INFO +117 -12
  4. {archunitpython-1.4.0 → archunitpython-1.6.0}/README.md +115 -10
  5. {archunitpython-1.4.0 → archunitpython-1.6.0}/pyproject.toml +1 -1
  6. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/__init__.py +5 -1
  7. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/extraction/extract_graph.py +152 -20
  8. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/extraction/graph.py +1 -0
  9. archunitpython-1.6.0/src/archunitpython/config/__init__.py +5 -0
  10. archunitpython-1.6.0/src/archunitpython/config/loader.py +119 -0
  11. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/custom_file_logic.py +3 -3
  12. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/depend_on_files.py +3 -3
  13. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/matching_files.py +3 -3
  14. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/assertion/admissible_edges.py +1 -1
  15. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_extract_graph.py +376 -0
  16. archunitpython-1.6.0/tests/config/__init__.py +1 -0
  17. archunitpython-1.6.0/tests/config/test_config_loader.py +115 -0
  18. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/integration/test_e2e.py +5 -0
  19. {archunitpython-1.4.0 → archunitpython-1.6.0}/.editorconfig +0 -0
  20. {archunitpython-1.4.0 → archunitpython-1.6.0}/.gitattributes +0 -0
  21. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/CODEOWNERS +0 -0
  22. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/FUNDING.yml +0 -0
  23. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  24. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
  25. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
  27. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/PAGES.md +0 -0
  28. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/dependabot.yml +0 -0
  29. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/pull_request_template.md +0 -0
  30. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/workflows/docs.yaml +0 -0
  31. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/workflows/integrate.yaml +0 -0
  32. {archunitpython-1.4.0 → archunitpython-1.6.0}/.github/workflows/stale.yaml +0 -0
  33. {archunitpython-1.4.0 → archunitpython-1.6.0}/.gitignore +0 -0
  34. {archunitpython-1.4.0 → archunitpython-1.6.0}/.releaserc.json +0 -0
  35. {archunitpython-1.4.0 → archunitpython-1.6.0}/CONTRIBUTING.md +0 -0
  36. {archunitpython-1.4.0 → archunitpython-1.6.0}/LICENSE +0 -0
  37. {archunitpython-1.4.0 → archunitpython-1.6.0}/assets/logo-rounded.png +0 -0
  38. {archunitpython-1.4.0 → archunitpython-1.6.0}/research/product-direction/architecture-testing-landscape.md +0 -0
  39. {archunitpython-1.4.0 → archunitpython-1.6.0}/scripts/bump_release_version.py +0 -0
  40. {archunitpython-1.4.0 → archunitpython-1.6.0}/scripts/check_release_metadata.py +0 -0
  41. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/__init__.py +0 -0
  42. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/assertion/__init__.py +0 -0
  43. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/assertion/violation.py +0 -0
  44. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/error/__init__.py +0 -0
  45. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/error/errors.py +0 -0
  46. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/extraction/__init__.py +0 -0
  47. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
  48. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
  49. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/logging/__init__.py +0 -0
  50. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/logging/types.py +0 -0
  51. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/pattern_matching.py +0 -0
  52. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/__init__.py +0 -0
  53. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
  54. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
  55. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
  56. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
  57. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
  58. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
  59. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
  60. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
  61. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/project_edges.py +0 -0
  62. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
  63. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/projection/types.py +0 -0
  64. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/regex_factory.py +0 -0
  65. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/types.py +0 -0
  66. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/util/__init__.py +0 -0
  67. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
  68. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/common/util/logger.py +0 -0
  69. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/__init__.py +0 -0
  70. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/__init__.py +0 -0
  71. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
  72. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
  73. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
  74. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/files/fluentapi/files.py +0 -0
  75. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/graph/__init__.py +0 -0
  76. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/graph/graph_reporter.py +0 -0
  77. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/layers/__init__.py +0 -0
  78. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
  79. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
  80. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
  81. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/layers/fluentapi/layers.py +0 -0
  82. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/__init__.py +0 -0
  83. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
  84. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
  85. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
  86. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/calculation/count.py +0 -0
  87. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
  88. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
  89. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/common/__init__.py +0 -0
  90. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/common/types.py +0 -0
  91. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
  92. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
  93. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
  94. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
  95. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/fluentapi/metrics.py +0 -0
  96. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
  97. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/py.typed +0 -0
  98. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/__init__.py +0 -0
  99. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
  100. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
  101. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/fluentapi/slices.py +0 -0
  102. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/projection/__init__.py +0 -0
  103. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
  104. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/uml/__init__.py +0 -0
  105. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
  106. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
  107. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/__init__.py +0 -0
  108. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/assertion.py +0 -0
  109. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/common/__init__.py +0 -0
  110. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/common/color_utils.py +0 -0
  111. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
  112. {archunitpython-1.4.0 → archunitpython-1.6.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
  113. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/__init__.py +0 -0
  114. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/__init__.py +0 -0
  115. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_core_types.py +0 -0
  116. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_cycles.py +0 -0
  117. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_declaration_detector.py +0 -0
  118. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_logger.py +0 -0
  119. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_pattern_matching.py +0 -0
  120. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/common/test_projection.py +0 -0
  121. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/files/__init__.py +0 -0
  122. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/files/test_file_assertions.py +0 -0
  123. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/files/test_files_fluentapi.py +0 -0
  124. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/metrics_project/service.py +0 -0
  125. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/__init__.py +0 -0
  126. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/architecture.puml +0 -0
  127. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
  128. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
  129. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
  130. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/models/model.py +0 -0
  131. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
  132. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/services/service.py +0 -0
  133. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
  134. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
  135. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
  136. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
  137. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/graph/test_graph_reporter.py +0 -0
  138. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/integration/__init__.py +0 -0
  139. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/layers/test_layers.py +0 -0
  140. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/metrics/__init__.py +0 -0
  141. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/metrics/test_export.py +0 -0
  142. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/metrics/test_metrics.py +0 -0
  143. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
  144. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/slices/__init__.py +0 -0
  145. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/slices/test_slices.py +0 -0
  146. {archunitpython-1.4.0 → archunitpython-1.6.0}/tests/test_setup.py +0 -0
@@ -13,14 +13,14 @@ This backlog collects product and maintenance ideas from project research.
13
13
 
14
14
  - [x] Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
15
15
  - [x] Add a `.because(...)` API so rules can carry user-facing rationale into failure messages and generated architecture documentation.
16
- - Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
16
+ - [x] 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
 
19
19
  ## P1 - Python Import Semantics
20
20
 
21
21
  - Add support for namespace packages that do not contain `__init__.py`.
22
- - Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
23
- - Detect conditional imports such as `try/except ImportError`.
22
+ - [x] Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
23
+ - [x] Detect conditional imports such as `try/except ImportError`.
24
24
  - Add better `TYPE_CHECKING` import handling, including options to ignore, include, or report type-only imports separately.
25
25
  - Improve external dependency rules so users can express allowed and forbidden third-party packages at module or slice level.
26
26
  - Consider a public-interface rule inspired by Tach, where modules may only import through declared package APIs.
@@ -31,11 +31,11 @@ This backlog collects product and maintenance ideas from project research.
31
31
  - Auto-generate architecture documentation based on tests and rule rationales.
32
32
  - Make logged paths clickable in IDEs and common terminal integrations.
33
33
  - Add PlantUML or Mermaid export for discovered architecture graphs.
34
- - Improve metric export examples and document how metric thresholds should be selected.
34
+ - [x] Improve metric export examples and document how metric thresholds should be selected.
35
35
 
36
36
  ## P2 - Rule Surface
37
37
 
38
- - Add first-class layered architecture helpers so common clean/hexagonal/layered rules require less boilerplate.
38
+ - [x] Add first-class layered architecture helpers so common clean/hexagonal/layered rules require less boilerplate.
39
39
  - Add slice isolation helpers for bounded contexts and modular monoliths.
40
40
  - Add richer custom rule hooks for dependency edges, files, classes, and metrics.
41
41
  - Add transitive dependency checks, especially for "domain must not transitively reach infrastructure" style rules.
@@ -56,7 +56,7 @@ This backlog collects product and maintenance ideas from project research.
56
56
 
57
57
  ## P3 - Packaging And Docs
58
58
 
59
- - Publish to PyPI as part of the release pipeline if this is not already automated.
59
+ - [x] Publish to PyPI as part of the release pipeline if this is not already automated.
60
60
  - Add a Sphinx or MkDocs documentation site.
61
61
  - Add a complete example repository or examples folder covering pytest, unittest, PlantUML, metrics, and CI.
62
62
  - Add contribution guidance for new rule types and metric implementations.
@@ -1,3 +1,23 @@
1
+ # [1.6.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.5.0...v1.6.0) (2026-09-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * preserve package targets for relative imports ([09c9d19](https://github.com/LukasNiessen/ArchUnitPython/commit/09c9d199dec095aaa60be227bfd7b5c2b7ffb107))
7
+ * preserve relative conditional import targets ([0ae22bb](https://github.com/LukasNiessen/ArchUnitPython/commit/0ae22bbed008c39f39b80ac6428fa1207115a394))
8
+
9
+
10
+ ### Features
11
+
12
+ * classify conditional imports ([5f0f600](https://github.com/LukasNiessen/ArchUnitPython/commit/5f0f600e02d82249e0735738b43cc75317eff790))
13
+
14
+ # [1.5.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.4.0...v1.5.0) (2026-07-18)
15
+
16
+
17
+ ### Features
18
+
19
+ * load common rules from config ([c7c968a](https://github.com/LukasNiessen/ArchUnitPython/commit/c7c968af20e929f7e7a2372275eaa7e26cb38e5f))
20
+
1
21
  # [1.4.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.3.0...v1.4.0) (2026-07-18)
2
22
 
3
23
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: archunitpython
3
- Version: 1.4.0
3
+ Version: 1.6.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
@@ -41,7 +41,6 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build & tests](https://img.shields.io/github/actions/workflow/status/LukasNiessen/ArchUnitPython/integrate.yaml?branch=main&label=build%20%26%20tests)](https://github.com/LukasNiessen/ArchUnitPython/actions/workflows/integrate.yaml) [![GitHub stars](https://img.shields.io/github/stars/LukasNiessen/ArchUnitPython.svg)](https://github.com/LukasNiessen/ArchUnitPython)<br>
43
43
  [![PyPI downloads](https://static.pepy.tech/badge/archunitpython/month)](https://pepy.tech/project/archunitpython) [![PyPI total downloads](https://img.shields.io/pepy/dt/archunitpython?label=total%20downloads&color=007ec6)](https://pepy.tech/project/archunitpython)
44
- <!-- [![PyPI version](https://img.shields.io/pypi/v/archunitpython.svg)](https://pypi.org/project/archunitpython/) -->
45
44
 
46
45
  </div>
47
46
 
@@ -51,7 +50,7 @@ The #1 ArchUnit-style architecture testing library for Python, measured by GitHu
51
50
 
52
51
  _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit._
53
52
 
54
- [Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Sponsor](https://github.com/sponsors/LukasNiessen) • [Contributing](CONTRIBUTING.md)
53
+ [Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Sponsor](https://github.com/sponsors/LukasNiessen) • [Contributing](CONTRIBUTING.md) • [Documentation](https://lukasniessen.github.io/ArchUnitPython/)
55
54
 
56
55
  ## ⚡ 5 min Quickstart
57
56
 
@@ -206,6 +205,42 @@ migrations/*.py
206
205
 
207
206
  Patterns support comments, blank lines, glob syntax, root-relative paths, path
208
207
  patterns, and directory patterns with a trailing `/`.
208
+
209
+ ### Loading Common Rules From Config
210
+
211
+ For straightforward shared rules, you can load a JSON config file and still run
212
+ the resulting rules in your normal test suite:
213
+
214
+ ```json
215
+ {
216
+ "project_path": "src",
217
+ "rules": [
218
+ {
219
+ "name": "controllers must not use services directly",
220
+ "type": "forbidden_dependency",
221
+ "source": "**/controllers/**",
222
+ "target": "**/services/**"
223
+ },
224
+ {
225
+ "name": "source files have no cycles",
226
+ "type": "no_cycles"
227
+ }
228
+ ]
229
+ }
230
+ ```
231
+
232
+ ```python
233
+ from archunitpython import assert_passes, rules_from_config
234
+
235
+ def test_configured_architecture_rules():
236
+ for rule in rules_from_config("archunitpython.json"):
237
+ assert_passes(rule)
238
+ ```
239
+
240
+ Supported rule types are `no_cycles`, `forbidden_dependency`, and
241
+ `forbidden_external_dependency`. The fluent Python API remains the primary and
242
+ most flexible interface.
243
+
209
244
  ### Explaining Rules With `.because(...)`
210
245
 
211
246
  Attach a rationale to a rule so failing assertions explain why the rule exists:
@@ -342,6 +377,17 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
342
377
  from my_app.adapters.sql import Repository # archunit: ignore
343
378
  ```
344
379
 
380
+ ### Conditional Imports
381
+
382
+ Imports inside `try` blocks that handle `ImportError` or
383
+ `ModuleNotFoundError` are marked as conditional dependencies. This helps graph
384
+ reports distinguish optional imports and fallback implementations from regular
385
+ runtime imports. Conditional dependencies remain part of architecture checks;
386
+ relative and dynamic imports also retain their original import kind in graph
387
+ reports. An edge may therefore have multiple kinds: CSV reports use
388
+ pipe-delimited values such as `relative|conditional`, while HTML reports list
389
+ both values separately.
390
+
345
391
  ### Naming Conventions
346
392
 
347
393
  ```python
@@ -357,6 +403,10 @@ def test_naming_patterns():
357
403
 
358
404
  ### Code Metrics
359
405
 
406
+ Metric rules evaluate every matching file or class independently. Use filters such as
407
+ `in_folder()`, `with_name()`, and `for_classes_matching()` when different parts of the
408
+ project need different limits.
409
+
360
410
  ```python
361
411
  def test_no_large_files():
362
412
  rule = metrics("src/").count().lines_of_code().should_be_below(1000)
@@ -367,7 +417,7 @@ def test_high_class_cohesion():
367
417
  assert_passes(rule)
368
418
 
369
419
  def test_method_count():
370
- rule = metrics("src/").count().method_count().should_be_below(20)
420
+ rule = metrics("src/").count().method_count().should_be_below_or_equal(20)
371
421
  assert_passes(rule)
372
422
 
373
423
  def test_field_count_for_data_classes():
@@ -381,6 +431,45 @@ def test_field_count_for_data_classes():
381
431
  assert_passes(rule)
382
432
  ```
383
433
 
434
+ #### Comparison Semantics
435
+
436
+ Metric comparisons are exact. In particular, `should_be_below(20)` means `< 20`, so
437
+ a value of exactly `20` is a violation. Use the inclusive form when the limit itself
438
+ should be accepted.
439
+
440
+ | Method | Passing values |
441
+ | --- | --- |
442
+ | `should_be_below(n)` | `< n` |
443
+ | `should_be_below_or_equal(n)` | `<= n` |
444
+ | `should_be_above(n)` | `> n` |
445
+ | `should_be_above_or_equal(n)` | `>= n` |
446
+ | `should_be(n)` | exactly `n` |
447
+
448
+ Not every metric builder exposes every comparison. The available methods are shown
449
+ by the fluent API after selecting a metric.
450
+
451
+ #### Choosing Thresholds
452
+
453
+ There is no universal correct limit for every codebase. Treat thresholds as
454
+ architecture decisions that should reflect the role and maturity of the code:
455
+
456
+ 1. Measure the current project before enabling a new rule.
457
+ 2. Start at the current maximum, or slightly above it, to prevent further regression.
458
+ 3. Use narrower filters when generated code, data classes, or adapters need different limits.
459
+ 4. Lower the threshold gradually as existing violations are removed.
460
+ 5. Record the reason with `.because(...)` so future maintainers understand the limit.
461
+
462
+ | Metric | Interpretation | Useful starting point |
463
+ | --- | --- | --- |
464
+ | Lines of code | File size and review burden | Current maximum for hand-written files |
465
+ | Method or field count | Class responsibility and size | Current maximum, split by class role |
466
+ | LCOM | Lack of class cohesion; lower is generally better | Baseline one LCOM variant and keep it consistent |
467
+ | Instability | Dependence on outgoing versus incoming dependencies | Compare files with similar architectural roles |
468
+ | Distance from main sequence | Balance between abstractness and instability; closer to zero is better | Observe the current distribution before tightening |
469
+
470
+ Thresholds are guardrails, not quality scores. A metric violation is a prompt to inspect
471
+ the design; it does not automatically mean the code is incorrect.
472
+
384
473
  ### Distance Metrics
385
474
 
386
475
  ```python
@@ -476,13 +565,13 @@ from archunitpython import project_graph
476
565
  def test_export_dependency_graph_reports():
477
566
  graph = project_graph("src/requests").titled("Application Architecture")
478
567
 
479
- graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
568
+ graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.mmd")
480
569
 
481
570
  if __name__ == "__main__":
482
571
  test_export_dependency_graph_reports()
483
572
  ```
484
- **Exported mermaid diagram**
485
- ``` mermaid
573
+ **Exported Mermaid diagram**
574
+ ```mermaid
486
575
  flowchart LR
487
576
  n0["__init__.py"]
488
577
  n1["__version__.py"]
@@ -600,20 +689,36 @@ When you create reports through `project_graph("src/")`, internal file paths are
600
689
 
601
690
  ### Reports
602
691
 
603
- Generate HTML reports for your metrics. _Note that this feature is in beta._
692
+ Generate an HTML report from metric values collected by your tests or build tooling.
693
+ `MetricsExporter` formats the supplied dictionary; it does not execute metric rules or
694
+ calculate the values itself. _This feature is in beta._
604
695
 
605
696
  ```python
606
697
  from archunitpython.metrics.fluentapi.export_utils import MetricsExporter, ExportOptions
607
698
 
608
- MetricsExporter.export_as_html(
609
- {"MethodCount": 5, "FieldCount": 3, "LinesOfCode": 150},
699
+ metric_summary = {
700
+ "Maximum method count": "18 (limit: <= 20)",
701
+ "Maximum field count": "9 (limit: <= 10)",
702
+ "Maximum lines of code": "420 (limit: < 500)",
703
+ "Highest LCOM96b": "0.24 (limit: < 0.30)",
704
+ }
705
+
706
+ html = MetricsExporter.export_as_html(
707
+ metric_summary,
610
708
  ExportOptions(
611
709
  output_path="reports/metrics.html",
612
710
  title="Architecture Metrics Dashboard",
711
+ include_timestamp=False,
613
712
  ),
614
713
  )
714
+
715
+ assert "Maximum method count" in html
615
716
  ```
616
717
 
718
+ Keep labels and units stable if these reports are stored as CI artifacts and compared
719
+ between builds. Continue using executable metric rules with `assert_passes()` as the
720
+ enforcement mechanism.
721
+
617
722
  ## 🔎 Pattern Matching System
618
723
 
619
724
  We offer three targeting options for pattern matching across all modules:
@@ -948,7 +1053,7 @@ If ArchUnitPython helps your project, please consider:
948
1053
 
949
1054
  ### Star History
950
1055
 
951
- [![Star History Chart](https://api.star-history.com/svg?repos=LukasNiessen/ArchUnitPython&type=Date)](https://www.star-history.com/#LukasNiessen/ArchUnitPython&Date)
1056
+ [![Star History Chart](https://star-history.dera.page/svg?repos=LukasNiessen/ArchUnitPython&type=Date)](https://star-history.dera.page/#LukasNiessen/ArchUnitPython&Date)
952
1057
 
953
1058
  ## 📄 License
954
1059
 
@@ -8,7 +8,6 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build & tests](https://img.shields.io/github/actions/workflow/status/LukasNiessen/ArchUnitPython/integrate.yaml?branch=main&label=build%20%26%20tests)](https://github.com/LukasNiessen/ArchUnitPython/actions/workflows/integrate.yaml) [![GitHub stars](https://img.shields.io/github/stars/LukasNiessen/ArchUnitPython.svg)](https://github.com/LukasNiessen/ArchUnitPython)<br>
10
10
  [![PyPI downloads](https://static.pepy.tech/badge/archunitpython/month)](https://pepy.tech/project/archunitpython) [![PyPI total downloads](https://img.shields.io/pepy/dt/archunitpython?label=total%20downloads&color=007ec6)](https://pepy.tech/project/archunitpython)
11
- <!-- [![PyPI version](https://img.shields.io/pypi/v/archunitpython.svg)](https://pypi.org/project/archunitpython/) -->
12
11
 
13
12
  </div>
14
13
 
@@ -18,7 +17,7 @@ The #1 ArchUnit-style architecture testing library for Python, measured by GitHu
18
17
 
19
18
  _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit._
20
19
 
21
- [Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Sponsor](https://github.com/sponsors/LukasNiessen) • [Contributing](CONTRIBUTING.md)
20
+ [Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Sponsor](https://github.com/sponsors/LukasNiessen) • [Contributing](CONTRIBUTING.md) • [Documentation](https://lukasniessen.github.io/ArchUnitPython/)
22
21
 
23
22
  ## ⚡ 5 min Quickstart
24
23
 
@@ -173,6 +172,42 @@ migrations/*.py
173
172
 
174
173
  Patterns support comments, blank lines, glob syntax, root-relative paths, path
175
174
  patterns, and directory patterns with a trailing `/`.
175
+
176
+ ### Loading Common Rules From Config
177
+
178
+ For straightforward shared rules, you can load a JSON config file and still run
179
+ the resulting rules in your normal test suite:
180
+
181
+ ```json
182
+ {
183
+ "project_path": "src",
184
+ "rules": [
185
+ {
186
+ "name": "controllers must not use services directly",
187
+ "type": "forbidden_dependency",
188
+ "source": "**/controllers/**",
189
+ "target": "**/services/**"
190
+ },
191
+ {
192
+ "name": "source files have no cycles",
193
+ "type": "no_cycles"
194
+ }
195
+ ]
196
+ }
197
+ ```
198
+
199
+ ```python
200
+ from archunitpython import assert_passes, rules_from_config
201
+
202
+ def test_configured_architecture_rules():
203
+ for rule in rules_from_config("archunitpython.json"):
204
+ assert_passes(rule)
205
+ ```
206
+
207
+ Supported rule types are `no_cycles`, `forbidden_dependency`, and
208
+ `forbidden_external_dependency`. The fluent Python API remains the primary and
209
+ most flexible interface.
210
+
176
211
  ### Explaining Rules With `.because(...)`
177
212
 
178
213
  Attach a rationale to a rule so failing assertions explain why the rule exists:
@@ -309,6 +344,17 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
309
344
  from my_app.adapters.sql import Repository # archunit: ignore
310
345
  ```
311
346
 
347
+ ### Conditional Imports
348
+
349
+ Imports inside `try` blocks that handle `ImportError` or
350
+ `ModuleNotFoundError` are marked as conditional dependencies. This helps graph
351
+ reports distinguish optional imports and fallback implementations from regular
352
+ runtime imports. Conditional dependencies remain part of architecture checks;
353
+ relative and dynamic imports also retain their original import kind in graph
354
+ reports. An edge may therefore have multiple kinds: CSV reports use
355
+ pipe-delimited values such as `relative|conditional`, while HTML reports list
356
+ both values separately.
357
+
312
358
  ### Naming Conventions
313
359
 
314
360
  ```python
@@ -324,6 +370,10 @@ def test_naming_patterns():
324
370
 
325
371
  ### Code Metrics
326
372
 
373
+ Metric rules evaluate every matching file or class independently. Use filters such as
374
+ `in_folder()`, `with_name()`, and `for_classes_matching()` when different parts of the
375
+ project need different limits.
376
+
327
377
  ```python
328
378
  def test_no_large_files():
329
379
  rule = metrics("src/").count().lines_of_code().should_be_below(1000)
@@ -334,7 +384,7 @@ def test_high_class_cohesion():
334
384
  assert_passes(rule)
335
385
 
336
386
  def test_method_count():
337
- rule = metrics("src/").count().method_count().should_be_below(20)
387
+ rule = metrics("src/").count().method_count().should_be_below_or_equal(20)
338
388
  assert_passes(rule)
339
389
 
340
390
  def test_field_count_for_data_classes():
@@ -348,6 +398,45 @@ def test_field_count_for_data_classes():
348
398
  assert_passes(rule)
349
399
  ```
350
400
 
401
+ #### Comparison Semantics
402
+
403
+ Metric comparisons are exact. In particular, `should_be_below(20)` means `< 20`, so
404
+ a value of exactly `20` is a violation. Use the inclusive form when the limit itself
405
+ should be accepted.
406
+
407
+ | Method | Passing values |
408
+ | --- | --- |
409
+ | `should_be_below(n)` | `< n` |
410
+ | `should_be_below_or_equal(n)` | `<= n` |
411
+ | `should_be_above(n)` | `> n` |
412
+ | `should_be_above_or_equal(n)` | `>= n` |
413
+ | `should_be(n)` | exactly `n` |
414
+
415
+ Not every metric builder exposes every comparison. The available methods are shown
416
+ by the fluent API after selecting a metric.
417
+
418
+ #### Choosing Thresholds
419
+
420
+ There is no universal correct limit for every codebase. Treat thresholds as
421
+ architecture decisions that should reflect the role and maturity of the code:
422
+
423
+ 1. Measure the current project before enabling a new rule.
424
+ 2. Start at the current maximum, or slightly above it, to prevent further regression.
425
+ 3. Use narrower filters when generated code, data classes, or adapters need different limits.
426
+ 4. Lower the threshold gradually as existing violations are removed.
427
+ 5. Record the reason with `.because(...)` so future maintainers understand the limit.
428
+
429
+ | Metric | Interpretation | Useful starting point |
430
+ | --- | --- | --- |
431
+ | Lines of code | File size and review burden | Current maximum for hand-written files |
432
+ | Method or field count | Class responsibility and size | Current maximum, split by class role |
433
+ | LCOM | Lack of class cohesion; lower is generally better | Baseline one LCOM variant and keep it consistent |
434
+ | Instability | Dependence on outgoing versus incoming dependencies | Compare files with similar architectural roles |
435
+ | Distance from main sequence | Balance between abstractness and instability; closer to zero is better | Observe the current distribution before tightening |
436
+
437
+ Thresholds are guardrails, not quality scores. A metric violation is a prompt to inspect
438
+ the design; it does not automatically mean the code is incorrect.
439
+
351
440
  ### Distance Metrics
352
441
 
353
442
  ```python
@@ -443,13 +532,13 @@ from archunitpython import project_graph
443
532
  def test_export_dependency_graph_reports():
444
533
  graph = project_graph("src/requests").titled("Application Architecture")
445
534
 
446
- graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
535
+ graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.mmd")
447
536
 
448
537
  if __name__ == "__main__":
449
538
  test_export_dependency_graph_reports()
450
539
  ```
451
- **Exported mermaid diagram**
452
- ``` mermaid
540
+ **Exported Mermaid diagram**
541
+ ```mermaid
453
542
  flowchart LR
454
543
  n0["__init__.py"]
455
544
  n1["__version__.py"]
@@ -567,20 +656,36 @@ When you create reports through `project_graph("src/")`, internal file paths are
567
656
 
568
657
  ### Reports
569
658
 
570
- Generate HTML reports for your metrics. _Note that this feature is in beta._
659
+ Generate an HTML report from metric values collected by your tests or build tooling.
660
+ `MetricsExporter` formats the supplied dictionary; it does not execute metric rules or
661
+ calculate the values itself. _This feature is in beta._
571
662
 
572
663
  ```python
573
664
  from archunitpython.metrics.fluentapi.export_utils import MetricsExporter, ExportOptions
574
665
 
575
- MetricsExporter.export_as_html(
576
- {"MethodCount": 5, "FieldCount": 3, "LinesOfCode": 150},
666
+ metric_summary = {
667
+ "Maximum method count": "18 (limit: <= 20)",
668
+ "Maximum field count": "9 (limit: <= 10)",
669
+ "Maximum lines of code": "420 (limit: < 500)",
670
+ "Highest LCOM96b": "0.24 (limit: < 0.30)",
671
+ }
672
+
673
+ html = MetricsExporter.export_as_html(
674
+ metric_summary,
577
675
  ExportOptions(
578
676
  output_path="reports/metrics.html",
579
677
  title="Architecture Metrics Dashboard",
678
+ include_timestamp=False,
580
679
  ),
581
680
  )
681
+
682
+ assert "Maximum method count" in html
582
683
  ```
583
684
 
685
+ Keep labels and units stable if these reports are stored as CI artifacts and compared
686
+ between builds. Continue using executable metric rules with `assert_passes()` as the
687
+ enforcement mechanism.
688
+
584
689
  ## 🔎 Pattern Matching System
585
690
 
586
691
  We offer three targeting options for pattern matching across all modules:
@@ -915,7 +1020,7 @@ If ArchUnitPython helps your project, please consider:
915
1020
 
916
1021
  ### Star History
917
1022
 
918
- [![Star History Chart](https://api.star-history.com/svg?repos=LukasNiessen/ArchUnitPython&type=Date)](https://www.star-history.com/#LukasNiessen/ArchUnitPython&Date)
1023
+ [![Star History Chart](https://star-history.dera.page/svg?repos=LukasNiessen/ArchUnitPython&type=Date)](https://star-history.dera.page/#LukasNiessen/ArchUnitPython&Date)
919
1024
 
920
1025
  ## 📄 License
921
1026
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "archunitpython"
7
- version = "1.4.0"
7
+ version = "1.6.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,6 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.6.0"
4
4
 
5
5
  # Files API
6
6
  # Common
@@ -12,6 +12,7 @@ from archunitpython.common import (
12
12
  Violation,
13
13
  )
14
14
  from archunitpython.common.extraction import clear_graph_cache, extract_graph
15
+ from archunitpython.config import ConfiguredRule, rules_from_config
15
16
  from archunitpython.files import files, project_files
16
17
  from archunitpython.graph import dependency_graph, project_graph
17
18
  from archunitpython.layers import layers, project_layers
@@ -35,6 +36,9 @@ __all__ = [
35
36
  # Layers
36
37
  "project_layers",
37
38
  "layers",
39
+ # Config
40
+ "rules_from_config",
41
+ "ConfiguredRule",
38
42
  # Slices
39
43
  "project_slices",
40
44
  # Metrics