archunitpython 1.0.1__tar.gz → 1.1.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.0.1 → archunitpython-1.1.1}/.github/workflows/docs.yaml +1 -1
- archunitpython-1.1.1/.releaserc.json +27 -0
- archunitpython-1.1.1/BACKLOG.md +62 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/CHANGELOG.md +19 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/CONTRIBUTING.md +1 -1
- {archunitpython-1.0.1 → archunitpython-1.1.1}/PKG-INFO +2 -2
- {archunitpython-1.0.1 → archunitpython-1.1.1}/README.md +1 -1
- {archunitpython-1.0.1 → archunitpython-1.1.1}/pyproject.toml +3 -3
- archunitpython-1.1.1/research/product-direction/architecture-testing-landscape.md +56 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/__init__.py +1 -1
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/extraction/extract_graph.py +37 -6
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/fluentapi/checkable.py +1 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/edge_projections.py +17 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/__init__.py +6 -0
- archunitpython-1.1.1/src/archunitpython/files/assertion/depend_on_external_modules.py +64 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/fluentapi/files.py +80 -1
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/common/violation_factory.py +12 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_core_types.py +3 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_extract_graph.py +98 -2
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_projection.py +22 -1
- archunitpython-1.1.1/tests/files/test_files_fluentapi.py +355 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/integration/test_e2e.py +1 -3
- archunitpython-1.1.1/tests/test_setup.py +21 -0
- archunitpython-1.0.1/.releaserc.json +0 -27
- archunitpython-1.0.1/TODO.md +0 -18
- archunitpython-1.0.1/tests/files/test_files_fluentapi.py +0 -174
- archunitpython-1.0.1/tests/test_setup.py +0 -7
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.editorconfig +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.gitattributes +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/PAGES.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/dependabot.yml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/pull_request_template.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/.gitignore +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/LICENSE +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/assets/logo-rounded.png +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/fluentapi/metrics.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/fluentapi/slices.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/assertion.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_logger.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/files/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/integration/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/slices/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.1}/tests/slices/test_slices.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
[
|
|
7
|
+
"@semantic-release/changelog",
|
|
8
|
+
{
|
|
9
|
+
"changelogFile": "CHANGELOG.md"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
"@semantic-release/exec",
|
|
14
|
+
{
|
|
15
|
+
"prepareCmd": "python -c \"import re, pathlib; files=[('pyproject.toml', r'(?m)^version = \\\"[^\\\"]+\\\"$', 'version = \\\"${nextRelease.version}\\\"'), ('src/archunitpython/__init__.py', r'(?m)^__version__ = \\\"[^\\\"]+\\\"$', '__version__ = \\\"${nextRelease.version}\\\"')]; [pathlib.Path(path).write_text(re.sub(pattern, replacement, pathlib.Path(path).read_text())) for path, pattern, replacement in files]\""
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"@semantic-release/git",
|
|
20
|
+
{
|
|
21
|
+
"assets": ["pyproject.toml", "src/archunitpython/__init__.py", "CHANGELOG.md"],
|
|
22
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"@semantic-release/github"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Backlog
|
|
2
|
+
|
|
3
|
+
This backlog collects product and maintenance ideas from project research.
|
|
4
|
+
|
|
5
|
+
## P0 - Maintenance And Correctness
|
|
6
|
+
|
|
7
|
+
- Keep package metadata synchronized across `pyproject.toml`, `CHANGELOG.md`, and `src/archunitpython/__init__.py`.
|
|
8
|
+
- Keep tool configuration valid for the supported Python range, especially mypy and Ruff target versions.
|
|
9
|
+
- Add a release metadata check that fails when the exported `__version__` differs from the project version.
|
|
10
|
+
- Add CI jobs that run tests, Ruff, mypy, and a package build from a clean checkout.
|
|
11
|
+
|
|
12
|
+
## P1 - Adoption Workflow
|
|
13
|
+
|
|
14
|
+
- Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
|
|
15
|
+
- 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.
|
|
17
|
+
- Add support for monorepo and multi-package Python projects.
|
|
18
|
+
|
|
19
|
+
## P1 - Python Import Semantics
|
|
20
|
+
|
|
21
|
+
- Add support for namespace packages that do not contain `__init__.py`.
|
|
22
|
+
- Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
|
|
23
|
+
- Detect conditional imports such as `try/except ImportError`.
|
|
24
|
+
- Add better `TYPE_CHECKING` import handling, including options to ignore, include, or report type-only imports separately.
|
|
25
|
+
- Improve external dependency rules so users can express allowed and forbidden third-party packages at module or slice level.
|
|
26
|
+
- Consider a public-interface rule inspired by Tach, where modules may only import through declared package APIs.
|
|
27
|
+
|
|
28
|
+
## P1 - Reporting And Documentation
|
|
29
|
+
|
|
30
|
+
- Add comprehensive HTML reports with dependency graphs, metric charts, and zone visualization.
|
|
31
|
+
- Auto-generate architecture documentation based on tests and rule rationales.
|
|
32
|
+
- Make logged paths clickable in IDEs and common terminal integrations.
|
|
33
|
+
- Add PlantUML or Mermaid export for discovered architecture graphs.
|
|
34
|
+
- Improve metric export examples and document how metric thresholds should be selected.
|
|
35
|
+
|
|
36
|
+
## P2 - Rule Surface
|
|
37
|
+
|
|
38
|
+
- Add first-class layered architecture helpers so common clean/hexagonal/layered rules require less boilerplate.
|
|
39
|
+
- Add slice isolation helpers for bounded contexts and modular monoliths.
|
|
40
|
+
- Add richer custom rule hooks for dependency edges, files, classes, and metrics.
|
|
41
|
+
- Add transitive dependency checks, especially for "domain must not transitively reach infrastructure" style rules.
|
|
42
|
+
- Add naming and placement conventions for classes/functions, not only files.
|
|
43
|
+
|
|
44
|
+
## P2 - Performance And Scale
|
|
45
|
+
|
|
46
|
+
- Improve performance for very large projects through parallel file parsing.
|
|
47
|
+
- Add persistent graph caching with invalidation based on file mtimes or content hashes.
|
|
48
|
+
- Add benchmarks for small, medium, large, and monorepo-style projects.
|
|
49
|
+
|
|
50
|
+
## P2 - Metrics
|
|
51
|
+
|
|
52
|
+
- Add more LCOM edge case handling.
|
|
53
|
+
- Add metric documentation with examples for good, suspicious, and failing values.
|
|
54
|
+
- Add trend-friendly metric exports for CI artifacts.
|
|
55
|
+
- Consider additional architecture metrics such as coupling counts per slice, fan-in/fan-out summaries, and instability per package.
|
|
56
|
+
|
|
57
|
+
## P3 - Packaging And Docs
|
|
58
|
+
|
|
59
|
+
- Publish to PyPI as part of the release pipeline if this is not already automated.
|
|
60
|
+
- Add a Sphinx or MkDocs documentation site.
|
|
61
|
+
- Add a complete example repository or examples folder covering pytest, unittest, PlantUML, metrics, and CI.
|
|
62
|
+
- Add contribution guidance for new rule types and metric implementations.
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [1.1.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.1.0...v1.1.1) (2026-05-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deps:** bump actions/upload-pages-artifact ([a4452fc](https://github.com/LukasNiessen/ArchUnitPython/commit/a4452fce2ce9428882cf686227de9f09cb6a21b1))
|
|
7
|
+
|
|
8
|
+
# [1.1.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.0.1...v1.1.0) (2026-04-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* linting errors ([bae7613](https://github.com/LukasNiessen/ArchUnitPython/commit/bae761376817a858a622f2320da3a4596823c891))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add external dependencies and TYPE_CHECKING ([02b8d01](https://github.com/LukasNiessen/ArchUnitPython/commit/02b8d01b37752e5ddd815ab5c10b36f26814f438))
|
|
19
|
+
|
|
1
20
|
## [1.0.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.0.0...v1.0.1) (2026-04-02)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -36,7 +36,7 @@ Releases are fully automated. When a PR is merged to `main`:
|
|
|
36
36
|
1. CI runs lint + type checking + tests (across Python 3.10-3.13)
|
|
37
37
|
2. If CI passes, [semantic-release](https://github.com/semantic-release/semantic-release) analyzes commit messages since the last release
|
|
38
38
|
3. If there are `fix:` or `feat:` commits, it automatically:
|
|
39
|
-
- Bumps the version in `pyproject.toml`
|
|
39
|
+
- Bumps the version in `pyproject.toml` and `src/archunitpython/__init__.py`
|
|
40
40
|
- Updates `CHANGELOG.md`
|
|
41
41
|
- Publishes to PyPI
|
|
42
42
|
- Creates a GitHub release with release notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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
|
|
@@ -189,7 +189,7 @@ Ensure services/modules don't have forbidden cross-dependencies.
|
|
|
189
189
|
|
|
190
190
|
Here is a repository with a fully functioning example that uses ArchUnitPython to ensure architectural rules:
|
|
191
191
|
|
|
192
|
-
- **[RAG Pipeline
|
|
192
|
+
- **[RAG Pipeline Test Showcase](https://github.com/LukasNiessen/ArchUnitPython-TestRepo-RAG)**: A test showcase demonstrating ArchUnitPython's architecture testing capabilities on a RAG pipeline
|
|
193
193
|
|
|
194
194
|
## 🐣 Features
|
|
195
195
|
|
|
@@ -157,7 +157,7 @@ Ensure services/modules don't have forbidden cross-dependencies.
|
|
|
157
157
|
|
|
158
158
|
Here is a repository with a fully functioning example that uses ArchUnitPython to ensure architectural rules:
|
|
159
159
|
|
|
160
|
-
- **[RAG Pipeline
|
|
160
|
+
- **[RAG Pipeline Test Showcase](https://github.com/LukasNiessen/ArchUnitPython-TestRepo-RAG)**: A test showcase demonstrating ArchUnitPython's architecture testing capabilities on a RAG pipeline
|
|
161
161
|
|
|
162
162
|
## 🐣 Features
|
|
163
163
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.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"
|
|
@@ -67,13 +67,13 @@ python_classes = ["Test*"]
|
|
|
67
67
|
python_functions = ["test_*"]
|
|
68
68
|
|
|
69
69
|
[tool.mypy]
|
|
70
|
-
python_version = "
|
|
70
|
+
python_version = "3.10"
|
|
71
71
|
strict = true
|
|
72
72
|
warn_return_any = true
|
|
73
73
|
warn_unused_configs = true
|
|
74
74
|
|
|
75
75
|
[tool.ruff]
|
|
76
|
-
target-version = "
|
|
76
|
+
target-version = "py310"
|
|
77
77
|
line-length = 100
|
|
78
78
|
|
|
79
79
|
[tool.ruff.lint]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Architecture Testing Landscape
|
|
2
|
+
|
|
3
|
+
Research date: 2026-05-23
|
|
4
|
+
|
|
5
|
+
This document compares ArchUnitPython with architecture testing tools in the broader ecosystem and with Python-specific alternatives. It focuses on user-facing features that can inform product direction.
|
|
6
|
+
|
|
7
|
+
## Current ArchUnitPython Position
|
|
8
|
+
|
|
9
|
+
ArchUnitPython already covers three valuable surfaces:
|
|
10
|
+
|
|
11
|
+
- File-level architecture rules through `project_files(...)`: dependency direction, cycle detection, naming/path conventions, external dependency checks, and custom file predicates.
|
|
12
|
+
- Slice-level architecture rules through `project_slices(...)`: slice extraction from path patterns or regexes, PlantUML diagram adherence, and forbidden slice dependencies.
|
|
13
|
+
- Code metrics through `metrics(...)`: count metrics, LCOM variants, abstractness, instability, distance from the main sequence, zone checks, and custom metrics.
|
|
14
|
+
|
|
15
|
+
That breadth makes the project closer to ArchUnit-style libraries than to pure import linters. The main gaps are around configuration ergonomics, reporting, incremental adoption, IDE/CI workflow polish, and deeper Python import semantics.
|
|
16
|
+
|
|
17
|
+
## General Architecture Testing Players
|
|
18
|
+
|
|
19
|
+
| Tool | Ecosystem | Rule definition style | Dependency/layer rules | Cycles | Diagram support | Metrics | Reports/visualization | Notable strengths | ArchUnitPython comparison |
|
|
20
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
21
|
+
| [ArchUnit](https://www.archunit.org/userguide/html/000_Index.html) | Java | Fluent Java API, JUnit integrations | Yes: packages, classes, layers, slices | Yes | PlantUML component diagrams | Yes: software architecture metrics | Strong test failure output; mature JUnit support | Mature reference implementation with a broad rule model and mature JUnit workflow | ArchUnitPython mirrors the core idea and already has files/slices/metrics, but lacks class/member/annotation richness and mature import options |
|
|
22
|
+
| [ArchUnitNET](https://github.com/TNG/ArchUnitNET) | .NET | Fluent C# API with xUnit/NUnit/MSTest integrations | Yes: assemblies, namespaces, types, members | Yes | PlantUML support and diagram generation in the project ecosystem | Limited compared with ArchUnit | Test framework integrations | Strong .NET analogue to ArchUnit with many framework adapters | ArchUnitPython has stronger built-in code metrics, but less type/member-level expressiveness |
|
|
23
|
+
| [NetArchTest](https://github.com/BenMorris/NetArchTest) | .NET | Fluent C# API | Yes: namespace/type dependency and conventions | Not a primary differentiator | No first-class PlantUML in core | No | Policy results; test framework agnostic | Simple, widely adopted dependency/convention tests for .NET | ArchUnitPython is broader on cycles, slices, PlantUML, and metrics |
|
|
24
|
+
| [ts-arch](https://github.com/ts-arch/ts-arch) | TypeScript/JavaScript | Fluent API, Jest matcher | Yes: files, folders, slices | Yes | PlantUML diagram adherence | No | Jest-oriented failure output | Very close conceptual sibling: file API, slice API, PlantUML, NX monorepo support | ArchUnitPython has a similar surface plus metrics; ts-arch has stronger monorepo/NX positioning |
|
|
25
|
+
| [jQAssistant](https://jqassistant.github.io/jqassistant/current/) | Mostly JVM, plugin-based | Scanner + graph database + rule concepts/constraints | Yes, via graph rules and plugins | Yes, rule-dependent | Documentation validation and living documentation workflows | Software analytics via graph queries | Reports, server/explore mode, graph-backed analysis | Enterprise-scale scanning, graph exploration, living documentation, baseline management | ArchUnitPython is much lighter and easier to embed in tests, but lacks graph database exploration and living-doc workflows |
|
|
26
|
+
|
|
27
|
+
## Python Architecture Testing Libraries
|
|
28
|
+
|
|
29
|
+
| Tool | Rule definition style | Dependency/layer rules | Cycles | External dependency rules | Diagram support | Metrics | Reports/visualization | Adoption workflow | How ArchUnitPython stacks up |
|
|
30
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
31
|
+
| [ArchUnitPython](https://github.com/LukasNiessen/ArchUnitPython) | Fluent Python API used from pytest/unittest/any test framework | Yes: file dependencies, external modules, slices, forbidden slice dependencies | Yes: file-level cycles | Yes: external module matching | Yes: PlantUML slice adherence | Yes: count, LCOM, distance, custom metrics | Experimental metric export; no full HTML report yet | Plain test functions; `assert_passes()` or `.check()` | Broadest feature mix among the Python tools reviewed, especially because metrics and PlantUML are built in |
|
|
32
|
+
| [Import Linter](https://import-linter.readthedocs.io/en/stable/index.html) | `.importlinter` configuration contracts plus CLI | Yes: forbidden, protected, layers, independence, acyclic siblings, custom contracts | Yes: acyclic siblings | Primarily first-party import contracts | No first-class PlantUML | No | Browser-based architecture UI | CLI, CI, config file, caching | More mature for config-driven import contracts and exploration; ArchUnitPython is more test-native and broader on metrics/PlantUML |
|
|
33
|
+
| [Tach](https://docs.gauge.sh/getting-started/introduction/) | `tach.toml` plus CLI commands | Yes: module dependencies and public interfaces | Dependency graph focused | Yes: `check-external` for third-party imports | No | No | `show`, `map`, `report`, VS Code integration | Incremental CLI workflow, sync, pre-commit/CI, public interface enforcement | Tach is stronger for modular-boundary workflow and public APIs; ArchUnitPython is stronger for test-suite rules, metrics, and diagram validation |
|
|
34
|
+
| [PyTestArch](https://zyskarch.github.io/pytestarch/latest/features/module_import_checks/) | Python query language evaluated in pytest | Yes: module dependency query language and layered architecture rules | Implicit through dependency rules; not positioned as a primary cycle API | Not a main focus | Yes: generates rules from PlantUML component diagrams | No | Optional matplotlib visualization | Pytest-centered evaluation structures | Similar to ArchUnitPython on Python tests and PlantUML; ArchUnitPython has simpler fluent API and built-in metrics |
|
|
35
|
+
| [pytest-archon](https://github.com/jwbargsten/pytest-archon) | Pytest-oriented architecture assertions | Yes: forbidden dependencies and project structure rules | Yes, positioned around avoiding cycles | Not a main differentiator | No | No | Pytest failure output | Lightweight pytest helper | ArchUnitPython is broader and more ArchUnit-like; pytest-archon is smaller and focused |
|
|
36
|
+
|
|
37
|
+
## Product Takeaways
|
|
38
|
+
|
|
39
|
+
- ArchUnitPython should lean into being the ArchUnit-style Python test library, not just another import linter.
|
|
40
|
+
- The nearest feature gap against Python tools is not raw rule breadth; it is workflow: config files, ignore files, public interfaces, incremental adoption, reporting, and IDE/CI polish.
|
|
41
|
+
- Against ArchUnit and ArchUnitNET, the biggest long-term gaps are richer semantic model support, import semantics, and mature documentation/reporting.
|
|
42
|
+
- Metrics are a meaningful differentiator in the Python space and should be kept visible in docs and examples.
|
|
43
|
+
|
|
44
|
+
## Sources
|
|
45
|
+
|
|
46
|
+
- ArchUnit User Guide: https://www.archunit.org/userguide/html/000_Index.html
|
|
47
|
+
- ArchUnitNET repository: https://github.com/TNG/ArchUnitNET
|
|
48
|
+
- NetArchTest repository: https://github.com/BenMorris/NetArchTest
|
|
49
|
+
- ts-arch repository: https://github.com/ts-arch/ts-arch
|
|
50
|
+
- jQAssistant User Manual: https://jqassistant.github.io/jqassistant/current/
|
|
51
|
+
- Import Linter documentation: https://import-linter.readthedocs.io/en/stable/index.html
|
|
52
|
+
- Tach documentation: https://docs.gauge.sh/getting-started/introduction/
|
|
53
|
+
- PyTestArch documentation: https://zyskarch.github.io/pytestarch/latest/features/module_import_checks/
|
|
54
|
+
- PyTestArch PlantUML documentation: https://zyskarch.github.io/pytestarch/latest/features/plantuml/
|
|
55
|
+
- PyTestArch visualization documentation: https://zyskarch.github.io/pytestarch/latest/features/visualization/
|
|
56
|
+
- pytest-archon repository: https://github.com/jwbargsten/pytest-archon
|
{archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
@@ -8,7 +8,9 @@ import os
|
|
|
8
8
|
from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
|
|
9
9
|
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
GraphCacheKey = tuple[str, tuple[str, ...], bool]
|
|
12
|
+
|
|
13
|
+
_graph_cache: dict[GraphCacheKey, Graph] = {}
|
|
12
14
|
|
|
13
15
|
_DEFAULT_EXCLUDE = [
|
|
14
16
|
"__pycache__",
|
|
@@ -56,7 +58,13 @@ def extract_graph(
|
|
|
56
58
|
project_path = os.getcwd()
|
|
57
59
|
|
|
58
60
|
project_path = os.path.abspath(project_path)
|
|
59
|
-
|
|
61
|
+
excludes = list(exclude_patterns) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
62
|
+
ignore_type_checking_imports = bool(
|
|
63
|
+
options and options.ignore_type_checking_imports
|
|
64
|
+
)
|
|
65
|
+
cache_key = _build_cache_key(
|
|
66
|
+
project_path, excludes, ignore_type_checking_imports
|
|
67
|
+
)
|
|
60
68
|
|
|
61
69
|
if options and options.clear_cache:
|
|
62
70
|
_graph_cache.pop(cache_key, None)
|
|
@@ -64,18 +72,36 @@ def extract_graph(
|
|
|
64
72
|
if cache_key in _graph_cache:
|
|
65
73
|
return _graph_cache[cache_key]
|
|
66
74
|
|
|
67
|
-
result = _extract_graph_uncached(
|
|
75
|
+
result = _extract_graph_uncached(
|
|
76
|
+
project_path,
|
|
77
|
+
excludes,
|
|
78
|
+
ignore_type_checking_imports=ignore_type_checking_imports,
|
|
79
|
+
)
|
|
68
80
|
_graph_cache[cache_key] = result
|
|
69
81
|
return result
|
|
70
82
|
|
|
71
83
|
|
|
84
|
+
def _build_cache_key(
|
|
85
|
+
project_path: str,
|
|
86
|
+
exclude_patterns: list[str],
|
|
87
|
+
ignore_type_checking_imports: bool,
|
|
88
|
+
) -> GraphCacheKey:
|
|
89
|
+
"""Build a stable cache key for graph extraction options."""
|
|
90
|
+
return (
|
|
91
|
+
project_path,
|
|
92
|
+
tuple(sorted(exclude_patterns)),
|
|
93
|
+
ignore_type_checking_imports,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
72
97
|
def _extract_graph_uncached(
|
|
73
98
|
project_path: str,
|
|
74
|
-
exclude_patterns: list[str]
|
|
99
|
+
exclude_patterns: list[str],
|
|
100
|
+
*,
|
|
101
|
+
ignore_type_checking_imports: bool = False,
|
|
75
102
|
) -> Graph:
|
|
76
103
|
"""Extract graph without caching."""
|
|
77
|
-
|
|
78
|
-
py_files = _find_python_files(project_path, excludes)
|
|
104
|
+
py_files = _find_python_files(project_path, exclude_patterns)
|
|
79
105
|
|
|
80
106
|
edges: list[Edge] = []
|
|
81
107
|
py_files_set = set(py_files)
|
|
@@ -93,6 +119,11 @@ def _extract_graph_uncached(
|
|
|
93
119
|
# Extract and resolve imports
|
|
94
120
|
imports = _extract_imports(file_path)
|
|
95
121
|
for module_name, import_kind in imports:
|
|
122
|
+
if (
|
|
123
|
+
ignore_type_checking_imports
|
|
124
|
+
and import_kind == ImportKind.TYPE_IMPORT
|
|
125
|
+
):
|
|
126
|
+
continue
|
|
96
127
|
resolved, is_external = _resolve_import(
|
|
97
128
|
module_name, file_path, project_path, import_kind
|
|
98
129
|
)
|
|
@@ -34,3 +34,20 @@ def per_edge() -> MapFunction:
|
|
|
34
34
|
return MappedEdge(source_label=edge.source, target_label=edge.target)
|
|
35
35
|
|
|
36
36
|
return mapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def per_external_edge() -> MapFunction:
|
|
40
|
+
"""Create a mapper that only passes external edges.
|
|
41
|
+
|
|
42
|
+
Self-referencing edges are filtered out, though they are not expected for
|
|
43
|
+
external imports.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def mapper(edge: Edge) -> MappedEdge | None:
|
|
47
|
+
if not edge.external:
|
|
48
|
+
return None
|
|
49
|
+
if edge.source == edge.target:
|
|
50
|
+
return None
|
|
51
|
+
return MappedEdge(source_label=edge.source, target_label=edge.target)
|
|
52
|
+
|
|
53
|
+
return mapper
|
{archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/files/assertion/__init__.py
RENAMED
|
@@ -5,6 +5,10 @@ from archunitpython.files.assertion.custom_file_logic import (
|
|
|
5
5
|
gather_custom_file_violations,
|
|
6
6
|
)
|
|
7
7
|
from archunitpython.files.assertion.cycle_free import ViolatingCycle, gather_cycle_violations
|
|
8
|
+
from archunitpython.files.assertion.depend_on_external_modules import (
|
|
9
|
+
ViolatingExternalModuleDependency,
|
|
10
|
+
gather_depend_on_external_module_violations,
|
|
11
|
+
)
|
|
8
12
|
from archunitpython.files.assertion.depend_on_files import (
|
|
9
13
|
ViolatingFileDependency,
|
|
10
14
|
gather_depend_on_file_violations,
|
|
@@ -20,9 +24,11 @@ __all__ = [
|
|
|
20
24
|
"FileInfo",
|
|
21
25
|
"ViolatingCycle",
|
|
22
26
|
"ViolatingFileDependency",
|
|
27
|
+
"ViolatingExternalModuleDependency",
|
|
23
28
|
"ViolatingNode",
|
|
24
29
|
"gather_custom_file_violations",
|
|
25
30
|
"gather_cycle_violations",
|
|
26
31
|
"gather_depend_on_file_violations",
|
|
32
|
+
"gather_depend_on_external_module_violations",
|
|
27
33
|
"gather_regex_matching_violations",
|
|
28
34
|
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Violation gathering for external module dependency rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from archunitpython.common.assertion.violation import Violation
|
|
8
|
+
from archunitpython.common.pattern_matching import matches_pattern
|
|
9
|
+
from archunitpython.common.projection.types import ProjectedEdge
|
|
10
|
+
from archunitpython.common.types import Filter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ViolatingExternalModuleDependency(Violation):
|
|
15
|
+
"""An external module dependency that violates a rule."""
|
|
16
|
+
|
|
17
|
+
dependency: ProjectedEdge
|
|
18
|
+
is_negated: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def gather_depend_on_external_module_violations(
|
|
22
|
+
edges: list[ProjectedEdge],
|
|
23
|
+
subject_filters: list[Filter],
|
|
24
|
+
module_filters: list[Filter],
|
|
25
|
+
is_negated: bool,
|
|
26
|
+
) -> list[Violation]:
|
|
27
|
+
"""Check if files depend on forbidden/allowed external modules.
|
|
28
|
+
|
|
29
|
+
Subject filters use AND semantics. Module filters use OR semantics, which
|
|
30
|
+
makes it possible to express useful allowlists or blocklists for external
|
|
31
|
+
module names.
|
|
32
|
+
"""
|
|
33
|
+
violations: list[Violation] = []
|
|
34
|
+
|
|
35
|
+
for edge in edges:
|
|
36
|
+
source_matches = all(
|
|
37
|
+
matches_pattern(edge.source_label, filter_)
|
|
38
|
+
for filter_ in subject_filters
|
|
39
|
+
)
|
|
40
|
+
if not source_matches:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
target_matches = (
|
|
44
|
+
any(matches_pattern(edge.target_label, filter_) for filter_ in module_filters)
|
|
45
|
+
if module_filters
|
|
46
|
+
else False
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if is_negated:
|
|
50
|
+
if target_matches:
|
|
51
|
+
violations.append(
|
|
52
|
+
ViolatingExternalModuleDependency(
|
|
53
|
+
dependency=edge, is_negated=True
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
if not target_matches:
|
|
58
|
+
violations.append(
|
|
59
|
+
ViolatingExternalModuleDependency(
|
|
60
|
+
dependency=edge, is_negated=False
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return violations
|
|
@@ -16,7 +16,10 @@ from archunitpython.common.assertion.violation import EmptyTestViolation, Violat
|
|
|
16
16
|
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
17
|
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
18
18
|
from archunitpython.common.pattern_matching import matches_all_patterns
|
|
19
|
-
from archunitpython.common.projection.edge_projections import
|
|
19
|
+
from archunitpython.common.projection.edge_projections import (
|
|
20
|
+
per_external_edge,
|
|
21
|
+
per_internal_edge,
|
|
22
|
+
)
|
|
20
23
|
from archunitpython.common.projection.project_cycles import project_cycles
|
|
21
24
|
from archunitpython.common.projection.project_edges import project_edges
|
|
22
25
|
from archunitpython.common.projection.project_nodes import project_to_nodes
|
|
@@ -28,6 +31,9 @@ from archunitpython.files.assertion.custom_file_logic import (
|
|
|
28
31
|
gather_custom_file_violations,
|
|
29
32
|
)
|
|
30
33
|
from archunitpython.files.assertion.cycle_free import gather_cycle_violations
|
|
34
|
+
from archunitpython.files.assertion.depend_on_external_modules import (
|
|
35
|
+
gather_depend_on_external_module_violations,
|
|
36
|
+
)
|
|
31
37
|
from archunitpython.files.assertion.depend_on_files import gather_depend_on_file_violations
|
|
32
38
|
from archunitpython.files.assertion.matching_files import gather_regex_matching_violations
|
|
33
39
|
|
|
@@ -133,6 +139,14 @@ class PositiveMatchPatternFileConditionBuilder:
|
|
|
133
139
|
self._project_path, self._filters, is_negated=False
|
|
134
140
|
)
|
|
135
141
|
|
|
142
|
+
def depend_on_external_modules(
|
|
143
|
+
self,
|
|
144
|
+
) -> "DependOnExternalModuleConditionBuilder":
|
|
145
|
+
"""Begin external dependency assertion for module names."""
|
|
146
|
+
return DependOnExternalModuleConditionBuilder(
|
|
147
|
+
self._project_path, self._filters, is_negated=False
|
|
148
|
+
)
|
|
149
|
+
|
|
136
150
|
def be_in_folder(self, folder: Pattern) -> "MatchPatternFileCondition":
|
|
137
151
|
"""Assert that files are in a certain folder."""
|
|
138
152
|
return MatchPatternFileCondition(
|
|
@@ -182,6 +196,14 @@ class NegatedMatchPatternFileConditionBuilder:
|
|
|
182
196
|
self._project_path, self._filters, is_negated=True
|
|
183
197
|
)
|
|
184
198
|
|
|
199
|
+
def depend_on_external_modules(
|
|
200
|
+
self,
|
|
201
|
+
) -> "DependOnExternalModuleConditionBuilder":
|
|
202
|
+
"""Begin negative external dependency assertion for module names."""
|
|
203
|
+
return DependOnExternalModuleConditionBuilder(
|
|
204
|
+
self._project_path, self._filters, is_negated=True
|
|
205
|
+
)
|
|
206
|
+
|
|
185
207
|
def be_in_folder(self, folder: Pattern) -> "MatchPatternFileCondition":
|
|
186
208
|
"""Assert that files are NOT in a certain folder."""
|
|
187
209
|
return MatchPatternFileCondition(
|
|
@@ -260,6 +282,31 @@ class DependOnFileConditionBuilder:
|
|
|
260
282
|
)
|
|
261
283
|
|
|
262
284
|
|
|
285
|
+
class DependOnExternalModuleConditionBuilder:
|
|
286
|
+
"""Configure external module dependency target patterns."""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self, project_path: str | None, filters: list[Filter], is_negated: bool
|
|
290
|
+
) -> None:
|
|
291
|
+
self._project_path = project_path
|
|
292
|
+
self._filters = filters
|
|
293
|
+
self._is_negated = is_negated
|
|
294
|
+
self._module_filters: list[Filter] = []
|
|
295
|
+
|
|
296
|
+
def matching(self, module_name: Pattern) -> "DependOnExternalModuleCondition":
|
|
297
|
+
"""Target external modules by dotted module name pattern.
|
|
298
|
+
|
|
299
|
+
Multiple calls are combined with OR semantics.
|
|
300
|
+
"""
|
|
301
|
+
self._module_filters.append(RegexFactory.path_matcher(module_name))
|
|
302
|
+
return DependOnExternalModuleCondition(
|
|
303
|
+
self._project_path,
|
|
304
|
+
self._filters,
|
|
305
|
+
list(self._module_filters),
|
|
306
|
+
self._is_negated,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
263
310
|
def _get_filtered_nodes(
|
|
264
311
|
project_path: str | None,
|
|
265
312
|
filters: list[Filter],
|
|
@@ -346,6 +393,38 @@ class DependOnFileCondition:
|
|
|
346
393
|
)
|
|
347
394
|
|
|
348
395
|
|
|
396
|
+
class DependOnExternalModuleCondition:
|
|
397
|
+
"""Checkable that verifies external module dependency rules."""
|
|
398
|
+
|
|
399
|
+
def __init__(
|
|
400
|
+
self,
|
|
401
|
+
project_path: str | None,
|
|
402
|
+
subject_filters: list[Filter],
|
|
403
|
+
module_filters: list[Filter],
|
|
404
|
+
is_negated: bool,
|
|
405
|
+
) -> None:
|
|
406
|
+
self._project_path = project_path
|
|
407
|
+
self._subject_filters = subject_filters
|
|
408
|
+
self._module_filters = module_filters
|
|
409
|
+
self._is_negated = is_negated
|
|
410
|
+
|
|
411
|
+
def matching(self, module_name: Pattern) -> "DependOnExternalModuleCondition":
|
|
412
|
+
"""Add another external module pattern using OR semantics."""
|
|
413
|
+
self._module_filters.append(RegexFactory.path_matcher(module_name))
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
417
|
+
graph = extract_graph(self._project_path, options=options)
|
|
418
|
+
edges = project_edges(graph, per_external_edge())
|
|
419
|
+
|
|
420
|
+
return gather_depend_on_external_module_violations(
|
|
421
|
+
edges,
|
|
422
|
+
self._subject_filters,
|
|
423
|
+
self._module_filters,
|
|
424
|
+
self._is_negated,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
349
428
|
class MatchPatternFileCondition:
|
|
350
429
|
"""Checkable that verifies files match/don't match patterns."""
|
|
351
430
|
|
{archunitpython-1.0.1 → archunitpython-1.1.1}/src/archunitpython/testing/common/violation_factory.py
RENAMED
|
@@ -7,6 +7,9 @@ from dataclasses import dataclass
|
|
|
7
7
|
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
8
8
|
from archunitpython.files.assertion.custom_file_logic import CustomFileViolation
|
|
9
9
|
from archunitpython.files.assertion.cycle_free import ViolatingCycle
|
|
10
|
+
from archunitpython.files.assertion.depend_on_external_modules import (
|
|
11
|
+
ViolatingExternalModuleDependency,
|
|
12
|
+
)
|
|
10
13
|
from archunitpython.files.assertion.depend_on_files import ViolatingFileDependency
|
|
11
14
|
from archunitpython.files.assertion.matching_files import ViolatingNode
|
|
12
15
|
from archunitpython.metrics.assertion.metric_thresholds import (
|
|
@@ -52,6 +55,15 @@ class ViolationFactory:
|
|
|
52
55
|
f"'{edge.target_label}'",
|
|
53
56
|
)
|
|
54
57
|
|
|
58
|
+
if isinstance(violation, ViolatingExternalModuleDependency):
|
|
59
|
+
edge = violation.dependency
|
|
60
|
+
return TestViolation(
|
|
61
|
+
message="External module dependency violation",
|
|
62
|
+
details=f"'{edge.source_label}' "
|
|
63
|
+
f"{'depends on' if violation.is_negated else 'does not depend on'} "
|
|
64
|
+
f"external module '{edge.target_label}'",
|
|
65
|
+
)
|
|
66
|
+
|
|
55
67
|
if isinstance(violation, ViolatingCycle):
|
|
56
68
|
cycle_str = " -> ".join(
|
|
57
69
|
e.source_label for e in violation.cycle
|
|
@@ -101,6 +101,7 @@ class TestCheckOptions:
|
|
|
101
101
|
assert opts.allow_empty_tests is False
|
|
102
102
|
assert opts.logging is None
|
|
103
103
|
assert opts.clear_cache is False
|
|
104
|
+
assert opts.ignore_type_checking_imports is False
|
|
104
105
|
|
|
105
106
|
def test_custom(self):
|
|
106
107
|
logging = LoggingOptions(enabled=True, level="debug")
|
|
@@ -108,12 +109,14 @@ class TestCheckOptions:
|
|
|
108
109
|
allow_empty_tests=True,
|
|
109
110
|
logging=logging,
|
|
110
111
|
clear_cache=True,
|
|
112
|
+
ignore_type_checking_imports=True,
|
|
111
113
|
)
|
|
112
114
|
assert opts.allow_empty_tests is True
|
|
113
115
|
assert opts.logging is not None
|
|
114
116
|
assert opts.logging.enabled is True
|
|
115
117
|
assert opts.logging.level == "debug"
|
|
116
118
|
assert opts.clear_cache is True
|
|
119
|
+
assert opts.ignore_type_checking_imports is True
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
class TestLoggingOptions:
|