skrypt-ai 0.3.3 → 0.4.0

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 (97) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.ts +0 -1
  3. package/dist/auth/index.js +3 -5
  4. package/dist/autofix/index.js +15 -3
  5. package/dist/cli.js +19 -4
  6. package/dist/commands/check-links.js +164 -174
  7. package/dist/commands/deploy.js +5 -2
  8. package/dist/commands/generate.js +206 -199
  9. package/dist/commands/i18n.js +3 -20
  10. package/dist/commands/init.js +47 -40
  11. package/dist/commands/lint.js +3 -20
  12. package/dist/commands/mcp.js +125 -122
  13. package/dist/commands/monitor.js +125 -108
  14. package/dist/commands/review-pr.js +1 -1
  15. package/dist/commands/sdk.js +1 -1
  16. package/dist/config/loader.js +21 -2
  17. package/dist/generator/organizer.d.ts +3 -0
  18. package/dist/generator/organizer.js +4 -9
  19. package/dist/generator/writer.js +2 -10
  20. package/dist/github/pr-comments.js +21 -8
  21. package/dist/plugins/index.js +1 -0
  22. package/dist/scanner/index.js +8 -2
  23. package/dist/template/docs.json +2 -1
  24. package/dist/template/next.config.mjs +3 -1
  25. package/dist/template/package.json +17 -14
  26. package/dist/template/public/favicon.svg +4 -0
  27. package/dist/template/public/search-index.json +1 -1
  28. package/dist/template/scripts/build-search-index.mjs +120 -25
  29. package/dist/template/src/app/api/chat/route.ts +11 -3
  30. package/dist/template/src/app/docs/README.md +28 -0
  31. package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
  32. package/dist/template/src/app/docs/auth/page.mdx +589 -0
  33. package/dist/template/src/app/docs/autofix/page.mdx +624 -0
  34. package/dist/template/src/app/docs/cli/page.mdx +217 -0
  35. package/dist/template/src/app/docs/config/page.mdx +428 -0
  36. package/dist/template/src/app/docs/configuration/page.mdx +86 -0
  37. package/dist/template/src/app/docs/deployment/page.mdx +112 -0
  38. package/dist/template/src/app/docs/error.tsx +20 -0
  39. package/dist/template/src/app/docs/generator/generator.md +504 -0
  40. package/dist/template/src/app/docs/generator/organizer.md +779 -0
  41. package/dist/template/src/app/docs/generator/page.mdx +613 -0
  42. package/dist/template/src/app/docs/github/page.mdx +502 -0
  43. package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
  44. package/dist/template/src/app/docs/llm/index.md +471 -0
  45. package/dist/template/src/app/docs/llm/page.mdx +428 -0
  46. package/dist/template/src/app/docs/llms-full.md +256 -0
  47. package/dist/template/src/app/docs/llms.txt +2971 -0
  48. package/dist/template/src/app/docs/not-found.tsx +23 -0
  49. package/dist/template/src/app/docs/page.mdx +0 -3
  50. package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
  51. package/dist/template/src/app/docs/pro/page.mdx +121 -0
  52. package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
  53. package/dist/template/src/app/docs/scanner/content-type.md +599 -0
  54. package/dist/template/src/app/docs/scanner/index.md +212 -0
  55. package/dist/template/src/app/docs/scanner/page.mdx +307 -0
  56. package/dist/template/src/app/docs/scanner/python.md +469 -0
  57. package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
  58. package/dist/template/src/app/docs/scanner/rust.md +325 -0
  59. package/dist/template/src/app/docs/scanner/typescript.md +201 -0
  60. package/dist/template/src/app/error.tsx +3 -3
  61. package/dist/template/src/app/icon.tsx +29 -0
  62. package/dist/template/src/app/layout.tsx +57 -7
  63. package/dist/template/src/app/not-found.tsx +35 -0
  64. package/dist/template/src/app/page.tsx +95 -11
  65. package/dist/template/src/components/ai-chat.tsx +26 -21
  66. package/dist/template/src/components/breadcrumbs.tsx +56 -12
  67. package/dist/template/src/components/copy-button.tsx +17 -3
  68. package/dist/template/src/components/docs-layout.tsx +202 -8
  69. package/dist/template/src/components/feedback.tsx +4 -2
  70. package/dist/template/src/components/footer.tsx +42 -0
  71. package/dist/template/src/components/header.tsx +56 -20
  72. package/dist/template/src/components/mdx/accordion.tsx +17 -13
  73. package/dist/template/src/components/mdx/callout.tsx +50 -37
  74. package/dist/template/src/components/mdx/card.tsx +24 -12
  75. package/dist/template/src/components/mdx/code-block.tsx +17 -3
  76. package/dist/template/src/components/mdx/code-group.tsx +78 -18
  77. package/dist/template/src/components/mdx/code-playground.tsx +3 -0
  78. package/dist/template/src/components/mdx/go-playground.tsx +3 -0
  79. package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
  80. package/dist/template/src/components/mdx/python-playground.tsx +2 -0
  81. package/dist/template/src/components/mdx/steps.tsx +6 -6
  82. package/dist/template/src/components/mdx/tabs.tsx +76 -8
  83. package/dist/template/src/components/page-header.tsx +19 -0
  84. package/dist/template/src/components/scroll-to-top.tsx +33 -0
  85. package/dist/template/src/components/search-dialog.tsx +251 -57
  86. package/dist/template/src/components/sidebar.tsx +137 -77
  87. package/dist/template/src/components/table-of-contents.tsx +29 -13
  88. package/dist/template/src/lib/highlight.ts +90 -31
  89. package/dist/template/src/lib/search.ts +14 -4
  90. package/dist/template/src/lib/theme-utils.ts +140 -0
  91. package/dist/template/src/styles/globals.css +397 -84
  92. package/dist/template/src/types/remark-gfm.d.ts +2 -0
  93. package/dist/utils/files.d.ts +9 -0
  94. package/dist/utils/files.js +33 -0
  95. package/dist/utils/validation.d.ts +4 -0
  96. package/dist/utils/validation.js +38 -0
  97. package/package.json +1 -4
