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.
Files changed (30) hide show
  1. archunitpython/__init__.py +9 -1
  2. archunitpython/common/extraction/extract_graph.py +122 -29
  3. archunitpython/common/pattern_matching.py +1 -3
  4. archunitpython/common/projection/cycles/johnsons_apsp.py +4 -9
  5. archunitpython/common/projection/cycles/tarjan_scc.py +2 -6
  6. archunitpython/common/projection/project_cycles.py +1 -2
  7. archunitpython/common/util/logger.py +1 -3
  8. archunitpython/files/assertion/custom_file_logic.py +3 -9
  9. archunitpython/files/assertion/depend_on_external_modules.py +3 -8
  10. archunitpython/files/assertion/depend_on_files.py +4 -12
  11. archunitpython/files/fluentapi/files.py +9 -27
  12. archunitpython/graph/__init__.py +35 -0
  13. archunitpython/graph/graph_reporter.py +795 -0
  14. archunitpython/layers/__init__.py +8 -0
  15. archunitpython/layers/assertion/__init__.py +9 -0
  16. archunitpython/layers/assertion/layer_dependencies.py +83 -0
  17. archunitpython/layers/fluentapi/__init__.py +6 -0
  18. archunitpython/layers/fluentapi/layers.py +116 -0
  19. archunitpython/metrics/assertion/metric_thresholds.py +1 -3
  20. archunitpython/metrics/calculation/distance.py +1 -3
  21. archunitpython/metrics/extraction/extract_class_info.py +3 -9
  22. archunitpython/metrics/fluentapi/export_utils.py +2 -6
  23. archunitpython/metrics/fluentapi/metrics.py +12 -38
  24. archunitpython/slices/fluentapi/slices.py +4 -12
  25. archunitpython/slices/uml/generate_rules.py +2 -6
  26. archunitpython/testing/common/violation_factory.py +11 -3
  27. {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/METADATA +161 -6
  28. {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/RECORD +30 -23
  29. {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/WHEEL +0 -0
  30. {archunitpython-1.1.2.dist-info → archunitpython-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.1.2"
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
- options and options.ignore_type_checking_imports
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
- # Extract and resolve imports
122
- imports = _extract_imports(file_path)
123
- for module_name, import_kind in imports:
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[tuple[str, ImportKind]] = []
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
- return imports
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
- cycle[0].from_node == self._start.node
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)
@@ -72,8 +72,7 @@ class _CycleProcessor:
72
72
  (
73
73
  se
74
74
  for se in self._source_edges
75
- if se.source_label == source_label
76
- and se.target_label == target_label
75
+ if se.source_label == source_label and se.target_label == target_label
77
76
  ),
78
77
  None,
79
78
  )
@@ -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
+ ]