modwire 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.
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def line_span(node: ast.AST) -> int:
11
+ return node.end_lineno - node.lineno + 1
12
+
13
+
14
+ def normalize_module_path(value: str) -> str:
15
+ return value.replace(".", "/").strip("/")
16
+
17
+
18
+ def import_path(node: ast.ImportFrom) -> str:
19
+ return f"{'.' * node.level}{node.module or ''}"
20
+
21
+
22
+ def normalized_import_path(
23
+ import_value: str,
24
+ is_relative: bool,
25
+ file_path: Path,
26
+ sources_root: Path,
27
+ ) -> str:
28
+ if not is_relative:
29
+ return normalize_module_path(import_value)
30
+
31
+ level = len(import_value) - len(import_value.lstrip("."))
32
+ module = import_value[level:]
33
+ package_dir = file_path.parent
34
+ for _ in range(max(level - 1, 0)):
35
+ package_dir = package_dir.parent
36
+
37
+ package_path = package_dir.relative_to(sources_root).as_posix()
38
+ module_path = normalize_module_path(module)
39
+ return "/".join(part for part in (package_path, module_path) if part)
40
+
41
+
42
+ def argument_counts(
43
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
44
+ *,
45
+ exclude_receiver: bool,
46
+ ) -> tuple[int, int]:
47
+ positional_args = [*node.args.posonlyargs, *node.args.args]
48
+ positional_required_count = len(positional_args) - len(node.args.defaults)
49
+ parameters = [
50
+ (arg.arg, index >= positional_required_count)
51
+ for index, arg in enumerate(positional_args)
52
+ ]
53
+ parameters.extend(
54
+ (arg.arg, default is not None)
55
+ for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults, strict=True)
56
+ )
57
+ if node.args.vararg is not None:
58
+ parameters.append((node.args.vararg.arg, True))
59
+ if node.args.kwarg is not None:
60
+ parameters.append((node.args.kwarg.arg, True))
61
+
62
+ if exclude_receiver and parameters and parameters[0][0] in {"self", "cls"}:
63
+ parameters = parameters[1:]
64
+
65
+ return len(parameters), sum(1 for _, is_optional in parameters if is_optional)
66
+
67
+
68
+ def has_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef, name: str) -> bool:
69
+ for decorator in node.decorator_list:
70
+ if isinstance(decorator, ast.Name) and decorator.id == name:
71
+ return True
72
+ if isinstance(decorator, ast.Attribute) and decorator.attr == name:
73
+ return True
74
+ return False
75
+
76
+
77
+ def node_name(node: ast.AST) -> str:
78
+ if isinstance(node, ast.Name):
79
+ return node.id
80
+ if isinstance(node, ast.Attribute):
81
+ parent_name = node_name(node.value)
82
+ return f"{parent_name}.{node.attr}" if parent_name else node.attr
83
+ return ""
84
+
85
+
86
+ def method_is_abstract(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
87
+ return has_decorator(node, "abstractmethod")
88
+
89
+
90
+ def class_is_abstract(node: ast.ClassDef) -> bool:
91
+ return any(node_name(base) in {"ABC", "abc.ABC"} for base in node.bases) or any(
92
+ isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
93
+ and method_is_abstract(child)
94
+ for child in node.body
95
+ )
96
+
97
+
98
+ def visibility_intent(name: str) -> str:
99
+ if name.startswith("__") and not (name.startswith("__") and name.endswith("__")):
100
+ return "private"
101
+ if name.startswith("_") and not (name.startswith("__") and name.endswith("__")):
102
+ return "protected"
103
+ return "public"
104
+
105
+
106
+ def annotation_is_optional(node: ast.AST | None) -> bool:
107
+ if node is None:
108
+ return False
109
+ if isinstance(node, ast.Constant) and node.value is None:
110
+ return True
111
+ if isinstance(node, ast.Name):
112
+ return node.id in {"None", "Optional"}
113
+ if isinstance(node, ast.Attribute):
114
+ return node.attr == "Optional"
115
+ if isinstance(node, ast.Subscript):
116
+ return annotation_is_optional(node.value) or annotation_is_optional(node.slice)
117
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
118
+ return annotation_is_optional(node.left) or annotation_is_optional(node.right)
119
+ if isinstance(node, ast.Tuple):
120
+ return any(annotation_is_optional(element) for element in node.elts)
121
+ return False
122
+
123
+
124
+ def value_is_none(node: ast.AST | None) -> bool:
125
+ return isinstance(node, ast.Constant) and node.value is None
126
+
127
+
128
+ def optional_constructor_parameters(
129
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
130
+ ) -> set[str]:
131
+ positional_args = [*node.args.posonlyargs, *node.args.args]
132
+ defaults_by_name = {}
133
+ if node.args.defaults:
134
+ defaults_by_name = {
135
+ arg.arg: default
136
+ for arg, default in zip(
137
+ positional_args[-len(node.args.defaults) :],
138
+ node.args.defaults,
139
+ strict=True,
140
+ )
141
+ }
142
+ optional_names = {
143
+ arg.arg
144
+ for arg in [*positional_args, *node.args.kwonlyargs]
145
+ if annotation_is_optional(arg.annotation)
146
+ }
147
+ optional_names.update(
148
+ name for name, default in defaults_by_name.items() if value_is_none(default)
149
+ )
150
+ optional_names.update(
151
+ arg.arg
152
+ for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults, strict=True)
153
+ if value_is_none(default)
154
+ )
155
+ return optional_names
156
+
157
+
158
+ def class_properties(node: ast.ClassDef) -> list[dict[str, object]]:
159
+ properties: dict[str, bool] = {}
160
+
161
+ def add_property(name: str, is_optional: bool) -> None:
162
+ properties[name] = properties.get(name, False) or is_optional
163
+
164
+ for child in node.body:
165
+ if isinstance(child, ast.AnnAssign):
166
+ target = child.target
167
+ if isinstance(target, ast.Name):
168
+ add_property(
169
+ target.id,
170
+ annotation_is_optional(child.annotation)
171
+ or value_is_none(child.value),
172
+ )
173
+ continue
174
+ if isinstance(child, ast.Assign):
175
+ for target in child.targets:
176
+ if isinstance(target, ast.Name):
177
+ add_property(target.id, value_is_none(child.value))
178
+ continue
179
+ if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
180
+ continue
181
+
182
+ optional_parameter_names = optional_constructor_parameters(child)
183
+ for descendant in ast.walk(child):
184
+ if isinstance(descendant, ast.AnnAssign):
185
+ target = descendant.target
186
+ if (
187
+ isinstance(target, ast.Attribute)
188
+ and isinstance(target.value, ast.Name)
189
+ and target.value.id in {"self", "cls"}
190
+ ):
191
+ add_property(
192
+ target.attr,
193
+ annotation_is_optional(descendant.annotation)
194
+ or value_is_none(descendant.value),
195
+ )
196
+ continue
197
+ if not isinstance(descendant, ast.Assign):
198
+ continue
199
+ is_optional = value_is_none(descendant.value) or (
200
+ isinstance(descendant.value, ast.Name)
201
+ and descendant.value.id in optional_parameter_names
202
+ )
203
+ for target in descendant.targets:
204
+ if (
205
+ isinstance(target, ast.Attribute)
206
+ and isinstance(target.value, ast.Name)
207
+ and target.value.id in {"self", "cls"}
208
+ ):
209
+ add_property(target.attr, is_optional)
210
+
211
+ return [
212
+ {"name": name, "is_optional": is_optional}
213
+ for name, is_optional in properties.items()
214
+ ]
215
+
216
+
217
+ def method_definition(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, object]:
218
+ declared_args, optional_args = argument_counts(
219
+ node,
220
+ exclude_receiver=not has_decorator(node, "staticmethod"),
221
+ )
222
+ return {
223
+ "name": node.name,
224
+ "visibility": "public",
225
+ "visibility_intent": visibility_intent(node.name),
226
+ "line_count": line_span(node),
227
+ "declared_args": declared_args,
228
+ "optional_args": optional_args,
229
+ }
230
+
231
+
232
+ def class_definition(node: ast.ClassDef) -> dict[str, object]:
233
+ return {
234
+ "name": node.name,
235
+ "visibility": "public",
236
+ "visibility_intent": visibility_intent(node.name),
237
+ "methods": [
238
+ method_definition(child)
239
+ for child in node.body
240
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
241
+ ],
242
+ "properties": class_properties(node),
243
+ "line_count": line_span(node),
244
+ }
245
+
246
+
247
+ def abstract_class_definition(node: ast.ClassDef) -> dict[str, object]:
248
+ methods = [
249
+ method_definition(child)
250
+ for child in node.body
251
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
252
+ ]
253
+ abstract_method_names = {
254
+ child.name
255
+ for child in node.body
256
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
257
+ and method_is_abstract(child)
258
+ }
259
+ return {
260
+ "name": node.name,
261
+ "visibility": "public",
262
+ "visibility_intent": visibility_intent(node.name),
263
+ "abstract_methods": [
264
+ method for method in methods if method["name"] in abstract_method_names
265
+ ],
266
+ "concrete_methods": [
267
+ method for method in methods if method["name"] not in abstract_method_names
268
+ ],
269
+ "properties": class_properties(node),
270
+ "line_count": line_span(node),
271
+ }
272
+
273
+
274
+ def function_definition(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, object]:
275
+ declared_args, optional_args = argument_counts(node, exclude_receiver=False)
276
+ return {
277
+ "name": node.name,
278
+ "visibility": "public",
279
+ "visibility_intent": visibility_intent(node.name),
280
+ "line_count": line_span(node),
281
+ "declared_args": declared_args,
282
+ "optional_args": optional_args,
283
+ }
284
+
285
+
286
+ def code_line_count(content: str) -> int:
287
+ return sum(
288
+ 1
289
+ for line in content.splitlines()
290
+ if line.strip() and not line.strip().startswith("#")
291
+ )
292
+
293
+
294
+ def import_join_key(import_path_value: str) -> str:
295
+ return normalize_module_path(import_path_value).rsplit("/", 1)[0]
296
+
297
+
298
+ def collect_imports(
299
+ tree: ast.Module,
300
+ path: Path,
301
+ sources_root: Path,
302
+ ) -> list[dict[str, object]]:
303
+ imports = []
304
+ for statement_id, node in enumerate(ast.walk(tree), start=1):
305
+ if isinstance(node, ast.Import):
306
+ for alias in node.names:
307
+ imports.append(
308
+ {
309
+ "path": alias.name,
310
+ "is_relative": False,
311
+ "normalized_path": normalized_import_path(
312
+ alias.name,
313
+ False,
314
+ path,
315
+ sources_root,
316
+ ),
317
+ "imported_name": "",
318
+ "is_aliased": alias.asname is not None,
319
+ "crossing_type": "module",
320
+ "file_barrier_crossed": True,
321
+ "statement_id": statement_id,
322
+ "join_key": import_join_key(alias.name),
323
+ "uses_joined_import": False,
324
+ }
325
+ )
326
+ continue
327
+ if isinstance(node, ast.ImportFrom):
328
+ node_path = import_path(node)
329
+ for alias in node.names:
330
+ imports.append(
331
+ {
332
+ "path": node_path,
333
+ "is_relative": node.level > 0,
334
+ "normalized_path": normalized_import_path(
335
+ node_path,
336
+ node.level > 0,
337
+ path,
338
+ sources_root,
339
+ ),
340
+ "imported_name": alias.name,
341
+ "is_aliased": alias.asname is not None,
342
+ "crossing_type": "symbol",
343
+ "file_barrier_crossed": True,
344
+ "statement_id": statement_id,
345
+ "join_key": node_path,
346
+ "uses_joined_import": True,
347
+ }
348
+ )
349
+ return imports
350
+
351
+
352
+ def extract_file(path: Path, sources_root: Path) -> dict[str, object]:
353
+ content = path.read_text(encoding="utf-8")
354
+ tree = ast.parse(content, filename=str(path))
355
+ class_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
356
+ classes = [
357
+ class_definition(node)
358
+ for node in class_nodes
359
+ if not class_is_abstract(node)
360
+ ]
361
+ abstract_classes = [
362
+ abstract_class_definition(node)
363
+ for node in class_nodes
364
+ if class_is_abstract(node)
365
+ ]
366
+ functions = [
367
+ function_definition(node)
368
+ for node in tree.body
369
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
370
+ ]
371
+
372
+ return {
373
+ "imports": collect_imports(tree, path, sources_root),
374
+ "classes": classes,
375
+ "interfaces": [],
376
+ "types": [],
377
+ "abstract_classes": abstract_classes,
378
+ "functions": functions,
379
+ "line_count": len(content.splitlines()),
380
+ "code_line_count": code_line_count(content),
381
+ "public_symbol_count": sum(
382
+ 1
383
+ for node in tree.body
384
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
385
+ and not node.name.startswith("_")
386
+ ),
387
+ }
388
+
389
+
390
+ def main() -> int:
391
+ file_path = Path(sys.argv[1]).resolve()
392
+ sources_root = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else Path.cwd()
393
+ print(json.dumps(extract_file(file_path, sources_root)))
394
+ return 0
395
+
396
+
397
+ if __name__ == "__main__":
398
+ raise SystemExit(main())