archunitpython 1.0.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 (75) hide show
  1. archunitpython/__init__.py +45 -0
  2. archunitpython/common/__init__.py +18 -0
  3. archunitpython/common/assertion/__init__.py +3 -0
  4. archunitpython/common/assertion/violation.py +21 -0
  5. archunitpython/common/error/__init__.py +3 -0
  6. archunitpython/common/error/errors.py +13 -0
  7. archunitpython/common/extraction/__init__.py +13 -0
  8. archunitpython/common/extraction/extract_graph.py +345 -0
  9. archunitpython/common/extraction/graph.py +39 -0
  10. archunitpython/common/fluentapi/__init__.py +3 -0
  11. archunitpython/common/fluentapi/checkable.py +28 -0
  12. archunitpython/common/logging/__init__.py +3 -0
  13. archunitpython/common/logging/types.py +18 -0
  14. archunitpython/common/pattern_matching.py +80 -0
  15. archunitpython/common/projection/__init__.py +30 -0
  16. archunitpython/common/projection/cycles/__init__.py +4 -0
  17. archunitpython/common/projection/cycles/cycle_utils.py +49 -0
  18. archunitpython/common/projection/cycles/cycles.py +26 -0
  19. archunitpython/common/projection/cycles/johnsons_apsp.py +110 -0
  20. archunitpython/common/projection/cycles/model.py +22 -0
  21. archunitpython/common/projection/cycles/tarjan_scc.py +86 -0
  22. archunitpython/common/projection/edge_projections.py +36 -0
  23. archunitpython/common/projection/project_cycles.py +85 -0
  24. archunitpython/common/projection/project_edges.py +43 -0
  25. archunitpython/common/projection/project_nodes.py +49 -0
  26. archunitpython/common/projection/types.py +40 -0
  27. archunitpython/common/regex_factory.py +76 -0
  28. archunitpython/common/types.py +29 -0
  29. archunitpython/common/util/__init__.py +3 -0
  30. archunitpython/common/util/declaration_detector.py +115 -0
  31. archunitpython/common/util/logger.py +100 -0
  32. archunitpython/files/__init__.py +3 -0
  33. archunitpython/files/assertion/__init__.py +28 -0
  34. archunitpython/files/assertion/custom_file_logic.py +107 -0
  35. archunitpython/files/assertion/cycle_free.py +29 -0
  36. archunitpython/files/assertion/depend_on_files.py +67 -0
  37. archunitpython/files/assertion/matching_files.py +64 -0
  38. archunitpython/files/fluentapi/__init__.py +3 -0
  39. archunitpython/files/fluentapi/files.py +403 -0
  40. archunitpython/metrics/__init__.py +3 -0
  41. archunitpython/metrics/assertion/__init__.py +0 -0
  42. archunitpython/metrics/assertion/metric_thresholds.py +51 -0
  43. archunitpython/metrics/calculation/__init__.py +0 -0
  44. archunitpython/metrics/calculation/count.py +148 -0
  45. archunitpython/metrics/calculation/distance.py +110 -0
  46. archunitpython/metrics/calculation/lcom.py +177 -0
  47. archunitpython/metrics/common/__init__.py +19 -0
  48. archunitpython/metrics/common/types.py +67 -0
  49. archunitpython/metrics/extraction/__init__.py +0 -0
  50. archunitpython/metrics/extraction/extract_class_info.py +246 -0
  51. archunitpython/metrics/fluentapi/__init__.py +3 -0
  52. archunitpython/metrics/fluentapi/export_utils.py +89 -0
  53. archunitpython/metrics/fluentapi/metrics.py +589 -0
  54. archunitpython/metrics/projection/__init__.py +0 -0
  55. archunitpython/py.typed +0 -0
  56. archunitpython/slices/__init__.py +3 -0
  57. archunitpython/slices/assertion/__init__.py +13 -0
  58. archunitpython/slices/assertion/admissible_edges.py +108 -0
  59. archunitpython/slices/fluentapi/__init__.py +3 -0
  60. archunitpython/slices/fluentapi/slices.py +220 -0
  61. archunitpython/slices/projection/__init__.py +8 -0
  62. archunitpython/slices/projection/slicing_projections.py +128 -0
  63. archunitpython/slices/uml/__init__.py +4 -0
  64. archunitpython/slices/uml/export_diagram.py +31 -0
  65. archunitpython/slices/uml/generate_rules.py +71 -0
  66. archunitpython/testing/__init__.py +3 -0
  67. archunitpython/testing/assertion.py +47 -0
  68. archunitpython/testing/common/__init__.py +4 -0
  69. archunitpython/testing/common/color_utils.py +57 -0
  70. archunitpython/testing/common/violation_factory.py +97 -0
  71. archunitpython/testing/pytest_plugin/__init__.py +0 -0
  72. archunitpython-1.0.0.dist-info/METADATA +660 -0
  73. archunitpython-1.0.0.dist-info/RECORD +75 -0
  74. archunitpython-1.0.0.dist-info/WHEEL +4 -0
  75. archunitpython-1.0.0.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,89 @@
