archunitpython 1.0.1__tar.gz → 1.1.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.0.1 → archunitpython-1.1.0}/CHANGELOG.md +12 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/PKG-INFO +2 -2
- {archunitpython-1.0.1 → archunitpython-1.1.0}/README.md +1 -1
- {archunitpython-1.0.1 → archunitpython-1.1.0}/pyproject.toml +3 -3
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/extraction/extract_graph.py +37 -6
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/fluentapi/checkable.py +1 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/edge_projections.py +17 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/assertion/__init__.py +6 -0
- archunitpython-1.1.0/src/archunitpython/files/assertion/depend_on_external_modules.py +64 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/fluentapi/files.py +80 -1
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/common/violation_factory.py +12 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_core_types.py +3 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_extract_graph.py +98 -2
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_projection.py +22 -1
- archunitpython-1.1.0/tests/files/test_files_fluentapi.py +355 -0
- archunitpython-1.0.1/tests/files/test_files_fluentapi.py +0 -174
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.editorconfig +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.gitattributes +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/PAGES.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.gitignore +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/.releaserc.json +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/LICENSE +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/TODO.md +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/fluentapi/metrics.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/fluentapi/slices.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/assertion.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.0.1 → archunitpython-1.1.0}/tests/test_setup.py +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.0.1...v1.1.0) (2026-04-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* linting errors ([bae7613](https://github.com/LukasNiessen/ArchUnitPython/commit/bae761376817a858a622f2320da3a4596823c891))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add external dependencies and TYPE_CHECKING ([02b8d01](https://github.com/LukasNiessen/ArchUnitPython/commit/02b8d01b37752e5ddd815ab5c10b36f26814f438))
|
|
12
|
+
|
|
1
13
|
## [1.0.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.0.0...v1.0.1) (2026-04-02)
|
|
2
14
|
|
|
3
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.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
|
|
@@ -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.0
|
|
7
|
+
version = "1.1.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"
|
|
@@ -67,13 +67,13 @@ python_classes = ["Test*"]
|
|
|
67
67
|
python_functions = ["test_*"]
|
|
68
68
|
|
|
69
69
|
[tool.mypy]
|
|
70
|
-
python_version = "1.0
|
|
70
|
+
python_version = "1.1.0"
|
|
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 = "1.0
|
|
76
|
+
target-version = "1.1.0"
|
|
77
77
|
line-length = 100
|
|
78
78
|
|
|
79
79
|
[tool.ruff.lint]
|
{archunitpython-1.0.1 → archunitpython-1.1.0}/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.0}/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.0}/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:
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Tests for graph extraction."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import uuid4
|
|
4
7
|
|
|
5
8
|
import pytest
|
|
6
9
|
|
|
@@ -12,6 +15,7 @@ from archunitpython.common.extraction.extract_graph import (
|
|
|
12
15
|
extract_graph,
|
|
13
16
|
)
|
|
14
17
|
from archunitpython.common.extraction.graph import Edge, ImportKind
|
|
18
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
15
19
|
|
|
16
20
|
FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
|
17
21
|
SAMPLE_PROJECT = os.path.join(FIXTURES_DIR, "sample_project")
|
|
@@ -116,8 +120,6 @@ class TestExtractGraph:
|
|
|
116
120
|
assert graph1 is graph2 # Same object reference (cached)
|
|
117
121
|
|
|
118
122
|
def test_cache_clear(self):
|
|
119
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
120
|
-
|
|
121
123
|
graph1 = extract_graph(SAMPLE_PROJECT)
|
|
122
124
|
graph2 = extract_graph(
|
|
123
125
|
SAMPLE_PROJECT, options=CheckOptions(clear_cache=True)
|
|
@@ -130,6 +132,100 @@ class TestExtractGraph:
|
|
|
130
132
|
assert len(edges_with_kinds) > 0
|
|
131
133
|
|
|
132
134
|
|
|
135
|
+
class TestTypeCheckingImportHandling:
|
|
136
|
+
def setup_method(self):
|
|
137
|
+
clear_graph_cache()
|
|
138
|
+
|
|
139
|
+
def _build_type_checking_project(self) -> str:
|
|
140
|
+
temp_root = Path(__file__).resolve().parent / ".tmp"
|
|
141
|
+
temp_root.mkdir(exist_ok=True)
|
|
142
|
+
project_root = temp_root / f"project_{uuid4().hex}"
|
|
143
|
+
project_root.mkdir()
|
|
144
|
+
|
|
145
|
+
package_dir = project_root / "sample_project"
|
|
146
|
+
package_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
(package_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
149
|
+
(package_dir / "models.py").write_text(
|
|
150
|
+
"class User:\n pass\n",
|
|
151
|
+
encoding="utf-8",
|
|
152
|
+
)
|
|
153
|
+
(package_dir / "service.py").write_text(
|
|
154
|
+
"\n".join(
|
|
155
|
+
[
|
|
156
|
+
"from typing import TYPE_CHECKING",
|
|
157
|
+
"",
|
|
158
|
+
"if TYPE_CHECKING:",
|
|
159
|
+
" from sample_project.models import User",
|
|
160
|
+
"",
|
|
161
|
+
"def get_user() -> str:",
|
|
162
|
+
' return "ok"',
|
|
163
|
+
"",
|
|
164
|
+
]
|
|
165
|
+
),
|
|
166
|
+
encoding="utf-8",
|
|
167
|
+
)
|
|
168
|
+
self._temp_dir = project_root
|
|
169
|
+
return str(project_root)
|
|
170
|
+
|
|
171
|
+
def teardown_method(self):
|
|
172
|
+
temp_dir = getattr(self, "_temp_dir", None)
|
|
173
|
+
if temp_dir is not None:
|
|
174
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
175
|
+
|
|
176
|
+
def test_type_checking_imports_included_by_default(self):
|
|
177
|
+
project_root = self._build_type_checking_project()
|
|
178
|
+
|
|
179
|
+
graph = extract_graph(project_root)
|
|
180
|
+
models_path = os.path.abspath(
|
|
181
|
+
os.path.join(project_root, "sample_project", "models.py")
|
|
182
|
+
).replace("\\", "/")
|
|
183
|
+
service_path = os.path.abspath(
|
|
184
|
+
os.path.join(project_root, "sample_project", "service.py")
|
|
185
|
+
).replace("\\", "/")
|
|
186
|
+
|
|
187
|
+
edges = [
|
|
188
|
+
edge
|
|
189
|
+
for edge in graph
|
|
190
|
+
if edge.source == service_path and edge.target == models_path
|
|
191
|
+
]
|
|
192
|
+
assert len(edges) == 1
|
|
193
|
+
assert ImportKind.TYPE_IMPORT in edges[0].import_kinds
|
|
194
|
+
|
|
195
|
+
def test_type_checking_imports_can_be_ignored(self):
|
|
196
|
+
project_root = self._build_type_checking_project()
|
|
197
|
+
|
|
198
|
+
graph = extract_graph(
|
|
199
|
+
project_root,
|
|
200
|
+
options=CheckOptions(ignore_type_checking_imports=True),
|
|
201
|
+
)
|
|
202
|
+
models_path = os.path.abspath(
|
|
203
|
+
os.path.join(project_root, "sample_project", "models.py")
|
|
204
|
+
).replace("\\", "/")
|
|
205
|
+
service_path = os.path.abspath(
|
|
206
|
+
os.path.join(project_root, "sample_project", "service.py")
|
|
207
|
+
).replace("\\", "/")
|
|
208
|
+
|
|
209
|
+
edges = [
|
|
210
|
+
edge
|
|
211
|
+
for edge in graph
|
|
212
|
+
if edge.source == service_path and edge.target == models_path
|
|
213
|
+
]
|
|
214
|
+
assert edges == []
|
|
215
|
+
|
|
216
|
+
def test_cache_key_includes_type_checking_option(self):
|
|
217
|
+
project_root = self._build_type_checking_project()
|
|
218
|
+
|
|
219
|
+
default_graph = extract_graph(project_root)
|
|
220
|
+
filtered_graph = extract_graph(
|
|
221
|
+
project_root,
|
|
222
|
+
options=CheckOptions(ignore_type_checking_imports=True),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
assert default_graph is not filtered_graph
|
|
226
|
+
assert len(default_graph) > len(filtered_graph)
|
|
227
|
+
|
|
228
|
+
|
|
133
229
|
class TestEdgeModel:
|
|
134
230
|
def test_edge_frozen(self):
|
|
135
231
|
edge = Edge(source="a.py", target="b.py", external=False)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""Tests for graph projection functions."""
|
|
2
2
|
|
|
3
3
|
from archunitpython.common.extraction.graph import Edge, ImportKind
|
|
4
|
-
from archunitpython.common.projection.edge_projections import
|
|
4
|
+
from archunitpython.common.projection.edge_projections import (
|
|
5
|
+
per_edge,
|
|
6
|
+
per_external_edge,
|
|
7
|
+
per_internal_edge,
|
|
8
|
+
)
|
|
5
9
|
from archunitpython.common.projection.project_edges import project_edges
|
|
6
10
|
from archunitpython.common.projection.project_nodes import project_to_nodes
|
|
7
11
|
from archunitpython.common.projection.types import MappedEdge
|
|
@@ -149,3 +153,20 @@ class TestPerEdge:
|
|
|
149
153
|
mapper = per_edge()
|
|
150
154
|
result = mapper(_make_edge("a", "b"))
|
|
151
155
|
assert result is not None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestPerExternalEdge:
|
|
159
|
+
def test_filters_internal(self):
|
|
160
|
+
mapper = per_external_edge()
|
|
161
|
+
assert mapper(_make_edge("a", "b")) is None
|
|
162
|
+
|
|
163
|
+
def test_filters_self_edge(self):
|
|
164
|
+
mapper = per_external_edge()
|
|
165
|
+
assert mapper(_make_edge("a", "a", external=True)) is None
|
|
166
|
+
|
|
167
|
+
def test_passes_external(self):
|
|
168
|
+
mapper = per_external_edge()
|
|
169
|
+
result = mapper(_make_edge("a.py", "json", external=True))
|
|
170
|
+
assert result is not None
|
|
171
|
+
assert result.source_label == "a.py"
|
|
172
|
+
assert result.target_label == "json"
|