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.
- archunitpython/__init__.py +45 -0
- archunitpython/common/__init__.py +18 -0
- archunitpython/common/assertion/__init__.py +3 -0
- archunitpython/common/assertion/violation.py +21 -0
- archunitpython/common/error/__init__.py +3 -0
- archunitpython/common/error/errors.py +13 -0
- archunitpython/common/extraction/__init__.py +13 -0
- archunitpython/common/extraction/extract_graph.py +345 -0
- archunitpython/common/extraction/graph.py +39 -0
- archunitpython/common/fluentapi/__init__.py +3 -0
- archunitpython/common/fluentapi/checkable.py +28 -0
- archunitpython/common/logging/__init__.py +3 -0
- archunitpython/common/logging/types.py +18 -0
- archunitpython/common/pattern_matching.py +80 -0
- archunitpython/common/projection/__init__.py +30 -0
- archunitpython/common/projection/cycles/__init__.py +4 -0
- archunitpython/common/projection/cycles/cycle_utils.py +49 -0
- archunitpython/common/projection/cycles/cycles.py +26 -0
- archunitpython/common/projection/cycles/johnsons_apsp.py +110 -0
- archunitpython/common/projection/cycles/model.py +22 -0
- archunitpython/common/projection/cycles/tarjan_scc.py +86 -0
- archunitpython/common/projection/edge_projections.py +36 -0
- archunitpython/common/projection/project_cycles.py +85 -0
- archunitpython/common/projection/project_edges.py +43 -0
- archunitpython/common/projection/project_nodes.py +49 -0
- archunitpython/common/projection/types.py +40 -0
- archunitpython/common/regex_factory.py +76 -0
- archunitpython/common/types.py +29 -0
- archunitpython/common/util/__init__.py +3 -0
- archunitpython/common/util/declaration_detector.py +115 -0
- archunitpython/common/util/logger.py +100 -0
- archunitpython/files/__init__.py +3 -0
- archunitpython/files/assertion/__init__.py +28 -0
- archunitpython/files/assertion/custom_file_logic.py +107 -0
- archunitpython/files/assertion/cycle_free.py +29 -0
- archunitpython/files/assertion/depend_on_files.py +67 -0
- archunitpython/files/assertion/matching_files.py +64 -0
- archunitpython/files/fluentapi/__init__.py +3 -0
- archunitpython/files/fluentapi/files.py +403 -0
- archunitpython/metrics/__init__.py +3 -0
- archunitpython/metrics/assertion/__init__.py +0 -0
- archunitpython/metrics/assertion/metric_thresholds.py +51 -0
- archunitpython/metrics/calculation/__init__.py +0 -0
- archunitpython/metrics/calculation/count.py +148 -0
- archunitpython/metrics/calculation/distance.py +110 -0
- archunitpython/metrics/calculation/lcom.py +177 -0
- archunitpython/metrics/common/__init__.py +19 -0
- archunitpython/metrics/common/types.py +67 -0
- archunitpython/metrics/extraction/__init__.py +0 -0
- archunitpython/metrics/extraction/extract_class_info.py +246 -0
- archunitpython/metrics/fluentapi/__init__.py +3 -0
- archunitpython/metrics/fluentapi/export_utils.py +89 -0
- archunitpython/metrics/fluentapi/metrics.py +589 -0
- archunitpython/metrics/projection/__init__.py +0 -0
- archunitpython/py.typed +0 -0
- archunitpython/slices/__init__.py +3 -0
- archunitpython/slices/assertion/__init__.py +13 -0
- archunitpython/slices/assertion/admissible_edges.py +108 -0
- archunitpython/slices/fluentapi/__init__.py +3 -0
- archunitpython/slices/fluentapi/slices.py +220 -0
- archunitpython/slices/projection/__init__.py +8 -0
- archunitpython/slices/projection/slicing_projections.py +128 -0
- archunitpython/slices/uml/__init__.py +4 -0
- archunitpython/slices/uml/export_diagram.py +31 -0
- archunitpython/slices/uml/generate_rules.py +71 -0
- archunitpython/testing/__init__.py +3 -0
- archunitpython/testing/assertion.py +47 -0
- archunitpython/testing/common/__init__.py +4 -0
- archunitpython/testing/common/color_utils.py +57 -0
- archunitpython/testing/common/violation_factory.py +97 -0
- archunitpython/testing/pytest_plugin/__init__.py +0 -0
- archunitpython-1.0.0.dist-info/METADATA +660 -0
- archunitpython-1.0.0.dist-info/RECORD +75 -0
- archunitpython-1.0.0.dist-info/WHEEL +4 -0
- 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
|
archunitpython/py.typed
ADDED
|
File without changes
|
|
@@ -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
|
+
]
|