1
+ """HTML report export for metrics results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+
9
+
10
+ @dataclass
11
+ class ExportOptions:
12
+ """Options for metrics export."""
13
+
14
+ output_path: str | None = None
15
+ title: str = "ArchUnitPython Metrics Report"
16
+ include_timestamp: bool = True
17
+ custom_css: str | None = None
18
+
19
+
20
+ class MetricsExporter:
21
+ """Export metrics results as HTML reports."""
22
+
23
+ @staticmethod
24
+ def export_as_html(
25
+ data: dict[str, object],
26
+ options: ExportOptions | None = None,
27
+ ) -> str:
28
+ """Export metric data as an HTML report.
29
+
30
+ Args:
31
+ data: Dictionary of metric results to include.
32
+ options: Export options.
33
+
34
+ Returns:
35
+ HTML content as a string. Also writes to file if output_path specified.
36
+ """
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
+ )
43
+
44
+ css = opts.custom_css or _DEFAULT_CSS
45
+
46
+ rows = ""
47
+ for key, value in data.items():
48
+ rows += f" <tr><td>{key}</td><td>{value}</td></tr>\n"
49
+
50
+ html = f"""<!DOCTYPE html>
51
+ <html>
52
+ <head>
53
+ <title>{opts.title}</title>
54
+ <style>{css}</style>
55
+ </head>
56
+ <body>
57
+ <h1>{opts.title}</h1>
58
+ {f'<p class="timestamp">Generated: {timestamp}</p>' if timestamp else ''}
59
+ <table>
60
+ <thead>
61
+ <tr><th>Metric</th><th>Value</th></tr>
62
+ </thead>
63
+ <tbody>
64
+ {rows} </tbody>
65
+ </table>
66
+ </body>
67
+ </html>"""
68
+
69
+ if opts.output_path:
70
+ path = opts.output_path
71
+ if not path.endswith(".html"):
72
+ path += ".html"
73
+ os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
74
+ with open(path, "w", encoding="utf-8") as f:
75
+ f.write(html)
76
+
77
+ return html
78
+
79
+
80
+ _DEFAULT_CSS = """
81
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
82
+ max-width: 900px; margin: 0 auto; padding: 20px; }
83
+ h1 { color: #333; }
84
+ .timestamp { color: #666; font-size: 0.9em; }
85
+ table { border-collapse: collapse; width: 100%; margin-top: 20px; }
86
+ th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
87
+ th { background-color: #f5f5f5; font-weight: 600; }
88
+ tr:nth-child(even) { background-color: #fafafa; }
89
+ """
@@ -0,0 +1,589 @@
1
+ """Fluent API builders for metrics rules.
2
+
3
+ Usage:
4
+ metrics('src/').count().lines_of_code().should_be_below(500).check()
5
+ metrics('src/').lcom().lcom96b().should_be_below(0.5).check()
6
+ metrics('src/').distance().abstractness().should_be_below(0.8).check()
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable
12
+
13
+ from archunitpython.common.assertion.violation import Violation
14
+ from archunitpython.common.fluentapi.checkable import CheckOptions
15
+ from archunitpython.common.pattern_matching import matches_pattern_classname
16
+ from archunitpython.common.regex_factory import RegexFactory
17
+ from archunitpython.common.types import Filter, Pattern
18
+ from archunitpython.metrics.assertion.metric_thresholds import (
19
+ FileCountViolation,
20
+ MetricViolation,
21
+ check_threshold,
22
+ )
23
+ from archunitpython.metrics.calculation.count import (
24
+ ClassCountMetric,
25
+ FieldCountMetric,
26
+ FunctionCountMetric,
27
+ ImportCountMetric,
28
+ LinesOfCodeMetric,
29
+ MethodCountMetric,
30
+ StatementCountMetric,
31
+ )
32
+ from archunitpython.metrics.calculation.distance import (
33
+ calculate_file_distance_metrics,
34
+ )
35
+ from archunitpython.metrics.calculation.lcom import (
36
+ LCOM1,
37
+ LCOM2,
38
+ LCOM3,
39
+ LCOM4,
40
+ LCOM5,
41
+ LCOM96a,
42
+ LCOM96b,
43
+ LCOMStar,
44
+ )
45
+ from archunitpython.metrics.common.types import ClassInfo, MetricComparison
46
+ from archunitpython.metrics.extraction.extract_class_info import (
47
+ extract_class_info,
48
+ extract_enhanced_class_info,
49
+ )
50
+
51
+
52
+ def metrics(project_path: str | None = None) -> "MetricsBuilder":
53
+ """Entry point for metrics rules."""
54
+ return MetricsBuilder(project_path)
55
+
56
+
57
+ class MetricsBuilder:
58
+ """Top-level metrics builder with filter methods."""
59
+
60
+ def __init__(self, project_path: str | None = None) -> None:
61
+ self._project_path = project_path
62
+ self._filters: list[Filter] = []
63
+
64
+ def with_name(self, name: Pattern) -> "MetricsBuilder":
65
+ self._filters.append(RegexFactory.filename_matcher(name))
66
+ return self
67
+
68
+ def in_folder(self, folder: Pattern) -> "MetricsBuilder":
69
+ self._filters.append(RegexFactory.folder_matcher(folder))
70
+ return self
71
+
72
+ def in_path(self, path: Pattern) -> "MetricsBuilder":
73
+ self._filters.append(RegexFactory.path_matcher(path))
74
+ return self
75
+
76
+ def for_classes_matching(self, pattern: Pattern) -> "MetricsBuilder":
77
+ self._filters.append(RegexFactory.classname_matcher(pattern))
78
+ return self
79
+
80
+ def count(self) -> "CountMetricsBuilder":
81
+ return CountMetricsBuilder(self._project_path, list(self._filters))
82
+
83
+ def lcom(self) -> "LCOMMetricsBuilder":
84
+ return LCOMMetricsBuilder(self._project_path, list(self._filters))
85
+
86
+ def distance(self) -> "DistanceMetricsBuilder":
87
+ return DistanceMetricsBuilder(self._project_path, list(self._filters))
88
+
89
+ def custom_metric(
90
+ self,
91
+ name: str,
92
+ description: str,
93
+ calculation: Callable[[ClassInfo], float],
94
+ ) -> "CustomMetricsBuilder":
95
+ return CustomMetricsBuilder(
96
+ self._project_path, list(self._filters), name, description, calculation
97
+ )
98
+
99
+
100
+ def _get_filtered_classes(
101
+ project_path: str | None, filters: list[Filter]
102
+ ) -> list[ClassInfo]:
103
+ classes = extract_class_info(project_path)
104
+ if not filters:
105
+ return classes
106
+ return [
107
+ c
108
+ for c in classes
109
+ if all(matches_pattern_classname(c.name, c.file_path, f) for f in filters)
110
+ ]
111
+
112
+
113
+ # --- Count Metrics ---
114
+
115
+
116
+ class CountMetricsBuilder:
117
+ def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
118
+ self._project_path = project_path
119
+ self._filters = filters
120
+
121
+ def method_count(self) -> "ClassMetricThresholdBuilder":
122
+ return ClassMetricThresholdBuilder(
123
+ self._project_path, self._filters, MethodCountMetric()
124
+ )
125
+
126
+ def field_count(self) -> "ClassMetricThresholdBuilder":
127
+ return ClassMetricThresholdBuilder(
128
+ self._project_path, self._filters, FieldCountMetric()
129
+ )
130
+
131
+ def lines_of_code(self) -> "FileMetricThresholdBuilder":
132
+ return FileMetricThresholdBuilder(
133
+ self._project_path, self._filters, LinesOfCodeMetric()
134
+ )
135
+
136
+ def statements(self) -> "FileMetricThresholdBuilder":
137
+ return FileMetricThresholdBuilder(
138
+ self._project_path, self._filters, StatementCountMetric()
139
+ )
140
+
141
+ def imports(self) -> "FileMetricThresholdBuilder":
142
+ return FileMetricThresholdBuilder(
143
+ self._project_path, self._filters, ImportCountMetric()
144
+ )
145
+
146
+ def classes(self) -> "FileMetricThresholdBuilder":
147
+ return FileMetricThresholdBuilder(
148
+ self._project_path, self._filters, ClassCountMetric()
149
+ )
150
+
151
+ def functions(self) -> "FileMetricThresholdBuilder":
152
+ return FileMetricThresholdBuilder(
153
+ self._project_path, self._filters, FunctionCountMetric()
154
+ )
155
+
156
+
157
+ class ClassMetricThresholdBuilder:
158
+ def __init__(self, project_path: str | None, filters: list[Filter], metric: Any) -> None:
159
+ self._project_path = project_path
160
+ self._filters = filters
161
+ self._metric = metric
162
+
163
+ def should_be_below(self, threshold: float) -> "ClassMetricCondition":
164
+ return ClassMetricCondition(
165
+ self._project_path, self._filters, self._metric, threshold, "below"
166
+ )
167
+
168
+ def should_be_above(self, threshold: float) -> "ClassMetricCondition":
169
+ return ClassMetricCondition(
170
+ self._project_path, self._filters, self._metric, threshold, "above"
171
+ )
172
+
173
+ def should_be(self, threshold: float) -> "ClassMetricCondition":
174
+ return ClassMetricCondition(
175
+ self._project_path, self._filters, self._metric, threshold, "equal"
176
+ )
177
+
178
+ def should_be_below_or_equal(self, threshold: float) -> "ClassMetricCondition":
179
+ return ClassMetricCondition(
180
+ self._project_path, self._filters, self._metric, threshold, "below_equal"
181
+ )
182
+
183
+ def should_be_above_or_equal(self, threshold: float) -> "ClassMetricCondition":
184
+ return ClassMetricCondition(
185
+ self._project_path, self._filters, self._metric, threshold, "above_equal"
186
+ )
187
+
188
+
189
+ class ClassMetricCondition:
190
+ """Checkable that verifies a class-level metric threshold."""
191
+
192
+ def __init__(
193
+ self,
194
+ project_path: str | None,
195
+ filters: list[Filter],
196
+ metric: Any,
197
+ threshold: float,
198
+ comparison: MetricComparison,
199
+ ) -> None:
200
+ self._project_path = project_path
201
+ self._filters = filters
202
+ self._metric = metric
203
+ self._threshold = threshold
204
+ self._comparison = comparison
205
+
206
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
207
+ classes = _get_filtered_classes(self._project_path, self._filters)
208
+ violations: list[Violation] = []
209
+
210
+ for cls in classes:
211
+ value = self._metric.calculate(cls)
212
+ if check_threshold(value, self._threshold, self._comparison):
213
+ violations.append(
214
+ MetricViolation(
215
+ class_name=cls.name,
216
+ file_path=cls.file_path,
217
+ metric_name=self._metric.name,
218
+ metric_value=value,
219
+ threshold=self._threshold,
220
+ comparison=self._comparison,
221
+ )
222
+ )
223
+
224
+ return violations
225
+
226
+
227
+ class FileMetricThresholdBuilder:
228
+ def __init__(self, project_path: str | None, filters: list[Filter], metric: Any) -> None:
229
+ self._project_path = project_path
230
+ self._filters = filters
231
+ self._metric = metric
232
+
233
+ def should_be_below(self, threshold: float) -> "FileMetricCondition":
234
+ return FileMetricCondition(
235
+ self._project_path, self._filters, self._metric, threshold, "below"
236
+ )
237
+
238
+ def should_be_above(self, threshold: float) -> "FileMetricCondition":
239
+ return FileMetricCondition(
240
+ self._project_path, self._filters, self._metric, threshold, "above"
241
+ )
242
+
243
+ def should_be_below_or_equal(self, threshold: float) -> "FileMetricCondition":
244
+ return FileMetricCondition(
245
+ self._project_path, self._filters, self._metric, threshold, "below_equal"
246
+ )
247
+
248
+
249
+ class FileMetricCondition:
250
+ """Checkable that verifies a file-level metric threshold."""
251
+
252
+ def __init__(
253
+ self,
254
+ project_path: str | None,
255
+ filters: list[Filter],
256
+ metric: Any,
257
+ threshold: float,
258
+ comparison: MetricComparison,
259
+ ) -> None:
260
+ self._project_path = project_path
261
+ self._filters = filters
262
+ self._metric = metric
263
+ self._threshold = threshold
264
+ self._comparison: MetricComparison = comparison
265
+
266
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
267
+ import os
268
+
269
+ from archunitpython.common.extraction.extract_graph import (
270
+ _DEFAULT_EXCLUDE,
271
+ _find_python_files,
272
+ )
273
+
274
+ project = self._project_path or os.getcwd()
275
+ project = os.path.abspath(project)
276
+ files = _find_python_files(project, _DEFAULT_EXCLUDE)
277
+ violations: list[Violation] = []
278
+
279
+ for file_path in files:
280
+ norm = file_path.replace("\\", "/")
281
+ if self._filters and not all(
282
+ matches_pattern_classname("", norm, f) for f in self._filters
283
+ ):
284
+ continue
285
+
286
+ value = self._metric.calculate_from_file(file_path)
287
+ if check_threshold(value, self._threshold, self._comparison):
288
+ violations.append(
289
+ FileCountViolation(
290
+ file_path=norm,
291
+ metric_name=self._metric.name,
292
+ metric_value=value,
293
+ threshold=self._threshold,
294
+ comparison=self._comparison,
295
+ )
296
+ )
297
+
298
+ return violations
299
+
300
+
301
+ # --- LCOM Metrics ---
302
+
303
+
304
+ class LCOMMetricsBuilder:
305
+ def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
306
+ self._project_path = project_path
307
+ self._filters = filters
308
+
309
+ def lcom96a(self) -> "ClassMetricThresholdBuilder":
310
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM96a())
311
+
312
+ def lcom96b(self) -> "ClassMetricThresholdBuilder":
313
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM96b())
314
+
315
+ def lcom1(self) -> "ClassMetricThresholdBuilder":
316
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM1())
317
+
318
+ def lcom2(self) -> "ClassMetricThresholdBuilder":
319
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM2())
320
+
321
+ def lcom3(self) -> "ClassMetricThresholdBuilder":
322
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM3())
323
+
324
+ def lcom4(self) -> "ClassMetricThresholdBuilder":
325
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM4())
326
+
327
+ def lcom5(self) -> "ClassMetricThresholdBuilder":
328
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOM5())
329
+
330
+ def lcomstar(self) -> "ClassMetricThresholdBuilder":
331
+ return ClassMetricThresholdBuilder(self._project_path, self._filters, LCOMStar())
332
+
333
+
334
+ # --- Distance Metrics ---
335
+
336
+
337
+ class DistanceMetricsBuilder:
338
+ def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
339
+ self._project_path = project_path
340
+ self._filters = filters
341
+
342
+ def abstractness(self) -> "DistanceThresholdBuilder":
343
+ return DistanceThresholdBuilder(
344
+ self._project_path, self._filters, "abstractness"
345
+ )
346
+
347
+ def instability(self) -> "DistanceThresholdBuilder":
348
+ return DistanceThresholdBuilder(
349
+ self._project_path, self._filters, "instability"
350
+ )
351
+
352
+ def distance_from_main_sequence(self) -> "DistanceThresholdBuilder":
353
+ return DistanceThresholdBuilder(
354
+ self._project_path, self._filters, "distance"
355
+ )
356
+
357
+ def not_in_zone_of_pain(self) -> "ZoneCondition":
358
+ return ZoneCondition(self._project_path, self._filters, "pain")
359
+
360
+ def not_in_zone_of_uselessness(self) -> "ZoneCondition":
361
+ return ZoneCondition(self._project_path, self._filters, "uselessness")
362
+
363
+
364
+ class DistanceThresholdBuilder:
365
+ def __init__(self, project_path: str | None, filters: list[Filter], metric_attr: str) -> None:
366
+ self._project_path = project_path
367
+ self._filters = filters
368
+ self._metric_attr = metric_attr
369
+
370
+ def should_be_below(self, threshold: float) -> "DistanceCondition":
371
+ return DistanceCondition(
372
+ self._project_path,
373
+ self._filters,
374
+ self._metric_attr,
375
+ threshold,
376
+ "below",
377
+ )
378
+
379
+ def should_be_above(self, threshold: float) -> "DistanceCondition":
380
+ return DistanceCondition(
381
+ self._project_path,
382
+ self._filters,
383
+ self._metric_attr,
384
+ threshold,
385
+ "above",
386
+ )
387
+
388
+
389
+ class DistanceCondition:
390
+ """Checkable for distance metric thresholds."""
391
+
392
+ def __init__(
393
+ self,
394
+ project_path: str | None,
395
+ filters: list[Filter],
396
+ metric_attr: str,
397
+ threshold: float,
398
+ comparison: MetricComparison,
399
+ ) -> None:
400
+ self._project_path = project_path
401
+ self._filters = filters
402
+ self._metric_attr = metric_attr
403
+ self._threshold = threshold
404
+ self._comparison: MetricComparison = comparison
405
+
406
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
407
+ files = extract_enhanced_class_info(self._project_path)
408
+ violations: list[Violation] = []
409
+
410
+ for file_result in files:
411
+ dm = calculate_file_distance_metrics(file_result, files)
412
+ value = getattr(dm, self._metric_attr)
413
+
414
+ if check_threshold(value, self._threshold, self._comparison):
415
+ violations.append(
416
+ MetricViolation(
417
+ class_name="",
418
+ file_path=file_result.file_path,
419
+ metric_name=self._metric_attr,
420
+ metric_value=value,
421
+ threshold=self._threshold,
422
+ comparison=self._comparison,
423
+ )
424
+ )
425
+
426
+ return violations
427
+
428
+
429
+ class ZoneCondition:
430
+ """Checkable for zone detection (pain/uselessness)."""
431
+
432
+ def __init__(self, project_path: str | None, filters: list[Filter], zone_type: str) -> None:
433
+ self._project_path = project_path
434
+ self._filters = filters
435
+ self._zone_type = zone_type
436
+
437
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
438
+ files = extract_enhanced_class_info(self._project_path)
439
+ violations: list[Violation] = []
440
+
441
+ for file_result in files:
442
+ 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
+ )
448
+
449
+ if in_zone:
450
+ violations.append(
451
+ MetricViolation(
452
+ class_name="",
453
+ file_path=file_result.file_path,
454
+ metric_name=f"zone_of_{self._zone_type}",
455
+ metric_value=1.0,
456
+ threshold=0.0,
457
+ comparison="equal",
458
+ )
459
+ )
460
+
461
+ return violations
462
+
463
+
464
+ # --- Custom Metrics ---
465
+
466
+
467
+ class CustomMetricsBuilder:
468
+ def __init__(
469
+ self,
470
+ project_path: str | None,
471
+ filters: list[Filter],
472
+ name: str,
473
+ description: str,
474
+ calculation: Callable[[ClassInfo], float],
475
+ ) -> None:
476
+ self._project_path = project_path
477
+ self._filters = filters
478
+ self._name = name
479
+ self._description = description
480
+ self._calculation = calculation
481
+
482
+ def should_be_below(self, threshold: float) -> "CustomMetricCondition":
483
+ return CustomMetricCondition(
484
+ self._project_path,
485
+ self._filters,
486
+ self._name,
487
+ self._calculation,
488
+ threshold,
489
+ "below",
490
+ )
491
+
492
+ def should_be_above(self, threshold: float) -> "CustomMetricCondition":
493
+ return CustomMetricCondition(
494
+ self._project_path,
495
+ self._filters,
496
+ self._name,
497
+ self._calculation,
498
+ threshold,
499
+ "above",
500
+ )
501
+
502
+ def should_satisfy(
503
+ self, assertion: Callable[[float, ClassInfo], bool]
504
+ ) -> "CustomAssertionCondition":
505
+ return CustomAssertionCondition(
506
+ self._project_path,
507
+ self._filters,
508
+ self._name,
509
+ self._calculation,
510
+ assertion,
511
+ )
512
+
513
+
514
+ class CustomMetricCondition:
515
+ """Checkable for custom metric thresholds."""
516
+
517
+ def __init__(
518
+ self,
519
+ project_path: str | None,
520
+ filters: list[Filter],
521
+ name: str,
522
+ calculation: Callable[[ClassInfo], float],
523
+ threshold: float,
524
+ comparison: MetricComparison,
525
+ ) -> None:
526
+ self._project_path = project_path
527
+ self._filters = filters
528
+ self._name = name
529
+ self._calculation = calculation
530
+ self._threshold = threshold
531
+ self._comparison = comparison
532
+
533
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
534
+ classes = _get_filtered_classes(self._project_path, self._filters)
535
+ violations: list[Violation] = []
536
+
537
+ for cls in classes:
538
+ value = self._calculation(cls)
539
+ if check_threshold(value, self._threshold, self._comparison):
540
+ violations.append(
541
+ MetricViolation(
542
+ class_name=cls.name,
543
+ file_path=cls.file_path,
544
+ metric_name=self._name,
545
+ metric_value=value,
546
+ threshold=self._threshold,
547
+ comparison=self._comparison,
548
+ )
549
+ )
550
+
551
+ return violations
552
+
553
+
554
+ class CustomAssertionCondition:
555
+ """Checkable for custom metric assertions."""
556
+
557
+ def __init__(
558
+ self,
559
+ project_path: str | None,
560
+ filters: list[Filter],
561
+ name: str,
562
+ calculation: Callable[[ClassInfo], float],
563
+ assertion: Callable[[float, ClassInfo], bool],
564
+ ) -> None:
565
+ self._project_path = project_path
566
+ self._filters = filters
567
+ self._name = name
568
+ self._calculation = calculation
569
+ self._assertion = assertion
570
+
571
+ def check(self, options: CheckOptions | None = None) -> list[Violation]:
572
+ classes = _get_filtered_classes(self._project_path, self._filters)
573
+ violations: list[Violation] = []
574
+
575
+ for cls in classes:
576
+ value = self._calculation(cls)
577
+ if not self._assertion(value, cls):
578
+ violations.append(
579
+ MetricViolation(
580
+ class_name=cls.name,
581
+ file_path=cls.file_path,
582
+ metric_name=self._name,
583
+ metric_value=value,
584
+ threshold=0,
585
+ comparison="equal",
586
+ )
587
+ )
588
+
589
+ return violations
File without changes
File without changes
@@ -0,0 +1,3 @@
1
+ from archunitpython.slices.fluentapi.slices import project_slices
2
+
3
+ __all__ = ["project_slices"]
@@ -0,0 +1,13 @@
1
+ from archunitpython.slices.assertion.admissible_edges import (
2
+ CoherenceOptions,
3
+ ViolatingEdge,
4
+ gather_positive_violations,
5
+ gather_violations,
6
+ )
7
+
8
+ __all__ = [
9
+ "CoherenceOptions",
10
+ "ViolatingEdge",
11
+ "gather_positive_violations",
12
+ "gather_violations",
13
+ ]