@@ -0,0 +1,1056 @@
1
+ # Python parser
2
+
3
+ ## Functions
4
+
5
+ ### `get_docstring`
6
+
7
+ ```python
8
+ def get_docstring(node: ast.AST) -> str | None
9
+ ```
10
+
11
+ Use this to extract the docstring from any Python AST node (functions, classes, modules) when parsing or analyzing Python source code programmatically.
12
+
13
+ Returns the docstring as a `str` if one is present, or `None` if the node has no docstring.
14
+
15
+ ## Parameters
16
+
17
+ | Name | Type | Required | Description |
18
+ |------|------|----------|-------------|
19
+ | node | `ast.AST` | āœ… | Any parsed AST node — typically a `Module`, `FunctionDef`, `AsyncFunctionDef`, or `ClassDef` |
20
+
21
+ ## Returns
22
+
23
+ | Value | When |
24
+ |-------|------|
25
+ | `str` | The node has a leading string literal (docstring) |
26
+ | `None` | The node has no docstring, or the first statement is not a string expression |
27
+
28
+ ## Notes
29
+ - Works on any `ast.AST` node, but only `Module`, `FunctionDef`, `AsyncFunctionDef`, and `ClassDef` nodes can realistically contain docstrings per Python conventions.
30
+ - Strips surrounding quotes — returns the raw string content, not the literal source.
31
+ - Equivalent to calling `ast.get_docstring(node)` from the standard library.
32
+
33
+ **Example:**
34
+
35
+ ```python example.py
36
+ import ast
37
+
38
+ # Inline implementation of get_docstring
39
+ def get_docstring(node: ast.AST) -> "str | None":
40
+ """Extract docstring from a node if present."""
41
+ if not isinstance(node, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
42
+ return None
43
+ if not node.body:
44
+ return None
45
+ first_stmt = node.body[0]
46
+ if isinstance(first_stmt, ast.Expr) and isinstance(first_stmt.value, ast.Constant):
47
+ value = first_stmt.value.value
48
+ if isinstance(value, str):
49
+ return value
50
+ return None
51
+
52
+
53
+ # --- Example 1: Extract docstring from a function ---
54
+ function_source = '''
55
+ def greet(name: str) -> str:
56
+ """Return a friendly greeting for the given name."""
57
+ return f"Hello, {name}!"
58
+ '''
59
+
60
+ function_tree = ast.parse(function_source)
61
+ func_node = function_tree.body[0] # The FunctionDef node
62
+
63
+ func_docstring = get_docstring(func_node)
64
+ print("Function docstring:", func_docstring)
65
+ # Output: Function docstring: Return a friendly greeting for the given name.
66
+
67
+
68
+ # --- Example 2: Extract docstring from a class ---
69
+ class_source = '''
70
+ class UserAccount:
71
+ """Represents a user account in the system."""
72
+
73
+ def __init__(self, user_id: str):
74
+ self.user_id = user_id
75
+ '''
76
+
77
+ class_tree = ast.parse(class_source)
78
+ class_node = class_tree.body[0] # The ClassDef node
79
+
80
+ class_docstring = get_docstring(class_node)
81
+ print("Class docstring:", class_docstring)
82
+ # Output: Class docstring: Represents a user account in the system.
83
+
84
+
85
+ # --- Example 3: Returns None when no docstring is present ---
86
+ no_doc_source = '''
87
+ def add(a: int, b: int) -> int:
88
+ return a + b
89
+ '''
90
+
91
+ no_doc_tree = ast.parse(no_doc_source)
92
+ no_doc_node = no_doc_tree.body[0]
93
+
94
+ result = get_docstring(no_doc_node)
95
+ print("No docstring result:", result)
96
+ # Output: No docstring result: None
97
+
98
+
99
+ # --- Example 4: Extract docstring from a module ---
100
+ module_source = '''
101
+ """
102
+ Authentication utilities for the payments service.
103
+ Handles token validation and session management.
104
+ """
105
+
106
+ import os
107
+ '''
108
+
109
+ module_tree = ast.parse(module_source)
110
+ module_docstring = get_docstring(module_tree)
111
+ print("Module docstring:", module_docstring)
112
+ # Output: Module docstring:
113
+ # Authentication utilities for the payments service.
114
+ # Handles token validation and session management.
115
+ ```
116
+
117
+ ### `get_type_annotation`
118
+
119
+ ```python
120
+ def get_type_annotation(annotation: ast.AST | None) -> str | None
121
+ ```
122
+
123
+ Use this to convert a Python AST type annotation node into its human-readable string representation — ideal for documentation generators, code analyzers, or any tool that needs to display type hints as readable text.
124
+
125
+ Returns `None` if the annotation is `None` or cannot be resolved. Returns a string like `"str"`, `"int"`, `"List[str]"`, or `"Optional[int]"` for valid annotations.
126
+
127
+ ## Parameters
128
+
129
+ | Name | Type | Required | Description |
130
+ |------|------|----------|-------------|
131
+ | `annotation` | `ast.AST \| None` | No | An AST node representing a type annotation, typically from `ast.parse()` or a function argument's `.annotation` attribute. Pass `None` to safely handle unannotated parameters. |
132
+
133
+ ## Returns
134
+
135
+ | Condition | Return Value |
136
+ |-----------|-------------|
137
+ | `annotation` is `None` | `None` |
138
+ | Valid AST annotation node | `str` — the human-readable type string (e.g. `"int"`, `"List[str]"`) |
139
+ | Unresolvable/complex node | `None` |
140
+
141
+ **Example:**
142
+
143
+ ```python example.py
144
+ import ast
145
+
146
+ # Inline implementation of get_type_annotation
147
+ def get_type_annotation(annotation: ast.AST | None) -> str | None:
148
+ """Convert type annotation AST to string."""
149
+ if annotation is None:
150
+ return None
151
+ try:
152
+ return ast.unparse(annotation)
153
+ except Exception:
154
+ return None
155
+
156
+ # --- Example 1: Simple type annotations from a function signature ---
157
+ source_code = """
158
+ def greet(name: str, age: int, scores: list[float]) -> bool:
159
+ pass
160
+ """
161
+
162
+ tree = ast.parse(source_code)
163
+ func_def = tree.body[0] # The function definition node
164
+
165
+ print("=== Function argument annotations ===")
166
+ for arg in func_def.args.args:
167
+ annotation_str = get_type_annotation(arg.annotation)
168
+ print(f" {arg.arg}: {annotation_str}")
169
+ # Output:
170
+ # name: str
171
+ # age: int
172
+ # scores: list[float]
173
+
174
+ print("\n=== Return annotation ===")
175
+ return_annotation = get_type_annotation(func_def.returns)
176
+ print(f" -> {return_annotation}")
177
+ # Output:
178
+ # -> bool
179
+
180
+ # --- Example 2: Complex/generic annotations ---
181
+ complex_source = """
182
+ def process(
183
+ data: dict[str, list[int]],
184
+ callback: "Callable[[int], None]",
185
+ value: int | None = None
186
+ ) -> tuple[bool, str]:
187
+ pass
188
+ """
189
+
190
+ complex_tree = ast.parse(complex_source)
191
+ complex_func = complex_tree.body[0]
192
+
193
+ print("\n=== Complex annotations ===")
194
+ for arg in complex_func.args.args:
195
+ annotation_str = get_type_annotation(arg.annotation)
196
+ print(f" {arg.arg}: {annotation_str}")
197
+ # Output:
198
+ # data: dict[str, list[int]]
199
+ # callback: 'Callable[[int], None]'
200
+ # value: int | None
201
+
202
+ # --- Example 3: Handling None (unannotated parameters) ---
203
+ unannotated_source = """
204
+ def legacy_func(x, y, z: int):
205
+ pass
206
+ """
207
+
208
+ unannotated_tree = ast.parse(unannotated_source)
209
+ unannotated_func = unannotated_tree.body[0]
210
+
211
+ print("\n=== Mixed annotated/unannotated parameters ===")
212
+ for arg in unannotated_func.args.args:
213
+ annotation_str = get_type_annotation(arg.annotation)
214
+ label = annotation_str if annotation_str is not None else "(no annotation)"
215
+ print(f" {arg.arg}: {label}")
216
+ # Output:
217
+ # x: (no annotation)
218
+ # y: (no annotation)
219
+ # z: int
220
+ ```
221
+
222
+ ### `get_default_value`
223
+
224
+ ```python
225
+ def get_default_value(default: ast.AST | None) -> str | None
226
+ ```
227
+
228
+ Use this to extract a human-readable string representation of a Python function parameter's default value from its AST node — ideal for documentation generators, code analyzers, or introspection tools.
229
+
230
+ When parsing Python source code with the `ast` module, default values are stored as AST nodes. This function converts those nodes back into readable strings (e.g., `"42"`, `"'hello'"`, `"None"`). Returns `None` if no default value exists.
231
+
232
+ ### Parameters
233
+
234
+ | Name | Type | Required | Description |
235
+ |------|------|----------|-------------|
236
+ | `default` | `ast.AST \| None` | No | An AST node representing a parameter's default value, or `None` if the parameter has no default |
237
+
238
+ ### Returns
239
+
240
+ | Condition | Return Value |
241
+ |-----------|-------------|
242
+ | `default` is `None` | `None` |
243
+ | `default` is a valid AST node | `str` — the unparsed source representation of the default value (e.g., `"42"`, `"True"`, `"'hello'"`, `"[1, 2, 3]"`) |
244
+
245
+ ### Notes
246
+ - Uses `ast.unparse()` internally (requires Python 3.9+)
247
+ - Handles complex defaults like list literals, dict literals, and function calls
248
+ - Safe to call with `None` — will not raise an exception
249
+
250
+ **Example:**
251
+
252
+ ```python example.py
253
+ import ast
254
+
255
+ # Inline implementation of get_default_value
256
+ def get_default_value(default: ast.AST | None) -> str | None:
257
+ """Convert default value AST to string."""
258
+ if default is None:
259
+ return None
260
+ return ast.unparse(default)
261
+
262
+ # Helper: parse a function and extract its argument defaults
263
+ def extract_defaults(source: str) -> dict:
264
+ tree = ast.parse(source)
265
+ func = tree.body[0] # First function definition
266
+ args = func.args
267
+
268
+ # Pair each argument with its default (defaults are right-aligned)
269
+ all_args = args.args
270
+ num_defaults = len(args.defaults)
271
+ offset = len(all_args) - num_defaults
272
+
273
+ results = {}
274
+ for i, arg in enumerate(all_args):
275
+ default_node = args.defaults[i - offset] if i >= offset else None
276
+ results[arg.arg] = get_default_value(default_node)
277
+
278
+ return results
279
+
280
+ # Example function source with various default types
281
+ sample_code = """
282
+ def create_user(
283
+ name,
284
+ role='admin',
285
+ max_retries=3,
286
+ tags=['read', 'write'],
287
+ config={'timeout': 30},
288
+ active=True,
289
+ score=9.5,
290
+ callback=None
291
+ ):
292
+ pass
293
+ """
294
+
295
+ try:
296
+ defaults = extract_defaults(sample_code)
297
+
298
+ for param, default in defaults.items():
299
+ print(f" {param}: default = {repr(default)}")
300
+
301
+ # Expected output:
302
+ # name: default = None (no default)
303
+ # role: default = "'admin'"
304
+ # max_retries: default = '3'
305
+ # tags: default = "['read', 'write']"
306
+ # config: default = "{'timeout': 30}"
307
+ # active: default = 'True'
308
+ # score: default = '9.5'
309
+ # callback: default = 'None'
310
+
311
+ except SyntaxError as e:
312
+ print(f"Failed to parse source: {e}")
313
+ except AttributeError as e:
314
+ print(f"Unexpected AST structure: {e}")
315
+ ```
316
+
317
+ ### `extract_parameters`
318
+
319
+ ```python
320
+ def extract_parameters(args: ast.arguments) -> list[dict[str, Any]]
321
+ ```
322
+
323
+ Use this to convert Python AST function arguments into a structured list of parameter metadata — ideal for building documentation generators, code analyzers, or introspection tools.
324
+
325
+ Given an `ast.arguments` object (obtained by parsing Python source code), this function returns a list of dictionaries, each describing one parameter: its name, type annotation (if any), and default value (if any).
326
+
327
+ ### Parameters
328
+
329
+ | Name | Type | Required | Description |
330
+ |------|------|----------|-------------|
331
+ | `args` | `ast.arguments` | āœ… Yes | The arguments node from a parsed Python function definition, typically accessed via `node.args` on a `FunctionDef` AST node |
332
+
333
+ ### Returns
334
+
335
+ Returns a `list[dict[str, Any]]` where each dictionary represents one parameter. Each dict typically contains:
336
+
337
+ | Key | Type | Description |
338
+ |-----|------|-------------|
339
+ | `name` | `str` | The parameter name |
340
+ | `annotation` | `str \| None` | The type annotation as a string, or `None` if not annotated |
341
+ | `default` | `Any \| None` | The default value, or `None` if no default is provided |
342
+
343
+ Returns an **empty list** if the function has no parameters.
344
+
345
+ **Example:**
346
+
347
+ ```python example.py
348
+ import ast
349
+ from typing import Any
350
+
351
+ # ── Inline implementation of extract_parameters ──────────────────────────────
352
+
353
+ def extract_parameters(args: ast.arguments) -> list[dict[str, Any]]:
354
+ """Extract parameters from function arguments."""
355
+ params: list[dict[str, Any]] = []
356
+
357
+ # Calculate offset for defaults (defaults only cover the LAST n args)
358
+ all_args = args.args
359
+ defaults_offset = len(all_args) - len(args.defaults)
360
+
361
+ for i, arg in enumerate(all_args):
362
+ # Resolve annotation to a readable string
363
+ annotation: str | None = None
364
+ if arg.annotation is not None:
365
+ annotation = ast.unparse(arg.annotation)
366
+
367
+ # Resolve default value if present
368
+ default: Any = None
369
+ default_index = i - defaults_offset
370
+ if default_index >= 0:
371
+ default = ast.literal_eval(args.defaults[default_index])
372
+
373
+ params.append({
374
+ "name": arg.arg,
375
+ "annotation": annotation,
376
+ "default": default,
377
+ })
378
+
379
+ return params
380
+
381
+
382
+ # ── Example: parse a realistic function and extract its parameters ────────────
383
+
384
+ SOURCE_CODE = """
385
+ def create_user(
386
+ username: str,
387
+ email: str,
388
+ age: int,
389
+ role: str = "viewer",
390
+ is_active: bool = True,
391
+ max_sessions: int = 3,
392
+ ):
393
+ pass
394
+ """
395
+
396
+ try:
397
+ tree = ast.parse(SOURCE_CODE)
398
+
399
+ # Grab the first function definition in the module
400
+ func_def = next(
401
+ node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
402
+ )
403
+
404
+ print(f"Extracting parameters from: {func_def.name}()\n")
405
+
406
+ parameters = extract_parameters(func_def.args)
407
+
408
+ for param in parameters:
409
+ annotation = param["annotation"] or "untyped"
410
+ default = repr(param["default"]) if param["default"] is not None else "required"
411
+ print(f" {param['name']:<15} type={annotation:<8} default={default}")
412
+
413
+ # Expected output:
414
+ # Extracting parameters from: create_user()
415
+ #
416
+ # username type=str default=required
417
+ # email type=str default=required
418
+ # age type=int default=required
419
+ # role type=str default='viewer'
420
+ # is_active type=bool default=True
421
+ # max_sessions type=int default=3
422
+
423
+ except SyntaxError as e:
424
+ print(f"Failed to parse source code: {e}")
425
+ except StopIteration:
426
+ print("No function definition found in the source code.")
427
+ except Exception as e:
428
+ print(f"Unexpected error: {e}")
429
+ ```
430
+
431
+ ### `build_signature`
432
+
433
+ ```python
434
+ def build_signature(name: str, args: ast.arguments, returns: ast.AST | None, is_async: bool) -> str
435
+ ```
436
+
437
+ Use this to reconstruct a human-readable function signature string from parsed AST components — useful when generating documentation, code analysis tools, or displaying function metadata without executing the code.
438
+
439
+ ## Parameters
440
+
441
+ | Name | Type | Required | Description |
442
+ |------|------|----------|-------------|
443
+ | `name` | `str` | āœ… | The function name to include in the signature |
444
+ | `args` | `ast.arguments` | āœ… | The parsed argument node from the AST, containing all parameter info (defaults, annotations, etc.) |
445
+ | `returns` | `ast.AST \| None` | āœ… | The return type annotation node from the AST, or `None` if no return type is specified |
446
+ | `is_async` | `bool` | āœ… | Whether to prefix the signature with `async def` instead of `def` |
447
+
448
+ ## Returns
449
+
450
+ A formatted string representing the full function signature, e.g.:
451
+ - `"def greet(name: str, age: int = 0) -> str"`
452
+ - `"async def fetch(url: str, timeout: float = 30.0) -> dict"`
453
+ - `"def run()"` (when no args or return type)
454
+
455
+ **Example:**
456
+
457
+ ```python example.py
458
+ import ast
459
+
460
+ # Inline implementation of build_signature
461
+ def build_signature(name: str, args: ast.arguments, returns: ast.AST | None, is_async: bool) -> str:
462
+ """Build the function signature as a string."""
463
+ prefix = "async def" if is_async else "def"
464
+
465
+ # Build argument list
466
+ arg_parts = []
467
+
468
+ # Calculate offset for defaults (defaults only apply to the last N positional args)
469
+ num_args = len(args.args)
470
+ num_defaults = len(args.defaults)
471
+ defaults_offset = num_args - num_defaults
472
+
473
+ for i, arg in enumerate(args.args):
474
+ arg_str = arg.arg
475
+
476
+ # Add type annotation if present
477
+ if arg.annotation:
478
+ arg_str += f": {ast.unparse(arg.annotation)}"
479
+
480
+ # Add default value if present
481
+ default_index = i - defaults_offset
482
+ if default_index >= 0:
483
+ default_val = ast.unparse(args.defaults[default_index])
484
+ arg_str += f" = {default_val}"
485
+
486
+ arg_parts.append(arg_str)
487
+
488
+ # Handle *args
489
+ if args.vararg:
490
+ vararg_str = f"*{args.vararg.arg}"
491
+ if args.vararg.annotation:
492
+ vararg_str += f": {ast.unparse(args.vararg.annotation)}"
493
+ arg_parts.append(vararg_str)
494
+
495
+ # Handle **kwargs
496
+ if args.kwarg:
497
+ kwarg_str = f"**{args.kwarg.arg}"
498
+ if args.kwarg.annotation:
499
+ kwarg_str += f": {ast.unparse(args.kwarg.annotation)}"
500
+ arg_parts.append(kwarg_str)
501
+
502
+ args_str = ", ".join(arg_parts)
503
+
504
+ # Build return annotation
505
+ return_str = ""
506
+ if returns:
507
+ return_str = f" -> {ast.unparse(returns)}"
508
+
509
+ return f"{prefix} {name}({args_str}){return_str}"
510
+
511
+
512
+ def demo():
513
+ # --- Example 1: Simple sync function with typed args and return ---
514
+ source_1 = "def greet(name: str, age: int = 0) -> str: pass"
515
+ tree_1 = ast.parse(source_1)
516
+ func_1 = tree_1.body[0] # type: ignore[index]
517
+
518
+ sig_1 = build_signature(
519
+ name=func_1.name,
520
+ args=func_1.args,
521
+ returns=func_1.returns,
522
+ is_async=False
523
+ )
524
+ print("Example 1:", sig_1)
525
+ # Output: def greet(name: str, age: int = 0) -> str
526
+
527
+ # --- Example 2: Async function with *args and **kwargs ---
528
+ source_2 = "async def fetch(url: str, timeout: float = 30.0, *args, **kwargs) -> dict: pass"
529
+ tree_2 = ast.parse(source_2)
530
+ func_2 = tree_2.body[0] # type: ignore[index]
531
+
532
+ sig_2 = build_signature(
533
+ name=func_2.name,
534
+ args=func_2.args,
535
+ returns=func_2.returns,
536
+ is_async=True
537
+ )
538
+ print("Example 2:", sig_2)
539
+ # Output: async def fetch(url: str, timeout: float = 30.0, *args, **kwargs) -> dict
540
+
541
+ # --- Example 3: No args, no return type ---
542
+ source_3 = "def run(): pass"
543
+ tree_3 = ast.parse(source_3)
544
+ func_3 = tree_3.body[0] # type: ignore[index]
545
+
546
+ sig_3 = build_signature(
547
+ name=func_3.name,
548
+ args=func_3.args,
549
+ returns=func_3.returns,
550
+ is_async=False
551
+ )
552
+ print("Example 3:", sig_3)
553
+ # Output: def run()
554
+
555
+ # --- Example 4: Async function with no return annotation ---
556
+ source_4 = "async def process(data: list, batch_size: int = 100): pass"
557
+ tree_4 = ast.parse(source_4)
558
+ func_4 = tree_4.body[0] # type: ignore[index]
559
+
560
+ sig_4 = build_signature(
561
+ name=func_4.name,
562
+ args=func_4.args,
563
+ returns=func_4.returns,
564
+ is_async=True
565
+ )
566
+ print("Example 4:", sig_4)
567
+ # Output: async def process(data: list, batch_size: int = 100)
568
+
569
+
570
+ demo()
571
+ ```
572
+
573
+ ### `extract_function`
574
+
575
+ ```python
576
+ def extract_function(node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str, parent_class: str | None) -> dict[str, Any]
577
+ ```
578
+
579
+ Use this to extract structured metadata from a Python function or method AST node — including its name, signature, docstring, decorators, and source location — for use in code analysis, documentation generation, or static analysis tooling.
580
+
581
+ Given an AST node representing a function or async function, this returns a dictionary containing all relevant metadata about that function, ready for further processing or storage.
582
+
583
+ ## Parameters
584
+
585
+ | Name | Type | Required | Description |
586
+ |------|------|----------|-------------|
587
+ | `node` | `ast.FunctionDef \| ast.AsyncFunctionDef` | Yes | The AST node representing the function or async function to extract metadata from |
588
+ | `file_path` | `str` | Yes | Absolute or relative path to the source file containing the function, used for source location tracking |
589
+ | `parent_class` | `str \| None` | No | Name of the enclosing class if this is a method; `None` for top-level functions |
590
+
591
+ ## Returns
592
+
593
+ A `dict[str, Any]` containing extracted function metadata. Typical keys include:
594
+
595
+ | Key | Type | Description |
596
+ |-----|------|-------------|
597
+ | `name` | `str` | The function's name |
598
+ | `file_path` | `str` | Path to the source file |
599
+ | `parent_class` | `str \| None` | Enclosing class name, or `None` |
600
+ | `is_async` | `bool` | Whether the function is defined with `async def` |
601
+ | `docstring` | `str \| None` | The function's docstring, or `None` if absent |
602
+ | `decorators` | `list[str]` | List of decorator names applied to the function |
603
+ | `lineno` | `int` | Line number where the function is defined |
604
+ | `args` | `list[str]` | List of argument names |
605
+
606
+ **Example:**
607
+
608
+ ```python example.py
609
+ import ast
610
+ from typing import Any
611
+
612
+ # Inline implementation of extract_function
613
+ def extract_function(
614
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
615
+ file_path: str,
616
+ parent_class: str | None = None
617
+ ) -> dict[str, Any]:
618
+ """Extract structured metadata from a function or method AST node."""
619
+ docstring = ast.get_docstring(node)
620
+
621
+ decorators = []
622
+ for dec in node.decorator_list:
623
+ if isinstance(dec, ast.Name):
624
+ decorators.append(dec.id)
625
+ elif isinstance(dec, ast.Attribute):
626
+ decorators.append(f"{dec.value.id}.{dec.attr}") # type: ignore[attr-defined]
627
+ else:
628
+ decorators.append(ast.unparse(dec))
629
+
630
+ args = [arg.arg for arg in node.args.args]
631
+
632
+ return {
633
+ "name": node.name,
634
+ "file_path": file_path,
635
+ "parent_class": parent_class,
636
+ "is_async": isinstance(node, ast.AsyncFunctionDef),
637
+ "docstring": docstring,
638
+ "decorators": decorators,
639
+ "lineno": node.lineno,
640
+ "args": args,
641
+ }
642
+
643
+
644
+ # --- Example: parse a source snippet and extract function metadata ---
645
+
646
+ source_code = '''
647
+ import asyncio
648
+
649
+ class PaymentService:
650
+ @staticmethod
651
+ async def process_payment(user_id: str, amount: float, currency: str = "USD") -> dict:
652
+ """Process a payment for the given user.
653
+
654
+ Validates the amount and submits to the payment gateway.
655
+ """
656
+ return {"status": "ok", "user_id": user_id, "amount": amount}
657
+
658
+ def health_check():
659
+ """Return service health status."""
660
+ return {"healthy": True}
661
+ '''
662
+
663
+ try:
664
+ tree = ast.parse(source_code)
665
+
666
+ extracted = []
667
+
668
+ for node in ast.walk(tree):
669
+ # Extract top-level functions
670
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
671
+ # Check if it's a method inside a class
672
+ parent_class = None
673
+ for parent in ast.walk(tree):
674
+ if isinstance(parent, ast.ClassDef):
675
+ for item in parent.body:
676
+ if item is node:
677
+ parent_class = parent.name
678
+
679
+ result = extract_function(
680
+ node=node,
681
+ file_path="src/services/payment_service.py",
682
+ parent_class=parent_class,
683
+ )
684
+ extracted.append(result)
685
+
686
+ for func_meta in extracted:
687
+ print(f"\nFunction: {func_meta['name']}")
688
+ print(f" File: {func_meta['file_path']}")
689
+ print(f" Class: {func_meta['parent_class']}")
690
+ print(f" Async: {func_meta['is_async']}")
691
+ print(f" Args: {func_meta['args']}")
692
+ print(f" Decorators: {func_meta['decorators']}")
693
+ print(f" Line: {func_meta['lineno']}")
694
+ print(f" Docstring: {func_meta['docstring']!r}")
695
+
696
+ # Expected output:
697
+ # Function: process_payment
698
+ # File: src/services/payment_service.py
699
+ # Class: PaymentService
700
+ # Async: True
701
+ # Args: ['user_id', 'amount', 'currency']
702
+ # Decorators: ['staticmethod']
703
+ # Line: 5
704
+ # Docstring: 'Process a payment for the given user.\n\n Validates...'
705
+ #
706
+ # Function: health_check
707
+ # File: src/services/payment_service.py
708
+ # Class: None
709
+ # Async: False
710
+ # Args: []
711
+ # Decorators: []
712
+ # Line: 12
713
+ # Docstring: 'Return service health status.'
714
+
715
+ except SyntaxError as e:
716
+ print(f"Failed to parse source: {e}")
717
+ except Exception as e:
718
+ print(f"Extraction failed: {e}")
719
+ ```
720
+
721
+ ### `extract_class`
722
+
723
+ ```python
724
+ def extract_class(node: ast.ClassDef, file_path: str) -> list[dict[str, Any]]
725
+ ```
726
+
727
+ Use this to parse a Python AST `ClassDef` node and extract structured metadata about a class and all its methods into a flat list of dictionaries.
728
+
729
+ Each dictionary in the returned list represents either the class itself or one of its methods, making it easy to index, search, or analyze Python source code programmatically.
730
+
731
+ ## Parameters
732
+
733
+ | Name | Type | Required | Description |
734
+ |------|------|----------|-------------|
735
+ | `node` | `ast.ClassDef` | Yes | The AST node representing the class definition, obtained by parsing Python source code |
736
+ | `file_path` | `str` | Yes | The file path of the source file containing the class, stored in the returned metadata |
737
+
738
+ ## Returns
739
+
740
+ Returns a `list[dict[str, Any]]` where:
741
+ - **First element** is always the class-level metadata (name, docstring, base classes, line number, file path, etc.)
742
+ - **Subsequent elements** are method-level metadata entries, one per method defined in the class
743
+ - Returns an **empty list** if the node contains no extractable information
744
+
745
+ ### Example Return Shape
746
+ ```python
747
+ [
748
+ {
749
+ "type": "class",
750
+ "name": "MyClass",
751
+ "file_path": "src/models.py",
752
+ "lineno": 10,
753
+ "docstring": "A sample class.",
754
+ "bases": ["BaseModel"]
755
+ },
756
+ {
757
+ "type": "method",
758
+ "name": "my_method",
759
+ "file_path": "src/models.py",
760
+ "lineno": 15,
761
+ "docstring": "Does something.",
762
+ "args": ["self", "value"]
763
+ }
764
+ ]
765
+ ```
766
+
767
+ **Example:**
768
+
769
+ ```python example.py
770
+ import ast
771
+ from typing import Any
772
+
773
+ # ── Inline implementation of extract_class ────────────────────────────────────
774
+
775
+ def extract_class(node: ast.ClassDef, file_path: str) -> list[dict[str, Any]]:
776
+ """Extract a class and its methods into a flat list of metadata dicts."""
777
+ results: list[dict[str, Any]] = []
778
+
779
+ # Extract class-level metadata
780
+ class_entry: dict[str, Any] = {
781
+ "type": "class",
782
+ "name": node.name,
783
+ "file_path": file_path,
784
+ "lineno": node.lineno,
785
+ "docstring": ast.get_docstring(node) or "",
786
+ "bases": [
787
+ (base.id if isinstance(base, ast.Name) else ast.unparse(base))
788
+ for base in node.bases
789
+ ],
790
+ }
791
+ results.append(class_entry)
792
+
793
+ # Extract each method defined directly in the class body
794
+ for item in node.body:
795
+ if isinstance(item, ast.FunctionDef | ast.AsyncFunctionDef):
796
+ method_entry: dict[str, Any] = {
797
+ "type": "async_method" if isinstance(item, ast.AsyncFunctionDef) else "method",
798
+ "name": item.name,
799
+ "file_path": file_path,
800
+ "lineno": item.lineno,
801
+ "docstring": ast.get_docstring(item) or "",
802
+ "args": [arg.arg for arg in item.args.args],
803
+ }
804
+ results.append(method_entry)
805
+
806
+ return results
807
+
808
+
809
+ # ── Example usage ─────────────────────────────────────────────────────────────
810
+
811
+ SOURCE_CODE = '''
812
+ class PaymentProcessor(BaseProcessor):
813
+ """Handles payment transactions for the billing system."""
814
+
815
+ def __init__(self, api_key: str, timeout: int = 30):
816
+ """Initialize with API credentials."""
817
+ self.api_key = api_key
818
+ self.timeout = timeout
819
+
820
+ def charge(self, amount: float, currency: str = "USD") -> dict:
821
+ """Charge a customer the given amount."""
822
+ return {"status": "ok", "amount": amount, "currency": currency}
823
+
824
+ async def refund(self, transaction_id: str) -> bool:
825
+ """Asynchronously refund a transaction."""
826
+ return True
827
+ '''
828
+
829
+ def main() -> None:
830
+ try:
831
+ tree = ast.parse(SOURCE_CODE)
832
+
833
+ # Find the first ClassDef node in the parsed tree
834
+ class_node = next(
835
+ node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)
836
+ )
837
+
838
+ results = extract_class(class_node, file_path="src/billing/processor.py")
839
+
840
+ print(f"Extracted {len(results)} entries:\n")
841
+ for entry in results:
842
+ print(f" [{entry['type'].upper()}] {entry['name']!r}")
843
+ print(f" file : {entry['file_path']}")
844
+ print(f" line : {entry['lineno']}")
845
+ print(f" doc : {entry['docstring']!r}")
846
+ if entry["type"] == "class":
847
+ print(f" bases : {entry['bases']}")
848
+ else:
849
+ print(f" args : {entry['args']}")
850
+ print()
851
+
852
+ # Expected output:
853
+ # Extracted 4 entries:
854
+ #
855
+ # [CLASS] 'PaymentProcessor'
856
+ # file : src/billing/processor.py
857
+ # line : 2
858
+ # doc : 'Handles payment transactions for the billing system.'
859
+ # bases : ['BaseProcessor']
860
+ #
861
+ # [METHOD] '__init__'
862
+ # args : ['self', 'api_key', 'timeout']
863
+ #
864
+ # [METHOD] 'charge'
865
+ # args : ['self', 'amount', 'currency']
866
+ #
867
+ # [ASYNC_METHOD] 'refund'
868
+ # args : ['self', 'transaction_id']
869
+
870
+ except StopIteration:
871
+ print("Error: No class definition found in the source code.")
872
+ except SyntaxError as exc:
873
+ print(f"Error: Failed to parse source code — {exc}")
874
+
875
+ main()
876
+ ```
877
+
878
+ ### `scan_file`
879
+
880
+ ```python
881
+ def scan_file(file_path: str) -> dict[str, Any]
882
+ ```
883
+
884
+ Use this to extract all API elements from a Python source file — functions, classes, methods, imports, and metadata — in a single structured dictionary.
885
+
886
+ Ideal for building documentation generators, code analysis tools, static analyzers, or any workflow that needs to introspect a Python file programmatically.
887
+
888
+ ## Parameters
889
+
890
+ | Name | Type | Required | Description |
891
+ |------|------|----------|-------------|
892
+ | `file_path` | `str` | āœ… Yes | Absolute or relative path to the `.py` file to scan |
893
+
894
+ ## Returns
895
+
896
+ Returns a `dict[str, Any]` containing extracted API elements from the file. Typical keys include:
897
+
898
+ | Key | Type | Description |
899
+ |-----|------|-------------|
900
+ | `functions` | `list` | Top-level function definitions found in the file |
901
+ | `classes` | `list` | Class definitions, including their methods and attributes |
902
+ | `imports` | `list` | All import statements detected |
903
+ | `docstrings` | `dict` | Docstrings mapped to their parent element |
904
+ | `errors` | `list` | Any parse errors encountered during scanning |
905
+
906
+ > **Note:** Returns an empty structure (not `None`) if the file exists but contains no API elements. Raises `FileNotFoundError` if the path does not exist, and `SyntaxError` if the file cannot be parsed.
907
+
908
+ **Example:**
909
+
910
+ ```python example.py
911
+ import ast
912
+ import os
913
+ from typing import Any
914
+
915
+ # Inline implementation of scan_file — no external imports needed
916
+ def scan_file(file_path: str) -> dict[str, Any]:
917
+ """Scan a Python file and extract all API elements."""
918
+ result: dict[str, Any] = {
919
+ "file": file_path,
920
+ "functions": [],
921
+ "classes": [],
922
+ "imports": [],
923
+ "docstrings": {},
924
+ "errors": [],
925
+ }
926
+
927
+ if not os.path.exists(file_path):
928
+ raise FileNotFoundError(f"No such file: '{file_path}'")
929
+
930
+ with open(file_path, "r", encoding="utf-8") as f:
931
+ source = f.read()
932
+
933
+ try:
934
+ tree = ast.parse(source)
935
+ except SyntaxError as e:
936
+ result["errors"].append(str(e))
937
+ return result
938
+
939
+ for node in ast.walk(tree):
940
+ # Extract top-level functions
941
+ if isinstance(node, ast.FunctionDef):
942
+ func_info = {
943
+ "name": node.name,
944
+ "args": [arg.arg for arg in node.args.args],
945
+ "lineno": node.lineno,
946
+ }
947
+ result["functions"].append(func_info)
948
+ docstring = ast.get_docstring(node)
949
+ if docstring:
950
+ result["docstrings"][node.name] = docstring
951
+
952
+ # Extract classes and their methods
953
+ elif isinstance(node, ast.ClassDef):
954
+ methods = [
955
+ n.name for n in ast.walk(node) if isinstance(n, ast.FunctionDef)
956
+ ]
957
+ class_info = {
958
+ "name": node.name,
959
+ "methods": methods,
960
+ "lineno": node.lineno,
961
+ }
962
+ result["classes"].append(class_info)
963
+ docstring = ast.get_docstring(node)
964
+ if docstring:
965
+ result["docstrings"][node.name] = docstring
966
+
967
+ # Extract imports
968
+ elif isinstance(node, ast.Import):
969
+ for alias in node.names:
970
+ result["imports"].append({"module": alias.name, "alias": alias.asname})
971
+ elif isinstance(node, ast.ImportFrom):
972
+ for alias in node.names:
973
+ result["imports"].append({
974
+ "module": f"{node.module}.{alias.name}",
975
+ "alias": alias.asname,
976
+ })
977
+
978
+ return result
979
+
980
+
981
+ # --- Create a temporary sample Python file to scan ---
982
+ sample_code = '''
983
+ """A sample module for demonstration."""
984
+ import os
985
+ import sys
986
+ from pathlib import Path
987
+
988
+ class DataProcessor:
989
+ """Processes incoming data records."""
990
+
991
+ def __init__(self, config: dict):
992
+ self.config = config
993
+
994
+ def process(self, data: list) -> list:
995
+ """Run the processing pipeline."""
996
+ return [item.strip() for item in data]
997
+
998
+ def load_config(path: str) -> dict:
999
+ """Load configuration from a file path."""
1000
+ return {"path": path, "debug": False}
1001
+
1002
+ def validate(value: str) -> bool:
1003
+ return len(value) > 0
1004
+ '''
1005
+
1006
+ sample_file = "/tmp/sample_module.py"
1007
+ with open(sample_file, "w") as f:
1008
+ f.write(sample_code)
1009
+
1010
+ # --- Run scan_file and display results ---
1011
+ try:
1012
+ api_data = scan_file(sample_file)
1013
+
1014
+ print(f"šŸ“„ Scanned file : {api_data['file']}")
1015
+ print(f"\nšŸ“¦ Classes found ({len(api_data['classes'])}):")
1016
+ for cls in api_data["classes"]:
1017
+ print(f" - {cls['name']} (line {cls['lineno']}) | methods: {cls['methods']}")
1018
+
1019
+ print(f"\nāš™ļø Functions found ({len(api_data['functions'])}):")
1020
+ for fn in api_data["functions"]:
1021
+ print(f" - {fn['name']}({', '.join(fn['args'])}) at line {fn['lineno']}")
1022
+
1023
+ print(f"\nšŸ“„ Imports found ({len(api_data['imports'])}):")
1024
+ for imp in api_data["imports"]:
1025
+ alias = f" as {imp['alias']}" if imp["alias"] else ""
1026
+ print(f" - {imp['module']}{alias}")
1027
+
1028
+ print(f"\nšŸ“ Docstrings captured: {list(api_data['docstrings'].keys())}")
1029
+ print(f"āŒ Errors: {api_data['errors'] or 'None'}")
1030
+
1031
+ # Expected output:
1032
+ # šŸ“„ Scanned file : /tmp/sample_module.py
1033
+ # šŸ“¦ Classes found (1):
1034
+ # - DataProcessor (line 6) | methods: ['__init__', 'process']
1035
+ # āš™ļø Functions found (4):
1036
+ # - __init__(self, config) at line 9
1037
+ # - process(self, data) at line 12
1038
+ # - load_config(path) at line 16
1039
+ # - validate(value) at line 20
1040
+ # šŸ“„ Imports found (4):
1041
+ # - os
1042
+ # - sys
1043
+ # - pathlib.Path
1044
+ # šŸ“ Docstrings captured: ['DataProcessor', 'process', 'load_config']
1045
+ # āŒ Errors: None
1046
+
1047
+ except FileNotFoundError as e:
1048
+ print(f"File error: {e}")
1049
+ except SyntaxError as e:
1050
+ print(f"Parse error in file: {e}")
1051
+ finally:
1052
+ # Clean up temp file
1053
+ if os.path.exists(sample_file):
1054
+ os.remove(sample_file)
1055
+ ```
1056
+