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.
- package/README.md +1 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +3 -5
- package/dist/autofix/index.js +15 -3
- package/dist/cli.js +19 -4
- package/dist/commands/check-links.js +164 -174
- package/dist/commands/deploy.js +5 -2
- package/dist/commands/generate.js +206 -199
- package/dist/commands/i18n.js +3 -20
- package/dist/commands/init.js +47 -40
- package/dist/commands/lint.js +3 -20
- package/dist/commands/mcp.js +125 -122
- package/dist/commands/monitor.js +125 -108
- package/dist/commands/review-pr.js +1 -1
- package/dist/commands/sdk.js +1 -1
- package/dist/config/loader.js +21 -2
- package/dist/generator/organizer.d.ts +3 -0
- package/dist/generator/organizer.js +4 -9
- package/dist/generator/writer.js +2 -10
- package/dist/github/pr-comments.js +21 -8
- package/dist/plugins/index.js +1 -0
- package/dist/scanner/index.js +8 -2
- package/dist/template/docs.json +2 -1
- package/dist/template/next.config.mjs +3 -1
- package/dist/template/package.json +17 -14
- package/dist/template/public/favicon.svg +4 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +120 -25
- package/dist/template/src/app/api/chat/route.ts +11 -3
- package/dist/template/src/app/docs/README.md +28 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
- package/dist/template/src/app/docs/auth/page.mdx +589 -0
- package/dist/template/src/app/docs/autofix/page.mdx +624 -0
- package/dist/template/src/app/docs/cli/page.mdx +217 -0
- package/dist/template/src/app/docs/config/page.mdx +428 -0
- package/dist/template/src/app/docs/configuration/page.mdx +86 -0
- package/dist/template/src/app/docs/deployment/page.mdx +112 -0
- package/dist/template/src/app/docs/error.tsx +20 -0
- package/dist/template/src/app/docs/generator/generator.md +504 -0
- package/dist/template/src/app/docs/generator/organizer.md +779 -0
- package/dist/template/src/app/docs/generator/page.mdx +613 -0
- package/dist/template/src/app/docs/github/page.mdx +502 -0
- package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
- package/dist/template/src/app/docs/llm/index.md +471 -0
- package/dist/template/src/app/docs/llm/page.mdx +428 -0
- package/dist/template/src/app/docs/llms-full.md +256 -0
- package/dist/template/src/app/docs/llms.txt +2971 -0
- package/dist/template/src/app/docs/not-found.tsx +23 -0
- package/dist/template/src/app/docs/page.mdx +0 -3
- package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
- package/dist/template/src/app/docs/pro/page.mdx +121 -0
- package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
- package/dist/template/src/app/docs/scanner/content-type.md +599 -0
- package/dist/template/src/app/docs/scanner/index.md +212 -0
- package/dist/template/src/app/docs/scanner/page.mdx +307 -0
- package/dist/template/src/app/docs/scanner/python.md +469 -0
- package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
- package/dist/template/src/app/docs/scanner/rust.md +325 -0
- package/dist/template/src/app/docs/scanner/typescript.md +201 -0
- package/dist/template/src/app/error.tsx +3 -3
- package/dist/template/src/app/icon.tsx +29 -0
- package/dist/template/src/app/layout.tsx +57 -7
- package/dist/template/src/app/not-found.tsx +35 -0
- package/dist/template/src/app/page.tsx +95 -11
- package/dist/template/src/components/ai-chat.tsx +26 -21
- package/dist/template/src/components/breadcrumbs.tsx +56 -12
- package/dist/template/src/components/copy-button.tsx +17 -3
- package/dist/template/src/components/docs-layout.tsx +202 -8
- package/dist/template/src/components/feedback.tsx +4 -2
- package/dist/template/src/components/footer.tsx +42 -0
- package/dist/template/src/components/header.tsx +56 -20
- package/dist/template/src/components/mdx/accordion.tsx +17 -13
- package/dist/template/src/components/mdx/callout.tsx +50 -37
- package/dist/template/src/components/mdx/card.tsx +24 -12
- package/dist/template/src/components/mdx/code-block.tsx +17 -3
- package/dist/template/src/components/mdx/code-group.tsx +78 -18
- package/dist/template/src/components/mdx/code-playground.tsx +3 -0
- package/dist/template/src/components/mdx/go-playground.tsx +3 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
- package/dist/template/src/components/mdx/python-playground.tsx +2 -0
- package/dist/template/src/components/mdx/steps.tsx +6 -6
- package/dist/template/src/components/mdx/tabs.tsx +76 -8
- package/dist/template/src/components/page-header.tsx +19 -0
- package/dist/template/src/components/scroll-to-top.tsx +33 -0
- package/dist/template/src/components/search-dialog.tsx +251 -57
- package/dist/template/src/components/sidebar.tsx +137 -77
- package/dist/template/src/components/table-of-contents.tsx +29 -13
- package/dist/template/src/lib/highlight.ts +90 -31
- package/dist/template/src/lib/search.ts +14 -4
- package/dist/template/src/lib/theme-utils.ts +140 -0
- package/dist/template/src/styles/globals.css +397 -84
- package/dist/template/src/types/remark-gfm.d.ts +2 -0
- package/dist/utils/files.d.ts +9 -0
- package/dist/utils/files.js +33 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.js +38 -0
- 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
|
+
|