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.
Files changed (150) hide show
  1. {archunitpython-1.7.0 → archunitpython-1.8.1}/CHANGELOG.md +23 -0
  2. {archunitpython-1.7.0 → archunitpython-1.8.1}/PKG-INFO +147 -6
  3. {archunitpython-1.7.0 → archunitpython-1.8.1}/README.md +146 -5
  4. {archunitpython-1.7.0 → archunitpython-1.8.1}/pyproject.toml +1 -1
  5. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/__init__.py +3 -1
  6. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/extract_graph.py +199 -81
  7. archunitpython-1.8.1/src/archunitpython/common/fluentapi/inspection.py +79 -0
  8. archunitpython-1.8.1/src/archunitpython/common/logging/inspection.py +99 -0
  9. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/logging/types.py +2 -0
  10. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/pattern_matching.py +9 -2
  11. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_edges.py +12 -1
  12. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/regex_factory.py +14 -0
  13. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/types.py +2 -1
  14. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_files.py +14 -3
  15. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/files.py +10 -2
  16. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/layers.py +2 -0
  17. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/extract_class_info.py +17 -0
  18. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/metrics.py +47 -0
  19. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/slices.py +3 -0
  20. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/assertion.py +16 -6
  21. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/color_utils.py +12 -1
  22. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_core_types.py +13 -0
  23. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_extract_graph.py +179 -58
  24. archunitpython-1.8.1/tests/common/test_inspection.py +315 -0
  25. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_pattern_matching.py +31 -0
  26. archunitpython-1.8.1/tests/common/test_pretty_output.py +51 -0
  27. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/test_file_assertions.py +28 -0
  28. {archunitpython-1.7.0 → archunitpython-1.8.1}/.editorconfig +0 -0
  29. {archunitpython-1.7.0 → archunitpython-1.8.1}/.gitattributes +0 -0
  30. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/CODEOWNERS +0 -0
  31. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/FUNDING.yml +0 -0
  32. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  33. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
  34. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/question.md +0 -0
  36. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/PAGES.md +0 -0
  37. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/dependabot.yml +0 -0
  38. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/pull_request_template.md +0 -0
  39. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/docs.yaml +0 -0
  40. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/integrate.yaml +0 -0
  41. {archunitpython-1.7.0 → archunitpython-1.8.1}/.github/workflows/stale.yaml +0 -0
  42. {archunitpython-1.7.0 → archunitpython-1.8.1}/.gitignore +0 -0
  43. {archunitpython-1.7.0 → archunitpython-1.8.1}/.releaserc.json +0 -0
  44. {archunitpython-1.7.0 → archunitpython-1.8.1}/BACKLOG.md +0 -0
  45. {archunitpython-1.7.0 → archunitpython-1.8.1}/CONTRIBUTING.md +0 -0
  46. {archunitpython-1.7.0 → archunitpython-1.8.1}/LICENSE +0 -0
  47. {archunitpython-1.7.0 → archunitpython-1.8.1}/assets/logo-rounded.png +0 -0
  48. {archunitpython-1.7.0 → archunitpython-1.8.1}/research/product-direction/architecture-testing-landscape.md +0 -0
  49. {archunitpython-1.7.0 → archunitpython-1.8.1}/scripts/bump_release_version.py +0 -0
  50. {archunitpython-1.7.0 → archunitpython-1.8.1}/scripts/check_release_metadata.py +0 -0
  51. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/__init__.py +0 -0
  52. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/assertion/__init__.py +0 -0
  53. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/assertion/violation.py +0 -0
  54. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/error/__init__.py +0 -0
  55. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/error/errors.py +0 -0
  56. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/__init__.py +0 -0
  57. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/extraction/graph.py +0 -0
  58. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/__init__.py +0 -0
  59. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/checkable.py +0 -0
  60. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/logging/__init__.py +0 -0
  61. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/__init__.py +0 -0
  62. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
  63. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
  64. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
  65. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
  66. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/model.py +0 -0
  67. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
  68. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/edge_projections.py +0 -0
  69. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_cycles.py +0 -0
  70. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_nodes.py +0 -0
  71. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/projection/types.py +0 -0
  72. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/__init__.py +0 -0
  73. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/declaration_detector.py +0 -0
  74. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/common/util/logger.py +0 -0
  75. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/config/__init__.py +0 -0
  76. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/config/loader.py +0 -0
  77. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/__init__.py +0 -0
  78. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/__init__.py +0 -0
  79. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
  80. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/cycle_free.py +0 -0
  81. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
  82. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/assertion/matching_files.py +0 -0
  83. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/__init__.py +0 -0
  84. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/graph/__init__.py +0 -0
  85. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/graph/graph_reporter.py +0 -0
  86. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/__init__.py +0 -0
  87. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/__init__.py +0 -0
  88. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
  89. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
  90. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/__init__.py +0 -0
  91. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/__init__.py +0 -0
  92. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
  93. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/__init__.py +0 -0
  94. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/count.py +0 -0
  95. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/distance.py +0 -0
  96. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/lcom.py +0 -0
  97. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/common/__init__.py +0 -0
  98. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/common/types.py +0 -0
  99. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/__init__.py +0 -0
  100. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
  101. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
  102. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/metrics/projection/__init__.py +0 -0
  103. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/py.typed +0 -0
  104. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/__init__.py +0 -0
  105. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/__init__.py +0 -0
  106. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
  107. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
  108. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/projection/__init__.py +0 -0
  109. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
  110. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/__init__.py +0 -0
  111. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/export_diagram.py +0 -0
  112. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/slices/uml/generate_rules.py +0 -0
  113. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/__init__.py +0 -0
  114. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/__init__.py +0 -0
  115. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/common/violation_factory.py +0 -0
  116. {archunitpython-1.7.0 → archunitpython-1.8.1}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
  117. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/__init__.py +0 -0
  118. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/__init__.py +0 -0
  119. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_cycles.py +0 -0
  120. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_declaration_detector.py +0 -0
  121. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_logger.py +0 -0
  122. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/common/test_projection.py +0 -0
  123. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/config/__init__.py +0 -0
  124. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/config/test_config_loader.py +0 -0
  125. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/__init__.py +0 -0
  126. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/files/test_files_fluentapi.py +0 -0
  127. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/metrics_project/service.py +0 -0
  128. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/__init__.py +0 -0
  129. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/architecture.puml +0 -0
  130. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
  131. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/controller.py +0 -0
  132. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/__init__.py +0 -0
  133. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/model.py +0 -0
  134. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/__init__.py +0 -0
  135. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service.py +0 -0
  136. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_a.py +0 -0
  137. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_b.py +0 -0
  138. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/__init__.py +0 -0
  139. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/helpers.py +0 -0
  140. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/graph/test_graph_reporter.py +0 -0
  141. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/integration/__init__.py +0 -0
  142. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/integration/test_e2e.py +0 -0
  143. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/layers/test_layers.py +0 -0
  144. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/__init__.py +0 -0
  145. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_export.py +0 -0
  146. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_metrics.py +0 -0
  147. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/metrics/test_metrics_fluentapi.py +0 -0
  148. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/slices/__init__.py +0 -0
  149. {archunitpython-1.7.0 → archunitpython-1.8.1}/tests/slices/test_slices.py +0 -0
  150. {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.7.0
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/").in_folder("src/**").should().have_no_cycles()
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
- We support logging to help you understand what files are being analyzed and troubleshoot test failures. Logging is disabled by default to keep test output clean.
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
- from archunitpython.common.logging.types import LoggingOptions
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/archunit-YYYY-MM-DD_HH-MM-SS.log
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/").in_folder("src/**").should().have_no_cycles()
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
- We support logging to help you understand what files are being analyzed and troubleshoot test failures. Logging is disabled by default to keep test output clean.
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
- from archunitpython.common.logging.types import LoggingOptions
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/archunit-YYYY-MM-DD_HH-MM-SS.log
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.0"
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.7.0"
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",