archunitpython 1.7.0__tar.gz → 1.8.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.7.0 → archunitpython-1.8.0}/CHANGELOG.md +7 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/PKG-INFO +75 -5
- {archunitpython-1.7.0 → archunitpython-1.8.0}/README.md +74 -4
- {archunitpython-1.7.0 → archunitpython-1.8.0}/pyproject.toml +1 -1
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/__init__.py +3 -1
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/extraction/extract_graph.py +45 -28
- archunitpython-1.8.0/src/archunitpython/common/fluentapi/inspection.py +79 -0
- archunitpython-1.8.0/src/archunitpython/common/logging/inspection.py +99 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/logging/types.py +2 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/pattern_matching.py +7 -2
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_edges.py +12 -1
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/fluentapi/files.py +6 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/layers/fluentapi/layers.py +2 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/extraction/extract_class_info.py +17 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/metrics.py +47 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/fluentapi/slices.py +3 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/assertion.py +16 -6
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/common/color_utils.py +12 -1
- archunitpython-1.8.0/tests/common/test_inspection.py +244 -0
- archunitpython-1.8.0/tests/common/test_pretty_output.py +51 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.editorconfig +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.gitattributes +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/PAGES.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.gitignore +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/.releaserc.json +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/BACKLOG.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/LICENSE +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/config/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/config/loader.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_extract_graph.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/config/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/config/test_config_loader.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.7.0 → archunitpython-1.8.0}/tests/test_setup.py +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.8.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.7.0...v1.8.0) (2026-09-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add detailed per-check inspection and styled reports ([#65](https://github.com/LukasNiessen/ArchUnitPython/issues/65)) ([ab0f9ef](https://github.com/LukasNiessen/ArchUnitPython/commit/ab0f9efb71c90712366490121e82c09b112a5d06))
|
|
7
|
+
|
|
1
8
|
# [1.7.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.1...v1.7.0) (2026-09-13)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -926,6 +926,10 @@ The most important differences:
|
|
|
926
926
|
|
|
927
927
|
When tests fail, you get helpful output with file paths and violation details:
|
|
928
928
|
|
|
929
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
930
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
931
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
932
|
+
|
|
929
933
|
```
|
|
930
934
|
Found 2 architecture violation(s):
|
|
931
935
|
|
|
@@ -938,25 +942,91 @@ Found 2 architecture violation(s):
|
|
|
938
942
|
|
|
939
943
|
## 📝 Debug Logging & Configuration
|
|
940
944
|
|
|
941
|
-
|
|
945
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
946
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
947
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
948
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
949
|
+
|
|
950
|
+
| Level | What you see |
|
|
951
|
+
| --- | --- |
|
|
952
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
953
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
954
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
955
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
942
956
|
|
|
943
957
|
### Enabling Debug Logging
|
|
944
958
|
|
|
945
959
|
```python
|
|
946
|
-
from archunitpython import CheckOptions
|
|
947
|
-
|
|
960
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
961
|
+
|
|
962
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
948
963
|
|
|
949
964
|
options = CheckOptions(
|
|
950
965
|
logging=LoggingOptions(
|
|
951
966
|
enabled=True,
|
|
952
967
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
953
|
-
log_file=True, # Creates logs/
|
|
968
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
954
969
|
),
|
|
955
970
|
)
|
|
956
971
|
|
|
957
972
|
violations = rule.check(options)
|
|
958
973
|
```
|
|
959
974
|
|
|
975
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
976
|
+
on your project):
|
|
977
|
+
|
|
978
|
+
```text
|
|
979
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
980
|
+
[DEBUG] Graph cache miss
|
|
981
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
982
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
983
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
984
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
985
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
989
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
990
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
991
|
+
Python's standard `logging` module in your application.
|
|
992
|
+
|
|
993
|
+
### Save detailed inspection without console noise
|
|
994
|
+
|
|
995
|
+
```python
|
|
996
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
997
|
+
enabled=True,
|
|
998
|
+
level="debug",
|
|
999
|
+
console=False,
|
|
1000
|
+
log_file=True,
|
|
1001
|
+
log_path="logs/architecture.log",
|
|
1002
|
+
append_to_log_file=True,
|
|
1003
|
+
))
|
|
1004
|
+
violations = rule.check(options)
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
1008
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
1009
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
1010
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
1011
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
1012
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
1013
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
1014
|
+
not re-run custom predicates or metric calculations.
|
|
1015
|
+
|
|
1016
|
+
### Format results independently of logging
|
|
1017
|
+
|
|
1018
|
+
```python
|
|
1019
|
+
from archunitpython import assert_passes, format_violations
|
|
1020
|
+
|
|
1021
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
1022
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
1023
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
1027
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
1028
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
1029
|
+
|
|
960
1030
|
### CI Pipeline Integration
|
|
961
1031
|
|
|
962
1032
|
```yaml
|
|
@@ -893,6 +893,10 @@ The most important differences:
|
|
|
893
893
|
|
|
894
894
|
When tests fail, you get helpful output with file paths and violation details:
|
|
895
895
|
|
|
896
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
897
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
898
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
899
|
+
|
|
896
900
|
```
|
|
897
901
|
Found 2 architecture violation(s):
|
|
898
902
|
|
|
@@ -905,25 +909,91 @@ Found 2 architecture violation(s):
|
|
|
905
909
|
|
|
906
910
|
## 📝 Debug Logging & Configuration
|
|
907
911
|
|
|
908
|
-
|
|
912
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
913
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
914
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
915
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
916
|
+
|
|
917
|
+
| Level | What you see |
|
|
918
|
+
| --- | --- |
|
|
919
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
920
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
921
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
922
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
909
923
|
|
|
910
924
|
### Enabling Debug Logging
|
|
911
925
|
|
|
912
926
|
```python
|
|
913
|
-
from archunitpython import CheckOptions
|
|
914
|
-
|
|
927
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
928
|
+
|
|
929
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
915
930
|
|
|
916
931
|
options = CheckOptions(
|
|
917
932
|
logging=LoggingOptions(
|
|
918
933
|
enabled=True,
|
|
919
934
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
920
|
-
log_file=True, # Creates logs/
|
|
935
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
921
936
|
),
|
|
922
937
|
)
|
|
923
938
|
|
|
924
939
|
violations = rule.check(options)
|
|
925
940
|
```
|
|
926
941
|
|
|
942
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
943
|
+
on your project):
|
|
944
|
+
|
|
945
|
+
```text
|
|
946
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
947
|
+
[DEBUG] Graph cache miss
|
|
948
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
949
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
950
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
951
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
952
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
956
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
957
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
958
|
+
Python's standard `logging` module in your application.
|
|
959
|
+
|
|
960
|
+
### Save detailed inspection without console noise
|
|
961
|
+
|
|
962
|
+
```python
|
|
963
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
964
|
+
enabled=True,
|
|
965
|
+
level="debug",
|
|
966
|
+
console=False,
|
|
967
|
+
log_file=True,
|
|
968
|
+
log_path="logs/architecture.log",
|
|
969
|
+
append_to_log_file=True,
|
|
970
|
+
))
|
|
971
|
+
violations = rule.check(options)
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
975
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
976
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
977
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
978
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
979
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
980
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
981
|
+
not re-run custom predicates or metric calculations.
|
|
982
|
+
|
|
983
|
+
### Format results independently of logging
|
|
984
|
+
|
|
985
|
+
```python
|
|
986
|
+
from archunitpython import assert_passes, format_violations
|
|
987
|
+
|
|
988
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
989
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
990
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
994
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
995
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
996
|
+
|
|
927
997
|
### CI Pipeline Integration
|
|
928
998
|
|
|
929
999
|
```yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.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,12 +1,13 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.8.0"
|
|
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",
|
{archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
|
|
10
10
|
from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
|
|
11
11
|
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
12
|
+
from archunitpython.common.logging.inspection import debug, debug_enabled
|
|
12
13
|
|
|
13
14
|
GraphCacheKey = tuple[str, tuple[str, ...], bool]
|
|
14
15
|
|
|
@@ -57,8 +58,7 @@ class _IgnoreDirective:
|
|
|
57
58
|
if not self.modules:
|
|
58
59
|
return True
|
|
59
60
|
return any(
|
|
60
|
-
import_.module_name == module
|
|
61
|
-
or import_.module_name.startswith(f"{module}.")
|
|
61
|
+
import_.module_name == module or import_.module_name.startswith(f"{module}.")
|
|
62
62
|
for module in self.modules
|
|
63
63
|
)
|
|
64
64
|
|
|
@@ -97,21 +97,46 @@ def extract_graph(
|
|
|
97
97
|
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
|
|
98
98
|
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
|
|
99
99
|
|
|
100
|
+
debug(
|
|
101
|
+
"Extraction root: %s; excludes: %s; ignore type-only imports: %s",
|
|
102
|
+
project_path,
|
|
103
|
+
excludes,
|
|
104
|
+
ignore_type_checking_imports,
|
|
105
|
+
)
|
|
100
106
|
if options and options.clear_cache:
|
|
107
|
+
debug("Clearing graph cache for %s", project_path)
|
|
101
108
|
_graph_cache.pop(cache_key, None)
|
|
102
109
|
|
|
103
110
|
if cache_key in _graph_cache:
|
|
111
|
+
debug("Graph cache hit: %d edges", len(_graph_cache[cache_key]))
|
|
112
|
+
_inspect_graph(_graph_cache[cache_key])
|
|
104
113
|
return _graph_cache[cache_key]
|
|
114
|
+
debug("Graph cache miss")
|
|
105
115
|
|
|
106
116
|
result = _extract_graph_uncached(
|
|
107
117
|
project_path,
|
|
108
118
|
excludes,
|
|
109
119
|
ignore_type_checking_imports=ignore_type_checking_imports,
|
|
110
120
|
)
|
|
121
|
+
_inspect_graph(result)
|
|
111
122
|
_graph_cache[cache_key] = result
|
|
112
123
|
return result
|
|
113
124
|
|
|
114
125
|
|
|
126
|
+
def _inspect_graph(graph: Graph) -> None:
|
|
127
|
+
if not debug_enabled():
|
|
128
|
+
return
|
|
129
|
+
debug("Extracted graph: %d edges (including self-edges)", len(graph))
|
|
130
|
+
for edge in graph:
|
|
131
|
+
debug(
|
|
132
|
+
"Dependency: %s -> %s; external=%s; kinds=%s",
|
|
133
|
+
edge.source,
|
|
134
|
+
edge.target,
|
|
135
|
+
edge.external,
|
|
136
|
+
edge.import_kinds,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
115
140
|
def _build_cache_key(
|
|
116
141
|
project_path: str,
|
|
117
142
|
exclude_patterns: list[str],
|
|
@@ -167,6 +192,7 @@ def _extract_graph_uncached(
|
|
|
167
192
|
normalized_py_file_set = {_normalize(f) for f in py_files_set}
|
|
168
193
|
|
|
169
194
|
for file_path in py_files:
|
|
195
|
+
debug("Processing file: %s", file_path)
|
|
170
196
|
# Add self-referencing edge (ensures the file appears as a node)
|
|
171
197
|
edges.append(
|
|
172
198
|
Edge(
|
|
@@ -179,10 +205,13 @@ def _extract_graph_uncached(
|
|
|
179
205
|
imports = _extract_located_imports(file_path)
|
|
180
206
|
for located_import in imports:
|
|
181
207
|
import_kind = located_import.import_kind
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
if ignore_type_checking_imports and import_kind == ImportKind.TYPE_IMPORT:
|
|
209
|
+
debug(
|
|
210
|
+
"Skipped type-only import: %s:%s -> %s",
|
|
211
|
+
file_path,
|
|
212
|
+
located_import.line_number,
|
|
213
|
+
located_import.module_name,
|
|
214
|
+
)
|
|
186
215
|
continue
|
|
187
216
|
for resolved, is_external in _resolve_import_targets(
|
|
188
217
|
located_import, file_path, project_path
|
|
@@ -190,6 +219,7 @@ def _extract_graph_uncached(
|
|
|
190
219
|
if resolved and resolved != _normalize(file_path):
|
|
191
220
|
# Check if the resolved path is in our project
|
|
192
221
|
if not is_external and resolved not in normalized_py_file_set:
|
|
222
|
+
debug("Skipped excluded dependency: %s -> %s", file_path, resolved)
|
|
193
223
|
continue
|
|
194
224
|
|
|
195
225
|
edges.append(
|
|
@@ -227,6 +257,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
|
227
257
|
full_path, root, exclude, is_dir=False
|
|
228
258
|
):
|
|
229
259
|
py_files.append(os.path.abspath(full_path))
|
|
260
|
+
debug("Discovered file: %s", full_path)
|
|
230
261
|
|
|
231
262
|
return py_files
|
|
232
263
|
|
|
@@ -313,9 +344,7 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
313
344
|
conditional_import_ranges,
|
|
314
345
|
)
|
|
315
346
|
for alias in node.names:
|
|
316
|
-
imports.append(
|
|
317
|
-
_LocatedImport(alias.name, kind, node.lineno, syntax_kind)
|
|
318
|
-
)
|
|
347
|
+
imports.append(_LocatedImport(alias.name, kind, node.lineno, syntax_kind))
|
|
319
348
|
|
|
320
349
|
elif isinstance(node, ast.ImportFrom):
|
|
321
350
|
syntax_kind = (
|
|
@@ -329,9 +358,7 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
329
358
|
type_checking_ranges,
|
|
330
359
|
conditional_import_ranges,
|
|
331
360
|
)
|
|
332
|
-
fallback_module_name =
|
|
333
|
-
"." * node.level if node.level and node.module is None else None
|
|
334
|
-
)
|
|
361
|
+
fallback_module_name = "." * node.level if node.level and node.module is None else None
|
|
335
362
|
aliases = _module_aliases(node) if node.module else ()
|
|
336
363
|
for module_name in _import_from_module_names(node):
|
|
337
364
|
imports.append(
|
|
@@ -354,15 +381,9 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
354
381
|
conditional_import_ranges,
|
|
355
382
|
)
|
|
356
383
|
for module_name in _extract_dynamic_import_names(node):
|
|
357
|
-
imports.append(
|
|
358
|
-
_LocatedImport(module_name, kind, node.lineno, syntax_kind)
|
|
359
|
-
)
|
|
384
|
+
imports.append(_LocatedImport(module_name, kind, node.lineno, syntax_kind))
|
|
360
385
|
|
|
361
|
-
return [
|
|
362
|
-
import_
|
|
363
|
-
for import_ in imports
|
|
364
|
-
if not _is_ignored_import(import_, ignore_directives)
|
|
365
|
-
]
|
|
386
|
+
return [import_ for import_ in imports if not _is_ignored_import(import_, ignore_directives)]
|
|
366
387
|
|
|
367
388
|
|
|
368
389
|
def _find_ignore_directives(source: str) -> dict[int, _IgnoreDirective]:
|
|
@@ -555,14 +576,10 @@ def _resolve_import(
|
|
|
555
576
|
Returns (resolved_path, is_external).
|
|
556
577
|
The path is normalized with forward slashes.
|
|
557
578
|
"""
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
ImportKind.TYPE_IMPORT,
|
|
563
|
-
)
|
|
564
|
-
and import_name.startswith(".")
|
|
565
|
-
):
|
|
579
|
+
if kind in (
|
|
580
|
+
ImportKind.RELATIVE_IMPORT,
|
|
581
|
+
ImportKind.TYPE_IMPORT,
|
|
582
|
+
) and import_name.startswith("."):
|
|
566
583
|
# Relative import
|
|
567
584
|
return _resolve_relative_import(import_name, source_file, project_root)
|
|
568
585
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Observational instrumentation for the existing check contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from time import perf_counter
|
|
7
|
+
from typing import Callable, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from archunitpython.common.assertion.violation import Violation
|
|
10
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
11
|
+
from archunitpython.common.logging.inspection import InspectionSession, current_session
|
|
12
|
+
|
|
13
|
+
F = TypeVar("F", bound=Callable[..., list[Violation]])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def inspect_check(check: F) -> F:
|
|
17
|
+
"""Trace a check while preserving its options, results, and exceptions."""
|
|
18
|
+
|
|
19
|
+
@wraps(check)
|
|
20
|
+
def inspected(self: object, options: CheckOptions | None = None) -> list[Violation]:
|
|
21
|
+
logging_options = options.logging if options else None
|
|
22
|
+
session = (
|
|
23
|
+
InspectionSession(logging_options)
|
|
24
|
+
if logging_options and logging_options.enabled
|
|
25
|
+
else None
|
|
26
|
+
)
|
|
27
|
+
token = current_session.set(session)
|
|
28
|
+
started = perf_counter() if session else 0.0
|
|
29
|
+
rule_name = type(self).__name__
|
|
30
|
+
try:
|
|
31
|
+
if session:
|
|
32
|
+
session.emit("info", "Starting check: %s", rule_name)
|
|
33
|
+
# Describe only known rule configuration, never user callbacks.
|
|
34
|
+
for name in (
|
|
35
|
+
"_project_path",
|
|
36
|
+
"_filters",
|
|
37
|
+
"_subject_filters",
|
|
38
|
+
"_object_filters",
|
|
39
|
+
"_module_filters",
|
|
40
|
+
"_pre_filters",
|
|
41
|
+
"_check_filters",
|
|
42
|
+
"_is_negated",
|
|
43
|
+
"_pattern",
|
|
44
|
+
"_regex",
|
|
45
|
+
"_source",
|
|
46
|
+
"_target",
|
|
47
|
+
"_layers",
|
|
48
|
+
"_allowed_dependencies",
|
|
49
|
+
"_forbidden_dependencies",
|
|
50
|
+
"_threshold",
|
|
51
|
+
"_comparison",
|
|
52
|
+
"_metric_attr",
|
|
53
|
+
"_name",
|
|
54
|
+
"_zone_type",
|
|
55
|
+
):
|
|
56
|
+
if session.options.level == "debug" and name in vars(self):
|
|
57
|
+
session.emit("debug", "%s: %s", name.removeprefix("_"), vars(self)[name])
|
|
58
|
+
violations = check(self, options)
|
|
59
|
+
if session:
|
|
60
|
+
session.emit(
|
|
61
|
+
"warn" if violations else "info",
|
|
62
|
+
"Finished check: %s - %d violation(s) (%.3fs)",
|
|
63
|
+
rule_name,
|
|
64
|
+
len(violations),
|
|
65
|
+
perf_counter() - started,
|
|
66
|
+
)
|
|
67
|
+
for violation in violations:
|
|
68
|
+
session.emit("warn", "Violation: %s", violation)
|
|
69
|
+
return violations
|
|
70
|
+
except Exception as error:
|
|
71
|
+
if session:
|
|
72
|
+
session.emit("error", "Check failed: %s - %s", rule_name, error)
|
|
73
|
+
raise
|
|
74
|
+
finally:
|
|
75
|
+
current_session.reset(token)
|
|
76
|
+
if session:
|
|
77
|
+
session.close()
|
|
78
|
+
|
|
79
|
+
return cast(F, inspected)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Per-check diagnostic sessions, isolated from architecture evaluation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from archunitpython.common.logging.types import LoggingOptions
|
|
14
|
+
|
|
15
|
+
_LEVELS = {
|
|
16
|
+
"debug": logging.DEBUG,
|
|
17
|
+
"info": logging.INFO,
|
|
18
|
+
"warn": logging.WARNING,
|
|
19
|
+
"error": logging.ERROR,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InspectionSession:
|
|
24
|
+
"""Own a check's output without attaching process-wide logging handlers.
|
|
25
|
+
|
|
26
|
+
Diagnostics are best effort: formatting and output errors must never replace
|
|
27
|
+
a rule result or an exception raised by the analysis itself.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, options: LoggingOptions) -> None:
|
|
31
|
+
self.options = options
|
|
32
|
+
self._file: TextIO | None = None
|
|
33
|
+
self._file_attempted = False
|
|
34
|
+
|
|
35
|
+
def emit(self, level: str, message: str, *args: object) -> None:
|
|
36
|
+
if _LEVELS[level] < _LEVELS.get(self.options.level, logging.INFO):
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
text = message % args if args else message
|
|
40
|
+
except Exception:
|
|
41
|
+
return
|
|
42
|
+
if self.options.console:
|
|
43
|
+
try:
|
|
44
|
+
logger = logging.getLogger("archunitpython")
|
|
45
|
+
if logger.hasHandlers():
|
|
46
|
+
logger.log(_LEVELS[level], text)
|
|
47
|
+
else:
|
|
48
|
+
sys.stderr.write(f"[{level.upper()}] {text}\n")
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
if self.options.log_file:
|
|
52
|
+
try:
|
|
53
|
+
if not self._file_attempted:
|
|
54
|
+
self._file_attempted = True
|
|
55
|
+
path = (
|
|
56
|
+
Path(self.options.log_path)
|
|
57
|
+
if self.options.log_path
|
|
58
|
+
else Path("logs")
|
|
59
|
+
/ (
|
|
60
|
+
datetime.now().strftime("archunit-%Y-%m-%d_%H-%M-%S-")
|
|
61
|
+
+ uuid4().hex
|
|
62
|
+
+ ".log"
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
self._file = path.open(
|
|
67
|
+
"a" if self.options.append_to_log_file else "w", encoding="utf-8"
|
|
68
|
+
)
|
|
69
|
+
if self._file is not None:
|
|
70
|
+
self._file.write(f"[{datetime.now().isoformat()}] [{level.upper()}] {text}\n")
|
|
71
|
+
self._file.flush()
|
|
72
|
+
except Exception:
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def close(self) -> None:
|
|
76
|
+
if self._file is not None:
|
|
77
|
+
try:
|
|
78
|
+
self._file.close()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
self._file = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
current_session: ContextVar[InspectionSession | None] = ContextVar(
|
|
85
|
+
"archunitpython_inspection", default=None
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def debug(message: str, *args: object) -> None:
|
|
90
|
+
"""Lazily emit details only inside an enabled debug check."""
|
|
91
|
+
session = current_session.get()
|
|
92
|
+
if session is not None:
|
|
93
|
+
session.emit("debug", message, *args)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def debug_enabled() -> bool:
|
|
97
|
+
"""Guard expensive diagnostic traversal before constructing its details."""
|
|
98
|
+
session = current_session.get()
|
|
99
|
+
return session is not None and session.options.level == "debug"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from archunitpython.common.logging.inspection import debug
|
|
5
6
|
from archunitpython.common.types import Filter
|
|
6
7
|
|
|
7
8
|
|
|
@@ -47,7 +48,9 @@ def matches_pattern(file_path: str, filter_: Filter) -> bool:
|
|
|
47
48
|
else:
|
|
48
49
|
target_string = normalize_path(file_path)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
matched = bool(filter_.regexp.search(target_string))
|
|
52
|
+
debug("Selector %s against %s (%s): %s", filter_.regexp.pattern, target_string, target, matched)
|
|
53
|
+
return matched
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter) -> bool:
|
|
@@ -65,7 +68,9 @@ def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter)
|
|
|
65
68
|
else:
|
|
66
69
|
target_string = normalize_path(file_path)
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
matched = bool(filter_.regexp.search(target_string))
|
|
72
|
+
debug("Selector %s against %s (%s): %s", filter_.regexp.pattern, target_string, target, matched)
|
|
73
|
+
return matched
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
def matches_all_patterns(file_path: str, filters: list[Filter]) -> bool:
|
{archunitpython-1.7.0 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_edges.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
|
|
7
7
|
from archunitpython.common.extraction.graph import Edge
|
|
8
|
+
from archunitpython.common.logging.inspection import debug
|
|
8
9
|
from archunitpython.common.projection.types import MapFunction, ProjectedEdge
|
|
9
10
|
|
|
10
11
|
|
|
@@ -29,11 +30,19 @@ def project_edges(
|
|
|
29
30
|
for edge in graph:
|
|
30
31
|
mapped = mapper(edge)
|
|
31
32
|
if mapped is None:
|
|
33
|
+
debug("Projection omitted: %s -> %s", edge.source, edge.target)
|
|
32
34
|
continue
|
|
35
|
+
debug(
|
|
36
|
+
"Projection: %s -> %s becomes %s -> %s",
|
|
37
|
+
edge.source,
|
|
38
|
+
edge.target,
|
|
39
|
+
mapped.source_label,
|
|
40
|
+
mapped.target_label,
|
|
41
|
+
)
|
|
33
42
|
key = (mapped.source_label, mapped.target_label)
|
|
34
43
|
groups[key].append(edge)
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
projected = [
|
|
37
46
|
ProjectedEdge(
|
|
38
47
|
source_label=source_label,
|
|
39
48
|
target_label=target_label,
|
|
@@ -41,3 +50,5 @@ def project_edges(
|
|
|
41
50
|
)
|
|
42
51
|
for (source_label, target_label), edges in groups.items()
|
|
43
52
|
]
|
|
53
|
+
debug("Projection complete: %d raw edges -> %d projected edges", len(graph), len(projected))
|
|
54
|
+
return projected
|