archunitpython 1.6.1__tar.gz → 1.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {archunitpython-1.6.1 → archunitpython-1.8.1}/BACKLOG.md +1 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/CHANGELOG.md +35 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/PKG-INFO +153 -6
- {archunitpython-1.6.1 → archunitpython-1.8.1}/README.md +152 -5
- {archunitpython-1.6.1 → archunitpython-1.8.1}/pyproject.toml +1 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/__init__.py +3 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/extraction/extract_graph.py +256 -89
- archunitpython-1.8.1/src/archunitpython/common/fluentapi/inspection.py +79 -0
- archunitpython-1.8.1/src/archunitpython/common/logging/inspection.py +99 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/logging/types.py +2 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/pattern_matching.py +9 -2
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_edges.py +12 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/regex_factory.py +14 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/types.py +2 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_files.py +14 -3
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/files.py +10 -2
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/layers.py +2 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/extract_class_info.py +17 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/metrics.py +47 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/slices.py +3 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/assertion.py +16 -6
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/common/color_utils.py +12 -1
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_core_types.py +13 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_extract_graph.py +312 -35
- archunitpython-1.8.1/tests/common/test_inspection.py +315 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_pattern_matching.py +31 -0
- archunitpython-1.8.1/tests/common/test_pretty_output.py +51 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/files/test_file_assertions.py +28 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.editorconfig +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.gitattributes +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/CODEOWNERS +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/FUNDING.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/PAGES.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/dependabot.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/pull_request_template.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.gitignore +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/.releaserc.json +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/CONTRIBUTING.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/LICENSE +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/assets/logo-rounded.png +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/config/loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/common/test_projection.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/config/test_config_loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/integration/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.1}/tests/test_setup.py +0 -0
|
@@ -18,7 +18,7 @@ This backlog collects product and maintenance ideas from project research.
|
|
|
18
18
|
|
|
19
19
|
## P1 - Python Import Semantics
|
|
20
20
|
|
|
21
|
-
- Add support for namespace packages that do not contain `__init__.py`.
|
|
21
|
+
- [x] Add support for namespace packages that do not contain `__init__.py`.
|
|
22
22
|
- [x] Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
|
|
23
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.
|
|
@@ -1,3 +1,38 @@
|
|
|
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
|
+
|
|
24
|
+
# [1.7.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.1...v1.7.0) (2026-09-13)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* preserve mixed namespace import targets ([489624b](https://github.com/LukasNiessen/ArchUnitPython/commit/489624b4de5465847f6e83106cbe0a398a7c1e8e))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* resolve namespace package submodule imports ([86c53e6](https://github.com/LukasNiessen/ArchUnitPython/commit/86c53e619ad25dfcdfb109de4593967d3d5603a6))
|
|
35
|
+
|
|
1
36
|
## [1.6.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.0...v1.6.1) (2026-09-13)
|
|
2
37
|
|
|
3
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics.
|
|
5
5
|
Project-URL: Homepage, https://github.com/LukasNiessen/ArchUnitPython
|
|
6
6
|
Project-URL: Repository, https://github.com/LukasNiessen/ArchUnitPython.git
|
|
@@ -60,6 +60,13 @@ _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUni
|
|
|
60
60
|
pip install archunitpython
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
> **Choose the import root:** Pass the directory containing your top-level Python
|
|
64
|
+
> packages to `project_files(...)` and `metrics(...)`. If `import taskboard` refers
|
|
65
|
+
> to `src/taskboard/`, use `"src"`; if `taskboard/` is at the repository root, use
|
|
66
|
+
> `"."`. This tells ArchUnitPython where to scan and how to resolve your imports.
|
|
67
|
+
> Omitting the argument uses the current working directory; it does not
|
|
68
|
+
> automatically discover a `src/` layout. [Details and examples](#choosing-the-import-root).
|
|
69
|
+
|
|
63
70
|
### Add tests
|
|
64
71
|
|
|
65
72
|
Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.
|
|
@@ -68,7 +75,7 @@ Simply add tests to your existing test suites. The following is an example using
|
|
|
68
75
|
from archunitpython import project_files, metrics, assert_passes
|
|
69
76
|
|
|
70
77
|
def test_no_circular_dependencies():
|
|
71
|
-
rule = project_files("src/").
|
|
78
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
72
79
|
assert_passes(rule)
|
|
73
80
|
```
|
|
74
81
|
|
|
@@ -147,6 +154,70 @@ pip install archunitpython
|
|
|
147
154
|
|
|
148
155
|
That's it. Works with **pytest**, **unittest**, or any Python testing framework.
|
|
149
156
|
|
|
157
|
+
### Choosing the import root
|
|
158
|
+
|
|
159
|
+
The optional `project_path` argument to `project_files(...)`, `metrics(...)`,
|
|
160
|
+
`project_layers(...)`, `project_slices(...)`, and `project_graph(...)` selects the
|
|
161
|
+
directory to analyze. Choose the directory containing your top-level Python
|
|
162
|
+
packages so absolute imports can be resolved within that directory. Relative
|
|
163
|
+
paths are interpreted from the current working directory; omitting the argument
|
|
164
|
+
uses that working directory.
|
|
165
|
+
|
|
166
|
+
For example, suppose your repository contains:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
my_repo/
|
|
170
|
+
src/
|
|
171
|
+
taskboard/
|
|
172
|
+
__init__.py
|
|
173
|
+
application/board_service.py
|
|
174
|
+
domain/task.py
|
|
175
|
+
tests/test_architecture.py
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
An import such as `from taskboard.domain.task import Task` names
|
|
179
|
+
`taskboard/domain/task.py` underneath `src/`. It does not include `src` in the
|
|
180
|
+
import name. Run your tests from `my_repo/` and use:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from archunitpython import assert_passes, metrics, project_files
|
|
184
|
+
|
|
185
|
+
def test_no_cycles():
|
|
186
|
+
rule = project_files("src").should().have_no_cycles()
|
|
187
|
+
assert_passes(rule)
|
|
188
|
+
|
|
189
|
+
def test_no_large_files():
|
|
190
|
+
rule = metrics("src").count().lines_of_code().should_be_below(1000)
|
|
191
|
+
assert_passes(rule)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The directory name itself has no special meaning:
|
|
195
|
+
|
|
196
|
+
| Package location, relative to your working directory | Import name | Argument |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| `src/taskboard/` | `taskboard` | `"src"` |
|
|
199
|
+
| `taskboard/` | `taskboard` | `"."` |
|
|
200
|
+
| `backend/taskboard/` | `taskboard` | `"backend"` |
|
|
201
|
+
|
|
202
|
+
Selectors such as `.in_path("**/domain/**")` and `.in_folder(...)` narrow which
|
|
203
|
+
files a rule checks. They do not change the import root. For example,
|
|
204
|
+
`project_files().in_path("src/**")` can select files under `src/` while still
|
|
205
|
+
resolving absolute imports from the repository root. Set `project_files("src")`
|
|
206
|
+
first, then add selectors if you need to narrow the rule.
|
|
207
|
+
|
|
208
|
+
With the wrong root, imports between your own modules can be classified as
|
|
209
|
+
external, causing cycle and internal-dependency rules to miss violations and
|
|
210
|
+
dependency-based metrics to report incorrect values. The root also determines
|
|
211
|
+
which files are included in metric measurements. ArchUnitPython does not infer a
|
|
212
|
+
`src/` import root from `pyproject.toml` or your Python environment; pass it
|
|
213
|
+
explicitly for this layout.
|
|
214
|
+
|
|
215
|
+
## 🎬 Demo
|
|
216
|
+
|
|
217
|
+
https://github.com/user-attachments/assets/56f2ad65-a2e1-4965-b84d-445cf7bf4cd2
|
|
218
|
+
|
|
219
|
+
## Testing Framework
|
|
220
|
+
|
|
150
221
|
### pytest (Recommended)
|
|
151
222
|
|
|
152
223
|
Use `assert_passes()` for clean assertion messages:
|
|
@@ -377,6 +448,12 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
|
|
|
377
448
|
from my_app.adapters.sql import Repository # archunit: ignore
|
|
378
449
|
```
|
|
379
450
|
|
|
451
|
+
### Namespace Packages
|
|
452
|
+
|
|
453
|
+
ArchUnitPython resolves imports from namespace packages that do not contain
|
|
454
|
+
`__init__.py` files. For example, `from my_app.domain import model` is resolved
|
|
455
|
+
to `my_app/domain/model.py` when `my_app/domain/` is a namespace package.
|
|
456
|
+
|
|
380
457
|
### Conditional Imports
|
|
381
458
|
|
|
382
459
|
Imports inside `try` blocks that handle `ImportError` or
|
|
@@ -920,6 +997,10 @@ The most important differences:
|
|
|
920
997
|
|
|
921
998
|
When tests fail, you get helpful output with file paths and violation details:
|
|
922
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
|
+
|
|
923
1004
|
```
|
|
924
1005
|
Found 2 architecture violation(s):
|
|
925
1006
|
|
|
@@ -932,25 +1013,91 @@ Found 2 architecture violation(s):
|
|
|
932
1013
|
|
|
933
1014
|
## 📝 Debug Logging & Configuration
|
|
934
1015
|
|
|
935
|
-
|
|
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. |
|
|
936
1027
|
|
|
937
1028
|
### Enabling Debug Logging
|
|
938
1029
|
|
|
939
1030
|
```python
|
|
940
|
-
from archunitpython import CheckOptions
|
|
941
|
-
|
|
1031
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
1032
|
+
|
|
1033
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
942
1034
|
|
|
943
1035
|
options = CheckOptions(
|
|
944
1036
|
logging=LoggingOptions(
|
|
945
1037
|
enabled=True,
|
|
946
1038
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
947
|
-
log_file=True, # Creates logs/
|
|
1039
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
948
1040
|
),
|
|
949
1041
|
)
|
|
950
1042
|
|
|
951
1043
|
violations = rule.check(options)
|
|
952
1044
|
```
|
|
953
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
|
+
|
|
954
1101
|
### CI Pipeline Integration
|
|
955
1102
|
|
|
956
1103
|
```yaml
|
|
@@ -27,6 +27,13 @@ _Inspired by the amazing ArchUnit library but we are not affiliated with ArchUni
|
|
|
27
27
|
pip install archunitpython
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
> **Choose the import root:** Pass the directory containing your top-level Python
|
|
31
|
+
> packages to `project_files(...)` and `metrics(...)`. If `import taskboard` refers
|
|
32
|
+
> to `src/taskboard/`, use `"src"`; if `taskboard/` is at the repository root, use
|
|
33
|
+
> `"."`. This tells ArchUnitPython where to scan and how to resolve your imports.
|
|
34
|
+
> Omitting the argument uses the current working directory; it does not
|
|
35
|
+
> automatically discover a `src/` layout. [Details and examples](#choosing-the-import-root).
|
|
36
|
+
|
|
30
37
|
### Add tests
|
|
31
38
|
|
|
32
39
|
Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.
|
|
@@ -35,7 +42,7 @@ Simply add tests to your existing test suites. The following is an example using
|
|
|
35
42
|
from archunitpython import project_files, metrics, assert_passes
|
|
36
43
|
|
|
37
44
|
def test_no_circular_dependencies():
|
|
38
|
-
rule = project_files("src/").
|
|
45
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
39
46
|
assert_passes(rule)
|
|
40
47
|
```
|
|
41
48
|
|
|
@@ -114,6 +121,70 @@ pip install archunitpython
|
|
|
114
121
|
|
|
115
122
|
That's it. Works with **pytest**, **unittest**, or any Python testing framework.
|
|
116
123
|
|
|
124
|
+
### Choosing the import root
|
|
125
|
+
|
|
126
|
+
The optional `project_path` argument to `project_files(...)`, `metrics(...)`,
|
|
127
|
+
`project_layers(...)`, `project_slices(...)`, and `project_graph(...)` selects the
|
|
128
|
+
directory to analyze. Choose the directory containing your top-level Python
|
|
129
|
+
packages so absolute imports can be resolved within that directory. Relative
|
|
130
|
+
paths are interpreted from the current working directory; omitting the argument
|
|
131
|
+
uses that working directory.
|
|
132
|
+
|
|
133
|
+
For example, suppose your repository contains:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
my_repo/
|
|
137
|
+
src/
|
|
138
|
+
taskboard/
|
|
139
|
+
__init__.py
|
|
140
|
+
application/board_service.py
|
|
141
|
+
domain/task.py
|
|
142
|
+
tests/test_architecture.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
An import such as `from taskboard.domain.task import Task` names
|
|
146
|
+
`taskboard/domain/task.py` underneath `src/`. It does not include `src` in the
|
|
147
|
+
import name. Run your tests from `my_repo/` and use:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from archunitpython import assert_passes, metrics, project_files
|
|
151
|
+
|
|
152
|
+
def test_no_cycles():
|
|
153
|
+
rule = project_files("src").should().have_no_cycles()
|
|
154
|
+
assert_passes(rule)
|
|
155
|
+
|
|
156
|
+
def test_no_large_files():
|
|
157
|
+
rule = metrics("src").count().lines_of_code().should_be_below(1000)
|
|
158
|
+
assert_passes(rule)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The directory name itself has no special meaning:
|
|
162
|
+
|
|
163
|
+
| Package location, relative to your working directory | Import name | Argument |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `src/taskboard/` | `taskboard` | `"src"` |
|
|
166
|
+
| `taskboard/` | `taskboard` | `"."` |
|
|
167
|
+
| `backend/taskboard/` | `taskboard` | `"backend"` |
|
|
168
|
+
|
|
169
|
+
Selectors such as `.in_path("**/domain/**")` and `.in_folder(...)` narrow which
|
|
170
|
+
files a rule checks. They do not change the import root. For example,
|
|
171
|
+
`project_files().in_path("src/**")` can select files under `src/` while still
|
|
172
|
+
resolving absolute imports from the repository root. Set `project_files("src")`
|
|
173
|
+
first, then add selectors if you need to narrow the rule.
|
|
174
|
+
|
|
175
|
+
With the wrong root, imports between your own modules can be classified as
|
|
176
|
+
external, causing cycle and internal-dependency rules to miss violations and
|
|
177
|
+
dependency-based metrics to report incorrect values. The root also determines
|
|
178
|
+
which files are included in metric measurements. ArchUnitPython does not infer a
|
|
179
|
+
`src/` import root from `pyproject.toml` or your Python environment; pass it
|
|
180
|
+
explicitly for this layout.
|
|
181
|
+
|
|
182
|
+
## 🎬 Demo
|
|
183
|
+
|
|
184
|
+
https://github.com/user-attachments/assets/56f2ad65-a2e1-4965-b84d-445cf7bf4cd2
|
|
185
|
+
|
|
186
|
+
## Testing Framework
|
|
187
|
+
|
|
117
188
|
### pytest (Recommended)
|
|
118
189
|
|
|
119
190
|
Use `assert_passes()` for clean assertion messages:
|
|
@@ -344,6 +415,12 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
|
|
|
344
415
|
from my_app.adapters.sql import Repository # archunit: ignore
|
|
345
416
|
```
|
|
346
417
|
|
|
418
|
+
### Namespace Packages
|
|
419
|
+
|
|
420
|
+
ArchUnitPython resolves imports from namespace packages that do not contain
|
|
421
|
+
`__init__.py` files. For example, `from my_app.domain import model` is resolved
|
|
422
|
+
to `my_app/domain/model.py` when `my_app/domain/` is a namespace package.
|
|
423
|
+
|
|
347
424
|
### Conditional Imports
|
|
348
425
|
|
|
349
426
|
Imports inside `try` blocks that handle `ImportError` or
|
|
@@ -887,6 +964,10 @@ The most important differences:
|
|
|
887
964
|
|
|
888
965
|
When tests fail, you get helpful output with file paths and violation details:
|
|
889
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
|
+
|
|
890
971
|
```
|
|
891
972
|
Found 2 architecture violation(s):
|
|
892
973
|
|
|
@@ -899,25 +980,91 @@ Found 2 architecture violation(s):
|
|
|
899
980
|
|
|
900
981
|
## 📝 Debug Logging & Configuration
|
|
901
982
|
|
|
902
|
-
|
|
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. |
|
|
903
994
|
|
|
904
995
|
### Enabling Debug Logging
|
|
905
996
|
|
|
906
997
|
```python
|
|
907
|
-
from archunitpython import CheckOptions
|
|
908
|
-
|
|
998
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
999
|
+
|
|
1000
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
909
1001
|
|
|
910
1002
|
options = CheckOptions(
|
|
911
1003
|
logging=LoggingOptions(
|
|
912
1004
|
enabled=True,
|
|
913
1005
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
914
|
-
log_file=True, # Creates logs/
|
|
1006
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
915
1007
|
),
|
|
916
1008
|
)
|
|
917
1009
|
|
|
918
1010
|
violations = rule.check(options)
|
|
919
1011
|
```
|
|
920
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
|
+
|
|
921
1068
|
### CI Pipeline Integration
|
|
922
1069
|
|
|
923
1070
|
```yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.1"
|
|
8
8
|
description = "Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.8.1"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
7
7
|
from archunitpython.common import (
|
|
8
8
|
CheckOptions,
|
|
9
9
|
EmptyTestViolation,
|
|
10
|
+
LoggingOptions,
|
|
10
11
|
TechnicalError,
|
|
11
12
|
UserError,
|
|
12
13
|
Violation,
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"Violation",
|
|
51
52
|
"EmptyTestViolation",
|
|
52
53
|
"CheckOptions",
|
|
54
|
+
"LoggingOptions",
|
|
53
55
|
"TechnicalError",
|
|
54
56
|
"UserError",
|
|
55
57
|
"extract_graph",
|