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.
Files changed (27) hide show
  1. archunitpython/__init__.py +1 -1
  2. archunitpython/common/__init__.py +6 -1
  3. archunitpython/common/extraction/extract_graph.py +8 -22
  4. archunitpython/common/fluentapi/__init__.py +6 -2
  5. archunitpython/common/fluentapi/checkable.py +23 -1
  6. archunitpython/common/pattern_matching.py +1 -3
  7. archunitpython/common/projection/cycles/johnsons_apsp.py +4 -9
  8. archunitpython/common/projection/cycles/tarjan_scc.py +2 -6
  9. archunitpython/common/projection/project_cycles.py +1 -2
  10. archunitpython/common/util/logger.py +1 -3
  11. archunitpython/files/assertion/custom_file_logic.py +3 -9
  12. archunitpython/files/assertion/depend_on_external_modules.py +3 -8
  13. archunitpython/files/assertion/depend_on_files.py +4 -12
  14. archunitpython/files/fluentapi/files.py +15 -33
  15. archunitpython/metrics/assertion/metric_thresholds.py +1 -3
  16. archunitpython/metrics/calculation/distance.py +1 -3
  17. archunitpython/metrics/extraction/extract_class_info.py +3 -9
  18. archunitpython/metrics/fluentapi/export_utils.py +2 -6
  19. archunitpython/metrics/fluentapi/metrics.py +19 -45
  20. archunitpython/slices/fluentapi/slices.py +7 -15
  21. archunitpython/slices/uml/generate_rules.py +2 -6
  22. archunitpython/testing/assertion.py +11 -3
  23. archunitpython/testing/common/violation_factory.py +1 -3
  24. {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/METADATA +127 -18
  25. {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/RECORD +27 -27
  26. {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/WHEEL +0 -0
  27. {archunitpython-1.2.0.dist-info → archunitpython-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.0"
4
4
 
5
5
  # Files API
6
6
  # Common
@@ -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 Checkable, CheckOptions
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
- options and options.ignore_type_checking_imports
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 Checkable, CheckOptions
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
- 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
@@ -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(violations: list[Violation]) -> str:
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
- raise AssertionError(format_violations(violations))
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.2.0
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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
- [![PyPI version](https://img.shields.io/pypi/v/archunitpython.svg)](https://pypi.org/project/archunitpython/)
44
- [![Downloads](https://static.pepy.tech/badge/archunitpython)](https://pepy.tech/project/archunitpython)
45
- [![Monthly downloads](https://static.pepy.tech/badge/archunitpython/month)](https://pepy.tech/project/archunitpython)
46
- [![GitHub stars](https://img.shields.io/github/stars/LukasNiessen/ArchUnitPython.svg)](https://github.com/LukasNiessen/ArchUnitPython)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build & tests](https://img.shields.io/github/actions/workflow/status/LukasNiessen/ArchUnitPython/integrate.yaml?branch=main&label=build%20%26%20tests)](https://github.com/LukasNiessen/ArchUnitPython/actions/workflows/integrate.yaml) [![GitHub stars](https://img.shields.io/github/stars/LukasNiessen/ArchUnitPython.svg)](https://github.com/LukasNiessen/ArchUnitPython)<br>
43
+ [![PyPI downloads](https://static.pepy.tech/badge/archunitpython/month)](https://pepy.tech/project/archunitpython) [![PyPI total downloads](https://img.shields.io/pepy/dt/archunitpython?label=total%20downloads&color=007ec6)](https://pepy.tech/project/archunitpython)
44
+ <!-- [![PyPI version](https://img.shields.io/pypi/v/archunitpython.svg)](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
- "reports/dependencies.mmd"
444
- )
445
-
446
- graph.focus_on("**/domain/**", 1).export_as_html(
447
- "reports/domain-dependencies.html"
448
- )
449
-
450
- assert graph.snapshot().summary.node_count >= 0
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=_YNTDiIuDgxiMhgS94vtZaqAVkum5-S0FViXN9p_Aqg,1154
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=u8jwbfALwIT_5ZqTrRchpZqkFHG5tTwM21LX5hR9ZSc,593
4
- archunitpython/common/pattern_matching.py,sha256=zhfglETSJfe5XPzueVflM14EKfllXuazqzd2XH5fuxw,2675
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=cXKPDQ2GEAA3pJ_2LzoeMzizWZjDdokc7QYm6k9EsCU,15366
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=J7PDgqBXQOpXqCMCGRyFNil0dCulufmhfirzvYvh-gk,119
15
- archunitpython/common/fluentapi/checkable.py,sha256=6Tsxtycc27-LaeE6cTtQF6NfepFzhgmCrFJAosN_chs,892
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=SA6eXzy599dJMvAOA6K1ONkOaFLiEXw8qixpgybtW-I,3105
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=vbAmR8QqGmM7hRgphngJvQ5yU8Og4RxYkGUQmKOWT2U,4087
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=49LnVrcFIRQr2yPi034YIdIBjmrFotrXsoS000Qz9Mc,2843
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=QT6ir8caMXu4VQombe0NjaT2NUm58kFdlnps4T5ECCE,3348
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=ygpc2NZghHBD-GTGrQW_dWzGZr_9DSvi0denC-cdwuo,3054
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=d16mK6lpr5MhxndigSZsGEOPF51UY0Gl5HddqP_MdXs,1949
38
- archunitpython/files/assertion/depend_on_files.py,sha256=B-zyFyXCt2ywCzO0BIH1lRGsre6PkAItv2ZfpM_u2Js,2132
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=uX__VDed_TLNiibK2pTMaPQ8RaCyyFf2ZClqdymvs98,17214
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=dijCzOr5-dhkdK-QAE3DAkfiSBgknZlxtp-ZrBj_Dmk,1268
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=zXwNrXxmgwBaexvjbODZ1VOSxJx_fZjy5jU0FbNjQcM,3611
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=eCNoHvOdQGenGDQcd9iucO9PqNv1hYhW9HTOUaZMFio,7825
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=qpv01BeoARhciyN7J05JIn-xMxbmxnoXGKBLwMYtlhM,2381
62
- archunitpython/metrics/fluentapi/metrics.py,sha256=0oo2EUEV8YWBiV1-v84LzPXX9sBBPOmmzgA34ZP0uQQ,19488
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=KpkIKV9RDLwTFe05cmksIVEAgoYGoJwlHd35KmN2EOc,7183
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=zxiRW4PjVZHXdkQcXAycosbZ1do96SwpuqUoB2h0vDc,2180
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=YTiNWHIQu4xlSqmlvqj1L-Z2Oo1Z-wOvN0VyZQB-CZE,1430
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=mdtUGGAv_U6hrmucA32Y9aVQ_GXkirGJ_6W_YcnLfmY,4707
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.2.0.dist-info/METADATA,sha256=GMRyeAEmJa3HMZRgDNPywINiQq8GTKdSMJGpK6MrVmA,28148
81
- archunitpython-1.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
82
- archunitpython-1.2.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
83
- archunitpython-1.2.0.dist-info/RECORD,,
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,,