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,403 @@
|
|
|
1
|
+
"""Fluent API builder chain for file-level architecture rules.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
project_files('src/')
|
|
5
|
+
.in_folder('**/services/**')
|
|
6
|
+
.should()
|
|
7
|
+
.have_no_cycles()
|
|
8
|
+
.check()
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from archunitpython.common.assertion.violation import EmptyTestViolation, Violation
|
|
16
|
+
from archunitpython.common.extraction.extract_graph import extract_graph
|
|
17
|
+
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
18
|
+
from archunitpython.common.pattern_matching import matches_all_patterns
|
|
19
|
+
from archunitpython.common.projection.edge_projections import per_internal_edge
|
|
20
|
+
from archunitpython.common.projection.project_cycles import project_cycles
|
|
21
|
+
from archunitpython.common.projection.project_edges import project_edges
|
|
22
|
+
from archunitpython.common.projection.project_nodes import project_to_nodes
|
|
23
|
+
from archunitpython.common.projection.types import ProjectedNode
|
|
24
|
+
from archunitpython.common.regex_factory import RegexFactory
|
|
25
|
+
from archunitpython.common.types import Filter, Pattern
|
|
26
|
+
from archunitpython.files.assertion.custom_file_logic import (
|
|
27
|
+
CustomFileCondition,
|
|
28
|
+
gather_custom_file_violations,
|
|
29
|
+
)
|
|
30
|
+
from archunitpython.files.assertion.cycle_free import gather_cycle_violations
|
|
31
|
+
from archunitpython.files.assertion.depend_on_files import gather_depend_on_file_violations
|
|
32
|
+
from archunitpython.files.assertion.matching_files import gather_regex_matching_violations
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def project_files(project_path: str | None = None) -> "FileConditionBuilder":
|
|
36
|
+
"""Entry point for file-level architecture rules.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
project_path: Root directory of the project to analyze.
|
|
40
|
+
Defaults to current working directory.
|
|
41
|
+
"""
|
|
42
|
+
return FileConditionBuilder(project_path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Alias
|
|
46
|
+
files = project_files
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FileConditionBuilder:
|
|
50
|
+
"""Initial builder for file rules - select files to apply rules to."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, project_path: str | None = None) -> None:
|
|
53
|
+
self._project_path = project_path
|
|
54
|
+
self._filters: list[Filter] = []
|
|
55
|
+
|
|
56
|
+
def with_name(self, name: Pattern) -> "FilesShouldCondition":
|
|
57
|
+
"""Filter files by filename pattern."""
|
|
58
|
+
self._filters.append(RegexFactory.filename_matcher(name))
|
|
59
|
+
return FilesShouldCondition(self._project_path, list(self._filters))
|
|
60
|
+
|
|
61
|
+
def in_folder(self, folder: Pattern) -> "FilesShouldCondition":
|
|
62
|
+
"""Filter files by folder pattern."""
|
|
63
|
+
self._filters.append(RegexFactory.folder_matcher(folder))
|
|
64
|
+
return FilesShouldCondition(self._project_path, list(self._filters))
|
|
65
|
+
|
|
66
|
+
def in_path(self, path: Pattern) -> "FilesShouldCondition":
|
|
67
|
+
"""Filter files by full path pattern."""
|
|
68
|
+
self._filters.append(RegexFactory.path_matcher(path))
|
|
69
|
+
return FilesShouldCondition(self._project_path, list(self._filters))
|
|
70
|
+
|
|
71
|
+
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
|
|
72
|
+
"""Begin positive assertion (files SHOULD ...)."""
|
|
73
|
+
return PositiveMatchPatternFileConditionBuilder(
|
|
74
|
+
self._project_path, list(self._filters)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
|
|
78
|
+
"""Begin negative assertion (files SHOULD NOT ...)."""
|
|
79
|
+
return NegatedMatchPatternFileConditionBuilder(
|
|
80
|
+
self._project_path, list(self._filters)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FilesShouldCondition:
|
|
85
|
+
"""Intermediate builder that allows adding more filters or moving to assertions."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
88
|
+
self._project_path = project_path
|
|
89
|
+
self._filters = filters
|
|
90
|
+
|
|
91
|
+
def with_name(self, name: Pattern) -> "FilesShouldCondition":
|
|
92
|
+
"""Add a filename pattern filter."""
|
|
93
|
+
self._filters.append(RegexFactory.filename_matcher(name))
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def in_folder(self, folder: Pattern) -> "FilesShouldCondition":
|
|
97
|
+
"""Add a folder pattern filter."""
|
|
98
|
+
self._filters.append(RegexFactory.folder_matcher(folder))
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def in_path(self, path: Pattern) -> "FilesShouldCondition":
|
|
102
|
+
"""Add a full path pattern filter."""
|
|
103
|
+
self._filters.append(RegexFactory.path_matcher(path))
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def should(self) -> "PositiveMatchPatternFileConditionBuilder":
|
|
107
|
+
"""Begin positive assertion (files SHOULD ...)."""
|
|
108
|
+
return PositiveMatchPatternFileConditionBuilder(
|
|
109
|
+
self._project_path, list(self._filters)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def should_not(self) -> "NegatedMatchPatternFileConditionBuilder":
|
|
113
|
+
"""Begin negative assertion (files SHOULD NOT ...)."""
|
|
114
|
+
return NegatedMatchPatternFileConditionBuilder(
|
|
115
|
+
self._project_path, list(self._filters)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PositiveMatchPatternFileConditionBuilder:
|
|
120
|
+
"""Positive assertion builder - files SHOULD have certain properties."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
123
|
+
self._project_path = project_path
|
|
124
|
+
self._filters = filters
|
|
125
|
+
|
|
126
|
+
def have_no_cycles(self) -> "CycleFreeFileCondition":
|
|
127
|
+
"""Assert that files have no circular dependencies."""
|
|
128
|
+
return CycleFreeFileCondition(self._project_path, self._filters)
|
|
129
|
+
|
|
130
|
+
def depend_on_files(self) -> "DependOnFileConditionBuilder":
|
|
131
|
+
"""Begin dependency assertion - files SHOULD depend on ..."""
|
|
132
|
+
return DependOnFileConditionBuilder(
|
|
133
|
+
self._project_path, self._filters, is_negated=False
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def be_in_folder(self, folder: Pattern) -> "MatchPatternFileCondition":
|
|
137
|
+
"""Assert that files are in a certain folder."""
|
|
138
|
+
return MatchPatternFileCondition(
|
|
139
|
+
self._project_path,
|
|
140
|
+
self._filters,
|
|
141
|
+
[RegexFactory.folder_matcher(folder)],
|
|
142
|
+
is_negated=False,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def have_name(self, name: Pattern) -> "MatchPatternFileCondition":
|
|
146
|
+
"""Assert that files match a name pattern."""
|
|
147
|
+
return MatchPatternFileCondition(
|
|
148
|
+
self._project_path,
|
|
149
|
+
self._filters,
|
|
150
|
+
[RegexFactory.filename_matcher(name)],
|
|
151
|
+
is_negated=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def be_in_path(self, path: Pattern) -> "MatchPatternFileCondition":
|
|
155
|
+
"""Assert that files match a path pattern."""
|
|
156
|
+
return MatchPatternFileCondition(
|
|
157
|
+
self._project_path,
|
|
158
|
+
self._filters,
|
|
159
|
+
[RegexFactory.path_matcher(path)],
|
|
160
|
+
is_negated=False,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def adhere_to(
|
|
164
|
+
self, condition: CustomFileCondition, message: str
|
|
165
|
+
) -> "CustomFileCheckableCondition":
|
|
166
|
+
"""Assert that files satisfy a custom condition."""
|
|
167
|
+
return CustomFileCheckableCondition(
|
|
168
|
+
self._project_path, self._filters, condition, message, is_negated=False
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class NegatedMatchPatternFileConditionBuilder:
|
|
173
|
+
"""Negative assertion builder - files SHOULD NOT have certain properties."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
176
|
+
self._project_path = project_path
|
|
177
|
+
self._filters = filters
|
|
178
|
+
|
|
179
|
+
def depend_on_files(self) -> "DependOnFileConditionBuilder":
|
|
180
|
+
"""Begin dependency assertion - files SHOULD NOT depend on ..."""
|
|
181
|
+
return DependOnFileConditionBuilder(
|
|
182
|
+
self._project_path, self._filters, is_negated=True
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def be_in_folder(self, folder: Pattern) -> "MatchPatternFileCondition":
|
|
186
|
+
"""Assert that files are NOT in a certain folder."""
|
|
187
|
+
return MatchPatternFileCondition(
|
|
188
|
+
self._project_path,
|
|
189
|
+
self._filters,
|
|
190
|
+
[RegexFactory.folder_matcher(folder)],
|
|
191
|
+
is_negated=True,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def have_name(self, name: Pattern) -> "MatchPatternFileCondition":
|
|
195
|
+
"""Assert that files do NOT match a name pattern."""
|
|
196
|
+
return MatchPatternFileCondition(
|
|
197
|
+
self._project_path,
|
|
198
|
+
self._filters,
|
|
199
|
+
[RegexFactory.filename_matcher(name)],
|
|
200
|
+
is_negated=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def be_in_path(self, path: Pattern) -> "MatchPatternFileCondition":
|
|
204
|
+
"""Assert that files do NOT match a path pattern."""
|
|
205
|
+
return MatchPatternFileCondition(
|
|
206
|
+
self._project_path,
|
|
207
|
+
self._filters,
|
|
208
|
+
[RegexFactory.path_matcher(path)],
|
|
209
|
+
is_negated=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def adhere_to(
|
|
213
|
+
self, condition: CustomFileCondition, message: str
|
|
214
|
+
) -> "CustomFileCheckableCondition":
|
|
215
|
+
"""Assert that files do NOT satisfy a custom condition."""
|
|
216
|
+
return CustomFileCheckableCondition(
|
|
217
|
+
self._project_path, self._filters, condition, message, is_negated=True
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class DependOnFileConditionBuilder:
|
|
222
|
+
"""Configure dependency target patterns."""
|
|
223
|
+
|
|
224
|
+
def __init__(
|
|
225
|
+
self, project_path: str | None, filters: list[Filter], is_negated: bool
|
|
226
|
+
) -> None:
|
|
227
|
+
self._project_path = project_path
|
|
228
|
+
self._filters = filters
|
|
229
|
+
self._is_negated = is_negated
|
|
230
|
+
self._object_filters: list[Filter] = []
|
|
231
|
+
|
|
232
|
+
def with_name(self, name: Pattern) -> "DependOnFileCondition":
|
|
233
|
+
"""Target files matching a name pattern."""
|
|
234
|
+
self._object_filters.append(RegexFactory.filename_matcher(name))
|
|
235
|
+
return DependOnFileCondition(
|
|
236
|
+
self._project_path,
|
|
237
|
+
self._filters,
|
|
238
|
+
list(self._object_filters),
|
|
239
|
+
self._is_negated,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def in_folder(self, folder: Pattern) -> "DependOnFileCondition":
|
|
243
|
+
"""Target files in a folder pattern."""
|
|
244
|
+
self._object_filters.append(RegexFactory.folder_matcher(folder))
|
|
245
|
+
return DependOnFileCondition(
|
|
246
|
+
self._project_path,
|
|
247
|
+
self._filters,
|
|
248
|
+
list(self._object_filters),
|
|
249
|
+
self._is_negated,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def in_path(self, path: Pattern) -> "DependOnFileCondition":
|
|
253
|
+
"""Target files matching a path pattern."""
|
|
254
|
+
self._object_filters.append(RegexFactory.path_matcher(path))
|
|
255
|
+
return DependOnFileCondition(
|
|
256
|
+
self._project_path,
|
|
257
|
+
self._filters,
|
|
258
|
+
list(self._object_filters),
|
|
259
|
+
self._is_negated,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _get_filtered_nodes(
|
|
264
|
+
project_path: str | None,
|
|
265
|
+
filters: list[Filter],
|
|
266
|
+
options: CheckOptions | None,
|
|
267
|
+
) -> list[ProjectedNode]:
|
|
268
|
+
"""Extract graph and get nodes matching filters."""
|
|
269
|
+
graph = extract_graph(project_path, options=options)
|
|
270
|
+
nodes = project_to_nodes(graph)
|
|
271
|
+
if not filters:
|
|
272
|
+
return nodes
|
|
273
|
+
return [n for n in nodes if matches_all_patterns(n.label, filters)]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _check_empty_test(
|
|
277
|
+
filtered_items: Sequence[object],
|
|
278
|
+
filters: list[Filter],
|
|
279
|
+
is_negated: bool,
|
|
280
|
+
options: CheckOptions | None,
|
|
281
|
+
) -> list[Violation] | None:
|
|
282
|
+
"""Check for empty test condition."""
|
|
283
|
+
if not filtered_items and not (options and options.allow_empty_tests):
|
|
284
|
+
return [
|
|
285
|
+
EmptyTestViolation(
|
|
286
|
+
filters=filters,
|
|
287
|
+
message="No files found matching the specified patterns",
|
|
288
|
+
is_negated=is_negated,
|
|
289
|
+
)
|
|
290
|
+
]
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class CycleFreeFileCondition:
|
|
295
|
+
"""Checkable that verifies no cycles exist among filtered files."""
|
|
296
|
+
|
|
297
|
+
def __init__(self, project_path: str | None, filters: list[Filter]) -> None:
|
|
298
|
+
self._project_path = project_path
|
|
299
|
+
self._filters = filters
|
|
300
|
+
|
|
301
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
302
|
+
graph = extract_graph(self._project_path, options=options)
|
|
303
|
+
edges = project_edges(graph, per_internal_edge())
|
|
304
|
+
|
|
305
|
+
# Filter edges to only those involving our filtered files
|
|
306
|
+
if self._filters:
|
|
307
|
+
edges = [
|
|
308
|
+
e
|
|
309
|
+
for e in edges
|
|
310
|
+
if matches_all_patterns(e.source_label, self._filters)
|
|
311
|
+
and matches_all_patterns(e.target_label, self._filters)
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
empty = _check_empty_test(edges, self._filters, False, options)
|
|
315
|
+
if empty is not None:
|
|
316
|
+
return empty
|
|
317
|
+
|
|
318
|
+
cycles = project_cycles(edges)
|
|
319
|
+
return gather_cycle_violations(cycles)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class DependOnFileCondition:
|
|
323
|
+
"""Checkable that verifies file dependency rules."""
|
|
324
|
+
|
|
325
|
+
def __init__(
|
|
326
|
+
self,
|
|
327
|
+
project_path: str | None,
|
|
328
|
+
subject_filters: list[Filter],
|
|
329
|
+
object_filters: list[Filter],
|
|
330
|
+
is_negated: bool,
|
|
331
|
+
) -> None:
|
|
332
|
+
self._project_path = project_path
|
|
333
|
+
self._subject_filters = subject_filters
|
|
334
|
+
self._object_filters = object_filters
|
|
335
|
+
self._is_negated = is_negated
|
|
336
|
+
|
|
337
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
338
|
+
graph = extract_graph(self._project_path, options=options)
|
|
339
|
+
edges = project_edges(graph, per_internal_edge())
|
|
340
|
+
|
|
341
|
+
return gather_depend_on_file_violations(
|
|
342
|
+
edges,
|
|
343
|
+
self._subject_filters,
|
|
344
|
+
self._object_filters,
|
|
345
|
+
self._is_negated,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class MatchPatternFileCondition:
|
|
350
|
+
"""Checkable that verifies files match/don't match patterns."""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
project_path: str | None,
|
|
355
|
+
pre_filters: list[Filter],
|
|
356
|
+
check_filters: list[Filter],
|
|
357
|
+
is_negated: bool,
|
|
358
|
+
) -> None:
|
|
359
|
+
self._project_path = project_path
|
|
360
|
+
self._pre_filters = pre_filters
|
|
361
|
+
self._check_filters = check_filters
|
|
362
|
+
self._is_negated = is_negated
|
|
363
|
+
|
|
364
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
365
|
+
nodes = _get_filtered_nodes(self._project_path, self._pre_filters, options)
|
|
366
|
+
|
|
367
|
+
empty = _check_empty_test(nodes, self._pre_filters, self._is_negated, options)
|
|
368
|
+
if empty is not None:
|
|
369
|
+
return empty
|
|
370
|
+
|
|
371
|
+
return gather_regex_matching_violations(
|
|
372
|
+
nodes, self._check_filters, self._is_negated
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class CustomFileCheckableCondition:
|
|
377
|
+
"""Checkable that evaluates a custom condition on files."""
|
|
378
|
+
|
|
379
|
+
def __init__(
|
|
380
|
+
self,
|
|
381
|
+
project_path: str | None,
|
|
382
|
+
filters: list[Filter],
|
|
383
|
+
condition: CustomFileCondition,
|
|
384
|
+
message: str,
|
|
385
|
+
is_negated: bool,
|
|
386
|
+
) -> None:
|
|
387
|
+
self._project_path = project_path
|
|
388
|
+
self._filters = filters
|
|
389
|
+
self._condition = condition
|
|
390
|
+
self._message = message
|
|
391
|
+
self._is_negated = is_negated
|
|
392
|
+
|
|
393
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
394
|
+
graph = extract_graph(self._project_path, options=options)
|
|
395
|
+
nodes = project_to_nodes(graph)
|
|
396
|
+
|
|
397
|
+
return gather_custom_file_violations(
|
|
398
|
+
nodes,
|
|
399
|
+
self._condition,
|
|
400
|
+
self._message,
|
|
401
|
+
self._is_negated,
|
|
402
|
+
self._filters,
|
|
403
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Metric threshold violations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from archunitpython.common.assertion.violation import Violation
|
|
8
|
+
from archunitpython.metrics.common.types import MetricComparison
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class MetricViolation(Violation):
|
|
13
|
+
"""A class metric that exceeds its threshold."""
|
|
14
|
+
|
|
15
|
+
class_name: str
|
|
16
|
+
file_path: str
|
|
17
|
+
metric_name: str
|
|
18
|
+
metric_value: float
|
|
19
|
+
threshold: float
|
|
20
|
+
comparison: MetricComparison
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileCountViolation(Violation):
|
|
25
|
+
"""A file-level metric that exceeds its threshold."""
|
|
26
|
+
|
|
27
|
+
file_path: str
|
|
28
|
+
metric_name: str
|
|
29
|
+
metric_value: float
|
|
30
|
+
threshold: float
|
|
31
|
+
comparison: MetricComparison
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_threshold(
|
|
35
|
+
value: float, threshold: float, comparison: MetricComparison
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""Check if a value violates a threshold.
|
|
38
|
+
|
|
39
|
+
Returns True if the value is a VIOLATION.
|
|
40
|
+
"""
|
|
41
|
+
if comparison == "below":
|
|
42
|
+
return value >= threshold
|
|
43
|
+
elif comparison == "above":
|
|
44
|
+
return value <= threshold
|
|
45
|
+
elif comparison == "equal":
|
|
46
|
+
return abs(value - threshold) > 1e-9
|
|
47
|
+
elif comparison == "below_equal":
|
|
48
|
+
return value > threshold
|
|
49
|
+
elif comparison == "above_equal":
|
|
50
|
+
return value < threshold
|
|
51
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Count metrics: method count, field count, lines of code, etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
from archunitpython.metrics.common.types import ClassInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MethodCountMetric:
|
|
11
|
+
"""Number of methods in a class."""
|
|
12
|
+
|
|
13
|
+
name = "MethodCount"
|
|
14
|
+
description = "Number of methods in a class"
|
|
15
|
+
|
|
16
|
+
def calculate(self, class_info: ClassInfo) -> float:
|
|
17
|
+
return float(len(class_info.methods))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FieldCountMetric:
|
|
21
|
+
"""Number of fields in a class."""
|
|
22
|
+
|
|
23
|
+
name = "FieldCount"
|
|
24
|
+
description = "Number of fields in a class"
|
|
25
|
+
|
|
26
|
+
def calculate(self, class_info: ClassInfo) -> float:
|
|
27
|
+
return float(len(class_info.fields))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LinesOfCodeMetric:
|
|
31
|
+
"""Number of non-blank, non-comment lines in a file."""
|
|
32
|
+
|
|
33
|
+
name = "LinesOfCode"
|
|
34
|
+
description = "Number of non-blank, non-comment lines"
|
|
35
|
+
|
|
36
|
+
def calculate_from_file(self, file_path: str) -> float:
|
|
37
|
+
try:
|
|
38
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
39
|
+
lines = f.readlines()
|
|
40
|
+
except (OSError, IOError):
|
|
41
|
+
return 0.0
|
|
42
|
+
|
|
43
|
+
count = 0
|
|
44
|
+
for line in lines:
|
|
45
|
+
stripped = line.strip()
|
|
46
|
+
if stripped and not stripped.startswith("#"):
|
|
47
|
+
count += 1
|
|
48
|
+
return float(count)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StatementCountMetric:
|
|
52
|
+
"""Number of AST statements in a file."""
|
|
53
|
+
|
|
54
|
+
name = "StatementCount"
|
|
55
|
+
description = "Number of statements in a file"
|
|
56
|
+
|
|
57
|
+
def calculate_from_file(self, file_path: str) -> float:
|
|
58
|
+
try:
|
|
59
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
60
|
+
source = f.read()
|
|
61
|
+
except (OSError, IOError):
|
|
62
|
+
return 0.0
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
tree = ast.parse(source)
|
|
66
|
+
except SyntaxError:
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
count = 0
|
|
70
|
+
for node in ast.walk(tree):
|
|
71
|
+
if isinstance(node, ast.stmt):
|
|
72
|
+
count += 1
|
|
73
|
+
return float(count)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ImportCountMetric:
|
|
77
|
+
"""Number of import statements in a file."""
|
|
78
|
+
|
|
79
|
+
name = "ImportCount"
|
|
80
|
+
description = "Number of import statements in a file"
|
|
81
|
+
|
|
82
|
+
def calculate_from_file(self, file_path: str) -> float:
|
|
83
|
+
try:
|
|
84
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
85
|
+
source = f.read()
|
|
86
|
+
except (OSError, IOError):
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
tree = ast.parse(source)
|
|
91
|
+
except SyntaxError:
|
|
92
|
+
return 0.0
|
|
93
|
+
|
|
94
|
+
count = 0
|
|
95
|
+
for node in ast.walk(tree):
|
|
96
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
97
|
+
count += 1
|
|
98
|
+
return float(count)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ClassCountMetric:
|
|
102
|
+
"""Number of class definitions in a file."""
|
|
103
|
+
|
|
104
|
+
name = "ClassCount"
|
|
105
|
+
description = "Number of class definitions in a file"
|
|
106
|
+
|
|
107
|
+
def calculate_from_file(self, file_path: str) -> float:
|
|
108
|
+
try:
|
|
109
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
110
|
+
source = f.read()
|
|
111
|
+
except (OSError, IOError):
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
tree = ast.parse(source)
|
|
116
|
+
except SyntaxError:
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
count = 0
|
|
120
|
+
for node in ast.walk(tree):
|
|
121
|
+
if isinstance(node, ast.ClassDef):
|
|
122
|
+
count += 1
|
|
123
|
+
return float(count)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class FunctionCountMetric:
|
|
127
|
+
"""Number of top-level function definitions in a file."""
|
|
128
|
+
|
|
129
|
+
name = "FunctionCount"
|
|
130
|
+
description = "Number of top-level function definitions in a file"
|
|
131
|
+
|
|
132
|
+
def calculate_from_file(self, file_path: str) -> float:
|
|
133
|
+
try:
|
|
134
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
135
|
+
source = f.read()
|
|
136
|
+
except (OSError, IOError):
|
|
137
|
+
return 0.0
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
tree = ast.parse(source)
|
|
141
|
+
except SyntaxError:
|
|
142
|
+
return 0.0
|
|
143
|
+
|
|
144
|
+
count = 0
|
|
145
|
+
for node in tree.body:
|
|
146
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
147
|
+
count += 1
|
|
148
|
+
return float(count)
|