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.
Files changed (137) hide show
  1. janito/__init__.py +6 -6
  2. janito/agent/setup_agent.py +14 -5
  3. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
  4. janito/cli/chat_mode/bindings.py +6 -0
  5. janito/cli/chat_mode/session.py +16 -0
  6. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  7. janito/cli/chat_mode/shell/commands/__init__.py +3 -2
  8. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  9. janito/cli/chat_mode/shell/commands/exec.py +27 -0
  10. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  11. janito/cli/chat_mode/shell/commands/tools.py +17 -6
  12. janito/cli/chat_mode/shell/input_history.py +62 -62
  13. janito/cli/chat_mode/shell/session/manager.py +1 -0
  14. janito/cli/chat_mode/toolbar.py +3 -1
  15. janito/cli/cli_commands/list_models.py +35 -35
  16. janito/cli/cli_commands/list_providers.py +9 -9
  17. janito/cli/cli_commands/list_tools.py +53 -53
  18. janito/cli/cli_commands/model_selection.py +50 -50
  19. janito/cli/cli_commands/model_utils.py +13 -2
  20. janito/cli/cli_commands/set_api_key.py +19 -19
  21. janito/cli/cli_commands/show_config.py +51 -51
  22. janito/cli/cli_commands/show_system_prompt.py +62 -62
  23. janito/cli/config.py +2 -1
  24. janito/cli/core/__init__.py +4 -4
  25. janito/cli/core/event_logger.py +59 -59
  26. janito/cli/core/getters.py +3 -1
  27. janito/cli/core/runner.py +27 -6
  28. janito/cli/core/setters.py +5 -1
  29. janito/cli/core/unsetters.py +54 -54
  30. janito/cli/main_cli.py +12 -1
  31. janito/cli/prompt_core.py +5 -2
  32. janito/cli/rich_terminal_reporter.py +22 -3
  33. janito/cli/single_shot_mode/__init__.py +6 -6
  34. janito/cli/single_shot_mode/handler.py +11 -1
  35. janito/cli/verbose_output.py +1 -1
  36. janito/config.py +5 -5
  37. janito/config_manager.py +2 -0
  38. janito/driver_events.py +14 -0
  39. janito/drivers/anthropic/driver.py +113 -113
  40. janito/drivers/azure_openai/driver.py +38 -3
  41. janito/drivers/driver_registry.py +0 -2
  42. janito/drivers/openai/driver.py +196 -36
  43. janito/formatting_token.py +54 -54
  44. janito/i18n/__init__.py +35 -35
  45. janito/i18n/messages.py +23 -23
  46. janito/i18n/pt.py +47 -47
  47. janito/llm/__init__.py +5 -5
  48. janito/llm/agent.py +443 -443
  49. janito/llm/auth.py +1 -0
  50. janito/llm/driver.py +7 -1
  51. janito/llm/driver_config.py +1 -0
  52. janito/llm/driver_config_builder.py +34 -34
  53. janito/llm/driver_input.py +12 -12
  54. janito/llm/message_parts.py +60 -60
  55. janito/llm/model.py +38 -38
  56. janito/llm/provider.py +196 -196
  57. janito/provider_config.py +7 -3
  58. janito/provider_registry.py +29 -5
  59. janito/providers/__init__.py +1 -0
  60. janito/providers/anthropic/model_info.py +22 -22
  61. janito/providers/anthropic/provider.py +2 -2
  62. janito/providers/azure_openai/model_info.py +7 -6
  63. janito/providers/azure_openai/provider.py +44 -2
  64. janito/providers/deepseek/__init__.py +1 -1
  65. janito/providers/deepseek/model_info.py +16 -16
  66. janito/providers/deepseek/provider.py +91 -91
  67. janito/providers/google/model_info.py +21 -29
  68. janito/providers/google/provider.py +49 -38
  69. janito/providers/mistralai/provider.py +2 -2
  70. janito/providers/openai/model_info.py +0 -11
  71. janito/providers/openai/provider.py +1 -1
  72. janito/providers/provider_static_info.py +2 -3
  73. janito/providers/registry.py +26 -26
  74. janito/tools/adapters/__init__.py +1 -1
  75. janito/tools/adapters/local/__init__.py +62 -62
  76. janito/tools/adapters/local/adapter.py +33 -11
  77. janito/tools/adapters/local/ask_user.py +102 -102
  78. janito/tools/adapters/local/copy_file.py +84 -84
  79. janito/tools/adapters/local/create_directory.py +69 -69
  80. janito/tools/adapters/local/create_file.py +82 -82
  81. janito/tools/adapters/local/delete_text_in_file.py +4 -7
  82. janito/tools/adapters/local/fetch_url.py +97 -97
  83. janito/tools/adapters/local/find_files.py +138 -140
  84. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  85. janito/tools/adapters/local/get_file_outline/core.py +117 -151
  86. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -0
  87. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  88. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  89. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  90. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  91. janito/tools/adapters/local/move_file.py +3 -13
  92. janito/tools/adapters/local/open_html_in_browser.py +24 -29
  93. janito/tools/adapters/local/open_url.py +3 -2
  94. janito/tools/adapters/local/python_code_run.py +166 -166
  95. janito/tools/adapters/local/python_command_run.py +164 -164
  96. janito/tools/adapters/local/python_file_run.py +163 -163
  97. janito/tools/adapters/local/remove_directory.py +6 -17
  98. janito/tools/adapters/local/remove_file.py +9 -15
  99. janito/tools/adapters/local/replace_text_in_file.py +6 -9
  100. janito/tools/adapters/local/run_bash_command.py +176 -176
  101. janito/tools/adapters/local/run_powershell_command.py +219 -219
  102. janito/tools/adapters/local/search_text/__init__.py +1 -1
  103. janito/tools/adapters/local/search_text/core.py +201 -201
  104. janito/tools/adapters/local/search_text/match_lines.py +1 -1
  105. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  106. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  107. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  108. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  109. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  110. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  111. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  112. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  113. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  114. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  115. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  116. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  117. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  118. janito/tools/adapters/local/view_file.py +167 -167
  119. janito/tools/inspect_registry.py +17 -17
  120. janito/tools/tool_base.py +105 -105
  121. janito/tools/tool_events.py +58 -58
  122. janito/tools/tool_run_exception.py +12 -12
  123. janito/tools/tool_use_tracker.py +81 -81
  124. janito/tools/tool_utils.py +45 -45
  125. janito/tools/tools_adapter.py +78 -6
  126. janito/tools/tools_schema.py +104 -104
  127. janito/version.py +4 -4
  128. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/METADATA +388 -232
  129. janito-2.3.0.dist-info/RECORD +181 -0
  130. janito-2.3.0.dist-info/licenses/LICENSE +21 -0
  131. janito/cli/chat_mode/shell/commands/last.py +0 -137
  132. janito/drivers/google_genai/driver.py +0 -54
  133. janito/drivers/google_genai/schema_generator.py +0 -67
  134. janito-2.1.1.dist-info/RECORD +0 -181
  135. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
  136. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
  137. {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 ""