janito 2.1.1__py3-none-any.whl → 2.3.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.
- janito/__init__.py +6 -6
- janito/agent/setup_agent.py +14 -5
- janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
- janito/cli/chat_mode/bindings.py +6 -0
- janito/cli/chat_mode/session.py +16 -0
- janito/cli/chat_mode/shell/autocomplete.py +21 -21
- janito/cli/chat_mode/shell/commands/__init__.py +3 -2
- janito/cli/chat_mode/shell/commands/clear.py +12 -12
- janito/cli/chat_mode/shell/commands/exec.py +27 -0
- janito/cli/chat_mode/shell/commands/multi.py +51 -51
- janito/cli/chat_mode/shell/commands/tools.py +17 -6
- janito/cli/chat_mode/shell/input_history.py +62 -62
- janito/cli/chat_mode/shell/session/manager.py +1 -0
- janito/cli/chat_mode/toolbar.py +3 -1
- janito/cli/cli_commands/list_models.py +35 -35
- janito/cli/cli_commands/list_providers.py +9 -9
- janito/cli/cli_commands/list_tools.py +53 -53
- janito/cli/cli_commands/model_selection.py +50 -50
- janito/cli/cli_commands/model_utils.py +13 -2
- janito/cli/cli_commands/set_api_key.py +19 -19
- janito/cli/cli_commands/show_config.py +51 -51
- janito/cli/cli_commands/show_system_prompt.py +62 -62
- janito/cli/config.py +2 -1
- janito/cli/core/__init__.py +4 -4
- janito/cli/core/event_logger.py +59 -59
- janito/cli/core/getters.py +3 -1
- janito/cli/core/runner.py +27 -6
- janito/cli/core/setters.py +5 -1
- janito/cli/core/unsetters.py +54 -54
- janito/cli/main_cli.py +12 -1
- janito/cli/prompt_core.py +5 -2
- janito/cli/rich_terminal_reporter.py +22 -3
- janito/cli/single_shot_mode/__init__.py +6 -6
- janito/cli/single_shot_mode/handler.py +11 -1
- janito/cli/verbose_output.py +1 -1
- janito/config.py +5 -5
- janito/config_manager.py +2 -0
- janito/driver_events.py +14 -0
- janito/drivers/anthropic/driver.py +113 -113
- janito/drivers/azure_openai/driver.py +38 -3
- janito/drivers/driver_registry.py +0 -2
- janito/drivers/openai/driver.py +196 -36
- janito/formatting_token.py +54 -54
- janito/i18n/__init__.py +35 -35
- janito/i18n/messages.py +23 -23
- janito/i18n/pt.py +47 -47
- janito/llm/__init__.py +5 -5
- janito/llm/agent.py +443 -443
- janito/llm/auth.py +1 -0
- janito/llm/driver.py +7 -1
- janito/llm/driver_config.py +1 -0
- janito/llm/driver_config_builder.py +34 -34
- janito/llm/driver_input.py +12 -12
- janito/llm/message_parts.py +60 -60
- janito/llm/model.py +38 -38
- janito/llm/provider.py +196 -196
- janito/provider_config.py +7 -3
- janito/provider_registry.py +29 -5
- janito/providers/__init__.py +1 -0
- janito/providers/anthropic/model_info.py +22 -22
- janito/providers/anthropic/provider.py +2 -2
- janito/providers/azure_openai/model_info.py +7 -6
- janito/providers/azure_openai/provider.py +44 -2
- janito/providers/deepseek/__init__.py +1 -1
- janito/providers/deepseek/model_info.py +16 -16
- janito/providers/deepseek/provider.py +91 -91
- janito/providers/google/model_info.py +21 -29
- janito/providers/google/provider.py +49 -38
- janito/providers/mistralai/provider.py +2 -2
- janito/providers/openai/model_info.py +0 -11
- janito/providers/openai/provider.py +1 -1
- janito/providers/provider_static_info.py +2 -3
- janito/providers/registry.py +26 -26
- janito/tools/adapters/__init__.py +1 -1
- janito/tools/adapters/local/__init__.py +62 -62
- janito/tools/adapters/local/adapter.py +33 -11
- janito/tools/adapters/local/ask_user.py +102 -102
- janito/tools/adapters/local/copy_file.py +84 -84
- janito/tools/adapters/local/create_directory.py +69 -69
- janito/tools/adapters/local/create_file.py +82 -82
- janito/tools/adapters/local/delete_text_in_file.py +4 -7
- janito/tools/adapters/local/fetch_url.py +97 -97
- janito/tools/adapters/local/find_files.py +138 -140
- janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
- janito/tools/adapters/local/get_file_outline/core.py +117 -151
- janito/tools/adapters/local/get_file_outline/java_outline.py +40 -0
- janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
- janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
- janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
- janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
- janito/tools/adapters/local/move_file.py +3 -13
- janito/tools/adapters/local/open_html_in_browser.py +24 -29
- janito/tools/adapters/local/open_url.py +3 -2
- janito/tools/adapters/local/python_code_run.py +166 -166
- janito/tools/adapters/local/python_command_run.py +164 -164
- janito/tools/adapters/local/python_file_run.py +163 -163
- janito/tools/adapters/local/remove_directory.py +6 -17
- janito/tools/adapters/local/remove_file.py +9 -15
- janito/tools/adapters/local/replace_text_in_file.py +6 -9
- janito/tools/adapters/local/run_bash_command.py +176 -176
- janito/tools/adapters/local/run_powershell_command.py +219 -219
- janito/tools/adapters/local/search_text/__init__.py +1 -1
- janito/tools/adapters/local/search_text/core.py +201 -201
- janito/tools/adapters/local/search_text/match_lines.py +1 -1
- janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
- janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
- janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
- janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
- janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
- janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
- janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
- janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
- janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
- janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
- janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
- janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
- janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
- janito/tools/adapters/local/view_file.py +167 -167
- janito/tools/inspect_registry.py +17 -17
- janito/tools/tool_base.py +105 -105
- janito/tools/tool_events.py +58 -58
- janito/tools/tool_run_exception.py +12 -12
- janito/tools/tool_use_tracker.py +81 -81
- janito/tools/tool_utils.py +45 -45
- janito/tools/tools_adapter.py +78 -6
- janito/tools/tools_schema.py +104 -104
- janito/version.py +4 -4
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/METADATA +388 -232
- janito-2.3.0.dist-info/RECORD +181 -0
- janito-2.3.0.dist-info/licenses/LICENSE +21 -0
- janito/cli/chat_mode/shell/commands/last.py +0 -137
- janito/drivers/google_genai/driver.py +0 -54
- janito/drivers/google_genai/schema_generator.py +0 -67
- janito-2.1.1.dist-info/RECORD +0 -181
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/top_level.txt +0 -0
@@ -1,303 +1,303 @@
|
|
1
|
-
import re
|
2
|
-
from typing import List
|
3
|
-
|
4
|
-
|
5
|
-
def handle_assignment(idx, assign_match, outline):
|
6
|
-
var_name = assign_match.group(2)
|
7
|
-
var_type = "const" if var_name.isupper() else "var"
|
8
|
-
outline.append(
|
9
|
-
{
|
10
|
-
"type": var_type,
|
11
|
-
"name": var_name,
|
12
|
-
"start": idx + 1,
|
13
|
-
"end": idx + 1,
|
14
|
-
"parent": "",
|
15
|
-
"docstring": "",
|
16
|
-
}
|
17
|
-
)
|
18
|
-
|
19
|
-
|
20
|
-
def handle_main(idx, outline):
|
21
|
-
outline.append(
|
22
|
-
{
|
23
|
-
"type": "main",
|
24
|
-
"name": "__main__",
|
25
|
-
"start": idx + 1,
|
26
|
-
"end": idx + 1,
|
27
|
-
"parent": "",
|
28
|
-
"docstring": "",
|
29
|
-
}
|
30
|
-
)
|
31
|
-
|
32
|
-
|
33
|
-
def close_stack_objects(idx, indent, stack, obj_ranges):
|
34
|
-
while stack and indent < stack[-1][2]:
|
35
|
-
popped = stack.pop()
|
36
|
-
obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
|
37
|
-
|
38
|
-
|
39
|
-
def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
|
40
|
-
if last_top_obj and last_top_obj in stack:
|
41
|
-
stack.remove(last_top_obj)
|
42
|
-
obj_ranges.append(
|
43
|
-
(
|
44
|
-
last_top_obj[0],
|
45
|
-
last_top_obj[1],
|
46
|
-
last_top_obj[3],
|
47
|
-
idx,
|
48
|
-
last_top_obj[4],
|
49
|
-
last_top_obj[2],
|
50
|
-
)
|
51
|
-
)
|
52
|
-
return None
|
53
|
-
return last_top_obj
|
54
|
-
|
55
|
-
|
56
|
-
def handle_class(idx, class_match, indent, stack, last_top_obj):
|
57
|
-
name = class_match.group(2)
|
58
|
-
parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
|
59
|
-
obj = ("class", name, indent, idx + 1, parent)
|
60
|
-
stack.append(obj)
|
61
|
-
if indent == 0:
|
62
|
-
last_top_obj = obj
|
63
|
-
return last_top_obj
|
64
|
-
|
65
|
-
|
66
|
-
def handle_function(idx, func_match, indent, stack, last_top_obj):
|
67
|
-
name = func_match.group(2)
|
68
|
-
parent = ""
|
69
|
-
for s in reversed(stack):
|
70
|
-
if s[0] == "class" and indent > s[2]:
|
71
|
-
parent = s[1]
|
72
|
-
break
|
73
|
-
obj = ("function", name, indent, idx + 1, parent)
|
74
|
-
stack.append(obj)
|
75
|
-
if indent == 0:
|
76
|
-
last_top_obj = obj
|
77
|
-
return last_top_obj
|
78
|
-
|
79
|
-
|
80
|
-
def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
|
81
|
-
class_pat, func_pat, assign_pat, main_pat = regexes
|
82
|
-
class_match = class_pat.match(line)
|
83
|
-
func_match = func_pat.match(line)
|
84
|
-
assign_match = assign_pat.match(line)
|
85
|
-
indent = len(line) - len(line.lstrip())
|
86
|
-
# If a new top-level class or function starts, close the previous one
|
87
|
-
if (class_match or func_match) and indent == 0 and last_top_obj:
|
88
|
-
last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
|
89
|
-
if class_match:
|
90
|
-
last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
|
91
|
-
elif func_match:
|
92
|
-
last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
|
93
|
-
elif assign_match and indent == 0:
|
94
|
-
handle_assignment(idx, assign_match, outline)
|
95
|
-
main_match = main_pat.match(line)
|
96
|
-
if main_match:
|
97
|
-
handle_main(idx, outline)
|
98
|
-
close_stack_objects(idx, indent, stack, obj_ranges)
|
99
|
-
return last_top_obj
|
100
|
-
|
101
|
-
|
102
|
-
def extract_signature_and_decorators(lines, start_idx):
|
103
|
-
"""
|
104
|
-
Extracts the signature line and leading decorators for a given function/class/method.
|
105
|
-
Returns (signature:str, decorators:List[str], signature_lineno:int)
|
106
|
-
"""
|
107
|
-
decorators = []
|
108
|
-
sig_line = None
|
109
|
-
sig_lineno = start_idx
|
110
|
-
for i in range(start_idx - 1, -1, -1):
|
111
|
-
striped = lines[i].strip()
|
112
|
-
if striped.startswith("@"):
|
113
|
-
decorators.insert(0, striped)
|
114
|
-
sig_lineno = i
|
115
|
-
elif not striped:
|
116
|
-
continue
|
117
|
-
else:
|
118
|
-
break
|
119
|
-
# Find the signature line itself
|
120
|
-
for k in range(start_idx, len(lines)):
|
121
|
-
striped = lines[k].strip()
|
122
|
-
if striped.startswith("def ") or striped.startswith("class "):
|
123
|
-
sig_line = striped
|
124
|
-
sig_lineno = k
|
125
|
-
break
|
126
|
-
return sig_line, decorators, sig_lineno
|
127
|
-
|
128
|
-
|
129
|
-
def extract_docstring(lines, start_idx, end_idx):
|
130
|
-
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
131
|
-
for i in range(start_idx, min(end_idx, len(lines))):
|
132
|
-
line = lines[i].lstrip()
|
133
|
-
if not line:
|
134
|
-
continue
|
135
|
-
if line.startswith('"""') or line.startswith("'''"):
|
136
|
-
quote = line[:3]
|
137
|
-
doc = line[3:]
|
138
|
-
if doc.strip().endswith(quote):
|
139
|
-
return doc.strip()[:-3].strip()
|
140
|
-
docstring_lines = [doc]
|
141
|
-
for j in range(i + 1, min(end_idx, len(lines))):
|
142
|
-
line = lines[j]
|
143
|
-
if line.strip().endswith(quote):
|
144
|
-
docstring_lines.append(line.strip()[:-3])
|
145
|
-
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
146
|
-
docstring_lines.append(line)
|
147
|
-
break
|
148
|
-
else:
|
149
|
-
break
|
150
|
-
return ""
|
151
|
-
|
152
|
-
|
153
|
-
def build_outline_entry(obj, lines, outline):
|
154
|
-
obj_type, name, start, end, parent, indent = obj
|
155
|
-
# Determine if this is a method
|
156
|
-
if obj_type == "function" and parent:
|
157
|
-
outline_type = "method"
|
158
|
-
elif obj_type == "function":
|
159
|
-
outline_type = "function"
|
160
|
-
else:
|
161
|
-
outline_type = obj_type
|
162
|
-
docstring = extract_docstring(lines, start, end)
|
163
|
-
outline.append(
|
164
|
-
{
|
165
|
-
"type": outline_type,
|
166
|
-
"name": name,
|
167
|
-
"start": start,
|
168
|
-
"end": end,
|
169
|
-
"parent": parent,
|
170
|
-
"docstring": docstring,
|
171
|
-
}
|
172
|
-
)
|
173
|
-
|
174
|
-
|
175
|
-
def process_lines(lines, regexes):
|
176
|
-
outline = []
|
177
|
-
stack = []
|
178
|
-
obj_ranges = []
|
179
|
-
last_top_obj = None
|
180
|
-
for idx, line in enumerate(lines):
|
181
|
-
last_top_obj = process_line(
|
182
|
-
idx, line, regexes, stack, obj_ranges, outline, last_top_obj
|
183
|
-
)
|
184
|
-
# Close any remaining open objects
|
185
|
-
for popped in stack:
|
186
|
-
obj_ranges.append(
|
187
|
-
(popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
|
188
|
-
)
|
189
|
-
return outline, obj_ranges
|
190
|
-
|
191
|
-
|
192
|
-
def build_outline(obj_ranges, lines, outline):
|
193
|
-
for obj in obj_ranges:
|
194
|
-
build_outline_entry(obj, lines, outline)
|
195
|
-
return outline
|
196
|
-
|
197
|
-
|
198
|
-
def parse_python_outline(lines: List[str]):
|
199
|
-
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
200
|
-
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
201
|
-
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
202
|
-
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
203
|
-
outline = []
|
204
|
-
stack = []
|
205
|
-
obj_ranges = []
|
206
|
-
last_top_obj = None
|
207
|
-
for idx, line in enumerate(lines):
|
208
|
-
class_match = class_pat.match(line)
|
209
|
-
func_match = func_pat.match(line)
|
210
|
-
assign_match = assign_pat.match(line)
|
211
|
-
indent = len(line) - len(line.lstrip())
|
212
|
-
parent = ""
|
213
|
-
for s in reversed(stack):
|
214
|
-
if s[0] == "class" and indent > s[2]:
|
215
|
-
parent = s[1]
|
216
|
-
break
|
217
|
-
if class_match:
|
218
|
-
obj = ("class", class_match.group(2), idx + 1, None, parent, indent)
|
219
|
-
stack.append(obj)
|
220
|
-
last_top_obj = obj
|
221
|
-
elif func_match:
|
222
|
-
obj = ("function", func_match.group(2), idx + 1, None, parent, indent)
|
223
|
-
stack.append(obj)
|
224
|
-
last_top_obj = obj
|
225
|
-
elif assign_match and indent == 0:
|
226
|
-
outline.append(
|
227
|
-
{
|
228
|
-
"type": "const" if assign_match.group(2).isupper() else "var",
|
229
|
-
"name": assign_match.group(2),
|
230
|
-
"start": idx + 1,
|
231
|
-
"end": idx + 1,
|
232
|
-
"parent": "",
|
233
|
-
"signature": line.strip(),
|
234
|
-
"decorators": [],
|
235
|
-
"docstring": "",
|
236
|
-
}
|
237
|
-
)
|
238
|
-
if line.strip().startswith("if __name__ == "):
|
239
|
-
outline.append(
|
240
|
-
{
|
241
|
-
"type": "main",
|
242
|
-
"name": "__main__",
|
243
|
-
"start": idx + 1,
|
244
|
-
"end": idx + 1,
|
245
|
-
"parent": "",
|
246
|
-
"signature": line.strip(),
|
247
|
-
"decorators": [],
|
248
|
-
"docstring": "",
|
249
|
-
}
|
250
|
-
)
|
251
|
-
# Close stack objects if indent falls back
|
252
|
-
while stack and indent <= stack[-1][5] and idx + 1 > stack[-1][2]:
|
253
|
-
finished = stack.pop()
|
254
|
-
outline_entry = finished[:2] + (
|
255
|
-
finished[2],
|
256
|
-
idx + 1,
|
257
|
-
finished[4],
|
258
|
-
finished[5],
|
259
|
-
)
|
260
|
-
build_outline_entry(outline_entry, lines, outline)
|
261
|
-
# Close any remaining objects
|
262
|
-
while stack:
|
263
|
-
finished = stack.pop()
|
264
|
-
outline_entry = finished[:2] + (
|
265
|
-
finished[2],
|
266
|
-
len(lines),
|
267
|
-
finished[4],
|
268
|
-
finished[5],
|
269
|
-
)
|
270
|
-
build_outline_entry(outline_entry, lines, outline)
|
271
|
-
return outline
|
272
|
-
|
273
|
-
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
274
|
-
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
275
|
-
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
276
|
-
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
277
|
-
regexes = (class_pat, func_pat, assign_pat, main_pat)
|
278
|
-
outline, obj_ranges = process_lines(lines, regexes)
|
279
|
-
return build_outline(obj_ranges, lines, outline)
|
280
|
-
|
281
|
-
|
282
|
-
def extract_docstring(lines, start_idx, end_idx):
|
283
|
-
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
284
|
-
for i in range(start_idx, min(end_idx, len(lines))):
|
285
|
-
line = lines[i].lstrip()
|
286
|
-
if not line:
|
287
|
-
continue
|
288
|
-
if line.startswith('"""') or line.startswith("'''"):
|
289
|
-
quote = line[:3]
|
290
|
-
doc = line[3:]
|
291
|
-
if doc.strip().endswith(quote):
|
292
|
-
return doc.strip()[:-3].strip()
|
293
|
-
docstring_lines = [doc]
|
294
|
-
for j in range(i + 1, min(end_idx, len(lines))):
|
295
|
-
line = lines[j]
|
296
|
-
if line.strip().endswith(quote):
|
297
|
-
docstring_lines.append(line.strip()[:-3])
|
298
|
-
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
299
|
-
docstring_lines.append(line)
|
300
|
-
break
|
301
|
-
else:
|
302
|
-
break
|
303
|
-
return ""
|
1
|
+
import re
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
|
5
|
+
def handle_assignment(idx, assign_match, outline):
|
6
|
+
var_name = assign_match.group(2)
|
7
|
+
var_type = "const" if var_name.isupper() else "var"
|
8
|
+
outline.append(
|
9
|
+
{
|
10
|
+
"type": var_type,
|
11
|
+
"name": var_name,
|
12
|
+
"start": idx + 1,
|
13
|
+
"end": idx + 1,
|
14
|
+
"parent": "",
|
15
|
+
"docstring": "",
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def handle_main(idx, outline):
|
21
|
+
outline.append(
|
22
|
+
{
|
23
|
+
"type": "main",
|
24
|
+
"name": "__main__",
|
25
|
+
"start": idx + 1,
|
26
|
+
"end": idx + 1,
|
27
|
+
"parent": "",
|
28
|
+
"docstring": "",
|
29
|
+
}
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
def close_stack_objects(idx, indent, stack, obj_ranges):
|
34
|
+
while stack and indent < stack[-1][2]:
|
35
|
+
popped = stack.pop()
|
36
|
+
obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
|
37
|
+
|
38
|
+
|
39
|
+
def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
|
40
|
+
if last_top_obj and last_top_obj in stack:
|
41
|
+
stack.remove(last_top_obj)
|
42
|
+
obj_ranges.append(
|
43
|
+
(
|
44
|
+
last_top_obj[0],
|
45
|
+
last_top_obj[1],
|
46
|
+
last_top_obj[3],
|
47
|
+
idx,
|
48
|
+
last_top_obj[4],
|
49
|
+
last_top_obj[2],
|
50
|
+
)
|
51
|
+
)
|
52
|
+
return None
|
53
|
+
return last_top_obj
|
54
|
+
|
55
|
+
|
56
|
+
def handle_class(idx, class_match, indent, stack, last_top_obj):
|
57
|
+
name = class_match.group(2)
|
58
|
+
parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
|
59
|
+
obj = ("class", name, indent, idx + 1, parent)
|
60
|
+
stack.append(obj)
|
61
|
+
if indent == 0:
|
62
|
+
last_top_obj = obj
|
63
|
+
return last_top_obj
|
64
|
+
|
65
|
+
|
66
|
+
def handle_function(idx, func_match, indent, stack, last_top_obj):
|
67
|
+
name = func_match.group(2)
|
68
|
+
parent = ""
|
69
|
+
for s in reversed(stack):
|
70
|
+
if s[0] == "class" and indent > s[2]:
|
71
|
+
parent = s[1]
|
72
|
+
break
|
73
|
+
obj = ("function", name, indent, idx + 1, parent)
|
74
|
+
stack.append(obj)
|
75
|
+
if indent == 0:
|
76
|
+
last_top_obj = obj
|
77
|
+
return last_top_obj
|
78
|
+
|
79
|
+
|
80
|
+
def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
|
81
|
+
class_pat, func_pat, assign_pat, main_pat = regexes
|
82
|
+
class_match = class_pat.match(line)
|
83
|
+
func_match = func_pat.match(line)
|
84
|
+
assign_match = assign_pat.match(line)
|
85
|
+
indent = len(line) - len(line.lstrip())
|
86
|
+
# If a new top-level class or function starts, close the previous one
|
87
|
+
if (class_match or func_match) and indent == 0 and last_top_obj:
|
88
|
+
last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
|
89
|
+
if class_match:
|
90
|
+
last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
|
91
|
+
elif func_match:
|
92
|
+
last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
|
93
|
+
elif assign_match and indent == 0:
|
94
|
+
handle_assignment(idx, assign_match, outline)
|
95
|
+
main_match = main_pat.match(line)
|
96
|
+
if main_match:
|
97
|
+
handle_main(idx, outline)
|
98
|
+
close_stack_objects(idx, indent, stack, obj_ranges)
|
99
|
+
return last_top_obj
|
100
|
+
|
101
|
+
|
102
|
+
def extract_signature_and_decorators(lines, start_idx):
|
103
|
+
"""
|
104
|
+
Extracts the signature line and leading decorators for a given function/class/method.
|
105
|
+
Returns (signature:str, decorators:List[str], signature_lineno:int)
|
106
|
+
"""
|
107
|
+
decorators = []
|
108
|
+
sig_line = None
|
109
|
+
sig_lineno = start_idx
|
110
|
+
for i in range(start_idx - 1, -1, -1):
|
111
|
+
striped = lines[i].strip()
|
112
|
+
if striped.startswith("@"):
|
113
|
+
decorators.insert(0, striped)
|
114
|
+
sig_lineno = i
|
115
|
+
elif not striped:
|
116
|
+
continue
|
117
|
+
else:
|
118
|
+
break
|
119
|
+
# Find the signature line itself
|
120
|
+
for k in range(start_idx, len(lines)):
|
121
|
+
striped = lines[k].strip()
|
122
|
+
if striped.startswith("def ") or striped.startswith("class "):
|
123
|
+
sig_line = striped
|
124
|
+
sig_lineno = k
|
125
|
+
break
|
126
|
+
return sig_line, decorators, sig_lineno
|
127
|
+
|
128
|
+
|
129
|
+
def extract_docstring(lines, start_idx, end_idx):
|
130
|
+
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
131
|
+
for i in range(start_idx, min(end_idx, len(lines))):
|
132
|
+
line = lines[i].lstrip()
|
133
|
+
if not line:
|
134
|
+
continue
|
135
|
+
if line.startswith('"""') or line.startswith("'''"):
|
136
|
+
quote = line[:3]
|
137
|
+
doc = line[3:]
|
138
|
+
if doc.strip().endswith(quote):
|
139
|
+
return doc.strip()[:-3].strip()
|
140
|
+
docstring_lines = [doc]
|
141
|
+
for j in range(i + 1, min(end_idx, len(lines))):
|
142
|
+
line = lines[j]
|
143
|
+
if line.strip().endswith(quote):
|
144
|
+
docstring_lines.append(line.strip()[:-3])
|
145
|
+
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
146
|
+
docstring_lines.append(line)
|
147
|
+
break
|
148
|
+
else:
|
149
|
+
break
|
150
|
+
return ""
|
151
|
+
|
152
|
+
|
153
|
+
def build_outline_entry(obj, lines, outline):
|
154
|
+
obj_type, name, start, end, parent, indent = obj
|
155
|
+
# Determine if this is a method
|
156
|
+
if obj_type == "function" and parent:
|
157
|
+
outline_type = "method"
|
158
|
+
elif obj_type == "function":
|
159
|
+
outline_type = "function"
|
160
|
+
else:
|
161
|
+
outline_type = obj_type
|
162
|
+
docstring = extract_docstring(lines, start, end)
|
163
|
+
outline.append(
|
164
|
+
{
|
165
|
+
"type": outline_type,
|
166
|
+
"name": name,
|
167
|
+
"start": start,
|
168
|
+
"end": end,
|
169
|
+
"parent": parent,
|
170
|
+
"docstring": docstring,
|
171
|
+
}
|
172
|
+
)
|
173
|
+
|
174
|
+
|
175
|
+
def process_lines(lines, regexes):
|
176
|
+
outline = []
|
177
|
+
stack = []
|
178
|
+
obj_ranges = []
|
179
|
+
last_top_obj = None
|
180
|
+
for idx, line in enumerate(lines):
|
181
|
+
last_top_obj = process_line(
|
182
|
+
idx, line, regexes, stack, obj_ranges, outline, last_top_obj
|
183
|
+
)
|
184
|
+
# Close any remaining open objects
|
185
|
+
for popped in stack:
|
186
|
+
obj_ranges.append(
|
187
|
+
(popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
|
188
|
+
)
|
189
|
+
return outline, obj_ranges
|
190
|
+
|
191
|
+
|
192
|
+
def build_outline(obj_ranges, lines, outline):
|
193
|
+
for obj in obj_ranges:
|
194
|
+
build_outline_entry(obj, lines, outline)
|
195
|
+
return outline
|
196
|
+
|
197
|
+
|
198
|
+
def parse_python_outline(lines: List[str]):
|
199
|
+
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
200
|
+
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
201
|
+
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
202
|
+
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
203
|
+
outline = []
|
204
|
+
stack = []
|
205
|
+
obj_ranges = []
|
206
|
+
last_top_obj = None
|
207
|
+
for idx, line in enumerate(lines):
|
208
|
+
class_match = class_pat.match(line)
|
209
|
+
func_match = func_pat.match(line)
|
210
|
+
assign_match = assign_pat.match(line)
|
211
|
+
indent = len(line) - len(line.lstrip())
|
212
|
+
parent = ""
|
213
|
+
for s in reversed(stack):
|
214
|
+
if s[0] == "class" and indent > s[2]:
|
215
|
+
parent = s[1]
|
216
|
+
break
|
217
|
+
if class_match:
|
218
|
+
obj = ("class", class_match.group(2), idx + 1, None, parent, indent)
|
219
|
+
stack.append(obj)
|
220
|
+
last_top_obj = obj
|
221
|
+
elif func_match:
|
222
|
+
obj = ("function", func_match.group(2), idx + 1, None, parent, indent)
|
223
|
+
stack.append(obj)
|
224
|
+
last_top_obj = obj
|
225
|
+
elif assign_match and indent == 0:
|
226
|
+
outline.append(
|
227
|
+
{
|
228
|
+
"type": "const" if assign_match.group(2).isupper() else "var",
|
229
|
+
"name": assign_match.group(2),
|
230
|
+
"start": idx + 1,
|
231
|
+
"end": idx + 1,
|
232
|
+
"parent": "",
|
233
|
+
"signature": line.strip(),
|
234
|
+
"decorators": [],
|
235
|
+
"docstring": "",
|
236
|
+
}
|
237
|
+
)
|
238
|
+
if line.strip().startswith("if __name__ == "):
|
239
|
+
outline.append(
|
240
|
+
{
|
241
|
+
"type": "main",
|
242
|
+
"name": "__main__",
|
243
|
+
"start": idx + 1,
|
244
|
+
"end": idx + 1,
|
245
|
+
"parent": "",
|
246
|
+
"signature": line.strip(),
|
247
|
+
"decorators": [],
|
248
|
+
"docstring": "",
|
249
|
+
}
|
250
|
+
)
|
251
|
+
# Close stack objects if indent falls back
|
252
|
+
while stack and indent <= stack[-1][5] and idx + 1 > stack[-1][2]:
|
253
|
+
finished = stack.pop()
|
254
|
+
outline_entry = finished[:2] + (
|
255
|
+
finished[2],
|
256
|
+
idx + 1,
|
257
|
+
finished[4],
|
258
|
+
finished[5],
|
259
|
+
)
|
260
|
+
build_outline_entry(outline_entry, lines, outline)
|
261
|
+
# Close any remaining objects
|
262
|
+
while stack:
|
263
|
+
finished = stack.pop()
|
264
|
+
outline_entry = finished[:2] + (
|
265
|
+
finished[2],
|
266
|
+
len(lines),
|
267
|
+
finished[4],
|
268
|
+
finished[5],
|
269
|
+
)
|
270
|
+
build_outline_entry(outline_entry, lines, outline)
|
271
|
+
return outline
|
272
|
+
|
273
|
+
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
274
|
+
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
275
|
+
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
276
|
+
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
277
|
+
regexes = (class_pat, func_pat, assign_pat, main_pat)
|
278
|
+
outline, obj_ranges = process_lines(lines, regexes)
|
279
|
+
return build_outline(obj_ranges, lines, outline)
|
280
|
+
|
281
|
+
|
282
|
+
def extract_docstring(lines, start_idx, end_idx):
|
283
|
+
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
284
|
+
for i in range(start_idx, min(end_idx, len(lines))):
|
285
|
+
line = lines[i].lstrip()
|
286
|
+
if not line:
|
287
|
+
continue
|
288
|
+
if line.startswith('"""') or line.startswith("'''"):
|
289
|
+
quote = line[:3]
|
290
|
+
doc = line[3:]
|
291
|
+
if doc.strip().endswith(quote):
|
292
|
+
return doc.strip()[:-3].strip()
|
293
|
+
docstring_lines = [doc]
|
294
|
+
for j in range(i + 1, min(end_idx, len(lines))):
|
295
|
+
line = lines[j]
|
296
|
+
if line.strip().endswith(quote):
|
297
|
+
docstring_lines.append(line.strip()[:-3])
|
298
|
+
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
299
|
+
docstring_lines.append(line)
|
300
|
+
break
|
301
|
+
else:
|
302
|
+
break
|
303
|
+
return ""
|