archunitpython 1.4.0__tar.gz → 1.5.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.4.0 → archunitpython-1.5.0}/BACKLOG.md +1 -1
- {archunitpython-1.4.0 → archunitpython-1.5.0}/CHANGELOG.md +7 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/PKG-INFO +37 -1
- {archunitpython-1.4.0 → archunitpython-1.5.0}/README.md +36 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/pyproject.toml +1 -1
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/__init__.py +5 -1
- archunitpython-1.5.0/src/archunitpython/config/__init__.py +5 -0
- archunitpython-1.5.0/src/archunitpython/config/loader.py +119 -0
- archunitpython-1.5.0/tests/config/__init__.py +1 -0
- archunitpython-1.5.0/tests/config/test_config_loader.py +115 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/integration/test_e2e.py +5 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.editorconfig +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.gitattributes +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/PAGES.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.gitignore +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/.releaserc.json +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/LICENSE +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/extract_graph.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/fluentapi/files.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/fluentapi/layers.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/metrics.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/fluentapi/slices.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/assertion.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_extract_graph.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.4.0 → archunitpython-1.5.0}/tests/test_setup.py +0 -0
|
@@ -13,7 +13,7 @@ This backlog collects product and maintenance ideas from project research.
|
|
|
13
13
|
|
|
14
14
|
- [x] Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
|
|
15
15
|
- [x] Add a `.because(...)` API so rules can carry user-facing rationale into failure messages and generated architecture documentation.
|
|
16
|
-
- Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
|
|
16
|
+
- [x] Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
|
|
17
17
|
- Add support for monorepo and multi-package Python projects.
|
|
18
18
|
|
|
19
19
|
## P1 - Python Import Semantics
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.5.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.4.0...v1.5.0) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* load common rules from config ([c7c968a](https://github.com/LukasNiessen/ArchUnitPython/commit/c7c968af20e929f7e7a2372275eaa7e26cb38e5f))
|
|
7
|
+
|
|
1
8
|
# [1.4.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.3.0...v1.4.0) (2026-07-18)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -206,6 +206,42 @@ migrations/*.py
|
|
|
206
206
|
|
|
207
207
|
Patterns support comments, blank lines, glob syntax, root-relative paths, path
|
|
208
208
|
patterns, and directory patterns with a trailing `/`.
|
|
209
|
+
|
|
210
|
+
### Loading Common Rules From Config
|
|
211
|
+
|
|
212
|
+
For straightforward shared rules, you can load a JSON config file and still run
|
|
213
|
+
the resulting rules in your normal test suite:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"project_path": "src",
|
|
218
|
+
"rules": [
|
|
219
|
+
{
|
|
220
|
+
"name": "controllers must not use services directly",
|
|
221
|
+
"type": "forbidden_dependency",
|
|
222
|
+
"source": "**/controllers/**",
|
|
223
|
+
"target": "**/services/**"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "source files have no cycles",
|
|
227
|
+
"type": "no_cycles"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from archunitpython import assert_passes, rules_from_config
|
|
235
|
+
|
|
236
|
+
def test_configured_architecture_rules():
|
|
237
|
+
for rule in rules_from_config("archunitpython.json"):
|
|
238
|
+
assert_passes(rule)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Supported rule types are `no_cycles`, `forbidden_dependency`, and
|
|
242
|
+
`forbidden_external_dependency`. The fluent Python API remains the primary and
|
|
243
|
+
most flexible interface.
|
|
244
|
+
|
|
209
245
|
### Explaining Rules With `.because(...)`
|
|
210
246
|
|
|
211
247
|
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
@@ -173,6 +173,42 @@ migrations/*.py
|
|
|
173
173
|
|
|
174
174
|
Patterns support comments, blank lines, glob syntax, root-relative paths, path
|
|
175
175
|
patterns, and directory patterns with a trailing `/`.
|
|
176
|
+
|
|
177
|
+
### Loading Common Rules From Config
|
|
178
|
+
|
|
179
|
+
For straightforward shared rules, you can load a JSON config file and still run
|
|
180
|
+
the resulting rules in your normal test suite:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"project_path": "src",
|
|
185
|
+
"rules": [
|
|
186
|
+
{
|
|
187
|
+
"name": "controllers must not use services directly",
|
|
188
|
+
"type": "forbidden_dependency",
|
|
189
|
+
"source": "**/controllers/**",
|
|
190
|
+
"target": "**/services/**"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "source files have no cycles",
|
|
194
|
+
"type": "no_cycles"
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from archunitpython import assert_passes, rules_from_config
|
|
202
|
+
|
|
203
|
+
def test_configured_architecture_rules():
|
|
204
|
+
for rule in rules_from_config("archunitpython.json"):
|
|
205
|
+
assert_passes(rule)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Supported rule types are `no_cycles`, `forbidden_dependency`, and
|
|
209
|
+
`forbidden_external_dependency`. The fluent Python API remains the primary and
|
|
210
|
+
most flexible interface.
|
|
211
|
+
|
|
176
212
|
### Explaining Rules With `.because(...)`
|
|
177
213
|
|
|
178
214
|
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.5.0"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
@@ -12,6 +12,7 @@ from archunitpython.common import (
|
|
|
12
12
|
Violation,
|
|
13
13
|
)
|
|
14
14
|
from archunitpython.common.extraction import clear_graph_cache, extract_graph
|
|
15
|
+
from archunitpython.config import ConfiguredRule, rules_from_config
|
|
15
16
|
from archunitpython.files import files, project_files
|
|
16
17
|
from archunitpython.graph import dependency_graph, project_graph
|
|
17
18
|
from archunitpython.layers import layers, project_layers
|
|
@@ -35,6 +36,9 @@ __all__ = [
|
|
|
35
36
|
# Layers
|
|
36
37
|
"project_layers",
|
|
37
38
|
"layers",
|
|
39
|
+
# Config
|
|
40
|
+
"rules_from_config",
|
|
41
|
+
"ConfiguredRule",
|
|
38
42
|
# Slices
|
|
39
43
|
"project_slices",
|
|
40
44
|
# Metrics
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Load common architecture rules from a JSON configuration file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from archunitpython.common.assertion.violation import Violation
|
|
12
|
+
from archunitpython.common.error.errors import UserError
|
|
13
|
+
from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
|
|
14
|
+
from archunitpython.files.fluentapi.files import project_files
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ConfiguredRule:
|
|
19
|
+
"""A named rule loaded from a configuration file."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
rule: Checkable
|
|
23
|
+
|
|
24
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
25
|
+
"""Run the configured rule."""
|
|
26
|
+
return self.rule.check(options)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def rules_from_config(config_path: str) -> list[ConfiguredRule]:
|
|
30
|
+
"""Load common architecture rules from a JSON config file.
|
|
31
|
+
|
|
32
|
+
The fluent Python API remains the primary interface. Config files provide a
|
|
33
|
+
lightweight way to share straightforward rules across projects or teams.
|
|
34
|
+
"""
|
|
35
|
+
path = Path(config_path)
|
|
36
|
+
try:
|
|
37
|
+
raw_config = json.loads(path.read_text(encoding="utf-8"))
|
|
38
|
+
except OSError as exc:
|
|
39
|
+
raise UserError(f"Could not read config file: {config_path}") from exc
|
|
40
|
+
except json.JSONDecodeError as exc:
|
|
41
|
+
raise UserError(f"Invalid JSON config file: {config_path}") from exc
|
|
42
|
+
|
|
43
|
+
if not isinstance(raw_config, dict):
|
|
44
|
+
raise UserError("Architecture config must be a JSON object.")
|
|
45
|
+
|
|
46
|
+
project_path = _optional_string(raw_config, "project_path") or os.getcwd()
|
|
47
|
+
rules = raw_config.get("rules")
|
|
48
|
+
if not isinstance(rules, list):
|
|
49
|
+
raise UserError("Architecture config must define a 'rules' list.")
|
|
50
|
+
|
|
51
|
+
base_dir = str(path.parent if path.parent != Path("") else Path.cwd())
|
|
52
|
+
resolved_project_path = _resolve_project_path(base_dir, project_path)
|
|
53
|
+
|
|
54
|
+
return [_build_rule(resolved_project_path, item, index) for index, item in enumerate(rules, 1)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_rule(project_path: str, item: Any, index: int) -> ConfiguredRule:
|
|
58
|
+
if not isinstance(item, dict):
|
|
59
|
+
raise UserError(f"Rule #{index} must be a JSON object.")
|
|
60
|
+
|
|
61
|
+
rule_type = _required_string(item, "type", index)
|
|
62
|
+
name = _optional_string(item, "name") or f"{rule_type} rule #{index}"
|
|
63
|
+
rule: Checkable
|
|
64
|
+
if rule_type == "no_cycles":
|
|
65
|
+
subject = _optional_string(item, "subject")
|
|
66
|
+
builder = project_files(project_path)
|
|
67
|
+
if subject is not None:
|
|
68
|
+
rule = builder.in_path(subject).should().have_no_cycles()
|
|
69
|
+
else:
|
|
70
|
+
rule = builder.should().have_no_cycles()
|
|
71
|
+
elif rule_type == "forbidden_dependency":
|
|
72
|
+
source = _required_string(item, "source", index)
|
|
73
|
+
target = _required_string(item, "target", index)
|
|
74
|
+
rule = (
|
|
75
|
+
project_files(project_path)
|
|
76
|
+
.in_path(source)
|
|
77
|
+
.should_not()
|
|
78
|
+
.depend_on_files()
|
|
79
|
+
.in_path(target)
|
|
80
|
+
)
|
|
81
|
+
elif rule_type == "forbidden_external_dependency":
|
|
82
|
+
source = _required_string(item, "source", index)
|
|
83
|
+
module = _required_string(item, "module", index)
|
|
84
|
+
rule = (
|
|
85
|
+
project_files(project_path)
|
|
86
|
+
.in_path(source)
|
|
87
|
+
.should_not()
|
|
88
|
+
.depend_on_external_modules()
|
|
89
|
+
.matching(module)
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
raise UserError(
|
|
93
|
+
f"Unsupported rule type '{rule_type}'. Supported types: "
|
|
94
|
+
"no_cycles, forbidden_dependency, forbidden_external_dependency."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return ConfiguredRule(name=name, rule=rule)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_project_path(base_dir: str, project_path: str) -> str:
|
|
101
|
+
if os.path.isabs(project_path):
|
|
102
|
+
return project_path
|
|
103
|
+
return os.path.abspath(os.path.join(base_dir, project_path))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _required_string(rule: dict[str, Any], key: str, index: int) -> str:
|
|
107
|
+
value = rule.get(key)
|
|
108
|
+
if not isinstance(value, str) or not value.strip():
|
|
109
|
+
raise UserError(f"Rule #{index} must define a non-empty string '{key}'.")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _optional_string(rule: dict[str, Any], key: str) -> str | None:
|
|
114
|
+
value = rule.get(key)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
if not isinstance(value, str) or not value.strip():
|
|
118
|
+
raise UserError(f"Config value '{key}' must be a non-empty string.")
|
|
119
|
+
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for configuration-file rule loading."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tests for loading architecture rules from JSON config files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from archunitpython.common.error.errors import UserError
|
|
11
|
+
from archunitpython.config import rules_from_config
|
|
12
|
+
from archunitpython.files.assertion.depend_on_external_modules import (
|
|
13
|
+
ViolatingExternalModuleDependency,
|
|
14
|
+
)
|
|
15
|
+
from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestRulesFromConfig:
|
|
19
|
+
def setup_method(self):
|
|
20
|
+
self._temp_dir = Path(__file__).resolve().parent / ".tmp" / f"project_{uuid4().hex}"
|
|
21
|
+
self._temp_dir.mkdir(parents=True)
|
|
22
|
+
|
|
23
|
+
def teardown_method(self):
|
|
24
|
+
shutil.rmtree(self._temp_dir, ignore_errors=True)
|
|
25
|
+
|
|
26
|
+
def _write(self, relative_path: str, content: str) -> None:
|
|
27
|
+
path = self._temp_dir / relative_path
|
|
28
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
path.write_text(content, encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
def _write_config(self, config: dict[str, object]) -> str:
|
|
32
|
+
path = self._temp_dir / "archunitpython.json"
|
|
33
|
+
path.write_text(json.dumps(config), encoding="utf-8")
|
|
34
|
+
return str(path)
|
|
35
|
+
|
|
36
|
+
def test_loads_no_cycles_rule(self):
|
|
37
|
+
self._write("src/model.py", "class Model:\n pass\n")
|
|
38
|
+
self._write("src/service.py", "from model import Model\n")
|
|
39
|
+
config_path = self._write_config(
|
|
40
|
+
{
|
|
41
|
+
"project_path": "src",
|
|
42
|
+
"rules": [
|
|
43
|
+
{
|
|
44
|
+
"name": "source files have no cycles",
|
|
45
|
+
"type": "no_cycles",
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
rules = rules_from_config(config_path)
|
|
52
|
+
|
|
53
|
+
assert [rule.name for rule in rules] == ["source files have no cycles"]
|
|
54
|
+
assert rules[0].check() == []
|
|
55
|
+
|
|
56
|
+
def test_loads_forbidden_dependency_rule(self):
|
|
57
|
+
self._write("src/controllers/controller.py", "from services.service import Service\n")
|
|
58
|
+
self._write("src/services/service.py", "class Service:\n pass\n")
|
|
59
|
+
config_path = self._write_config(
|
|
60
|
+
{
|
|
61
|
+
"project_path": "src",
|
|
62
|
+
"rules": [
|
|
63
|
+
{
|
|
64
|
+
"type": "forbidden_dependency",
|
|
65
|
+
"source": "**/controllers/**",
|
|
66
|
+
"target": "**/services/**",
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
rules = rules_from_config(config_path)
|
|
73
|
+
violations = rules[0].check()
|
|
74
|
+
|
|
75
|
+
assert any(isinstance(v, ViolatingFileDependency) for v in violations)
|
|
76
|
+
|
|
77
|
+
def test_loads_forbidden_external_dependency_rule(self):
|
|
78
|
+
self._write("src/utils/helpers.py", "import json\n")
|
|
79
|
+
config_path = self._write_config(
|
|
80
|
+
{
|
|
81
|
+
"project_path": "src",
|
|
82
|
+
"rules": [
|
|
83
|
+
{
|
|
84
|
+
"type": "forbidden_external_dependency",
|
|
85
|
+
"source": "**/utils/**",
|
|
86
|
+
"module": "json",
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
rules = rules_from_config(config_path)
|
|
93
|
+
violations = rules[0].check()
|
|
94
|
+
|
|
95
|
+
assert any(isinstance(v, ViolatingExternalModuleDependency) for v in violations)
|
|
96
|
+
|
|
97
|
+
def test_rejects_missing_rules_list(self):
|
|
98
|
+
config_path = self._write_config({"project_path": "src"})
|
|
99
|
+
|
|
100
|
+
with pytest.raises(UserError, match="rules"):
|
|
101
|
+
rules_from_config(config_path)
|
|
102
|
+
|
|
103
|
+
def test_rejects_unknown_rule_type(self):
|
|
104
|
+
config_path = self._write_config(
|
|
105
|
+
{
|
|
106
|
+
"rules": [
|
|
107
|
+
{
|
|
108
|
+
"type": "unknown",
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
with pytest.raises(UserError, match="Unsupported rule type"):
|
|
115
|
+
rules_from_config(config_path)
|
|
@@ -42,6 +42,11 @@ class TestTopLevelImports:
|
|
|
42
42
|
def test_import_project_layers(self):
|
|
43
43
|
assert callable(project_layers)
|
|
44
44
|
|
|
45
|
+
def test_import_rules_from_config(self):
|
|
46
|
+
from archunitpython import rules_from_config
|
|
47
|
+
|
|
48
|
+
assert callable(rules_from_config)
|
|
49
|
+
|
|
45
50
|
def test_import_metrics(self):
|
|
46
51
|
from archunitpython import metrics
|
|
47
52
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/violation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/checkable.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/cycles.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_cycles.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_edges.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_nodes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/common/util/declaration_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/cycle_free.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/depend_on_files.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/matching_files.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/files/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/layers/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/count.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/distance.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/lcom.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/extraction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/export_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/metrics.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/metrics/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/export_diagram.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/generate_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/color_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/violation_factory.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/src/archunitpython/testing/pytest_plugin/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/architecture.puml
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/controllers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_a.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_b.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.4.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|