archunitpython 1.2.0__py3-none-any.whl → 1.3.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 +8 -22
- archunitpython/common/fluentapi/__init__.py +6 -2
- archunitpython/common/fluentapi/checkable.py +23 -1
- archunitpython/common/pattern_matching.py +1 -3
- archunitpython/common/projection/cycles/johnsons_apsp.py +4 -9
- archunitpython/common/projection/cycles/tarjan_scc.py +2 -6
- archunitpython/common/projection/project_cycles.py +1 -2
- archunitpython/common/util/logger.py +1 -3
- archunitpython/files/assertion/custom_file_logic.py +3 -9
- archunitpython/files/assertion/depend_on_external_modules.py +3 -8
- archunitpython/files/assertion/depend_on_files.py +4 -12
- archunitpython/files/fluentapi/files.py +15 -33
- archunitpython/metrics/assertion/metric_thresholds.py +1 -3
- archunitpython/metrics/calculation/distance.py +1 -3
- archunitpython/metrics/extraction/extract_class_info.py +3 -9
- archunitpython/metrics/fluentapi/export_utils.py +2 -6
- archunitpython/metrics/fluentapi/metrics.py +19 -45
- archunitpython/slices/fluentapi/slices.py +7 -15
- archunitpython/slices/uml/generate_rules.py +2 -6
- archunitpython/testing/assertion.py +11 -3
- archunitpython/testing/common/violation_factory.py +1 -3
- {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/METADATA +127 -18
- {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/RECORD +27 -27
- {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/WHEEL +0 -0
- {archunitpython-1.2.0.dist-info → archunitpython-1.3.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",
|
|
@@ -91,12 +91,8 @@ def extract_graph(
|
|
|
91
91
|
excludes = (
|
|
92
92
|
list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
93
93
|
)
|
|
94
|
-
ignore_type_checking_imports = bool(
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
cache_key = _build_cache_key(
|
|
98
|
-
project_path, excludes, ignore_type_checking_imports
|
|
99
|
-
)
|
|
94
|
+
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
|
|
95
|
+
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
|
|
100
96
|
|
|
101
97
|
if options and options.clear_cache:
|
|
102
98
|
_graph_cache.pop(cache_key, None)
|
|
@@ -137,6 +133,7 @@ def _extract_graph_uncached(
|
|
|
137
133
|
|
|
138
134
|
edges: list[Edge] = []
|
|
139
135
|
py_files_set = set(py_files)
|
|
136
|
+
normalized_py_file_set = {_normalize(f) for f in py_files_set}
|
|
140
137
|
|
|
141
138
|
for file_path in py_files:
|
|
142
139
|
# Add self-referencing edge (ensures the file appears as a node)
|
|
@@ -148,7 +145,6 @@ def _extract_graph_uncached(
|
|
|
148
145
|
)
|
|
149
146
|
)
|
|
150
147
|
|
|
151
|
-
# Extract and resolve imports
|
|
152
148
|
imports = _extract_located_imports(file_path)
|
|
153
149
|
for located_import in imports:
|
|
154
150
|
module_name = located_import.module_name
|
|
@@ -163,9 +159,7 @@ def _extract_graph_uncached(
|
|
|
163
159
|
)
|
|
164
160
|
if resolved and resolved != _normalize(file_path):
|
|
165
161
|
# Check if the resolved path is in our project
|
|
166
|
-
if not is_external and resolved not in
|
|
167
|
-
_normalize(f) for f in py_files_set
|
|
168
|
-
}:
|
|
162
|
+
if not is_external and resolved not in normalized_py_file_set:
|
|
169
163
|
is_external = True
|
|
170
164
|
|
|
171
165
|
edges.append(
|
|
@@ -190,11 +184,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
|
190
184
|
py_files: list[str] = []
|
|
191
185
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
192
186
|
# Filter out excluded directories in-place
|
|
193
|
-
dirnames[:] = [
|
|
194
|
-
d
|
|
195
|
-
for d in dirnames
|
|
196
|
-
if not _should_exclude(d, exclude)
|
|
197
|
-
]
|
|
187
|
+
dirnames[:] = [d for d in dirnames if not _should_exclude(d, exclude)]
|
|
198
188
|
|
|
199
189
|
for filename in filenames:
|
|
200
190
|
if filename.endswith(".py") and not _should_exclude(filename, exclude):
|
|
@@ -349,18 +339,14 @@ def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
|
|
|
349
339
|
if is_type_checking and node.body:
|
|
350
340
|
start = node.body[0].lineno
|
|
351
341
|
end = max(
|
|
352
|
-
getattr(n, "end_lineno", n.lineno)
|
|
353
|
-
for n in node.body
|
|
354
|
-
if hasattr(n, "lineno")
|
|
342
|
+
getattr(n, "end_lineno", n.lineno) for n in node.body if hasattr(n, "lineno")
|
|
355
343
|
)
|
|
356
344
|
ranges.append((start, end))
|
|
357
345
|
|
|
358
|
-
return ranges
|
|
346
|
+
return sorted(ranges, key=lambda ele: ele[0])
|
|
359
347
|
|
|
360
348
|
|
|
361
|
-
def _in_type_checking(
|
|
362
|
-
node: ast.AST, ranges: list[tuple[int, int]]
|
|
363
|
-
) -> bool:
|
|
349
|
+
def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool:
|
|
364
350
|
"""Check if a node is inside a TYPE_CHECKING block."""
|
|
365
351
|
if not hasattr(node, "lineno"):
|
|
366
352
|
return False
|
|
@@ -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
|
|
|
@@ -50,9 +50,7 @@ def matches_pattern(file_path: str, filter_: Filter) -> bool:
|
|
|
50
50
|
return bool(filter_.regexp.search(target_string))
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
def matches_pattern_classname(
|
|
54
|
-
class_name: str, file_path: str, filter_: Filter
|
|
55
|
-
) -> bool:
|
|
53
|
+
def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter) -> bool:
|
|
56
54
|
"""Check if a class/file matches a filter, supporting classname target."""
|
|
57
55
|
target = filter_.options.target
|
|
58
56
|
|
|
@@ -53,13 +53,9 @@ class JohnsonsAPSP:
|
|
|
53
53
|
if self._is_part_of_current_start_cycle(current_node):
|
|
54
54
|
self._unblock(current_node)
|
|
55
55
|
else:
|
|
56
|
-
for neighbour in CycleUtils.get_outgoing_neighbours(
|
|
57
|
-
current_node, self._graph
|
|
58
|
-
):
|
|
56
|
+
for neighbour in CycleUtils.get_outgoing_neighbours(current_node, self._graph):
|
|
59
57
|
if self._is_blocked(neighbour):
|
|
60
|
-
self._blocked_map.append(
|
|
61
|
-
_BlockedBy(blocked=current_node, by=neighbour)
|
|
62
|
-
)
|
|
58
|
+
self._blocked_map.append(_BlockedBy(blocked=current_node, by=neighbour))
|
|
63
59
|
|
|
64
60
|
def _unblock(self, node: NumberNode) -> None:
|
|
65
61
|
self._blocked = [n for n in self._blocked if n is not node]
|
|
@@ -74,9 +70,8 @@ class JohnsonsAPSP:
|
|
|
74
70
|
if self._start is None:
|
|
75
71
|
return False
|
|
76
72
|
for cycle in self._cycles:
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
and any(e.from_node == current_node.node for e in cycle)
|
|
73
|
+
if cycle[0].from_node == self._start.node and any(
|
|
74
|
+
e.from_node == current_node.node for e in cycle
|
|
80
75
|
):
|
|
81
76
|
return True
|
|
82
77
|
return False
|
|
@@ -18,9 +18,7 @@ class _Vertex:
|
|
|
18
18
|
class TarjanSCC:
|
|
19
19
|
"""Tarjan's algorithm for finding strongly connected components."""
|
|
20
20
|
|
|
21
|
-
def find_strongly_connected_components(
|
|
22
|
-
self, edges: list[NumberEdge]
|
|
23
|
-
) -> list[list[NumberEdge]]:
|
|
21
|
+
def find_strongly_connected_components(self, edges: list[NumberEdge]) -> list[list[NumberEdge]]:
|
|
24
22
|
"""Find all strongly connected components in the graph.
|
|
25
23
|
|
|
26
24
|
Returns a list of edge lists, where each inner list contains
|
|
@@ -78,9 +76,7 @@ class TarjanSCC:
|
|
|
78
76
|
if scc_vertices:
|
|
79
77
|
scc_ids = {v.id for v in scc_vertices}
|
|
80
78
|
scc_edges = [
|
|
81
|
-
e
|
|
82
|
-
for e in self._edges
|
|
83
|
-
if e.from_node in scc_ids and e.to_node in scc_ids
|
|
79
|
+
e for e in self._edges if e.from_node in scc_ids and e.to_node in scc_ids
|
|
84
80
|
]
|
|
85
81
|
if scc_edges:
|
|
86
82
|
self._sccs.append(scc_edges)
|
|
@@ -44,9 +44,7 @@ class CheckLogger:
|
|
|
44
44
|
|
|
45
45
|
mode = "a" if options.append_to_log_file else "w"
|
|
46
46
|
self._file_handler = logging.FileHandler(str(log_path), mode=mode)
|
|
47
|
-
self._file_handler.setFormatter(
|
|
48
|
-
logging.Formatter("[%(levelname)s] %(message)s")
|
|
49
|
-
)
|
|
47
|
+
self._file_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
50
48
|
self._logger.addHandler(self._file_handler)
|
|
51
49
|
|
|
52
50
|
def _log(self, level: str, options: LoggingOptions | None, message: str) -> None:
|
|
@@ -83,9 +83,7 @@ def gather_custom_file_violations(
|
|
|
83
83
|
|
|
84
84
|
for node in nodes:
|
|
85
85
|
# Check if node matches all pre-filters
|
|
86
|
-
if pre_filters and not all(
|
|
87
|
-
matches_pattern(node.label, f) for f in pre_filters
|
|
88
|
-
):
|
|
86
|
+
if pre_filters and not all(matches_pattern(node.label, f) for f in pre_filters):
|
|
89
87
|
continue
|
|
90
88
|
|
|
91
89
|
file_info = _build_file_info(node.label)
|
|
@@ -94,14 +92,10 @@ def gather_custom_file_violations(
|
|
|
94
92
|
if is_negated:
|
|
95
93
|
# shouldNot: violation if condition IS True
|
|
96
94
|
if result:
|
|
97
|
-
violations.append(
|
|
98
|
-
CustomFileViolation(message=message, file_info=file_info)
|
|
99
|
-
)
|
|
95
|
+
violations.append(CustomFileViolation(message=message, file_info=file_info))
|
|
100
96
|
else:
|
|
101
97
|
# should: violation if condition is NOT True
|
|
102
98
|
if not result:
|
|
103
|
-
violations.append(
|
|
104
|
-
CustomFileViolation(message=message, file_info=file_info)
|
|
105
|
-
)
|
|
99
|
+
violations.append(CustomFileViolation(message=message, file_info=file_info))
|
|
106
100
|
|
|
107
101
|
return violations
|
|
@@ -34,8 +34,7 @@ def gather_depend_on_external_module_violations(
|
|
|
34
34
|
|
|
35
35
|
for edge in edges:
|
|
36
36
|
source_matches = all(
|
|
37
|
-
matches_pattern(edge.source_label, filter_)
|
|
38
|
-
for filter_ in subject_filters
|
|
37
|
+
matches_pattern(edge.source_label, filter_) for filter_ in subject_filters
|
|
39
38
|
)
|
|
40
39
|
if not source_matches:
|
|
41
40
|
continue
|
|
@@ -49,16 +48,12 @@ def gather_depend_on_external_module_violations(
|
|
|
49
48
|
if is_negated:
|
|
50
49
|
if target_matches:
|
|
51
50
|
violations.append(
|
|
52
|
-
ViolatingExternalModuleDependency(
|
|
53
|
-
dependency=edge, is_negated=True
|
|
54
|
-
)
|
|
51
|
+
ViolatingExternalModuleDependency(dependency=edge, is_negated=True)
|
|
55
52
|
)
|
|
56
53
|
else:
|
|
57
54
|
if not target_matches:
|
|
58
55
|
violations.append(
|
|
59
|
-
ViolatingExternalModuleDependency(
|
|
60
|
-
dependency=edge, is_negated=False
|
|
61
|
-
)
|
|
56
|
+
ViolatingExternalModuleDependency(dependency=edge, is_negated=False)
|
|
62
57
|
)
|
|
63
58
|
|
|
64
59
|
return violations
|
|
@@ -41,27 +41,19 @@ def gather_depend_on_file_violations(
|
|
|
41
41
|
violations: list[Violation] = []
|
|
42
42
|
|
|
43
43
|
for edge in edges:
|
|
44
|
-
source_matches = all(
|
|
45
|
-
matches_pattern(edge.source_label, f) for f in subject_filters
|
|
46
|
-
)
|
|
44
|
+
source_matches = all(matches_pattern(edge.source_label, f) for f in subject_filters)
|
|
47
45
|
if not source_matches:
|
|
48
46
|
continue
|
|
49
47
|
|
|
50
|
-
target_matches = all(
|
|
51
|
-
matches_pattern(edge.target_label, f) for f in object_filters
|
|
52
|
-
)
|
|
48
|
+
target_matches = all(matches_pattern(edge.target_label, f) for f in object_filters)
|
|
53
49
|
|
|
54
50
|
if is_negated:
|
|
55
51
|
# shouldNot: violation if dependency EXISTS
|
|
56
52
|
if target_matches:
|
|
57
|
-
violations.append(
|
|
58
|
-
ViolatingFileDependency(dependency=edge, is_negated=True)
|
|
59
|
-
)
|
|
53
|
+
violations.append(ViolatingFileDependency(dependency=edge, is_negated=True))
|
|
60
54
|
else:
|
|
61
55
|
# should: violation if dependency does NOT match
|
|
62
56
|
if not target_matches:
|
|
63
|
-
violations.append(
|
|
64
|
-
ViolatingFileDependency(dependency=edge, is_negated=False)
|
|
65
|
-
)
|
|
57
|
+
violations.append(ViolatingFileDependency(dependency=edge, is_negated=False))
|
|
66
58
|
|
|
67
59
|
return violations
|
|
@@ -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,
|
|
@@ -76,15 +76,11 @@ class FileConditionBuilder:
|
|
|
76
76
|
|
|
77
77
|
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
|
|
78
78
|
"""Begin positive assertion (files SHOULD ...)."""
|
|
79
|
-
return PositiveMatchPatternFileConditionBuilder(
|
|
80
|
-
self._project_path, list(self._filters)
|
|
81
|
-
)
|
|
79
|
+
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
|
|
82
80
|
|
|
83
81
|
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
|
|
84
82
|
"""Begin negative assertion (files SHOULD NOT ...)."""
|
|
85
|
-
return NegatedMatchPatternFileConditionBuilder(
|
|
86
|
-
self._project_path, list(self._filters)
|
|
87
|
-
)
|
|
83
|
+
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
|
|
88
84
|
|
|
89
85
|
|
|
90
86
|
class FilesShouldCondition:
|
|
@@ -111,15 +107,11 @@ class FilesShouldCondition:
|
|
|
111
107
|
|
|
112
108
|
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
|
|
113
109
|
"""Begin positive assertion (files SHOULD ...)."""
|
|
114
|
-
return PositiveMatchPatternFileConditionBuilder(
|
|
115
|
-
self._project_path, list(self._filters)
|
|
116
|
-
)
|
|
110
|
+
return PositiveMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
|
|
117
111
|
|
|
118
112
|
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
|
|
119
113
|
"""Begin negative assertion (files SHOULD NOT ...)."""
|
|
120
|
-
return NegatedMatchPatternFileConditionBuilder(
|
|
121
|
-
self._project_path, list(self._filters)
|
|
122
|
-
)
|
|
114
|
+
return NegatedMatchPatternFileConditionBuilder(self._project_path, list(self._filters))
|
|
123
115
|
|
|
124
116
|
|
|
125
117
|
class PositiveMatchPatternFileConditionBuilder:
|
|
@@ -135,9 +127,7 @@ class PositiveMatchPatternFileConditionBuilder:
|
|
|
135
127
|
|
|
136
128
|
def depend_on_files(self) -> "DependOnFileConditionBuilder":
|
|
137
129
|
"""Begin dependency assertion - files SHOULD depend on ..."""
|
|
138
|
-
return DependOnFileConditionBuilder(
|
|
139
|
-
self._project_path, self._filters, is_negated=False
|
|
140
|
-
)
|
|
130
|
+
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=False)
|
|
141
131
|
|
|
142
132
|
def depend_on_external_modules(
|
|
143
133
|
self,
|
|
@@ -192,9 +182,7 @@ class NegatedMatchPatternFileConditionBuilder:
|
|
|
192
182
|
|
|
193
183
|
def depend_on_files(self) -> "DependOnFileConditionBuilder":
|
|
194
184
|
"""Begin dependency assertion - files SHOULD NOT depend on ..."""
|
|
195
|
-
return DependOnFileConditionBuilder(
|
|
196
|
-
self._project_path, self._filters, is_negated=True
|
|
197
|
-
)
|
|
185
|
+
return DependOnFileConditionBuilder(self._project_path, self._filters, is_negated=True)
|
|
198
186
|
|
|
199
187
|
def depend_on_external_modules(
|
|
200
188
|
self,
|
|
@@ -243,9 +231,7 @@ class NegatedMatchPatternFileConditionBuilder:
|
|
|
243
231
|
class DependOnFileConditionBuilder:
|
|
244
232
|
"""Configure dependency target patterns."""
|
|
245
233
|
|
|
246
|
-
def __init__(
|
|
247
|
-
self, project_path: str | None, filters: list[Filter], is_negated: bool
|
|
248
|
-
) -> None:
|
|
234
|
+
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
|
|
249
235
|
self._project_path = project_path
|
|
250
236
|
self._filters = filters
|
|
251
237
|
self._is_negated = is_negated
|
|
@@ -285,9 +271,7 @@ class DependOnFileConditionBuilder:
|
|
|
285
271
|
class DependOnExternalModuleConditionBuilder:
|
|
286
272
|
"""Configure external module dependency target patterns."""
|
|
287
273
|
|
|
288
|
-
def __init__(
|
|
289
|
-
self, project_path: str | None, filters: list[Filter], is_negated: bool
|
|
290
|
-
) -> None:
|
|
274
|
+
def __init__(self, project_path: str | None, filters: list[Filter], is_negated: bool) -> None:
|
|
291
275
|
self._project_path = project_path
|
|
292
276
|
self._filters = filters
|
|
293
277
|
self._is_negated = is_negated
|
|
@@ -338,7 +322,7 @@ def _check_empty_test(
|
|
|
338
322
|
return None
|
|
339
323
|
|
|
340
324
|
|
|
341
|
-
class CycleFreeFileCondition:
|
|
325
|
+
class CycleFreeFileCondition(RuleRationaleMixin):
|
|
342
326
|
"""Checkable that verifies no cycles exist among filtered files."""
|
|
343
327
|
|
|
344
328
|
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
@@ -366,7 +350,7 @@ class CycleFreeFileCondition:
|
|
|
366
350
|
return gather_cycle_violations(cycles)
|
|
367
351
|
|
|
368
352
|
|
|
369
|
-
class DependOnFileCondition:
|
|
353
|
+
class DependOnFileCondition(RuleRationaleMixin):
|
|
370
354
|
"""Checkable that verifies file dependency rules."""
|
|
371
355
|
|
|
372
356
|
def __init__(
|
|
@@ -393,7 +377,7 @@ class DependOnFileCondition:
|
|
|
393
377
|
)
|
|
394
378
|
|
|
395
379
|
|
|
396
|
-
class DependOnExternalModuleCondition:
|
|
380
|
+
class DependOnExternalModuleCondition(RuleRationaleMixin):
|
|
397
381
|
"""Checkable that verifies external module dependency rules."""
|
|
398
382
|
|
|
399
383
|
def __init__(
|
|
@@ -425,7 +409,7 @@ class DependOnExternalModuleCondition:
|
|
|
425
409
|
)
|
|
426
410
|
|
|
427
411
|
|
|
428
|
-
class MatchPatternFileCondition:
|
|
412
|
+
class MatchPatternFileCondition(RuleRationaleMixin):
|
|
429
413
|
"""Checkable that verifies files match/don't match patterns."""
|
|
430
414
|
|
|
431
415
|
def __init__(
|
|
@@ -447,12 +431,10 @@ class MatchPatternFileCondition:
|
|
|
447
431
|
if empty is not None:
|
|
448
432
|
return empty
|
|
449
433
|
|
|
450
|
-
return gather_regex_matching_violations(
|
|
451
|
-
nodes, self._check_filters, self._is_negated
|
|
452
|
-
)
|
|
434
|
+
return gather_regex_matching_violations(nodes, self._check_filters, self._is_negated)
|
|
453
435
|
|
|
454
436
|
|
|
455
|
-
class CustomFileCheckableCondition:
|
|
437
|
+
class CustomFileCheckableCondition(RuleRationaleMixin):
|
|
456
438
|
"""Checkable that evaluates a custom condition on files."""
|
|
457
439
|
|
|
458
440
|
def __init__(
|
|
@@ -31,9 +31,7 @@ class FileCountViolation(Violation):
|
|
|
31
31
|
comparison: MetricComparison
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def check_threshold(
|
|
35
|
-
value: float, threshold: float, comparison: MetricComparison
|
|
36
|
-
) -> bool:
|
|
34
|
+
def check_threshold(value: float, threshold: float, comparison: MetricComparison) -> bool:
|
|
37
35
|
"""Check if a value violates a threshold.
|
|
38
36
|
|
|
39
37
|
Returns True if the value is a VIOLATION.
|
|
@@ -103,8 +103,6 @@ def calculate_distance_metrics_for_project(
|
|
|
103
103
|
average_instability=sum(m.instability for m in metrics) / len(metrics),
|
|
104
104
|
average_distance=sum(m.distance for m in metrics) / len(metrics),
|
|
105
105
|
files_in_zone_of_pain=sum(1 for m in metrics if m.in_zone_of_pain),
|
|
106
|
-
files_in_zone_of_uselessness=sum(
|
|
107
|
-
1 for m in metrics if m.in_zone_of_uselessness
|
|
108
|
-
),
|
|
106
|
+
files_in_zone_of_uselessness=sum(1 for m in metrics if m.in_zone_of_uselessness),
|
|
109
107
|
total_files=len(files),
|
|
110
108
|
)
|
|
@@ -129,9 +129,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
|
|
|
129
129
|
for item in ast.walk(node):
|
|
130
130
|
if isinstance(item, ast.Assign):
|
|
131
131
|
for target in item.targets:
|
|
132
|
-
if isinstance(target, ast.Attribute) and isinstance(
|
|
133
|
-
target.value, ast.Name
|
|
134
|
-
):
|
|
132
|
+
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name):
|
|
135
133
|
if target.value.id == "self":
|
|
136
134
|
field_name = target.attr
|
|
137
135
|
if field_name not in fields:
|
|
@@ -142,9 +140,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
|
|
|
142
140
|
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
143
141
|
method_name = item.name
|
|
144
142
|
accessed = _find_field_accesses(item, set(fields.keys()))
|
|
145
|
-
methods.append(
|
|
146
|
-
MethodInfo(name=method_name, accessed_fields=accessed)
|
|
147
|
-
)
|
|
143
|
+
methods.append(MethodInfo(name=method_name, accessed_fields=accessed))
|
|
148
144
|
# Update field access tracking
|
|
149
145
|
for field_name in accessed:
|
|
150
146
|
if field_name in fields:
|
|
@@ -159,9 +155,7 @@ def _extract_class(node: ast.ClassDef, file_path: str) -> ClassInfo:
|
|
|
159
155
|
)
|
|
160
156
|
|
|
161
157
|
|
|
162
|
-
def _extract_enhanced_class(
|
|
163
|
-
node: ast.ClassDef, file_path: str
|
|
164
|
-
) -> EnhancedClassInfo:
|
|
158
|
+
def _extract_enhanced_class(node: ast.ClassDef, file_path: str) -> EnhancedClassInfo:
|
|
165
159
|
"""Extract EnhancedClassInfo from a ClassDef AST node."""
|
|
166
160
|
base = _extract_class(node, file_path)
|
|
167
161
|
|
|
@@ -35,11 +35,7 @@ class MetricsExporter:
|
|
|
35
35
|
HTML content as a string. Also writes to file if output_path specified.
|
|
36
36
|
"""
|
|
37
37
|
opts = options or ExportOptions()
|
|
38
|
-
timestamp = (
|
|
39
|
-
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
40
|
-
if opts.include_timestamp
|
|
41
|
-
else ""
|
|
42
|
-
)
|
|
38
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if opts.include_timestamp else ""
|
|
43
39
|
|
|
44
40
|
css = opts.custom_css or _DEFAULT_CSS
|
|
45
41
|
|
|
@@ -55,7 +51,7 @@ class MetricsExporter:
|
|
|
55
51
|
</head>
|
|
56
52
|
<body>
|
|
57
53
|
<h1>{opts.title}</h1>
|
|
58
|
-
{f'<p class="timestamp">Generated: {timestamp}</p>' if timestamp else
|
|
54
|
+
{f'<p class="timestamp">Generated: {timestamp}</p>' if timestamp else ""}
|
|
59
55
|
<table>
|
|
60
56
|
<thead>
|
|
61
57
|
<tr><th>Metric</th><th>Value</th></tr>
|
|
@@ -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
|
|
@@ -97,9 +97,7 @@ class MetricsBuilder:
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def _get_filtered_classes(
|
|
101
|
-
project_path: str | None, filters: list[Filter]
|
|
102
|
-
) -> list[ClassInfo]:
|
|
100
|
+
def _get_filtered_classes(project_path: str | None, filters: list[Filter]) -> list[ClassInfo]:
|
|
103
101
|
classes = extract_class_info(project_path)
|
|
104
102
|
if not filters:
|
|
105
103
|
return classes
|
|
@@ -119,39 +117,25 @@ class CountMetricsBuilder:
|
|
|
119
117
|
self._filters = filters
|
|
120
118
|
|
|
121
119
|
def method_count(self) -> "ClassMetricThresholdBuilder":
|
|
122
|
-
return ClassMetricThresholdBuilder(
|
|
123
|
-
self._project_path, self._filters, MethodCountMetric()
|
|
124
|
-
)
|
|
120
|
+
return ClassMetricThresholdBuilder(self._project_path, self._filters, MethodCountMetric())
|
|
125
121
|
|
|
126
122
|
def field_count(self) -> "ClassMetricThresholdBuilder":
|
|
127
|
-
return ClassMetricThresholdBuilder(
|
|
128
|
-
self._project_path, self._filters, FieldCountMetric()
|
|
129
|
-
)
|
|
123
|
+
return ClassMetricThresholdBuilder(self._project_path, self._filters, FieldCountMetric())
|
|
130
124
|
|
|
131
125
|
def lines_of_code(self) -> "FileMetricThresholdBuilder":
|
|
132
|
-
return FileMetricThresholdBuilder(
|
|
133
|
-
self._project_path, self._filters, LinesOfCodeMetric()
|
|
134
|
-
)
|
|
126
|
+
return FileMetricThresholdBuilder(self._project_path, self._filters, LinesOfCodeMetric())
|
|
135
127
|
|
|
136
128
|
def statements(self) -> "FileMetricThresholdBuilder":
|
|
137
|
-
return FileMetricThresholdBuilder(
|
|
138
|
-
self._project_path, self._filters, StatementCountMetric()
|
|
139
|
-
)
|
|
129
|
+
return FileMetricThresholdBuilder(self._project_path, self._filters, StatementCountMetric())
|
|
140
130
|
|
|
141
131
|
def imports(self) -> "FileMetricThresholdBuilder":
|
|
142
|
-
return FileMetricThresholdBuilder(
|
|
143
|
-
self._project_path, self._filters, ImportCountMetric()
|
|
144
|
-
)
|
|
132
|
+
return FileMetricThresholdBuilder(self._project_path, self._filters, ImportCountMetric())
|
|
145
133
|
|
|
146
134
|
def classes(self) -> "FileMetricThresholdBuilder":
|
|
147
|
-
return FileMetricThresholdBuilder(
|
|
148
|
-
self._project_path, self._filters, ClassCountMetric()
|
|
149
|
-
)
|
|
135
|
+
return FileMetricThresholdBuilder(self._project_path, self._filters, ClassCountMetric())
|
|
150
136
|
|
|
151
137
|
def functions(self) -> "FileMetricThresholdBuilder":
|
|
152
|
-
return FileMetricThresholdBuilder(
|
|
153
|
-
self._project_path, self._filters, FunctionCountMetric()
|
|
154
|
-
)
|
|
138
|
+
return FileMetricThresholdBuilder(self._project_path, self._filters, FunctionCountMetric())
|
|
155
139
|
|
|
156
140
|
|
|
157
141
|
class ClassMetricThresholdBuilder:
|
|
@@ -186,7 +170,7 @@ class ClassMetricThresholdBuilder:
|
|
|
186
170
|
)
|
|
187
171
|
|
|
188
172
|
|
|
189
|
-
class ClassMetricCondition:
|
|
173
|
+
class ClassMetricCondition(RuleRationaleMixin):
|
|
190
174
|
"""Checkable that verifies a class-level metric threshold."""
|
|
191
175
|
|
|
192
176
|
def __init__(
|
|
@@ -246,7 +230,7 @@ class FileMetricThresholdBuilder:
|
|
|
246
230
|
)
|
|
247
231
|
|
|
248
232
|
|
|
249
|
-
class FileMetricCondition:
|
|
233
|
+
class FileMetricCondition(RuleRationaleMixin):
|
|
250
234
|
"""Checkable that verifies a file-level metric threshold."""
|
|
251
235
|
|
|
252
236
|
def __init__(
|
|
@@ -340,19 +324,13 @@ class DistanceMetricsBuilder:
|
|
|
340
324
|
self._filters = filters
|
|
341
325
|
|
|
342
326
|
def abstractness(self) -> "DistanceThresholdBuilder":
|
|
343
|
-
return DistanceThresholdBuilder(
|
|
344
|
-
self._project_path, self._filters, "abstractness"
|
|
345
|
-
)
|
|
327
|
+
return DistanceThresholdBuilder(self._project_path, self._filters, "abstractness")
|
|
346
328
|
|
|
347
329
|
def instability(self) -> "DistanceThresholdBuilder":
|
|
348
|
-
return DistanceThresholdBuilder(
|
|
349
|
-
self._project_path, self._filters, "instability"
|
|
350
|
-
)
|
|
330
|
+
return DistanceThresholdBuilder(self._project_path, self._filters, "instability")
|
|
351
331
|
|
|
352
332
|
def distance_from_main_sequence(self) -> "DistanceThresholdBuilder":
|
|
353
|
-
return DistanceThresholdBuilder(
|
|
354
|
-
self._project_path, self._filters, "distance"
|
|
355
|
-
)
|
|
333
|
+
return DistanceThresholdBuilder(self._project_path, self._filters, "distance")
|
|
356
334
|
|
|
357
335
|
def not_in_zone_of_pain(self) -> "ZoneCondition":
|
|
358
336
|
return ZoneCondition(self._project_path, self._filters, "pain")
|
|
@@ -386,7 +364,7 @@ class DistanceThresholdBuilder:
|
|
|
386
364
|
)
|
|
387
365
|
|
|
388
366
|
|
|
389
|
-
class DistanceCondition:
|
|
367
|
+
class DistanceCondition(RuleRationaleMixin):
|
|
390
368
|
"""Checkable for distance metric thresholds."""
|
|
391
369
|
|
|
392
370
|
def __init__(
|
|
@@ -426,7 +404,7 @@ class DistanceCondition:
|
|
|
426
404
|
return violations
|
|
427
405
|
|
|
428
406
|
|
|
429
|
-
class ZoneCondition:
|
|
407
|
+
class ZoneCondition(RuleRationaleMixin):
|
|
430
408
|
"""Checkable for zone detection (pain/uselessness)."""
|
|
431
409
|
|
|
432
410
|
def __init__(self, project_path: str | None, filters: list[Filter], zone_type: str) -> None:
|
|
@@ -440,11 +418,7 @@ class ZoneCondition:
|
|
|
440
418
|
|
|
441
419
|
for file_result in files:
|
|
442
420
|
dm = calculate_file_distance_metrics(file_result, files)
|
|
443
|
-
in_zone =
|
|
444
|
-
dm.in_zone_of_pain
|
|
445
|
-
if self._zone_type == "pain"
|
|
446
|
-
else dm.in_zone_of_uselessness
|
|
447
|
-
)
|
|
421
|
+
in_zone = dm.in_zone_of_pain if self._zone_type == "pain" else dm.in_zone_of_uselessness
|
|
448
422
|
|
|
449
423
|
if in_zone:
|
|
450
424
|
violations.append(
|
|
@@ -511,7 +485,7 @@ class CustomMetricsBuilder:
|
|
|
511
485
|
)
|
|
512
486
|
|
|
513
487
|
|
|
514
|
-
class CustomMetricCondition:
|
|
488
|
+
class CustomMetricCondition(RuleRationaleMixin):
|
|
515
489
|
"""Checkable for custom metric thresholds."""
|
|
516
490
|
|
|
517
491
|
def __init__(
|
|
@@ -551,7 +525,7 @@ class CustomMetricCondition:
|
|
|
551
525
|
return violations
|
|
552
526
|
|
|
553
527
|
|
|
554
|
-
class CustomAssertionCondition:
|
|
528
|
+
class CustomAssertionCondition(RuleRationaleMixin):
|
|
555
529
|
"""Checkable for custom metric assertions."""
|
|
556
530
|
|
|
557
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 (
|
|
@@ -61,15 +61,11 @@ class SliceConditionBuilder:
|
|
|
61
61
|
|
|
62
62
|
def should(self) -> "PositiveConditionBuilder":
|
|
63
63
|
"""Begin positive assertion (slices SHOULD ...)."""
|
|
64
|
-
return PositiveConditionBuilder(
|
|
65
|
-
self._project_path, self._pattern, self._regex
|
|
66
|
-
)
|
|
64
|
+
return PositiveConditionBuilder(self._project_path, self._pattern, self._regex)
|
|
67
65
|
|
|
68
66
|
def should_not(self) -> "NegativeConditionBuilder":
|
|
69
67
|
"""Begin negative assertion (slices SHOULD NOT ...)."""
|
|
70
|
-
return NegativeConditionBuilder(
|
|
71
|
-
self._project_path, self._pattern, self._regex
|
|
72
|
-
)
|
|
68
|
+
return NegativeConditionBuilder(self._project_path, self._pattern, self._regex)
|
|
73
69
|
|
|
74
70
|
|
|
75
71
|
class PositiveConditionBuilder:
|
|
@@ -133,9 +129,7 @@ class NegativeConditionBuilder:
|
|
|
133
129
|
self._regex = regex
|
|
134
130
|
self._forbidden_deps: list[tuple[str, str]] = []
|
|
135
131
|
|
|
136
|
-
def contain_dependency(
|
|
137
|
-
self, source: str, target: str
|
|
138
|
-
) -> "NegativeSliceCondition":
|
|
132
|
+
def contain_dependency(self, source: str, target: str) -> "NegativeSliceCondition":
|
|
139
133
|
"""Assert that a specific dependency should NOT exist."""
|
|
140
134
|
return NegativeSliceCondition(
|
|
141
135
|
self._project_path,
|
|
@@ -146,7 +140,7 @@ class NegativeConditionBuilder:
|
|
|
146
140
|
)
|
|
147
141
|
|
|
148
142
|
|
|
149
|
-
class PositiveSliceCondition:
|
|
143
|
+
class PositiveSliceCondition(RuleRationaleMixin):
|
|
150
144
|
"""Checkable that verifies slices adhere to a diagram."""
|
|
151
145
|
|
|
152
146
|
def __init__(
|
|
@@ -170,9 +164,7 @@ class PositiveSliceCondition:
|
|
|
170
164
|
mapper = self._get_mapper()
|
|
171
165
|
edges = project_edges(graph, mapper)
|
|
172
166
|
|
|
173
|
-
return gather_positive_violations(
|
|
174
|
-
edges, rules, contained_nodes, self._coherence_options
|
|
175
|
-
)
|
|
167
|
+
return gather_positive_violations(edges, rules, contained_nodes, self._coherence_options)
|
|
176
168
|
|
|
177
169
|
def _get_mapper(self) -> MapFunction:
|
|
178
170
|
if self._pattern:
|
|
@@ -184,7 +176,7 @@ class PositiveSliceCondition:
|
|
|
184
176
|
return identity()
|
|
185
177
|
|
|
186
178
|
|
|
187
|
-
class NegativeSliceCondition:
|
|
179
|
+
class NegativeSliceCondition(RuleRationaleMixin):
|
|
188
180
|
"""Checkable that verifies a specific dependency does NOT exist."""
|
|
189
181
|
|
|
190
182
|
def __init__(
|
|
@@ -43,9 +43,7 @@ def generate_rule(puml_content: str) -> tuple[list[Rule], list[str]]:
|
|
|
43
43
|
continue
|
|
44
44
|
|
|
45
45
|
# Match component declarations: component [Name] or component [Name] #Color
|
|
46
|
-
comp_match = re.match(
|
|
47
|
-
r"component\s+\[([^\]]+)\]", stripped
|
|
48
|
-
)
|
|
46
|
+
comp_match = re.match(r"component\s+\[([^\]]+)\]", stripped)
|
|
49
47
|
if comp_match:
|
|
50
48
|
name = comp_match.group(1).strip()
|
|
51
49
|
if name not in contained_nodes:
|
|
@@ -53,9 +51,7 @@ def generate_rule(puml_content: str) -> tuple[list[Rule], list[str]]:
|
|
|
53
51
|
continue
|
|
54
52
|
|
|
55
53
|
# Match relationships: [Source] --> [Target] or [Source] -> [Target]
|
|
56
|
-
rel_match = re.match(
|
|
57
|
-
r"\[([^\]]+)\]\s*-+>\s*\[([^\]]+)\]", stripped
|
|
58
|
-
)
|
|
54
|
+
rel_match = re.match(r"\[([^\]]+)\]\s*-+>\s*\[([^\]]+)\]", stripped)
|
|
59
55
|
if rel_match:
|
|
60
56
|
source = rel_match.group(1).strip()
|
|
61
57
|
target = rel_match.group(2).strip()
|
|
@@ -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))
|
|
@@ -66,9 +66,7 @@ class ViolationFactory:
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
if isinstance(violation, ViolatingCycle):
|
|
69
|
-
cycle_str = " -> ".join(
|
|
70
|
-
e.source_label for e in violation.cycle
|
|
71
|
-
)
|
|
69
|
+
cycle_str = " -> ".join(e.source_label for e in violation.cycle)
|
|
72
70
|
return TestViolation(
|
|
73
71
|
message="Circular dependency detected",
|
|
74
72
|
details=f"Cycle: {cycle_str}",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -39,11 +39,9 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
<!-- spacer -->
|
|
40
40
|
<p></p>
|
|
41
41
|
|
|
42
|
-
[](https://opensource.org/licenses/MIT)
|
|
43
|
-
[](https://pepy.tech/project/archunitpython)
|
|
46
|
-
[](https://github.com/LukasNiessen/ArchUnitPython)
|
|
42
|
+
[](https://opensource.org/licenses/MIT) [](https://github.com/LukasNiessen/ArchUnitPython/actions/workflows/integrate.yaml) [](https://github.com/LukasNiessen/ArchUnitPython)<br>
|
|
43
|
+
[](https://pepy.tech/project/archunitpython) [](https://pepy.tech/project/archunitpython)
|
|
44
|
+
<!-- [](https://pypi.org/project/archunitpython/) -->
|
|
47
45
|
|
|
48
46
|
</div>
|
|
49
47
|
|
|
@@ -53,7 +51,7 @@ The #1 ArchUnit-style architecture testing library for Python, measured by GitHu
|
|
|
53
51
|
|
|
54
52
|
_Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit._
|
|
55
53
|
|
|
56
|
-
[Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Contributing](CONTRIBUTING.md)
|
|
54
|
+
[Setup](#-setup) • [Use Cases](#-use-cases) • [Features](#-features) • [Why ArchUnitPython?](#-library-comparison) • [Sponsor](https://github.com/sponsors/LukasNiessen) • [Contributing](CONTRIBUTING.md)
|
|
57
55
|
|
|
58
56
|
## ⚡ 5 min Quickstart
|
|
59
57
|
|
|
@@ -190,6 +188,25 @@ options = CheckOptions(
|
|
|
190
188
|
violations = rule.check(options)
|
|
191
189
|
```
|
|
192
190
|
|
|
191
|
+
### Explaining Rules With `.because(...)`
|
|
192
|
+
|
|
193
|
+
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
rule = (
|
|
197
|
+
project_files("src/")
|
|
198
|
+
.in_folder("**/controllers/**")
|
|
199
|
+
.should_not()
|
|
200
|
+
.depend_on_files()
|
|
201
|
+
.in_folder("**/database/**")
|
|
202
|
+
.because("controllers should stay thin and delegate persistence")
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assert_passes(rule)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
When the rule fails, the rationale is included in the assertion message.
|
|
209
|
+
|
|
193
210
|
## 🐹 Use Cases
|
|
194
211
|
|
|
195
212
|
Here is an overview of common use cases.
|
|
@@ -433,21 +450,113 @@ def test_no_forbidden_dependency():
|
|
|
433
450
|
|
|
434
451
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
435
452
|
|
|
453
|
+
**Using `requests` library repo for example**
|
|
454
|
+
|
|
436
455
|
```python
|
|
437
456
|
from archunitpython import project_graph
|
|
438
457
|
|
|
439
458
|
def test_export_dependency_graph_reports():
|
|
440
|
-
graph = project_graph("src/").titled("Application Architecture")
|
|
441
|
-
|
|
442
|
-
graph.collapse_to_folder_depth(2).export_as_mermaid(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
459
|
+
graph = project_graph("src/requests").titled("Application Architecture")
|
|
460
|
+
|
|
461
|
+
graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
test_export_dependency_graph_reports()
|
|
465
|
+
```
|
|
466
|
+
**Rendered mermain diagram**
|
|
467
|
+
``` mermaid
|
|
468
|
+
flowchart LR
|
|
469
|
+
n0["__init__.py"]
|
|
470
|
+
n1["__version__.py"]
|
|
471
|
+
n2["_internal_utils.py"]
|
|
472
|
+
n3["_types.py"]
|
|
473
|
+
n4["adapters.py"]
|
|
474
|
+
n5["api.py"]
|
|
475
|
+
n6["auth.py"]
|
|
476
|
+
n7["certs.py"]
|
|
477
|
+
n8["compat.py"]
|
|
478
|
+
n9["cookies.py"]
|
|
479
|
+
n10["exceptions.py"]
|
|
480
|
+
n11["help.py"]
|
|
481
|
+
n12["hooks.py"]
|
|
482
|
+
n13["models.py"]
|
|
483
|
+
n14["packages.py"]
|
|
484
|
+
n15["sessions.py"]
|
|
485
|
+
n16["status_codes.py"]
|
|
486
|
+
n17["structures.py"]
|
|
487
|
+
n18["utils.py"]
|
|
488
|
+
n0 --> n1
|
|
489
|
+
n0 --> n5
|
|
490
|
+
n0 --> n10
|
|
491
|
+
n0 --> n13
|
|
492
|
+
n0 --> n15
|
|
493
|
+
n0 --> n16
|
|
494
|
+
n2 --> n8
|
|
495
|
+
n3 --> n6
|
|
496
|
+
n3 --> n9
|
|
497
|
+
n3 --> n13
|
|
498
|
+
n3 --> n17
|
|
499
|
+
n4 --> n0
|
|
500
|
+
n4 --> n3
|
|
501
|
+
n4 --> n6
|
|
502
|
+
n4 --> n8
|
|
503
|
+
n4 --> n9
|
|
504
|
+
n4 --> n10
|
|
505
|
+
n4 --> n13
|
|
506
|
+
n4 --> n17
|
|
507
|
+
n4 --> n18
|
|
508
|
+
n5 --> n0
|
|
509
|
+
n5 --> n13
|
|
510
|
+
n6 --> n2
|
|
511
|
+
n6 --> n8
|
|
512
|
+
n6 --> n9
|
|
513
|
+
n6 --> n13
|
|
514
|
+
n6 --> n18
|
|
515
|
+
n9 --> n2
|
|
516
|
+
n9 --> n3
|
|
517
|
+
n9 --> n8
|
|
518
|
+
n9 --> n13
|
|
519
|
+
n10 --> n8
|
|
520
|
+
n10 --> n13
|
|
521
|
+
n11 --> n0
|
|
522
|
+
n12 --> n0
|
|
523
|
+
n12 --> n13
|
|
524
|
+
n13 --> n0
|
|
525
|
+
n13 --> n2
|
|
526
|
+
n13 --> n4
|
|
527
|
+
n13 --> n6
|
|
528
|
+
n13 --> n8
|
|
529
|
+
n13 --> n9
|
|
530
|
+
n13 --> n10
|
|
531
|
+
n13 --> n12
|
|
532
|
+
n13 --> n16
|
|
533
|
+
n13 --> n17
|
|
534
|
+
n13 --> n18
|
|
535
|
+
n14 --> n8
|
|
536
|
+
n15 --> n0
|
|
537
|
+
n15 --> n2
|
|
538
|
+
n15 --> n3
|
|
539
|
+
n15 --> n4
|
|
540
|
+
n15 --> n6
|
|
541
|
+
n15 --> n8
|
|
542
|
+
n15 --> n9
|
|
543
|
+
n15 --> n10
|
|
544
|
+
n15 --> n12
|
|
545
|
+
n15 --> n13
|
|
546
|
+
n15 --> n16
|
|
547
|
+
n15 --> n17
|
|
548
|
+
n15 --> n18
|
|
549
|
+
n16 --> n17
|
|
550
|
+
n17 --> n8
|
|
551
|
+
n18 --> n0
|
|
552
|
+
n18 --> n1
|
|
553
|
+
n18 --> n2
|
|
554
|
+
n18 --> n3
|
|
555
|
+
n18 --> n8
|
|
556
|
+
n18 --> n9
|
|
557
|
+
n18 --> n10
|
|
558
|
+
n18 --> n13
|
|
559
|
+
n18 --> n17
|
|
451
560
|
```
|
|
452
561
|
|
|
453
562
|
Supported formats:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
archunitpython/__init__.py,sha256=
|
|
1
|
+
archunitpython/__init__.py,sha256=alLY-bGp-tUtdb8BRV0r8K-qIZKkWYnJ5K-mTDnFOrQ,1154
|
|
2
2
|
archunitpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
archunitpython/common/__init__.py,sha256=
|
|
4
|
-
archunitpython/common/pattern_matching.py,sha256=
|
|
3
|
+
archunitpython/common/__init__.py,sha256=TKL39Z0kBpWqMH99jU4LsDSUZ5lQ_rcAJfLnmUTDTOI,656
|
|
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
|
|
7
7
|
archunitpython/common/assertion/__init__.py,sha256=qpFkUrRoJWbwL7KPWaVTqlx70fhUJtwjyY0elsm8tv0,131
|
|
@@ -9,36 +9,36 @@ 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=HIfAZLZiv4Zbi1Nm8jmNnnu8ggn0wQXunBlZP_QK1x8,15254
|
|
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
|
|
19
19
|
archunitpython/common/projection/edge_projections.py,sha256=Mud8DL6Y8H66CObqyikKYAxdXMRENuAFRtwhCxpwxoU,1524
|
|
20
|
-
archunitpython/common/projection/project_cycles.py,sha256=
|
|
20
|
+
archunitpython/common/projection/project_cycles.py,sha256=TQ4Ok1hT4T_znqFbwV4VTj-BXmlFr1O7TtZdVOd5oPg,3081
|
|
21
21
|
archunitpython/common/projection/project_edges.py,sha256=v7hDMSeghiLz0xHbG4BNF7jNQphcWobU7rgoiuw3fh4,1268
|
|
22
22
|
archunitpython/common/projection/project_nodes.py,sha256=U6sLYvJd6NBgg39V0Ry7r_-57gdXaaXsbH-D12nipCg,1497
|
|
23
23
|
archunitpython/common/projection/types.py,sha256=EP5yJd-uXYdACFdFzeID8bUVsg_vdhgiOzshrC-CC8Y,937
|
|
24
24
|
archunitpython/common/projection/cycles/__init__.py,sha256=giGUcwpEMr91XST-biyCmLUjP5okhwTA4CcCaxjZdmg,217
|
|
25
25
|
archunitpython/common/projection/cycles/cycle_utils.py,sha256=eoVyZAapTWwpELGV9rY8e4ug2khmsGiEUz-3fD-JKBM,1746
|
|
26
26
|
archunitpython/common/projection/cycles/cycles.py,sha256=lHocmN7xnlfbrz6ZqKwuECHynnJNBe8Zb2nVcFVyOhA,906
|
|
27
|
-
archunitpython/common/projection/cycles/johnsons_apsp.py,sha256=
|
|
27
|
+
archunitpython/common/projection/cycles/johnsons_apsp.py,sha256=lfnTMIvv3mTeIlh0D9QzwJUGPgz9spcDvwSgPJhz69s,3993
|
|
28
28
|
archunitpython/common/projection/cycles/model.py,sha256=sgpb0HpYzQND0KPb56BWlnrv683UkE3MZ3NfAJ9ePK0,483
|
|
29
|
-
archunitpython/common/projection/cycles/tarjan_scc.py,sha256=
|
|
29
|
+
archunitpython/common/projection/cycles/tarjan_scc.py,sha256=pIj1ub8XgcqVLj8skdZdOvXqwX10U8kOmlnUIIHYCME,2789
|
|
30
30
|
archunitpython/common/util/__init__.py,sha256=g-W8kWiVqwdohxm74J3yUonPkb_yDUJrp73S5s4jpgI,117
|
|
31
31
|
archunitpython/common/util/declaration_detector.py,sha256=XgrpgaUIQa9MNFlnhVQz2X_uW7zCO9nrMbGa3tVFyPM,3407
|
|
32
|
-
archunitpython/common/util/logger.py,sha256=
|
|
32
|
+
archunitpython/common/util/logger.py,sha256=2did2mRAlkrElH9SYgv9xYiSzr4A-jXHg3cp6TodhoA,3326
|
|
33
33
|
archunitpython/files/__init__.py,sha256=wgl8IOqTeQGpNISu-Q8b_j6rTn8aEgoz6t1S5j8z13g,108
|
|
34
34
|
archunitpython/files/assertion/__init__.py,sha256=K2qiEFfmo6SCgxfOKNrPzYgT5ScKUaw2W2CrDa3jBTw,1068
|
|
35
|
-
archunitpython/files/assertion/custom_file_logic.py,sha256=
|
|
35
|
+
archunitpython/files/assertion/custom_file_logic.py,sha256=1v5D80QX_NnAKBwKwyhetY4rA8rrXyxN2BsHlvnLgq8,2956
|
|
36
36
|
archunitpython/files/assertion/cycle_free.py,sha256=Ib3tmVVErmTpORmRn_rZqOqY0gk54crN_d_IapeWqdI,738
|
|
37
|
-
archunitpython/files/assertion/depend_on_external_modules.py,sha256=
|
|
38
|
-
archunitpython/files/assertion/depend_on_files.py,sha256=
|
|
37
|
+
archunitpython/files/assertion/depend_on_external_modules.py,sha256=BXGpN-301YOcB446e3U0i2rNNliQuaSJCfdZwKVzs0U,1845
|
|
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
|
|
@@ -48,36 +48,36 @@ archunitpython/layers/fluentapi/__init__.py,sha256=psKFB-5IXS1SJ8g62HthExKYJio3b
|
|
|
48
48
|
archunitpython/layers/fluentapi/layers.py,sha256=Wl926_aWpNrlbWatDi3QDR-8geDflbYFEivWJv-X9dI,4438
|
|
49
49
|
archunitpython/metrics/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
|
|
50
50
|
archunitpython/metrics/assertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
-
archunitpython/metrics/assertion/metric_thresholds.py,sha256=
|
|
51
|
+
archunitpython/metrics/assertion/metric_thresholds.py,sha256=kwX1iRFnF7oLaLMlQsYrDsNRrivDE8pI6nrrZYAafvg,1262
|
|
52
52
|
archunitpython/metrics/calculation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
archunitpython/metrics/calculation/count.py,sha256=tMbAi51hkrViW7dM8SM1Qhh09kuwI3UeWmytAWNLEq0,3997
|
|
54
|
-
archunitpython/metrics/calculation/distance.py,sha256=
|
|
54
|
+
archunitpython/metrics/calculation/distance.py,sha256=W2agAQVmGJftPbHDUToKy66Fp8-h7WZspg15skyYWnc,3589
|
|
55
55
|
archunitpython/metrics/calculation/lcom.py,sha256=LK_kNT-BM6yuR5TUOLWQLkVvMpqR6q207p7RR9Rb35o,5040
|
|
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=IFokUFwNaExmPtKnHW5hgnYWodRtBM3cWhXBGb7lcjs,7751
|
|
60
60
|
archunitpython/metrics/fluentapi/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
|
|
61
|
-
archunitpython/metrics/fluentapi/export_utils.py,sha256=
|
|
62
|
-
archunitpython/metrics/fluentapi/metrics.py,sha256=
|
|
61
|
+
archunitpython/metrics/fluentapi/export_utils.py,sha256=1r-zTQAUqn_agp4_lhbcffdzfdASNY3LebYvKAG9kcQ,2333
|
|
62
|
+
archunitpython/metrics/fluentapi/metrics.py,sha256=EaH1seQKppXyx7DiPz1D-S1YYRt48OBWqdKn47BQhoA,19338
|
|
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
|
-
archunitpython/slices/uml/generate_rules.py,sha256=
|
|
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
|
-
archunitpython/testing/common/violation_factory.py,sha256=
|
|
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.3.0.dist-info/METADATA,sha256=79jbCuDyAMQr3aY3A_fesygIwS8hF8ceYaPRzJvUmrU,30275
|
|
81
|
+
archunitpython-1.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
82
|
+
archunitpython-1.3.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
|
|
83
|
+
archunitpython-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|