archunitpython 1.2.1__py3-none-any.whl → 1.4.0__py3-none-any.whl
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/__init__.py +1 -1
- archunitpython/common/__init__.py +6 -1
- archunitpython/common/extraction/extract_graph.py +79 -11
- archunitpython/common/fluentapi/__init__.py +6 -2
- archunitpython/common/fluentapi/checkable.py +23 -1
- archunitpython/files/fluentapi/files.py +6 -6
- archunitpython/metrics/extraction/extract_class_info.py +6 -3
- archunitpython/metrics/fluentapi/metrics.py +9 -9
- archunitpython/slices/fluentapi/slices.py +3 -3
- archunitpython/testing/assertion.py +11 -3
- {archunitpython-1.2.1.dist-info → archunitpython-1.4.0.dist-info}/METADATA +141 -12
- {archunitpython-1.2.1.dist-info → archunitpython-1.4.0.dist-info}/RECORD +14 -14
- {archunitpython-1.2.1.dist-info → archunitpython-1.4.0.dist-info}/WHEEL +1 -1
- {archunitpython-1.2.1.dist-info → archunitpython-1.4.0.dist-info}/licenses/LICENSE +0 -0
archunitpython/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
2
2
|
from archunitpython.common.error.errors import TechnicalError, UserError
|
|
3
|
-
from archunitpython.common.fluentapi.checkable import
|
|
3
|
+
from archunitpython.common.fluentapi.checkable import (
|
|
4
|
+
Checkable,
|
|
5
|
+
CheckOptions,
|
|
6
|
+
RuleRationaleMixin,
|
|
7
|
+
)
|
|
4
8
|
from archunitpython.common.logging.types import LoggingOptions
|
|
5
9
|
from archunitpython.common.types import Filter, Pattern, PatternMatchingOptions
|
|
6
10
|
|
|
@@ -11,6 +15,7 @@ __all__ = [
|
|
|
11
15
|
"UserError",
|
|
12
16
|
"Checkable",
|
|
13
17
|
"CheckOptions",
|
|
18
|
+
"RuleRationaleMixin",
|
|
14
19
|
"LoggingOptions",
|
|
15
20
|
"Pattern",
|
|
16
21
|
"Filter",
|
|
@@ -29,6 +29,8 @@ _DEFAULT_EXCLUDE = [
|
|
|
29
29
|
"*.egg-info",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
_ARCHIGNORE_FILE = ".archignore"
|
|
33
|
+
|
|
32
34
|
_IGNORE_DIRECTIVE_REGEX = re.compile(
|
|
33
35
|
r"#\s*archunit(?::|-)\s*ignore"
|
|
34
36
|
r"(?:\([^)]*\))?"
|
|
@@ -88,9 +90,7 @@ def extract_graph(
|
|
|
88
90
|
project_path = os.getcwd()
|
|
89
91
|
|
|
90
92
|
project_path = os.path.abspath(project_path)
|
|
91
|
-
excludes = (
|
|
92
|
-
list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
93
|
-
)
|
|
93
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
94
94
|
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
|
|
95
95
|
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
|
|
96
96
|
|
|
@@ -122,6 +122,34 @@ def _build_cache_key(
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _resolve_exclude_patterns(
|
|
126
|
+
project_path: str,
|
|
127
|
+
exclude_patterns: list[str] | None,
|
|
128
|
+
) -> list[str]:
|
|
129
|
+
"""Resolve exclude patterns (explicit or defaults) plus any .archignore patterns."""
|
|
130
|
+
excludes = list(exclude_patterns) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
131
|
+
excludes.extend(_load_archignore_patterns(project_path))
|
|
132
|
+
return excludes
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_archignore_patterns(project_path: str) -> list[str]:
|
|
136
|
+
"""Load .archignore patterns from a project root, if present."""
|
|
137
|
+
archignore_path = os.path.join(project_path, _ARCHIGNORE_FILE)
|
|
138
|
+
try:
|
|
139
|
+
with open(archignore_path, "r", encoding="utf-8", errors="replace") as f:
|
|
140
|
+
lines = f.readlines()
|
|
141
|
+
except OSError:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
patterns: list[str] = []
|
|
145
|
+
for line in lines:
|
|
146
|
+
pattern = line.strip()
|
|
147
|
+
if not pattern or pattern.startswith("#"):
|
|
148
|
+
continue
|
|
149
|
+
patterns.append(pattern)
|
|
150
|
+
return patterns
|
|
151
|
+
|
|
152
|
+
|
|
125
153
|
def _extract_graph_uncached(
|
|
126
154
|
project_path: str,
|
|
127
155
|
exclude_patterns: list[str],
|
|
@@ -160,7 +188,7 @@ def _extract_graph_uncached(
|
|
|
160
188
|
if resolved and resolved != _normalize(file_path):
|
|
161
189
|
# Check if the resolved path is in our project
|
|
162
190
|
if not is_external and resolved not in normalized_py_file_set:
|
|
163
|
-
|
|
191
|
+
continue
|
|
164
192
|
|
|
165
193
|
edges.append(
|
|
166
194
|
Edge(
|
|
@@ -182,25 +210,65 @@ def _normalize(path: str) -> str:
|
|
|
182
210
|
def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
183
211
|
"""Recursively find all .py files, excluding specified patterns."""
|
|
184
212
|
py_files: list[str] = []
|
|
213
|
+
root = os.path.abspath(root)
|
|
185
214
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
186
215
|
# Filter out excluded directories in-place
|
|
187
|
-
dirnames[:] = [
|
|
216
|
+
dirnames[:] = [
|
|
217
|
+
d
|
|
218
|
+
for d in dirnames
|
|
219
|
+
if not _should_exclude_path(os.path.join(dirpath, d), root, exclude, is_dir=True)
|
|
220
|
+
]
|
|
188
221
|
|
|
189
222
|
for filename in filenames:
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
full_path = os.path.join(dirpath, filename)
|
|
224
|
+
if filename.endswith(".py") and not _should_exclude_path(
|
|
225
|
+
full_path, root, exclude, is_dir=False
|
|
226
|
+
):
|
|
192
227
|
py_files.append(os.path.abspath(full_path))
|
|
193
228
|
|
|
194
229
|
return py_files
|
|
195
230
|
|
|
196
231
|
|
|
197
|
-
def
|
|
198
|
-
|
|
232
|
+
def _should_exclude_path(
|
|
233
|
+
path: str,
|
|
234
|
+
root: str,
|
|
235
|
+
patterns: list[str],
|
|
236
|
+
*,
|
|
237
|
+
is_dir: bool,
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""Check if a path matches any exclude pattern."""
|
|
199
240
|
import fnmatch
|
|
200
241
|
|
|
201
|
-
|
|
202
|
-
|
|
242
|
+
rel_path = _normalize(os.path.relpath(path, root))
|
|
243
|
+
name = os.path.basename(path)
|
|
244
|
+
|
|
245
|
+
for raw_pattern in patterns:
|
|
246
|
+
pattern = raw_pattern.strip().replace("\\", "/")
|
|
247
|
+
if not pattern or pattern.startswith("#"):
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
pattern = pattern.removeprefix("./")
|
|
251
|
+
anchored = pattern.startswith("/")
|
|
252
|
+
if anchored:
|
|
253
|
+
pattern = pattern[1:]
|
|
254
|
+
|
|
255
|
+
dir_only = pattern.endswith("/")
|
|
256
|
+
if dir_only:
|
|
257
|
+
pattern = pattern.rstrip("/")
|
|
258
|
+
if not is_dir:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
if not pattern:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if "/" in pattern or anchored:
|
|
265
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
266
|
+
return True
|
|
267
|
+
if is_dir and rel_path == pattern:
|
|
268
|
+
return True
|
|
269
|
+
elif fnmatch.fnmatch(name, pattern):
|
|
203
270
|
return True
|
|
271
|
+
|
|
204
272
|
return False
|
|
205
273
|
|
|
206
274
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
from archunitpython.common.fluentapi.checkable import
|
|
1
|
+
from archunitpython.common.fluentapi.checkable import (
|
|
2
|
+
Checkable,
|
|
3
|
+
CheckOptions,
|
|
4
|
+
RuleRationaleMixin,
|
|
5
|
+
)
|
|
2
6
|
|
|
3
|
-
__all__ = ["Checkable", "CheckOptions"]
|
|
7
|
+
__all__ = ["Checkable", "CheckOptions", "RuleRationaleMixin"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Protocol
|
|
6
|
+
from typing import Protocol, TypeVar
|
|
7
7
|
|
|
8
8
|
from archunitpython.common.assertion.violation import Violation
|
|
9
9
|
from archunitpython.common.logging.types import LoggingOptions
|
|
@@ -19,6 +19,28 @@ class CheckOptions:
|
|
|
19
19
|
ignore_type_checking_imports: bool = False
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
T = TypeVar("T", bound="RuleRationaleMixin")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RuleRationaleMixin:
|
|
26
|
+
"""Mixin for checkable rules that can carry a human-readable rationale."""
|
|
27
|
+
|
|
28
|
+
_because_reason: str | None = None
|
|
29
|
+
|
|
30
|
+
def because(self: T, reason: str) -> T:
|
|
31
|
+
"""Attach a rationale explaining why the rule exists."""
|
|
32
|
+
reason = reason.strip()
|
|
33
|
+
if not reason:
|
|
34
|
+
raise ValueError("Rule rationale must not be empty.")
|
|
35
|
+
self._because_reason = reason
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def because_reason(self) -> str | None:
|
|
40
|
+
"""Return the rationale attached with because(), if any."""
|
|
41
|
+
return self._because_reason
|
|
42
|
+
|
|
43
|
+
|
|
22
44
|
class Checkable(Protocol):
|
|
23
45
|
"""Protocol for any architecture rule that can be checked.
|
|
24
46
|
|
|
@@ -14,7 +14,7 @@ from collections.abc import Sequence
|
|
|
14
14
|
|
|
15
15
|
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
16
16
|
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
17
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
18
18
|
from archunitpython.common.pattern_matching import matches_all_patterns
|
|
19
19
|
from archunitpython.common.projection.edge_projections import (
|
|
20
20
|
per_external_edge,
|
|
@@ -322,7 +322,7 @@ def _check_empty_test(
|
|
|
322
322
|
return None
|
|
323
323
|
|
|
324
324
|
|
|
325
|
-
class CycleFreeFileCondition:
|
|
325
|
+
class CycleFreeFileCondition(RuleRationaleMixin):
|
|
326
326
|
"""Checkable that verifies no cycles exist among filtered files."""
|
|
327
327
|
|
|
328
328
|
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
@@ -350,7 +350,7 @@ class CycleFreeFileCondition:
|
|
|
350
350
|
return gather_cycle_violations(cycles)
|
|
351
351
|
|
|
352
352
|
|
|
353
|
-
class DependOnFileCondition:
|
|
353
|
+
class DependOnFileCondition(RuleRationaleMixin):
|
|
354
354
|
"""Checkable that verifies file dependency rules."""
|
|
355
355
|
|
|
356
356
|
def __init__(
|
|
@@ -377,7 +377,7 @@ class DependOnFileCondition:
|
|
|
377
377
|
)
|
|
378
378
|
|
|
379
379
|
|
|
380
|
-
class DependOnExternalModuleCondition:
|
|
380
|
+
class DependOnExternalModuleCondition(RuleRationaleMixin):
|
|
381
381
|
"""Checkable that verifies external module dependency rules."""
|
|
382
382
|
|
|
383
383
|
def __init__(
|
|
@@ -409,7 +409,7 @@ class DependOnExternalModuleCondition:
|
|
|
409
409
|
)
|
|
410
410
|
|
|
411
411
|
|
|
412
|
-
class MatchPatternFileCondition:
|
|
412
|
+
class MatchPatternFileCondition(RuleRationaleMixin):
|
|
413
413
|
"""Checkable that verifies files match/don't match patterns."""
|
|
414
414
|
|
|
415
415
|
def __init__(
|
|
@@ -434,7 +434,7 @@ class MatchPatternFileCondition:
|
|
|
434
434
|
return gather_regex_matching_violations(nodes, self._check_filters, self._is_negated)
|
|
435
435
|
|
|
436
436
|
|
|
437
|
-
class CustomFileCheckableCondition:
|
|
437
|
+
class CustomFileCheckableCondition(RuleRationaleMixin):
|
|
438
438
|
"""Checkable that evaluates a custom condition on files."""
|
|
439
439
|
|
|
440
440
|
def __init__(
|
|
@@ -5,7 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import ast
|
|
6
6
|
import os
|
|
7
7
|
|
|
8
|
-
from archunitpython.common.extraction.extract_graph import
|
|
8
|
+
from archunitpython.common.extraction.extract_graph import (
|
|
9
|
+
_find_python_files,
|
|
10
|
+
_resolve_exclude_patterns,
|
|
11
|
+
)
|
|
9
12
|
from archunitpython.metrics.common.types import (
|
|
10
13
|
ClassInfo,
|
|
11
14
|
EnhancedClassInfo,
|
|
@@ -33,7 +36,7 @@ def extract_class_info(
|
|
|
33
36
|
project_path = os.getcwd()
|
|
34
37
|
|
|
35
38
|
project_path = os.path.abspath(project_path)
|
|
36
|
-
excludes =
|
|
39
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
37
40
|
py_files = _find_python_files(project_path, excludes)
|
|
38
41
|
|
|
39
42
|
classes: list[ClassInfo] = []
|
|
@@ -53,7 +56,7 @@ def extract_enhanced_class_info(
|
|
|
53
56
|
project_path = os.getcwd()
|
|
54
57
|
|
|
55
58
|
project_path = os.path.abspath(project_path)
|
|
56
|
-
excludes =
|
|
59
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
57
60
|
py_files = _find_python_files(project_path, excludes)
|
|
58
61
|
|
|
59
62
|
results: list[FileAnalysisResult] = []
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import Any, Callable
|
|
12
12
|
|
|
13
13
|
from archunitpython.common.assertion.violation import Violation
|
|
14
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
14
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
15
15
|
from archunitpython.common.pattern_matching import matches_pattern_classname
|
|
16
16
|
from archunitpython.common.regex_factory import RegexFactory
|
|
17
17
|
from archunitpython.common.types import Filter, Pattern
|
|
@@ -170,7 +170,7 @@ class ClassMetricThresholdBuilder:
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
class ClassMetricCondition:
|
|
173
|
+
class ClassMetricCondition(RuleRationaleMixin):
|
|
174
174
|
"""Checkable that verifies a class-level metric threshold."""
|
|
175
175
|
|
|
176
176
|
def __init__(
|
|
@@ -230,7 +230,7 @@ class FileMetricThresholdBuilder:
|
|
|
230
230
|
)
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
class FileMetricCondition:
|
|
233
|
+
class FileMetricCondition(RuleRationaleMixin):
|
|
234
234
|
"""Checkable that verifies a file-level metric threshold."""
|
|
235
235
|
|
|
236
236
|
def __init__(
|
|
@@ -251,13 +251,13 @@ class FileMetricCondition:
|
|
|
251
251
|
import os
|
|
252
252
|
|
|
253
253
|
from archunitpython.common.extraction.extract_graph import (
|
|
254
|
-
_DEFAULT_EXCLUDE,
|
|
255
254
|
_find_python_files,
|
|
255
|
+
_resolve_exclude_patterns,
|
|
256
256
|
)
|
|
257
257
|
|
|
258
258
|
project = self._project_path or os.getcwd()
|
|
259
259
|
project = os.path.abspath(project)
|
|
260
|
-
files = _find_python_files(project,
|
|
260
|
+
files = _find_python_files(project, _resolve_exclude_patterns(project, None))
|
|
261
261
|
violations: list[Violation] = []
|
|
262
262
|
|
|
263
263
|
for file_path in files:
|
|
@@ -364,7 +364,7 @@ class DistanceThresholdBuilder:
|
|
|
364
364
|
)
|
|
365
365
|
|
|
366
366
|
|
|
367
|
-
class DistanceCondition:
|
|
367
|
+
class DistanceCondition(RuleRationaleMixin):
|
|
368
368
|
"""Checkable for distance metric thresholds."""
|
|
369
369
|
|
|
370
370
|
def __init__(
|
|
@@ -404,7 +404,7 @@ class DistanceCondition:
|
|
|
404
404
|
return violations
|
|
405
405
|
|
|
406
406
|
|
|
407
|
-
class ZoneCondition:
|
|
407
|
+
class ZoneCondition(RuleRationaleMixin):
|
|
408
408
|
"""Checkable for zone detection (pain/uselessness)."""
|
|
409
409
|
|
|
410
410
|
def __init__(self, project_path: str | None, filters: list[Filter], zone_type: str) -> None:
|
|
@@ -485,7 +485,7 @@ class CustomMetricsBuilder:
|
|
|
485
485
|
)
|
|
486
486
|
|
|
487
487
|
|
|
488
|
-
class CustomMetricCondition:
|
|
488
|
+
class CustomMetricCondition(RuleRationaleMixin):
|
|
489
489
|
"""Checkable for custom metric thresholds."""
|
|
490
490
|
|
|
491
491
|
def __init__(
|
|
@@ -525,7 +525,7 @@ class CustomMetricCondition:
|
|
|
525
525
|
return violations
|
|
526
526
|
|
|
527
527
|
|
|
528
|
-
class CustomAssertionCondition:
|
|
528
|
+
class CustomAssertionCondition(RuleRationaleMixin):
|
|
529
529
|
"""Checkable for custom metric assertions."""
|
|
530
530
|
|
|
531
531
|
def __init__(
|
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
|
|
15
15
|
from archunitpython.common.assertion.violation import Violation
|
|
16
16
|
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
|
-
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
17
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
|
|
18
18
|
from archunitpython.common.projection.project_edges import project_edges
|
|
19
19
|
from archunitpython.common.projection.types import MapFunction
|
|
20
20
|
from archunitpython.slices.assertion.admissible_edges import (
|
|
@@ -140,7 +140,7 @@ class NegativeConditionBuilder:
|
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
class PositiveSliceCondition:
|
|
143
|
+
class PositiveSliceCondition(RuleRationaleMixin):
|
|
144
144
|
"""Checkable that verifies slices adhere to a diagram."""
|
|
145
145
|
|
|
146
146
|
def __init__(
|
|
@@ -176,7 +176,7 @@ class PositiveSliceCondition:
|
|
|
176
176
|
return identity()
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
class NegativeSliceCondition:
|
|
179
|
+
class NegativeSliceCondition(RuleRationaleMixin):
|
|
180
180
|
"""Checkable that verifies a specific dependency does NOT exist."""
|
|
181
181
|
|
|
182
182
|
def __init__(
|
|
@@ -7,7 +7,11 @@ from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
|
|
|
7
7
|
from archunitpython.testing.common.violation_factory import ViolationFactory
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def format_violations(
|
|
10
|
+
def format_violations(
|
|
11
|
+
violations: list[Violation],
|
|
12
|
+
*,
|
|
13
|
+
because: str | None = None,
|
|
14
|
+
) -> str:
|
|
11
15
|
"""Format violations into a human-readable string.
|
|
12
16
|
|
|
13
17
|
Args:
|
|
@@ -19,7 +23,10 @@ def format_violations(violations: list[Violation]) -> str:
|
|
|
19
23
|
if not violations:
|
|
20
24
|
return "No violations found."
|
|
21
25
|
|
|
22
|
-
lines = [f"Found {len(violations)} architecture violation(s):"
|
|
26
|
+
lines = [f"Found {len(violations)} architecture violation(s):"]
|
|
27
|
+
if because:
|
|
28
|
+
lines.extend(["", f"Because: {because}"])
|
|
29
|
+
lines.append("")
|
|
23
30
|
for i, violation in enumerate(violations, 1):
|
|
24
31
|
tv = ViolationFactory.from_violation(violation)
|
|
25
32
|
lines.append(f" {i}. {tv.message}")
|
|
@@ -44,4 +51,5 @@ def assert_passes(
|
|
|
44
51
|
"""
|
|
45
52
|
violations = checkable.check(options)
|
|
46
53
|
if violations:
|
|
47
|
-
|
|
54
|
+
because = getattr(checkable, "because_reason", None)
|
|
55
|
+
raise AssertionError(format_violations(violations, because=because))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -188,6 +188,43 @@ options = CheckOptions(
|
|
|
188
188
|
violations = rule.check(options)
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
### Excluding Files With `.archignore`
|
|
192
|
+
|
|
193
|
+
Add a `.archignore` file to your project root to permanently exclude generated or
|
|
194
|
+
irrelevant files from architecture checks and file-based metrics:
|
|
195
|
+
|
|
196
|
+
```gitignore
|
|
197
|
+
# Generated code
|
|
198
|
+
generated/
|
|
199
|
+
|
|
200
|
+
# Migration scripts
|
|
201
|
+
migrations/*.py
|
|
202
|
+
|
|
203
|
+
# A single root-level file
|
|
204
|
+
/legacy_adapter.py
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Patterns support comments, blank lines, glob syntax, root-relative paths, path
|
|
208
|
+
patterns, and directory patterns with a trailing `/`.
|
|
209
|
+
### Explaining Rules With `.because(...)`
|
|
210
|
+
|
|
211
|
+
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
rule = (
|
|
215
|
+
project_files("src/")
|
|
216
|
+
.in_folder("**/controllers/**")
|
|
217
|
+
.should_not()
|
|
218
|
+
.depend_on_files()
|
|
219
|
+
.in_folder("**/database/**")
|
|
220
|
+
.because("controllers should stay thin and delegate persistence")
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
assert_passes(rule)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
When the rule fails, the rationale is included in the assertion message.
|
|
227
|
+
|
|
191
228
|
## 🐹 Use Cases
|
|
192
229
|
|
|
193
230
|
Here is an overview of common use cases.
|
|
@@ -431,21 +468,113 @@ def test_no_forbidden_dependency():
|
|
|
431
468
|
|
|
432
469
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
433
470
|
|
|
471
|
+
**Using [`requests`](https://github.com/psf/requests) library repo for example**
|
|
472
|
+
|
|
434
473
|
```python
|
|
435
474
|
from archunitpython import project_graph
|
|
436
475
|
|
|
437
476
|
def test_export_dependency_graph_reports():
|
|
438
|
-
graph = project_graph("src/").titled("Application Architecture")
|
|
439
|
-
|
|
440
|
-
graph.collapse_to_folder_depth(2).export_as_mermaid(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
477
|
+
graph = project_graph("src/requests").titled("Application Architecture")
|
|
478
|
+
|
|
479
|
+
graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
|
|
480
|
+
|
|
481
|
+
if __name__ == "__main__":
|
|
482
|
+
test_export_dependency_graph_reports()
|
|
483
|
+
```
|
|
484
|
+
**Exported mermaid diagram**
|
|
485
|
+
``` mermaid
|
|
486
|
+
flowchart LR
|
|
487
|
+
n0["__init__.py"]
|
|
488
|
+
n1["__version__.py"]
|
|
489
|
+
n2["_internal_utils.py"]
|
|
490
|
+
n3["_types.py"]
|
|
491
|
+
n4["adapters.py"]
|
|
492
|
+
n5["api.py"]
|
|
493
|
+
n6["auth.py"]
|
|
494
|
+
n7["certs.py"]
|
|
495
|
+
n8["compat.py"]
|
|
496
|
+
n9["cookies.py"]
|
|
497
|
+
n10["exceptions.py"]
|
|
498
|
+
n11["help.py"]
|
|
499
|
+
n12["hooks.py"]
|
|
500
|
+
n13["models.py"]
|
|
501
|
+
n14["packages.py"]
|
|
502
|
+
n15["sessions.py"]
|
|
503
|
+
n16["status_codes.py"]
|
|
504
|
+
n17["structures.py"]
|
|
505
|
+
n18["utils.py"]
|
|
506
|
+
n0 --> n1
|
|
507
|
+
n0 --> n5
|
|
508
|
+
n0 --> n10
|
|
509
|
+
n0 --> n13
|
|
510
|
+
n0 --> n15
|
|
511
|
+
n0 --> n16
|
|
512
|
+
n2 --> n8
|
|
513
|
+
n3 --> n6
|
|
514
|
+
n3 --> n9
|
|
515
|
+
n3 --> n13
|
|
516
|
+
n3 --> n17
|
|
517
|
+
n4 --> n0
|
|
518
|
+
n4 --> n3
|
|
519
|
+
n4 --> n6
|
|
520
|
+
n4 --> n8
|
|
521
|
+
n4 --> n9
|
|
522
|
+
n4 --> n10
|
|
523
|
+
n4 --> n13
|
|
524
|
+
n4 --> n17
|
|
525
|
+
n4 --> n18
|
|
526
|
+
n5 --> n0
|
|
527
|
+
n5 --> n13
|
|
528
|
+
n6 --> n2
|
|
529
|
+
n6 --> n8
|
|
530
|
+
n6 --> n9
|
|
531
|
+
n6 --> n13
|
|
532
|
+
n6 --> n18
|
|
533
|
+
n9 --> n2
|
|
534
|
+
n9 --> n3
|
|
535
|
+
n9 --> n8
|
|
536
|
+
n9 --> n13
|
|
537
|
+
n10 --> n8
|
|
538
|
+
n10 --> n13
|
|
539
|
+
n11 --> n0
|
|
540
|
+
n12 --> n0
|
|
541
|
+
n12 --> n13
|
|
542
|
+
n13 --> n0
|
|
543
|
+
n13 --> n2
|
|
544
|
+
n13 --> n4
|
|
545
|
+
n13 --> n6
|
|
546
|
+
n13 --> n8
|
|
547
|
+
n13 --> n9
|
|
548
|
+
n13 --> n10
|
|
549
|
+
n13 --> n12
|
|
550
|
+
n13 --> n16
|
|
551
|
+
n13 --> n17
|
|
552
|
+
n13 --> n18
|
|
553
|
+
n14 --> n8
|
|
554
|
+
n15 --> n0
|
|
555
|
+
n15 --> n2
|
|
556
|
+
n15 --> n3
|
|
557
|
+
n15 --> n4
|
|
558
|
+
n15 --> n6
|
|
559
|
+
n15 --> n8
|
|
560
|
+
n15 --> n9
|
|
561
|
+
n15 --> n10
|
|
562
|
+
n15 --> n12
|
|
563
|
+
n15 --> n13
|
|
564
|
+
n15 --> n16
|
|
565
|
+
n15 --> n17
|
|
566
|
+
n15 --> n18
|
|
567
|
+
n16 --> n17
|
|
568
|
+
n17 --> n8
|
|
569
|
+
n18 --> n0
|
|
570
|
+
n18 --> n1
|
|
571
|
+
n18 --> n2
|
|
572
|
+
n18 --> n3
|
|
573
|
+
n18 --> n8
|
|
574
|
+
n18 --> n9
|
|
575
|
+
n18 --> n10
|
|
576
|
+
n18 --> n13
|
|
577
|
+
n18 --> n17
|
|
449
578
|
```
|
|
450
579
|
|
|
451
580
|
Supported formats:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
archunitpython/__init__.py,sha256=
|
|
1
|
+
archunitpython/__init__.py,sha256=8MQMKFXjGNHJdCtifcWJcAgXLkjqJhDiXYxqp2fmxlQ,1154
|
|
2
2
|
archunitpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
archunitpython/common/__init__.py,sha256=
|
|
3
|
+
archunitpython/common/__init__.py,sha256=TKL39Z0kBpWqMH99jU4LsDSUZ5lQ_rcAJfLnmUTDTOI,656
|
|
4
4
|
archunitpython/common/pattern_matching.py,sha256=HMAfo8GsooHvA4d2IxCi3btrYTdwfOQXw4bDNr117P0,2669
|
|
5
5
|
archunitpython/common/regex_factory.py,sha256=UtlElM0Uty3SuD7tPZeoDnpbFipFUuYDpWfuV-Xq4c4,2459
|
|
6
6
|
archunitpython/common/types.py,sha256=aA6hxCvmOy7_q74pE0sDRILNB8KxmHx9G4j5ql87mWs,755
|
|
@@ -9,10 +9,10 @@ archunitpython/common/assertion/violation.py,sha256=TnMOykN3kPoGrSf1KcxBXYRfaxfX
|
|
|
9
9
|
archunitpython/common/error/__init__.py,sha256=UWcdIKpGAJvo4WEGOVSaUjQQnKoPi8R496uDGXBWKsE,116
|
|
10
10
|
archunitpython/common/error/errors.py,sha256=y7mcXoZPyK7uD2dMMO1qBt1y7KcIeGlRYiVeI6bjms4,249
|
|
11
11
|
archunitpython/common/extraction/__init__.py,sha256=RkJOcxJoLYQxEagh3uWhjsUCprK3Nr3Bn3o7nmzk4_Q,284
|
|
12
|
-
archunitpython/common/extraction/extract_graph.py,sha256=
|
|
12
|
+
archunitpython/common/extraction/extract_graph.py,sha256=PcKInkTFLBxWO5RqppcGJKDii2LrTXgkxm7a0G16ZAg,17186
|
|
13
13
|
archunitpython/common/extraction/graph.py,sha256=Rk-0eDDOLoHvScO27J2JzXjyMq3e5eKtrHnFBh5B6SU,1052
|
|
14
|
-
archunitpython/common/fluentapi/__init__.py,sha256=
|
|
15
|
-
archunitpython/common/fluentapi/checkable.py,sha256=
|
|
14
|
+
archunitpython/common/fluentapi/__init__.py,sha256=LeS7qS2p9-FqqBhDL8xRPex__W5Qk0UWfcpl7nVVLfI,178
|
|
15
|
+
archunitpython/common/fluentapi/checkable.py,sha256=BJFYibVhHLMFlnWdISPCIL3DYI1_3JN2-1jEQOaVaGE,1547
|
|
16
16
|
archunitpython/common/logging/__init__.py,sha256=u3-2_jYmiyoQ-C534SmQXvR1L9h3lbJqA9Yt1Nmv9HA,115
|
|
17
17
|
archunitpython/common/logging/types.py,sha256=wUriZeaUawA3JoBfJVLC6lIMglbLG955nXWdJGwxW4I,425
|
|
18
18
|
archunitpython/common/projection/__init__.py,sha256=-Sx-b80b2N3ap3ahAcLUCXqYYK3ccgTDpV-K1jSOSYQ,797
|
|
@@ -38,7 +38,7 @@ archunitpython/files/assertion/depend_on_external_modules.py,sha256=BXGpN-301YOc
|
|
|
38
38
|
archunitpython/files/assertion/depend_on_files.py,sha256=EGMtDD8KT09BJQwOEMoKj-yTQfY3KLLYWN5SM1BCmpY,2012
|
|
39
39
|
archunitpython/files/assertion/matching_files.py,sha256=qNbvU2o_MM6rzM-YUdJZzkltVbenWGC0u5kLH7su6CA,2003
|
|
40
40
|
archunitpython/files/fluentapi/__init__.py,sha256=wgl8IOqTeQGpNISu-Q8b_j6rTn8aEgoz6t1S5j8z13g,108
|
|
41
|
-
archunitpython/files/fluentapi/files.py,sha256=
|
|
41
|
+
archunitpython/files/fluentapi/files.py,sha256=oZG0lWV6p0AiGANdoZfYd51H8zY-J4eTSPRTRPBYhs0,17152
|
|
42
42
|
archunitpython/graph/__init__.py,sha256=vm1jMOoN35IIU7YDs59qPtf_F82YV-QrEGfXhozAOek,756
|
|
43
43
|
archunitpython/graph/graph_reporter.py,sha256=D_KH01CM7TzeMFB6o5NIQfLVJq0rvKZn9l7WumXV6lo,25598
|
|
44
44
|
archunitpython/layers/__init__.py,sha256=DCRxqLhluTxTaT9ZM2MbfAdJVgqcOO9jtkZHzBbeLOw,164
|
|
@@ -56,28 +56,28 @@ archunitpython/metrics/calculation/lcom.py,sha256=LK_kNT-BM6yuR5TUOLWQLkVvMpqR6q
|
|
|
56
56
|
archunitpython/metrics/common/__init__.py,sha256=xtEyAhS4X0UbJXHxnpdiXljVZmKIKKekZAZ1zflSPFE,335
|
|
57
57
|
archunitpython/metrics/common/types.py,sha256=w1BUlw6p_3K8qZDoQXv50P_CcKixGYiyS_YBGYILySA,1601
|
|
58
58
|
archunitpython/metrics/extraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
archunitpython/metrics/extraction/extract_class_info.py,sha256=
|
|
59
|
+
archunitpython/metrics/extraction/extract_class_info.py,sha256=u-i_AkRRdIbOVXhH7LVCCVIxRrz4IX6nOqukPxbB5Iw,7747
|
|
60
60
|
archunitpython/metrics/fluentapi/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
|
|
61
61
|
archunitpython/metrics/fluentapi/export_utils.py,sha256=1r-zTQAUqn_agp4_lhbcffdzfdASNY3LebYvKAG9kcQ,2333
|
|
62
|
-
archunitpython/metrics/fluentapi/metrics.py,sha256=
|
|
62
|
+
archunitpython/metrics/fluentapi/metrics.py,sha256=8OBOL3jVKbM9v_CjNNM9FMfV1JBfl5UEd1-PPOEScRs,19371
|
|
63
63
|
archunitpython/metrics/projection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
archunitpython/slices/__init__.py,sha256=tb8MoZiEqIdmWy7uyhmEtMNm5Fxjuw6atSvDsD131DE,96
|
|
65
65
|
archunitpython/slices/assertion/__init__.py,sha256=MRwV3d69ljAJ7hFYdxI6UDNC8U4Dm1ImeuLUVKEC9To,280
|
|
66
66
|
archunitpython/slices/assertion/admissible_edges.py,sha256=-3o5mGtl81i5I_572x3Z4ZDfqPCyFqjGd7Dg8GRSKhY,3119
|
|
67
67
|
archunitpython/slices/fluentapi/__init__.py,sha256=tb8MoZiEqIdmWy7uyhmEtMNm5Fxjuw6atSvDsD131DE,96
|
|
68
|
-
archunitpython/slices/fluentapi/slices.py,sha256=
|
|
68
|
+
archunitpython/slices/fluentapi/slices.py,sha256=vNoyqmleTDTjnf2i35i2nte-Ycjm-oapfgaf3AhN0QI,7163
|
|
69
69
|
archunitpython/slices/projection/__init__.py,sha256=tVgcK4gw-bqhUq98vU7_iPE0NEhOPUr7vr1-p7JI1A4,237
|
|
70
70
|
archunitpython/slices/projection/slicing_projections.py,sha256=BHBkuVG80mqzT7N4FlCCfY5U_NvlXmAgTmHf_SPTXVM,4215
|
|
71
71
|
archunitpython/slices/uml/__init__.py,sha256=GQFA5WBKfi_YElWiNWNm9c9IM-VaW83_Cg6kpwkzxkw,196
|
|
72
72
|
archunitpython/slices/uml/export_diagram.py,sha256=FeBRV8Vt8LuhLqEbg5JzZOmEk2i49JPkozMcI5r335g,818
|
|
73
73
|
archunitpython/slices/uml/generate_rules.py,sha256=D9vvqDzHAM0F80z4u5yBalEqIiNfJJ5HGXP0fVlSzHw,2136
|
|
74
74
|
archunitpython/testing/__init__.py,sha256=MJm6CHGhBGFg1um2L20-GD2DEHKqg8SiUoI-sRsoXQk,128
|
|
75
|
-
archunitpython/testing/assertion.py,sha256=
|
|
75
|
+
archunitpython/testing/assertion.py,sha256=7cXlNwBklIGAyUJlPGFGAw97xZ9UgpkZvoO_QBdqm1c,1637
|
|
76
76
|
archunitpython/testing/common/__init__.py,sha256=Wc4bC-N4t6giChKjj6wuTGKkb39thcYS_xKWG9paYeY,220
|
|
77
77
|
archunitpython/testing/common/color_utils.py,sha256=2I8Z1SZfWhhgudMgmXY6PPydGxGl35cK_To-GXnSyJg,1226
|
|
78
78
|
archunitpython/testing/common/violation_factory.py,sha256=yWlvv2U7u-7KoTMfp1QCeJXxTLpPEagKqDHZmPSyrlk,4677
|
|
79
79
|
archunitpython/testing/pytest_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
|
-
archunitpython-1.
|
|
81
|
-
archunitpython-1.
|
|
82
|
-
archunitpython-1.
|
|
83
|
-
archunitpython-1.
|
|
80
|
+
archunitpython-1.4.0.dist-info/METADATA,sha256=z6zirY281brtJcOrmADYb_Mrll-MNzEC6GKuTPQRkA4,30762
|
|
81
|
+
archunitpython-1.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
82
|
+
archunitpython-1.4.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
|
|
83
|
+
archunitpython-1.4.0.dist-info/RECORD,,
|
|
File without changes
|