archunitpython 1.1.2__py3-none-any.whl → 1.2.1__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 +9 -1
- archunitpython/common/extraction/extract_graph.py +122 -29
- 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 +9 -27
- archunitpython/graph/__init__.py +35 -0
- archunitpython/graph/graph_reporter.py +795 -0
- archunitpython/layers/__init__.py +8 -0
- archunitpython/layers/assertion/__init__.py +9 -0
- archunitpython/layers/assertion/layer_dependencies.py +83 -0
- archunitpython/layers/fluentapi/__init__.py +6 -0
- archunitpython/layers/fluentapi/layers.py +116 -0
- 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 +12 -38
- archunitpython/slices/fluentapi/slices.py +4 -12
- archunitpython/slices/uml/generate_rules.py +2 -6
- archunitpython/testing/common/violation_factory.py +11 -3
- {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/METADATA +161 -6
- {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/RECORD +30 -23
- {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/WHEEL +0 -0
- {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/licenses/LICENSE +0 -0
archunitpython/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.1
|
|
3
|
+
__version__ = "1.2.1"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
@@ -13,6 +13,8 @@ from archunitpython.common import (
|
|
|
13
13
|
)
|
|
14
14
|
from archunitpython.common.extraction import clear_graph_cache, extract_graph
|
|
15
15
|
from archunitpython.files import files, project_files
|
|
16
|
+
from archunitpython.graph import dependency_graph, project_graph
|
|
17
|
+
from archunitpython.layers import layers, project_layers
|
|
16
18
|
|
|
17
19
|
# Metrics API
|
|
18
20
|
from archunitpython.metrics import metrics
|
|
@@ -27,6 +29,12 @@ __all__ = [
|
|
|
27
29
|
# Files
|
|
28
30
|
"project_files",
|
|
29
31
|
"files",
|
|
32
|
+
# Graph
|
|
33
|
+
"project_graph",
|
|
34
|
+
"dependency_graph",
|
|
35
|
+
# Layers
|
|
36
|
+
"project_layers",
|
|
37
|
+
"layers",
|
|
30
38
|
# Slices
|
|
31
39
|
"project_slices",
|
|
32
40
|
# Metrics
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
7
9
|
|
|
8
10
|
from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
|
|
9
11
|
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
@@ -27,6 +29,34 @@ _DEFAULT_EXCLUDE = [
|
|
|
27
29
|
"*.egg-info",
|
|
28
30
|
]
|
|
29
31
|
|
|
32
|
+
_IGNORE_DIRECTIVE_REGEX = re.compile(
|
|
33
|
+
r"#\s*archunit(?::|-)\s*ignore"
|
|
34
|
+
r"(?:\([^)]*\))?"
|
|
35
|
+
r"(?P<modules>(?:\s+[\w.]+)*)\s*$"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class _LocatedImport:
|
|
41
|
+
module_name: str
|
|
42
|
+
import_kind: ImportKind
|
|
43
|
+
line_number: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _IgnoreDirective:
|
|
48
|
+
line_number: int
|
|
49
|
+
modules: tuple[str, ...] = ()
|
|
50
|
+
|
|
51
|
+
def matches(self, import_: _LocatedImport) -> bool:
|
|
52
|
+
if not self.modules:
|
|
53
|
+
return True
|
|
54
|
+
return any(
|
|
55
|
+
import_.module_name == module
|
|
56
|
+
or import_.module_name.startswith(f"{module}.")
|
|
57
|
+
for module in self.modules
|
|
58
|
+
)
|
|
59
|
+
|
|
30
60
|
|
|
31
61
|
def clear_graph_cache(options: CheckOptions | None = None) -> None:
|
|
32
62
|
"""Clear the cached dependency graphs."""
|
|
@@ -61,12 +91,8 @@ def extract_graph(
|
|
|
61
91
|
excludes = (
|
|
62
92
|
list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
63
93
|
)
|
|
64
|
-
ignore_type_checking_imports = bool(
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
cache_key = _build_cache_key(
|
|
68
|
-
project_path, excludes, ignore_type_checking_imports
|
|
69
|
-
)
|
|
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)
|
|
70
96
|
|
|
71
97
|
if options and options.clear_cache:
|
|
72
98
|
_graph_cache.pop(cache_key, None)
|
|
@@ -107,6 +133,7 @@ def _extract_graph_uncached(
|
|
|
107
133
|
|
|
108
134
|
edges: list[Edge] = []
|
|
109
135
|
py_files_set = set(py_files)
|
|
136
|
+
normalized_py_file_set = {_normalize(f) for f in py_files_set}
|
|
110
137
|
|
|
111
138
|
for file_path in py_files:
|
|
112
139
|
# Add self-referencing edge (ensures the file appears as a node)
|
|
@@ -118,9 +145,10 @@ def _extract_graph_uncached(
|
|
|
118
145
|
)
|
|
119
146
|
)
|
|
120
147
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
imports = _extract_located_imports(file_path)
|
|
149
|
+
for located_import in imports:
|
|
150
|
+
module_name = located_import.module_name
|
|
151
|
+
import_kind = located_import.import_kind
|
|
124
152
|
if (
|
|
125
153
|
ignore_type_checking_imports
|
|
126
154
|
and import_kind == ImportKind.TYPE_IMPORT
|
|
@@ -131,9 +159,7 @@ def _extract_graph_uncached(
|
|
|
131
159
|
)
|
|
132
160
|
if resolved and resolved != _normalize(file_path):
|
|
133
161
|
# Check if the resolved path is in our project
|
|
134
|
-
if not is_external and resolved not in
|
|
135
|
-
_normalize(f) for f in py_files_set
|
|
136
|
-
}:
|
|
162
|
+
if not is_external and resolved not in normalized_py_file_set:
|
|
137
163
|
is_external = True
|
|
138
164
|
|
|
139
165
|
edges.append(
|
|
@@ -158,11 +184,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
|
158
184
|
py_files: list[str] = []
|
|
159
185
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
160
186
|
# Filter out excluded directories in-place
|
|
161
|
-
dirnames[:] = [
|
|
162
|
-
d
|
|
163
|
-
for d in dirnames
|
|
164
|
-
if not _should_exclude(d, exclude)
|
|
165
|
-
]
|
|
187
|
+
dirnames[:] = [d for d in dirnames if not _should_exclude(d, exclude)]
|
|
166
188
|
|
|
167
189
|
for filename in filenames:
|
|
168
190
|
if filename.endswith(".py") and not _should_exclude(filename, exclude):
|
|
@@ -187,6 +209,14 @@ def _extract_imports(file_path: str) -> list[tuple[str, ImportKind]]:
|
|
|
187
209
|
|
|
188
210
|
Returns list of (module_name, import_kind) tuples.
|
|
189
211
|
"""
|
|
212
|
+
return [
|
|
213
|
+
(import_.module_name, import_.import_kind)
|
|
214
|
+
for import_ in _extract_located_imports(file_path)
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
219
|
+
"""Parse a Python file and extract imports with line numbers."""
|
|
190
220
|
try:
|
|
191
221
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
192
222
|
source = f.read()
|
|
@@ -198,7 +228,8 @@ def _extract_imports(file_path: str) -> list[tuple[str, ImportKind]]:
|
|
|
198
228
|
except SyntaxError:
|
|
199
229
|
return []
|
|
200
230
|
|
|
201
|
-
imports: list[
|
|
231
|
+
imports: list[_LocatedImport] = []
|
|
232
|
+
ignore_directives = _find_ignore_directives(source)
|
|
202
233
|
type_checking_ranges = _find_type_checking_ranges(tree)
|
|
203
234
|
|
|
204
235
|
for node in ast.walk(tree):
|
|
@@ -206,7 +237,7 @@ def _extract_imports(file_path: str) -> list[tuple[str, ImportKind]]:
|
|
|
206
237
|
is_type = _in_type_checking(node, type_checking_ranges)
|
|
207
238
|
kind = ImportKind.TYPE_IMPORT if is_type else ImportKind.IMPORT
|
|
208
239
|
for alias in node.names:
|
|
209
|
-
imports.append((alias.name, kind))
|
|
240
|
+
imports.append(_LocatedImport(alias.name, kind, node.lineno))
|
|
210
241
|
|
|
211
242
|
elif isinstance(node, ast.ImportFrom):
|
|
212
243
|
is_type = _in_type_checking(node, type_checking_ranges)
|
|
@@ -215,13 +246,79 @@ def _extract_imports(file_path: str) -> list[tuple[str, ImportKind]]:
|
|
|
215
246
|
kind = ImportKind.TYPE_IMPORT if is_type else ImportKind.RELATIVE_IMPORT
|
|
216
247
|
module = node.module or ""
|
|
217
248
|
dots = "." * node.level
|
|
218
|
-
imports.append((f"{dots}{module}", kind))
|
|
249
|
+
imports.append(_LocatedImport(f"{dots}{module}", kind, node.lineno))
|
|
219
250
|
else:
|
|
220
251
|
kind = ImportKind.TYPE_IMPORT if is_type else ImportKind.FROM_IMPORT
|
|
221
252
|
if node.module:
|
|
222
|
-
imports.append((node.module, kind))
|
|
253
|
+
imports.append(_LocatedImport(node.module, kind, node.lineno))
|
|
254
|
+
|
|
255
|
+
elif isinstance(node, ast.Call):
|
|
256
|
+
is_type = _in_type_checking(node, type_checking_ranges)
|
|
257
|
+
kind = ImportKind.TYPE_IMPORT if is_type else ImportKind.DYNAMIC_IMPORT
|
|
258
|
+
for module_name in _extract_dynamic_import_names(node):
|
|
259
|
+
imports.append(_LocatedImport(module_name, kind, node.lineno))
|
|
260
|
+
|
|
261
|
+
return [
|
|
262
|
+
import_
|
|
263
|
+
for import_ in imports
|
|
264
|
+
if not _is_ignored_import(import_, ignore_directives)
|
|
265
|
+
]
|
|
266
|
+
|
|
223
267
|
|
|
224
|
-
|
|
268
|
+
def _find_ignore_directives(source: str) -> dict[int, _IgnoreDirective]:
|
|
269
|
+
"""Find architecture-ignore directives.
|
|
270
|
+
|
|
271
|
+
Supports inline directives on an import line and standalone directives that
|
|
272
|
+
apply to the following line, for example:
|
|
273
|
+
|
|
274
|
+
- from x import y # archunit: ignore
|
|
275
|
+
- # archunit: ignore
|
|
276
|
+
from x import y
|
|
277
|
+
"""
|
|
278
|
+
directives: dict[int, _IgnoreDirective] = {}
|
|
279
|
+
for index, line in enumerate(source.splitlines(), start=1):
|
|
280
|
+
match = _IGNORE_DIRECTIVE_REGEX.search(line)
|
|
281
|
+
if match is None:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
modules = tuple(match.group("modules").split())
|
|
285
|
+
target_line = index + 1 if line.strip().startswith("#") else index
|
|
286
|
+
directives[target_line] = _IgnoreDirective(target_line, modules)
|
|
287
|
+
return directives
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _is_ignored_import(
|
|
291
|
+
import_: _LocatedImport,
|
|
292
|
+
directives: dict[int, _IgnoreDirective],
|
|
293
|
+
) -> bool:
|
|
294
|
+
directive = directives.get(import_.line_number)
|
|
295
|
+
return directive is not None and directive.matches(import_)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
|
|
299
|
+
"""Extract literal module names from common dynamic import calls."""
|
|
300
|
+
if not node.args:
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
first_arg = node.args[0]
|
|
304
|
+
if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
if isinstance(node.func, ast.Name) and node.func.id in {
|
|
308
|
+
"__import__",
|
|
309
|
+
"import_module",
|
|
310
|
+
}:
|
|
311
|
+
return [first_arg.value]
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
isinstance(node.func, ast.Attribute)
|
|
315
|
+
and node.func.attr == "import_module"
|
|
316
|
+
and isinstance(node.func.value, ast.Name)
|
|
317
|
+
and node.func.value.id == "importlib"
|
|
318
|
+
):
|
|
319
|
+
return [first_arg.value]
|
|
320
|
+
|
|
321
|
+
return []
|
|
225
322
|
|
|
226
323
|
|
|
227
324
|
def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
|
|
@@ -242,18 +339,14 @@ def _find_type_checking_ranges(tree: ast.Module) -> list[tuple[int, int]]:
|
|
|
242
339
|
if is_type_checking and node.body:
|
|
243
340
|
start = node.body[0].lineno
|
|
244
341
|
end = max(
|
|
245
|
-
getattr(n, "end_lineno", n.lineno)
|
|
246
|
-
for n in node.body
|
|
247
|
-
if hasattr(n, "lineno")
|
|
342
|
+
getattr(n, "end_lineno", n.lineno) for n in node.body if hasattr(n, "lineno")
|
|
248
343
|
)
|
|
249
344
|
ranges.append((start, end))
|
|
250
345
|
|
|
251
|
-
return ranges
|
|
346
|
+
return sorted(ranges, key=lambda ele: ele[0])
|
|
252
347
|
|
|
253
348
|
|
|
254
|
-
def _in_type_checking(
|
|
255
|
-
node: ast.AST, ranges: list[tuple[int, int]]
|
|
256
|
-
) -> bool:
|
|
349
|
+
def _in_type_checking(node: ast.AST, ranges: list[tuple[int, int]]) -> bool:
|
|
257
350
|
"""Check if a node is inside a TYPE_CHECKING block."""
|
|
258
351
|
if not hasattr(node, "lineno"):
|
|
259
352
|
return False
|
|
@@ -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
|
|
@@ -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
|
|
@@ -447,9 +431,7 @@ 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
437
|
class CustomFileCheckableCondition:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Dependency graph reports."""
|
|
2
|
+
|
|
3
|
+
from archunitpython.graph.graph_reporter import (
|
|
4
|
+
DEFAULT_TITLE,
|
|
5
|
+
FolderDepthCollapse,
|
|
6
|
+
GraphCollapseStrategy,
|
|
7
|
+
GraphQueryOptions,
|
|
8
|
+
GraphReportEdge,
|
|
9
|
+
GraphReporter,
|
|
10
|
+
GraphReportFormat,
|
|
11
|
+
GraphReportNode,
|
|
12
|
+
GraphReportSnapshot,
|
|
13
|
+
GraphReportSummary,
|
|
14
|
+
PatternCollapse,
|
|
15
|
+
ProjectGraphBuilder,
|
|
16
|
+
dependency_graph,
|
|
17
|
+
project_graph,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DEFAULT_TITLE",
|
|
22
|
+
"FolderDepthCollapse",
|
|
23
|
+
"GraphCollapseStrategy",
|
|
24
|
+
"GraphQueryOptions",
|
|
25
|
+
"GraphReportEdge",
|
|
26
|
+
"GraphReportFormat",
|
|
27
|
+
"GraphReportNode",
|
|
28
|
+
"GraphReportSnapshot",
|
|
29
|
+
"GraphReportSummary",
|
|
30
|
+
"GraphReporter",
|
|
31
|
+
"PatternCollapse",
|
|
32
|
+
"ProjectGraphBuilder",
|
|
33
|
+
"dependency_graph",
|
|
34
|
+
"project_graph",
|
|
35
|
+
]
|