archunitpython 1.7.0__py3-none-any.whl → 1.8.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.
@@ -1,12 +1,13 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.7.0"
3
+ __version__ = "1.8.0"
4
4
 
5
5
  # Files API
6
6
  # Common
7
7
  from archunitpython.common import (
8
8
  CheckOptions,
9
9
  EmptyTestViolation,
10
+ LoggingOptions,
10
11
  TechnicalError,
11
12
  UserError,
12
13
  Violation,
@@ -50,6 +51,7 @@ __all__ = [
50
51
  "Violation",
51
52
  "EmptyTestViolation",
52
53
  "CheckOptions",
54
+ "LoggingOptions",
53
55
  "TechnicalError",
54
56
  "UserError",
55
57
  "extract_graph",
@@ -9,6 +9,7 @@ from dataclasses import dataclass
9
9
 
10
10
  from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
11
11
  from archunitpython.common.fluentapi.checkable import CheckOptions
12
+ from archunitpython.common.logging.inspection import debug, debug_enabled
12
13
 
13
14
  GraphCacheKey = tuple[str, tuple[str, ...], bool]
14
15
 
@@ -57,8 +58,7 @@ class _IgnoreDirective:
57
58
  if not self.modules:
58
59
  return True
59
60
  return any(
60
- import_.module_name == module
61
- or import_.module_name.startswith(f"{module}.")
61
+ import_.module_name == module or import_.module_name.startswith(f"{module}.")
62
62
  for module in self.modules
63
63
  )
64
64
 
@@ -97,21 +97,46 @@ def extract_graph(
97
97
  ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
98
98
  cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
99
99
 
100
+ debug(
101
+ "Extraction root: %s; excludes: %s; ignore type-only imports: %s",
102
+ project_path,
103
+ excludes,
104
+ ignore_type_checking_imports,
105
+ )
100
106
  if options and options.clear_cache:
107
+ debug("Clearing graph cache for %s", project_path)
101
108
  _graph_cache.pop(cache_key, None)
102
109
 
103
110
  if cache_key in _graph_cache:
111
+ debug("Graph cache hit: %d edges", len(_graph_cache[cache_key]))
112
+ _inspect_graph(_graph_cache[cache_key])
104
113
  return _graph_cache[cache_key]
114
+ debug("Graph cache miss")
105
115
 
106
116
  result = _extract_graph_uncached(
107
117
  project_path,
108
118
  excludes,
109
119
  ignore_type_checking_imports=ignore_type_checking_imports,
110
120
  )
121
+ _inspect_graph(result)
111
122
  _graph_cache[cache_key] = result
112
123
  return result
113
124
 
114
125
 
126
+ def _inspect_graph(graph: Graph) -> None:
127
+ if not debug_enabled():
128
+ return
129
+ debug("Extracted graph: %d edges (including self-edges)", len(graph))
130
+ for edge in graph:
131
+ debug(
132
+ "Dependency: %s -> %s; external=%s; kinds=%s",
133
+ edge.source,
134
+ edge.target,
135
+ edge.external,
136
+ edge.import_kinds,
137
+ )
138
+
139
+
115
140
  def _build_cache_key(
116
141
  project_path: str,
117
142
  exclude_patterns: list[str],
@@ -167,6 +192,7 @@ def _extract_graph_uncached(
167
192
  normalized_py_file_set = {_normalize(f) for f in py_files_set}
168
193
 
169
194
  for file_path in py_files:
195
+ debug("Processing file: %s", file_path)
170
196
  # Add self-referencing edge (ensures the file appears as a node)
171
197
  edges.append(
172
198
  Edge(
@@ -179,10 +205,13 @@ def _extract_graph_uncached(
179
205
  imports = _extract_located_imports(file_path)
180
206
  for located_import in imports:
181
207
  import_kind = located_import.import_kind
182
- if (
183
- ignore_type_checking_imports
184
- and import_kind == ImportKind.TYPE_IMPORT
185
- ):
208
+ if ignore_type_checking_imports and import_kind == ImportKind.TYPE_IMPORT:
209
+ debug(
210
+ "Skipped type-only import: %s:%s -> %s",
211
+ file_path,
212
+ located_import.line_number,
213
+ located_import.module_name,
214
+ )
186
215
  continue
187
216
  for resolved, is_external in _resolve_import_targets(
188
217
  located_import, file_path, project_path
@@ -190,6 +219,7 @@ def _extract_graph_uncached(
190
219
  if resolved and resolved != _normalize(file_path):
191
220
  # Check if the resolved path is in our project
192
221
  if not is_external and resolved not in normalized_py_file_set:
222
+ debug("Skipped excluded dependency: %s -> %s", file_path, resolved)
193
223
  continue
194
224
 
195
225
  edges.append(
@@ -227,6 +257,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
227
257
  full_path, root, exclude, is_dir=False
228
258
  ):
229
259
  py_files.append(os.path.abspath(full_path))
260
+ debug("Discovered file: %s", full_path)
230
261
 
231
262
  return py_files
232
263
 
@@ -313,9 +344,7 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
313
344
  conditional_import_ranges,
314
345
  )
315
346
  for alias in node.names:
316
- imports.append(
317
- _LocatedImport(alias.name, kind, node.lineno, syntax_kind)
318
- )
347
+ imports.append(_LocatedImport(alias.name, kind, node.lineno, syntax_kind))
319
348
 
320
349
  elif isinstance(node, ast.ImportFrom):
321
350
  syntax_kind = (
@@ -329,9 +358,7 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
329
358
  type_checking_ranges,
330
359
  conditional_import_ranges,
331
360
  )
332
- fallback_module_name = (
333
- "." * node.level if node.level and node.module is None else None
334
- )
361
+ fallback_module_name = "." * node.level if node.level and node.module is None else None
335
362
  aliases = _module_aliases(node) if node.module else ()
336
363
  for module_name in _import_from_module_names(node):
337
364
  imports.append(
@@ -354,15 +381,9 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
354
381
  conditional_import_ranges,
355
382
  )
356
383
  for module_name in _extract_dynamic_import_names(node):
357
- imports.append(
358
- _LocatedImport(module_name, kind, node.lineno, syntax_kind)
359
- )
384
+ imports.append(_LocatedImport(module_name, kind, node.lineno, syntax_kind))
360
385
 
361
- return [
362
- import_
363
- for import_ in imports
364
- if not _is_ignored_import(import_, ignore_directives)
365
- ]
386
+ return [import_ for import_ in imports if not _is_ignored_import(import_, ignore_directives)]
366
387
 
367
388
 
368
389
  def _find_ignore_directives(source: str) -> dict[int, _IgnoreDirective]:
@@ -555,14 +576,10 @@ def _resolve_import(
555
576
  Returns (resolved_path, is_external).
556
577
  The path is normalized with forward slashes.
557
578
  """
558
- if (
559
- kind
560
- in (
561
- ImportKind.RELATIVE_IMPORT,
562
- ImportKind.TYPE_IMPORT,
563
- )
564
- and import_name.startswith(".")
565
- ):
579
+ if kind in (
580
+ ImportKind.RELATIVE_IMPORT,
581
+ ImportKind.TYPE_IMPORT,
582
+ ) and import_name.startswith("."):
566
583
  # Relative import
567
584
  return _resolve_relative_import(import_name, source_file, project_root)
568
585
 
@@ -0,0 +1,79 @@
1
+ """Observational instrumentation for the existing check contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import wraps
6
+ from time import perf_counter
7
+ from typing import Callable, TypeVar, cast
8
+
9
+ from archunitpython.common.assertion.violation import Violation
10
+ from archunitpython.common.fluentapi.checkable import CheckOptions
11
+ from archunitpython.common.logging.inspection import InspectionSession, current_session
12
+
13
+ F = TypeVar("F", bound=Callable[..., list[Violation]])
14
+
15
+
16
+ def inspect_check(check: F) -> F:
17
+ """Trace a check while preserving its options, results, and exceptions."""
18
+
19
+ @wraps(check)
20
+ def inspected(self: object, options: CheckOptions | None = None) -> list[Violation]:
21
+ logging_options = options.logging if options else None
22
+ session = (
23
+ InspectionSession(logging_options)
24
+ if logging_options and logging_options.enabled
25
+ else None
26
+ )
27
+ token = current_session.set(session)
28
+ started = perf_counter() if session else 0.0
29
+ rule_name = type(self).__name__
30
+ try:
31
+ if session:
32
+ session.emit("info", "Starting check: %s", rule_name)
33
+ # Describe only known rule configuration, never user callbacks.
34
+ for name in (
35
+ "_project_path",
36
+ "_filters",
37
+ "_subject_filters",
38
+ "_object_filters",
39
+ "_module_filters",
40
+ "_pre_filters",
41
+ "_check_filters",
42
+ "_is_negated",
43
+ "_pattern",
44
+ "_regex",
45
+ "_source",
46
+ "_target",
47
+ "_layers",
48
+ "_allowed_dependencies",
49
+ "_forbidden_dependencies",
50
+ "_threshold",
51
+ "_comparison",
52
+ "_metric_attr",
53
+ "_name",
54
+ "_zone_type",
55
+ ):
56
+ if session.options.level == "debug" and name in vars(self):
57
+ session.emit("debug", "%s: %s", name.removeprefix("_"), vars(self)[name])
58
+ violations = check(self, options)
59
+ if session:
60
+ session.emit(
61
+ "warn" if violations else "info",
62
+ "Finished check: %s - %d violation(s) (%.3fs)",
63
+ rule_name,
64
+ len(violations),
65
+ perf_counter() - started,
66
+ )
67
+ for violation in violations:
68
+ session.emit("warn", "Violation: %s", violation)
69
+ return violations
70
+ except Exception as error:
71
+ if session:
72
+ session.emit("error", "Check failed: %s - %s", rule_name, error)
73
+ raise
74
+ finally:
75
+ current_session.reset(token)
76
+ if session:
77
+ session.close()
78
+
79
+ return cast(F, inspected)
@@ -0,0 +1,99 @@
1
+ """Per-check diagnostic sessions, isolated from architecture evaluation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from contextvars import ContextVar
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import TextIO
11
+ from uuid import uuid4
12
+
13
+ from archunitpython.common.logging.types import LoggingOptions
14
+
15
+ _LEVELS = {
16
+ "debug": logging.DEBUG,
17
+ "info": logging.INFO,
18
+ "warn": logging.WARNING,
19
+ "error": logging.ERROR,
20
+ }
21
+
22
+
23
+ class InspectionSession:
24
+ """Own a check's output without attaching process-wide logging handlers.
25
+
26
+ Diagnostics are best effort: formatting and output errors must never replace
27
+ a rule result or an exception raised by the analysis itself.
28
+ """
29
+
30
+ def __init__(self, options: LoggingOptions) -> None:
31
+ self.options = options
32
+ self._file: TextIO | None = None
33
+ self._file_attempted = False
34
+
35
+ def emit(self, level: str, message: str, *args: object) -> None:
36
+ if _LEVELS[level] < _LEVELS.get(self.options.level, logging.INFO):
37
+ return
38
+ try:
39
+ text = message % args if args else message
40
+ except Exception:
41
+ return
42
+ if self.options.console:
43
+ try:
44
+ logger = logging.getLogger("archunitpython")
45
+ if logger.hasHandlers():
46
+ logger.log(_LEVELS[level], text)
47
+ else:
48
+ sys.stderr.write(f"[{level.upper()}] {text}\n")
49
+ except Exception:
50
+ pass
51
+ if self.options.log_file:
52
+ try:
53
+ if not self._file_attempted:
54
+ self._file_attempted = True
55
+ path = (
56
+ Path(self.options.log_path)
57
+ if self.options.log_path
58
+ else Path("logs")
59
+ / (
60
+ datetime.now().strftime("archunit-%Y-%m-%d_%H-%M-%S-")
61
+ + uuid4().hex
62
+ + ".log"
63
+ )
64
+ )
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ self._file = path.open(
67
+ "a" if self.options.append_to_log_file else "w", encoding="utf-8"
68
+ )
69
+ if self._file is not None:
70
+ self._file.write(f"[{datetime.now().isoformat()}] [{level.upper()}] {text}\n")
71
+ self._file.flush()
72
+ except Exception:
73
+ self.close()
74
+
75
+ def close(self) -> None:
76
+ if self._file is not None:
77
+ try:
78
+ self._file.close()
79
+ except Exception:
80
+ pass
81
+ self._file = None
82
+
83
+
84
+ current_session: ContextVar[InspectionSession | None] = ContextVar(
85
+ "archunitpython_inspection", default=None
86
+ )
87
+
88
+
89
+ def debug(message: str, *args: object) -> None:
90
+ """Lazily emit details only inside an enabled debug check."""
91
+ session = current_session.get()
92
+ if session is not None:
93
+ session.emit("debug", message, *args)
94
+
95
+
96
+ def debug_enabled() -> bool:
97
+ """Guard expensive diagnostic traversal before constructing its details."""
98
+ session = current_session.get()
99
+ return session is not None and session.options.level == "debug"
@@ -16,3 +16,5 @@ class LoggingOptions:
16
16
  level: LogLevel = "info"
17
17
  log_file: bool = False
18
18
  append_to_log_file: bool = False
19
+ console: bool = True
20
+ log_path: str | None = None
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from archunitpython.common.logging.inspection import debug
5
6
  from archunitpython.common.types import Filter
6
7
 
7
8
 
@@ -47,7 +48,9 @@ def matches_pattern(file_path: str, filter_: Filter) -> bool:
47
48
  else:
48
49
  target_string = normalize_path(file_path)
49
50
 
50
- return bool(filter_.regexp.search(target_string))
51
+ matched = bool(filter_.regexp.search(target_string))
52
+ debug("Selector %s against %s (%s): %s", filter_.regexp.pattern, target_string, target, matched)
53
+ return matched
51
54
 
52
55
 
53
56
  def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter) -> bool:
@@ -65,7 +68,9 @@ def matches_pattern_classname(class_name: str, file_path: str, filter_: Filter)
65
68
  else:
66
69
  target_string = normalize_path(file_path)
67
70
 
68
- return bool(filter_.regexp.search(target_string))
71
+ matched = bool(filter_.regexp.search(target_string))
72
+ debug("Selector %s against %s (%s): %s", filter_.regexp.pattern, target_string, target, matched)
73
+ return matched
69
74
 
70
75
 
71
76
  def matches_all_patterns(file_path: str, filters: list[Filter]) -> bool:
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from collections import defaultdict
6
6
 
7
7
  from archunitpython.common.extraction.graph import Edge
8
+ from archunitpython.common.logging.inspection import debug
8
9
  from archunitpython.common.projection.types import MapFunction, ProjectedEdge
9
10
 
10
11
 
@@ -29,11 +30,19 @@ def project_edges(
29
30
  for edge in graph:
30
31
  mapped = mapper(edge)
31
32
  if mapped is None:
33
+ debug("Projection omitted: %s -> %s", edge.source, edge.target)
32
34
  continue
35
+ debug(
36
+ "Projection: %s -> %s becomes %s -> %s",
37
+ edge.source,
38
+ edge.target,
39
+ mapped.source_label,
40
+ mapped.target_label,
41
+ )
33
42
  key = (mapped.source_label, mapped.target_label)
34
43
  groups[key].append(edge)
35
44
 
36
- return [
45
+ projected = [
37
46
  ProjectedEdge(
38
47
  source_label=source_label,
39
48
  target_label=target_label,
@@ -41,3 +50,5 @@ def project_edges(
41
50
  )
42
51
  for (source_label, target_label), edges in groups.items()
43
52
  ]
53
+ debug("Projection complete: %d raw edges -> %d projected edges", len(graph), len(projected))
54
+ return projected
@@ -15,6 +15,7 @@ from collections.abc import Sequence
15
15
  from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
16
16
  from archunitpython.common.extraction.extract_graph import extract_graph
17
17
  from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
18
+ from archunitpython.common.fluentapi.inspection import inspect_check
18
19
  from archunitpython.common.pattern_matching import matches_all_patterns
19
20
  from archunitpython.common.projection.edge_projections import (
20
21
  per_external_edge,
@@ -329,6 +330,7 @@ class CycleFreeFileCondition(RuleRationaleMixin):
329
330
  self._project_path = project_path
330
331
  self._filters = filters
331
332
 
333
+ @inspect_check
332
334
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
333
335
  graph = extract_graph(self._project_path, options=options)
334
336
  edges = project_edges(graph, per_internal_edge())
@@ -365,6 +367,7 @@ class DependOnFileCondition(RuleRationaleMixin):
365
367
  self._object_filters = object_filters
366
368
  self._is_negated = is_negated
367
369
 
370
+ @inspect_check
368
371
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
369
372
  graph = extract_graph(self._project_path, options=options)
370
373
  edges = project_edges(graph, per_internal_edge())
@@ -397,6 +400,7 @@ class DependOnExternalModuleCondition(RuleRationaleMixin):
397
400
  self._module_filters.append(RegexFactory.path_matcher(module_name))
398
401
  return self
399
402
 
403
+ @inspect_check
400
404
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
401
405
  graph = extract_graph(self._project_path, options=options)
402
406
  edges = project_edges(graph, per_external_edge())
@@ -424,6 +428,7 @@ class MatchPatternFileCondition(RuleRationaleMixin):
424
428
  self._check_filters = check_filters
425
429
  self._is_negated = is_negated
426
430
 
431
+ @inspect_check
427
432
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
428
433
  nodes = _get_filtered_nodes(self._project_path, self._pre_filters, options)
429
434
 
@@ -451,6 +456,7 @@ class CustomFileCheckableCondition(RuleRationaleMixin):
451
456
  self._message = message
452
457
  self._is_negated = is_negated
453
458
 
459
+ @inspect_check
454
460
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
455
461
  graph = extract_graph(self._project_path, options=options)
456
462
  nodes = project_to_nodes(graph)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from archunitpython.common.assertion.violation import Violation
6
6
  from archunitpython.common.extraction.extract_graph import extract_graph
7
7
  from archunitpython.common.fluentapi.checkable import CheckOptions
8
+ from archunitpython.common.fluentapi.inspection import inspect_check
8
9
  from archunitpython.common.projection.edge_projections import per_internal_edge
9
10
  from archunitpython.common.projection.project_edges import project_edges
10
11
  from archunitpython.common.regex_factory import RegexFactory
@@ -42,6 +43,7 @@ class LayeredArchitecture:
42
43
  self._layers.setdefault(name, [])
43
44
  return LayerDependencyRuleBuilder(self, name)
44
45
 
46
+ @inspect_check
45
47
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
46
48
  graph = extract_graph(self._project_path, options=options)
47
49
  edges = project_edges(graph, per_internal_edge())
@@ -9,6 +9,7 @@ from archunitpython.common.extraction.extract_graph import (
9
9
  _find_python_files,
10
10
  _resolve_exclude_patterns,
11
11
  )
12
+ from archunitpython.common.logging.inspection import debug
12
13
  from archunitpython.metrics.common.types import (
13
14
  ClassInfo,
14
15
  EnhancedClassInfo,
@@ -41,6 +42,7 @@ def extract_class_info(
41
42
 
42
43
  classes: list[ClassInfo] = []
43
44
  for file_path in py_files:
45
+ debug("Analyzing classes in: %s", file_path)
44
46
  classes.extend(_process_source_file(file_path))
45
47
 
46
48
  return classes
@@ -61,6 +63,7 @@ def extract_enhanced_class_info(
61
63
 
62
64
  results: list[FileAnalysisResult] = []
63
65
  for file_path in py_files:
66
+ debug("Analyzing classes in: %s", file_path)
64
67
  result = _process_source_file_enhanced(file_path)
65
68
  if result.classes:
66
69
  results.append(result)
@@ -88,6 +91,13 @@ def _process_source_file(file_path: str) -> list[ClassInfo]:
88
91
  if isinstance(node, ast.ClassDef):
89
92
  class_info = _extract_class(node, norm_path)
90
93
  classes.append(class_info)
94
+ debug(
95
+ "Class: %s in %s; %d methods; %d fields",
96
+ class_info.name,
97
+ norm_path,
98
+ len(class_info.methods),
99
+ len(class_info.fields),
100
+ )
91
101
 
92
102
  return classes
93
103
 
@@ -112,6 +122,13 @@ def _process_source_file_enhanced(file_path: str) -> FileAnalysisResult:
112
122
  if isinstance(node, ast.ClassDef):
113
123
  enhanced = _extract_enhanced_class(node, norm_path)
114
124
  result.classes.append(enhanced)
125
+ debug(
126
+ "Enhanced class: %s in %s; abstract=%s; protocol=%s",
127
+ enhanced.name,
128
+ norm_path,
129
+ enhanced.is_abstract,
130
+ enhanced.is_protocol,
131
+ )
115
132
  result.total_types += 1
116
133
  if enhanced.is_protocol:
117
134
  result.protocols += 1
@@ -12,6 +12,8 @@ from typing import Any, Callable
12
12
 
13
13
  from archunitpython.common.assertion.violation import Violation
14
14
  from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
15
+ from archunitpython.common.fluentapi.inspection import inspect_check
16
+ from archunitpython.common.logging.inspection import debug, debug_enabled
15
17
  from archunitpython.common.pattern_matching import matches_pattern_classname
16
18
  from archunitpython.common.regex_factory import RegexFactory
17
19
  from archunitpython.common.types import Filter, Pattern
@@ -97,6 +99,20 @@ class MetricsBuilder:
97
99
  )
98
100
 
99
101
 
102
+ def _log_metric_value(
103
+ metric: Any, subject: str, value: float, threshold: float, comparison: MetricComparison
104
+ ) -> None:
105
+ if not debug_enabled():
106
+ return
107
+ try:
108
+ name = metric.name
109
+ except Exception:
110
+ name = type(metric).__name__
111
+ debug(
112
+ "Metric %s for %s: value=%s; threshold=%s (%s)", name, subject, value, threshold, comparison
113
+ )
114
+
115
+
100
116
  def _get_filtered_classes(project_path: str | None, filters: list[Filter]) -> list[ClassInfo]:
101
117
  classes = extract_class_info(project_path)
102
118
  if not filters:
@@ -187,12 +203,20 @@ class ClassMetricCondition(RuleRationaleMixin):
187
203
  self._threshold = threshold
188
204
  self._comparison = comparison
189
205
 
206
+ @inspect_check
190
207
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
191
208
  classes = _get_filtered_classes(self._project_path, self._filters)
192
209
  violations: list[Violation] = []
193
210
 
194
211
  for cls in classes:
195
212
  value = self._metric.calculate(cls)
213
+ _log_metric_value(
214
+ self._metric,
215
+ cls.name,
216
+ value,
217
+ self._threshold,
218
+ self._comparison,
219
+ )
196
220
  if check_threshold(value, self._threshold, self._comparison):
197
221
  violations.append(
198
222
  MetricViolation(
@@ -247,6 +271,7 @@ class FileMetricCondition(RuleRationaleMixin):
247
271
  self._threshold = threshold
248
272
  self._comparison: MetricComparison = comparison
249
273
 
274
+ @inspect_check
250
275
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
251
276
  import os
252
277
 
@@ -268,6 +293,13 @@ class FileMetricCondition(RuleRationaleMixin):
268
293
  continue
269
294
 
270
295
  value = self._metric.calculate_from_file(file_path)
296
+ _log_metric_value(
297
+ self._metric,
298
+ file_path,
299
+ value,
300
+ self._threshold,
301
+ self._comparison,
302
+ )
271
303
  if check_threshold(value, self._threshold, self._comparison):
272
304
  violations.append(
273
305
  FileCountViolation(
@@ -381,6 +413,7 @@ class DistanceCondition(RuleRationaleMixin):
381
413
  self._threshold = threshold
382
414
  self._comparison: MetricComparison = comparison
383
415
 
416
+ @inspect_check
384
417
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
385
418
  files = extract_enhanced_class_info(self._project_path)
386
419
  violations: list[Violation] = []
@@ -388,6 +421,14 @@ class DistanceCondition(RuleRationaleMixin):
388
421
  for file_result in files:
389
422
  dm = calculate_file_distance_metrics(file_result, files)
390
423
  value = getattr(dm, self._metric_attr)
424
+ debug(
425
+ "Metric %s for %s: value=%s; threshold=%s (%s)",
426
+ self._metric_attr,
427
+ file_result.file_path,
428
+ value,
429
+ self._threshold,
430
+ self._comparison,
431
+ )
391
432
 
392
433
  if check_threshold(value, self._threshold, self._comparison):
393
434
  violations.append(
@@ -412,6 +453,7 @@ class ZoneCondition(RuleRationaleMixin):
412
453
  self._filters = filters
413
454
  self._zone_type = zone_type
414
455
 
456
+ @inspect_check
415
457
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
416
458
  files = extract_enhanced_class_info(self._project_path)
417
459
  violations: list[Violation] = []
@@ -420,6 +462,7 @@ class ZoneCondition(RuleRationaleMixin):
420
462
  dm = calculate_file_distance_metrics(file_result, files)
421
463
  in_zone = dm.in_zone_of_pain if self._zone_type == "pain" else dm.in_zone_of_uselessness
422
464
 
465
+ debug("Zone %s for %s: in_zone=%s", self._zone_type, file_result.file_path, in_zone)
423
466
  if in_zone:
424
467
  violations.append(
425
468
  MetricViolation(
@@ -504,12 +547,14 @@ class CustomMetricCondition(RuleRationaleMixin):
504
547
  self._threshold = threshold
505
548
  self._comparison = comparison
506
549
 
550
+ @inspect_check
507
551
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
508
552
  classes = _get_filtered_classes(self._project_path, self._filters)
509
553
  violations: list[Violation] = []
510
554
 
511
555
  for cls in classes:
512
556
  value = self._calculation(cls)
557
+ debug("Custom metric %s for %s: value=%s", self._name, cls.name, value)
513
558
  if check_threshold(value, self._threshold, self._comparison):
514
559
  violations.append(
515
560
  MetricViolation(
@@ -542,12 +587,14 @@ class CustomAssertionCondition(RuleRationaleMixin):
542
587
  self._calculation = calculation
543
588
  self._assertion = assertion
544
589
 
590
+ @inspect_check
545
591
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
546
592
  classes = _get_filtered_classes(self._project_path, self._filters)
547
593
  violations: list[Violation] = []
548
594
 
549
595
  for cls in classes:
550
596
  value = self._calculation(cls)
597
+ debug("Custom metric %s for %s: value=%s", self._name, cls.name, value)
551
598
  if not self._assertion(value, cls):
552
599
  violations.append(
553
600
  MetricViolation(
@@ -15,6 +15,7 @@ import re
15
15
  from archunitpython.common.assertion.violation import Violation
16
16
  from archunitpython.common.extraction.extract_graph import extract_graph
17
17
  from archunitpython.common.fluentapi.checkable import CheckOptions, RuleRationaleMixin
18
+ from archunitpython.common.fluentapi.inspection import inspect_check
18
19
  from archunitpython.common.projection.project_edges import project_edges
19
20
  from archunitpython.common.projection.types import MapFunction
20
21
  from archunitpython.slices.assertion.admissible_edges import (
@@ -157,6 +158,7 @@ class PositiveSliceCondition(RuleRationaleMixin):
157
158
  self._puml_content = puml_content
158
159
  self._coherence_options = coherence_options
159
160
 
161
+ @inspect_check
160
162
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
161
163
  graph = extract_graph(self._project_path, options=options)
162
164
  rules, contained_nodes = generate_rule(self._puml_content)
@@ -193,6 +195,7 @@ class NegativeSliceCondition(RuleRationaleMixin):
193
195
  self._source = source
194
196
  self._target = target
195
197
 
198
+ @inspect_check
196
199
  def check(self, options: CheckOptions | None = None) -> list[Violation]:
197
200
  graph = extract_graph(self._project_path, options=options)
198
201
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from archunitpython.common.assertion.violation import Violation
6
6
  from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
7
+ from archunitpython.testing.common.color_utils import ColorUtils
7
8
  from archunitpython.testing.common.violation_factory import ViolationFactory
8
9
 
9
10
 
@@ -11,26 +12,32 @@ def format_violations(
11
12
  violations: list[Violation],
12
13
  *,
13
14
  because: str | None = None,
15
+ color: bool | None = None,
14
16
  ) -> str:
15
17
  """Format violations into a human-readable string.
16
18
 
17
19
  Args:
18
20
  violations: List of violations to format.
21
+ because: Optional rationale for the rule.
22
+ color: None detects interactive terminals; False guarantees plain text.
23
+ True explicitly enables ANSI colors, unless NO_COLOR is set.
19
24
 
20
25
  Returns:
21
26
  Formatted string describing all violations.
22
27
  """
23
28
  if not violations:
24
- return "No violations found."
29
+ return ColorUtils.style("No violations found.", "32", color)
25
30
 
26
- lines = [f"Found {len(violations)} architecture violation(s):"]
31
+ lines = [ColorUtils.style(f"Found {len(violations)} architecture violation(s):", "1;31", color)]
27
32
  if because:
28
- lines.extend(["", f"Because: {because}"])
33
+ lines.extend(["", ColorUtils.style(f"Because: {because}", "33", color)])
29
34
  lines.append("")
30
35
  for i, violation in enumerate(violations, 1):
31
36
  tv = ViolationFactory.from_violation(violation)
32
- lines.append(f" {i}. {tv.message}")
33
- lines.append(f" {tv.details}")
37
+ lines.append(f" {i}. {ColorUtils.style(tv.message, '1;33', color)}")
38
+ detail_color = "36" if "value=" in tv.details else "34"
39
+ for detail in tv.details.split("\n"):
40
+ lines.append(f" {ColorUtils.style(detail, detail_color, color)}")
34
41
  lines.append("")
35
42
 
36
43
  return "\n".join(lines)
@@ -39,12 +46,15 @@ def format_violations(
39
46
  def assert_passes(
40
47
  checkable: Checkable,
41
48
  options: CheckOptions | None = None,
49
+ *,
50
+ color: bool | None = None,
42
51
  ) -> None:
43
52
  """Assert that an architecture rule passes (no violations).
44
53
 
45
54
  Args:
46
55
  checkable: Any object with a check() method (implements Checkable).
47
56
  options: Optional check options.
57
+ color: Optional color override for failure output.
48
58
 
49
59
  Raises:
50
60
  AssertionError: If the rule has violations.
@@ -52,4 +62,4 @@ def assert_passes(
52
62
  violations = checkable.check(options)
53
63
  if violations:
54
64
  because = getattr(checkable, "because_reason", None)
55
- raise AssertionError(format_violations(violations, because=because))
65
+ raise AssertionError(format_violations(violations, because=because, color=color))
@@ -8,7 +8,9 @@ import sys
8
8
 
9
9
  def _supports_color() -> bool:
10
10
  """Check if the terminal supports ANSI colors."""
11
- if os.environ.get("NO_COLOR"):
11
+ if "NO_COLOR" in os.environ or os.environ.get("CI", "").lower() == "true":
12
+ return False
13
+ if os.environ.get("TERM") == "dumb":
12
14
  return False
13
15
  if not hasattr(sys.stdout, "isatty"):
14
16
  return False
@@ -24,6 +26,15 @@ def _wrap(code: str, text: str) -> str:
24
26
  class ColorUtils:
25
27
  """ANSI color utilities for terminal output."""
26
28
 
29
+ @staticmethod
30
+ def style(text: str, code: str, enabled: bool | None = None) -> str:
31
+ """Style text with automatic detection or an explicit color choice."""
32
+ if enabled is False or "NO_COLOR" in os.environ:
33
+ return text
34
+ if enabled is None and not _supports_color():
35
+ return text
36
+ return f"\033[{code}m{text}\033[0m"
37
+
27
38
  @staticmethod
28
39
  def red(text: str) -> str:
29
40
  return _wrap("31", text)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: archunitpython
3
- Version: 1.7.0
3
+ Version: 1.8.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
@@ -926,6 +926,10 @@ The most important differences:
926
926
 
927
927
  When tests fail, you get helpful output with file paths and violation details:
928
928
 
929
+ Interactive terminals also highlight the failure summary, numbered headings,
930
+ paths, metric values, and rule rationale. Output stays plain when redirected,
931
+ when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
932
+
929
933
  ```
930
934
  Found 2 architecture violation(s):
931
935
 
@@ -938,25 +942,91 @@ Found 2 architecture violation(s):
938
942
 
939
943
  ## 📝 Debug Logging & Configuration
940
944
 
941
- We support logging to help you understand what files are being analyzed and troubleshoot test failures. Logging is disabled by default to keep test output clean.
945
+ Logging is disabled by default. Enable it per check to inspect file discovery,
946
+ graph extraction and cache use, selector decisions, edge projections, class
947
+ analysis, metric values, and the resulting violations. File, layer, slice,
948
+ count/LCOM/distance, and custom metric checks use the same logging options.
949
+
950
+ | Level | What you see |
951
+ | --- | --- |
952
+ | `error` | Analysis exceptions (the original exception is still raised). |
953
+ | `warn` | Failed check summaries and every violation, plus errors. |
954
+ | `info` | Check start, completion, duration, and violations, plus errors. |
955
+ | `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
942
956
 
943
957
  ### Enabling Debug Logging
944
958
 
945
959
  ```python
946
- from archunitpython import CheckOptions
947
- from archunitpython.common.logging.types import LoggingOptions
960
+ from archunitpython import CheckOptions, LoggingOptions, project_files
961
+
962
+ rule = project_files("src/").should().have_no_cycles()
948
963
 
949
964
  options = CheckOptions(
950
965
  logging=LoggingOptions(
951
966
  enabled=True,
952
967
  level="debug", # "error" | "warn" | "info" | "debug"
953
- log_file=True, # Creates logs/archunit-YYYY-MM-DD_HH-MM-SS.log
968
+ log_file=True, # Creates a unique timestamped file in logs/
954
969
  ),
955
970
  )
956
971
 
957
972
  violations = rule.check(options)
958
973
  ```
959
974
 
975
+ For example, a debug trace includes entries like these (paths and timings depend
976
+ on your project):
977
+
978
+ ```text
979
+ [INFO] Starting check: CycleFreeFileCondition
980
+ [DEBUG] Graph cache miss
981
+ [DEBUG] Discovered file: /project/src/api.py
982
+ [DEBUG] Processing file: /project/src/api.py
983
+ [DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
984
+ [DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
985
+ [INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
986
+ ```
987
+
988
+ Console diagnostics use the standard `archunitpython` logger when handlers are
989
+ configured, respecting their levels and formatting; otherwise they go to stderr.
990
+ For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
991
+ Python's standard `logging` module in your application.
992
+
993
+ ### Save detailed inspection without console noise
994
+
995
+ ```python
996
+ options = CheckOptions(logging=LoggingOptions(
997
+ enabled=True,
998
+ level="debug",
999
+ console=False,
1000
+ log_file=True,
1001
+ log_path="logs/architecture.log",
1002
+ append_to_log_file=True,
1003
+ ))
1004
+ violations = rule.check(options)
1005
+ ```
1006
+
1007
+ File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
1008
+ paths are unique per check; an explicit `log_path` replaces that path.
1009
+ `append_to_log_file=False` overwrites an explicit file at the start of each check.
1010
+ Each check closes its own output file and isolates its diagnostics from nested
1011
+ and concurrent checks. Use separate paths when concurrent checks write to files.
1012
+ Diagnostic output is best effort: an unavailable file or failing logging handler
1013
+ does not change rule results or hide analysis exceptions. Debug inspection does
1014
+ not re-run custom predicates or metric calculations.
1015
+
1016
+ ### Format results independently of logging
1017
+
1018
+ ```python
1019
+ from archunitpython import assert_passes, format_violations
1020
+
1021
+ print(format_violations(violations)) # Detect terminal colors
1022
+ text = format_violations(violations, color=False) # Stable plain text for artifacts
1023
+ assert_passes(rule, options, color=False) # Plain assertion failure
1024
+ ```
1025
+
1026
+ Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
1027
+ The numbered report, rationale, and details remain available at every log level,
1028
+ including when logging is disabled. Existing plain-text report wording is retained.
1029
+
960
1030
  ### CI Pipeline Integration
961
1031
 
962
1032
  ```yaml
@@ -1,7 +1,7 @@
1
- archunitpython/__init__.py,sha256=2GNl1l7I8BSz7KehGw8thDEMYr09vjFDgfcnVXxaK9I,1282
1
+ archunitpython/__init__.py,sha256=JaKTTUg_yKFG5p-rh1VNOua4-aFpo7CBaYiAGvzn708,1324
2
2
  archunitpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  archunitpython/common/__init__.py,sha256=TKL39Z0kBpWqMH99jU4LsDSUZ5lQ_rcAJfLnmUTDTOI,656
4
- archunitpython/common/pattern_matching.py,sha256=HMAfo8GsooHvA4d2IxCi3btrYTdwfOQXw4bDNr117P0,2669
4
+ archunitpython/common/pattern_matching.py,sha256=WLURKcd9tWXG8MPPNWGh8craK-UP37oW_k39g04THQE,2974
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,16 +9,18 @@ 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=wxLpOtEIOjpXTUU0LXAcwsGiup9sh2W3NgjykuYX-gI,23268
12
+ archunitpython/common/extraction/extract_graph.py,sha256=GGXyoxHGyvWF4iyqIueuj89ObvRUuK3RsuxALJIvByk,24308
13
13
  archunitpython/common/extraction/graph.py,sha256=s4QW0WYfwjqXteBNygaNBdTgSC1XKil8cBFLtxl5rJE,1124
14
14
  archunitpython/common/fluentapi/__init__.py,sha256=LeS7qS2p9-FqqBhDL8xRPex__W5Qk0UWfcpl7nVVLfI,178
15
15
  archunitpython/common/fluentapi/checkable.py,sha256=BJFYibVhHLMFlnWdISPCIL3DYI1_3JN2-1jEQOaVaGE,1547
16
+ archunitpython/common/fluentapi/inspection.py,sha256=VnrQIHH0tKW-QeexG7HiMU-DyvxjNcpAWsuEdhfRyR8,2991
16
17
  archunitpython/common/logging/__init__.py,sha256=u3-2_jYmiyoQ-C534SmQXvR1L9h3lbJqA9Yt1Nmv9HA,115
17
- archunitpython/common/logging/types.py,sha256=wUriZeaUawA3JoBfJVLC6lIMglbLG955nXWdJGwxW4I,425
18
+ archunitpython/common/logging/inspection.py,sha256=7akoJaRsS9vVz8joiMIcTh8LD1ocKjUiURDFsmhGN2k,3291
19
+ archunitpython/common/logging/types.py,sha256=WOWi1UECxSlABGkYn7JcxooqVrC4ZOXSUBbOQjkdnKc,482
18
20
  archunitpython/common/projection/__init__.py,sha256=-Sx-b80b2N3ap3ahAcLUCXqYYK3ccgTDpV-K1jSOSYQ,797
19
21
  archunitpython/common/projection/edge_projections.py,sha256=Mud8DL6Y8H66CObqyikKYAxdXMRENuAFRtwhCxpwxoU,1524
20
22
  archunitpython/common/projection/project_cycles.py,sha256=TQ4Ok1hT4T_znqFbwV4VTj-BXmlFr1O7TtZdVOd5oPg,3081
21
- archunitpython/common/projection/project_edges.py,sha256=v7hDMSeghiLz0xHbG4BNF7jNQphcWobU7rgoiuw3fh4,1268
23
+ archunitpython/common/projection/project_edges.py,sha256=Rul-BpDT549ZqSlnG6qOriO0bREA-cJqg4TatOKL8fA,1720
22
24
  archunitpython/common/projection/project_nodes.py,sha256=U6sLYvJd6NBgg39V0Ry7r_-57gdXaaXsbH-D12nipCg,1497
23
25
  archunitpython/common/projection/types.py,sha256=EP5yJd-uXYdACFdFzeID8bUVsg_vdhgiOzshrC-CC8Y,937
24
26
  archunitpython/common/projection/cycles/__init__.py,sha256=giGUcwpEMr91XST-biyCmLUjP5okhwTA4CcCaxjZdmg,217
@@ -40,14 +42,14 @@ archunitpython/files/assertion/depend_on_external_modules.py,sha256=BXGpN-301YOc
40
42
  archunitpython/files/assertion/depend_on_files.py,sha256=9lBj4YNe1jjKJV3r0ntsXfPHoKc_IbwgHhLgMYOs9Ok,2024
41
43
  archunitpython/files/assertion/matching_files.py,sha256=1RyYqFA-IzzAXlgqRfiL18jk9xEJzEBY2fp_A3fxjdE,2015
42
44
  archunitpython/files/fluentapi/__init__.py,sha256=wgl8IOqTeQGpNISu-Q8b_j6rTn8aEgoz6t1S5j8z13g,108
43
- archunitpython/files/fluentapi/files.py,sha256=oZG0lWV6p0AiGANdoZfYd51H8zY-J4eTSPRTRPBYhs0,17152
45
+ archunitpython/files/fluentapi/files.py,sha256=qJW7n6uX4-pfIGox-BdVHK_PVu9ZN_lxpKSXidp8_4s,17316
44
46
  archunitpython/graph/__init__.py,sha256=vm1jMOoN35IIU7YDs59qPtf_F82YV-QrEGfXhozAOek,756
45
47
  archunitpython/graph/graph_reporter.py,sha256=D_KH01CM7TzeMFB6o5NIQfLVJq0rvKZn9l7WumXV6lo,25598
46
48
  archunitpython/layers/__init__.py,sha256=DCRxqLhluTxTaT9ZM2MbfAdJVgqcOO9jtkZHzBbeLOw,164
47
49
  archunitpython/layers/assertion/__init__.py,sha256=_VRKjO86psSnmX29EfikDe3RIpJMczTyn6nkSM7xQwc,226
48
50
  archunitpython/layers/assertion/layer_dependencies.py,sha256=I8Y3DJnWhCsJfhgQB4lTHBobFG3_bB9a_ljqCNcYeBM,2646
49
51
  archunitpython/layers/fluentapi/__init__.py,sha256=psKFB-5IXS1SJ8g62HthExKYJio3bgdVzIleOSiF1gA,125
50
- archunitpython/layers/fluentapi/layers.py,sha256=Wl926_aWpNrlbWatDi3QDR-8geDflbYFEivWJv-X9dI,4438
52
+ archunitpython/layers/fluentapi/layers.py,sha256=iPI4XxP8iqvRfoJWJE9hrjYDeeQq0fbP6KQG8hIJKT0,4526
51
53
  archunitpython/metrics/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
52
54
  archunitpython/metrics/assertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
55
  archunitpython/metrics/assertion/metric_thresholds.py,sha256=kwX1iRFnF7oLaLMlQsYrDsNRrivDE8pI6nrrZYAafvg,1262
@@ -58,28 +60,28 @@ archunitpython/metrics/calculation/lcom.py,sha256=LK_kNT-BM6yuR5TUOLWQLkVvMpqR6q
58
60
  archunitpython/metrics/common/__init__.py,sha256=xtEyAhS4X0UbJXHxnpdiXljVZmKIKKekZAZ1zflSPFE,335
59
61
  archunitpython/metrics/common/types.py,sha256=w1BUlw6p_3K8qZDoQXv50P_CcKixGYiyS_YBGYILySA,1601
60
62
  archunitpython/metrics/extraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- archunitpython/metrics/extraction/extract_class_info.py,sha256=u-i_AkRRdIbOVXhH7LVCCVIxRrz4IX6nOqukPxbB5Iw,7747
63
+ archunitpython/metrics/extraction/extract_class_info.py,sha256=iQuug7gRAdnegOVWCKGJo4YrdVaPC7nkMlrQ940Vi8Q,8381
62
64
  archunitpython/metrics/fluentapi/__init__.py,sha256=HD72MWF0hCh2LzKfyk3mMJp2tf7TJebpwTtZ_kppjso,84
63
65
  archunitpython/metrics/fluentapi/export_utils.py,sha256=1r-zTQAUqn_agp4_lhbcffdzfdASNY3LebYvKAG9kcQ,2333
64
- archunitpython/metrics/fluentapi/metrics.py,sha256=8OBOL3jVKbM9v_CjNNM9FMfV1JBfl5UEd1-PPOEScRs,19371
66
+ archunitpython/metrics/fluentapi/metrics.py,sha256=bFGO7DJiRrtDSxcwF9u0OMSpj9OZKBj9Pw0kxEHoVTU,20922
65
67
  archunitpython/metrics/projection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
68
  archunitpython/slices/__init__.py,sha256=tb8MoZiEqIdmWy7uyhmEtMNm5Fxjuw6atSvDsD131DE,96
67
69
  archunitpython/slices/assertion/__init__.py,sha256=MRwV3d69ljAJ7hFYdxI6UDNC8U4Dm1ImeuLUVKEC9To,280
68
70
  archunitpython/slices/assertion/admissible_edges.py,sha256=NNn1PGsK0LS_Msa3g1cAMQwLzxbot4rQNVRAHzW11pk,3124
69
71
  archunitpython/slices/fluentapi/__init__.py,sha256=tb8MoZiEqIdmWy7uyhmEtMNm5Fxjuw6atSvDsD131DE,96
70
- archunitpython/slices/fluentapi/slices.py,sha256=vNoyqmleTDTjnf2i35i2nte-Ycjm-oapfgaf3AhN0QI,7163
72
+ archunitpython/slices/fluentapi/slices.py,sha256=NVH7hm2ywwEi4vJPYe098-_Au4sPfu3pTnGW2zlK_qw,7270
71
73
  archunitpython/slices/projection/__init__.py,sha256=tVgcK4gw-bqhUq98vU7_iPE0NEhOPUr7vr1-p7JI1A4,237
72
74
  archunitpython/slices/projection/slicing_projections.py,sha256=BHBkuVG80mqzT7N4FlCCfY5U_NvlXmAgTmHf_SPTXVM,4215
73
75
  archunitpython/slices/uml/__init__.py,sha256=GQFA5WBKfi_YElWiNWNm9c9IM-VaW83_Cg6kpwkzxkw,196
74
76
  archunitpython/slices/uml/export_diagram.py,sha256=FeBRV8Vt8LuhLqEbg5JzZOmEk2i49JPkozMcI5r335g,818
75
77
  archunitpython/slices/uml/generate_rules.py,sha256=D9vvqDzHAM0F80z4u5yBalEqIiNfJJ5HGXP0fVlSzHw,2136
76
78
  archunitpython/testing/__init__.py,sha256=MJm6CHGhBGFg1um2L20-GD2DEHKqg8SiUoI-sRsoXQk,128
77
- archunitpython/testing/assertion.py,sha256=7cXlNwBklIGAyUJlPGFGAw97xZ9UgpkZvoO_QBdqm1c,1637
79
+ archunitpython/testing/assertion.py,sha256=sqZHVq1GTuKzil7FgXwuslcs8qtvIejQqblKeCh3eeU,2323
78
80
  archunitpython/testing/common/__init__.py,sha256=Wc4bC-N4t6giChKjj6wuTGKkb39thcYS_xKWG9paYeY,220
79
- archunitpython/testing/common/color_utils.py,sha256=2I8Z1SZfWhhgudMgmXY6PPydGxGl35cK_To-GXnSyJg,1226
81
+ archunitpython/testing/common/color_utils.py,sha256=Bqmx7l8Aw3s3brblyqhWdVRFn0WQiQWiR9y9kz072Wg,1706
80
82
  archunitpython/testing/common/violation_factory.py,sha256=yWlvv2U7u-7KoTMfp1QCeJXxTLpPEagKqDHZmPSyrlk,4677
81
83
  archunitpython/testing/pytest_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
- archunitpython-1.7.0.dist-info/METADATA,sha256=KSwZ_XskzaMaiY0ZK1VxbfSiadBtKyq0u5ycdyZ6PZ4,35168
83
- archunitpython-1.7.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
84
- archunitpython-1.7.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
85
- archunitpython-1.7.0.dist-info/RECORD,,
84
+ archunitpython-1.8.0.dist-info/METADATA,sha256=6RS5qNk1Aj81iipmKRzmVLr1CcFZUzCO9OF86sSc2bU,38359
85
+ archunitpython-1.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
86
+ archunitpython-1.8.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
87
+ archunitpython-1.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any