bioguider 0.2.52__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 (84) hide show
  1. bioguider/__init__.py +0 -0
  2. bioguider/agents/__init__.py +0 -0
  3. bioguider/agents/agent_task.py +92 -0
  4. bioguider/agents/agent_tools.py +176 -0
  5. bioguider/agents/agent_utils.py +504 -0
  6. bioguider/agents/collection_execute_step.py +182 -0
  7. bioguider/agents/collection_observe_step.py +125 -0
  8. bioguider/agents/collection_plan_step.py +156 -0
  9. bioguider/agents/collection_task.py +184 -0
  10. bioguider/agents/collection_task_utils.py +142 -0
  11. bioguider/agents/common_agent.py +137 -0
  12. bioguider/agents/common_agent_2step.py +215 -0
  13. bioguider/agents/common_conversation.py +61 -0
  14. bioguider/agents/common_step.py +85 -0
  15. bioguider/agents/consistency_collection_step.py +102 -0
  16. bioguider/agents/consistency_evaluation_task.py +57 -0
  17. bioguider/agents/consistency_evaluation_task_utils.py +14 -0
  18. bioguider/agents/consistency_observe_step.py +110 -0
  19. bioguider/agents/consistency_query_step.py +77 -0
  20. bioguider/agents/dockergeneration_execute_step.py +186 -0
  21. bioguider/agents/dockergeneration_observe_step.py +154 -0
  22. bioguider/agents/dockergeneration_plan_step.py +158 -0
  23. bioguider/agents/dockergeneration_task.py +158 -0
  24. bioguider/agents/dockergeneration_task_utils.py +220 -0
  25. bioguider/agents/evaluation_installation_task.py +270 -0
  26. bioguider/agents/evaluation_readme_task.py +767 -0
  27. bioguider/agents/evaluation_submission_requirements_task.py +172 -0
  28. bioguider/agents/evaluation_task.py +206 -0
  29. bioguider/agents/evaluation_tutorial_task.py +169 -0
  30. bioguider/agents/evaluation_tutorial_task_prompts.py +187 -0
  31. bioguider/agents/evaluation_userguide_prompts.py +179 -0
  32. bioguider/agents/evaluation_userguide_task.py +154 -0
  33. bioguider/agents/evaluation_utils.py +127 -0
  34. bioguider/agents/identification_execute_step.py +181 -0
  35. bioguider/agents/identification_observe_step.py +104 -0
  36. bioguider/agents/identification_plan_step.py +140 -0
  37. bioguider/agents/identification_task.py +270 -0
  38. bioguider/agents/identification_task_utils.py +22 -0
  39. bioguider/agents/peo_common_step.py +64 -0
  40. bioguider/agents/prompt_utils.py +253 -0
  41. bioguider/agents/python_ast_repl_tool.py +69 -0
  42. bioguider/agents/rag_collection_task.py +130 -0
  43. bioguider/conversation.py +67 -0
  44. bioguider/database/code_structure_db.py +500 -0
  45. bioguider/database/summarized_file_db.py +146 -0
  46. bioguider/generation/__init__.py +39 -0
  47. bioguider/generation/benchmark_metrics.py +610 -0
  48. bioguider/generation/change_planner.py +189 -0
  49. bioguider/generation/document_renderer.py +157 -0
  50. bioguider/generation/llm_cleaner.py +67 -0
  51. bioguider/generation/llm_content_generator.py +1128 -0
  52. bioguider/generation/llm_injector.py +809 -0
  53. bioguider/generation/models.py +85 -0
  54. bioguider/generation/output_manager.py +74 -0
  55. bioguider/generation/repo_reader.py +37 -0
  56. bioguider/generation/report_loader.py +166 -0
  57. bioguider/generation/style_analyzer.py +36 -0
  58. bioguider/generation/suggestion_extractor.py +436 -0
  59. bioguider/generation/test_metrics.py +189 -0
  60. bioguider/managers/benchmark_manager.py +785 -0
  61. bioguider/managers/evaluation_manager.py +215 -0
  62. bioguider/managers/generation_manager.py +686 -0
  63. bioguider/managers/generation_test_manager.py +107 -0
  64. bioguider/managers/generation_test_manager_v2.py +525 -0
  65. bioguider/rag/__init__.py +0 -0
  66. bioguider/rag/config.py +117 -0
  67. bioguider/rag/data_pipeline.py +651 -0
  68. bioguider/rag/embedder.py +24 -0
  69. bioguider/rag/rag.py +138 -0
  70. bioguider/settings.py +103 -0
  71. bioguider/utils/code_structure_builder.py +59 -0
  72. bioguider/utils/constants.py +135 -0
  73. bioguider/utils/default.gitignore +140 -0
  74. bioguider/utils/file_utils.py +215 -0
  75. bioguider/utils/gitignore_checker.py +175 -0
  76. bioguider/utils/notebook_utils.py +117 -0
  77. bioguider/utils/pyphen_utils.py +73 -0
  78. bioguider/utils/python_file_handler.py +65 -0
  79. bioguider/utils/r_file_handler.py +551 -0
  80. bioguider/utils/utils.py +163 -0
  81. bioguider-0.2.52.dist-info/LICENSE +21 -0
  82. bioguider-0.2.52.dist-info/METADATA +51 -0
  83. bioguider-0.2.52.dist-info/RECORD +84 -0
  84. bioguider-0.2.52.dist-info/WHEEL +4 -0
@@ -0,0 +1,551 @@
1
+ import os
2
+ import re
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional, Tuple
5
+
6
+ @dataclass
7
+ class RSymbol:
8
+ name: str
9
+ parent: Optional[str]
10
+ start_line: int
11
+ end_line: int
12
+ docstring: Optional[str]
13
+ params: List[str]
14
+
15
+
16
+ class RFileHandler:
17
+ # only up to "function("
18
+ FUNC_DEF_HEAD_RE = re.compile(
19
+ r'(?P<name>[A-Za-z.][\w.]*)\s*<-\s*function\s*\(',
20
+ re.MULTILINE,
21
+ )
22
+
23
+ S3_METHOD_HEAD_RE = re.compile(
24
+ r'(?P<generic>[A-Za-z.][\w.]*)\.(?P<class>[A-Za-z.][\w.]*)\s*<-\s*function\s*\(',
25
+ re.MULTILINE,
26
+ )
27
+
28
+ # R6 method head: "name = function("
29
+ R6_METHOD_HEAD_RE = re.compile(
30
+ r'(?P<mname>[A-Za-z.][\w.]*)\s*=\s*function\s*\(',
31
+ re.MULTILINE,
32
+ )
33
+
34
+ # S4 method head inside setMethod(... function(
35
+ S4_METHOD_HEAD_RE = re.compile(
36
+ r'setMethod\s*\(\s*["\'](?P<generic>[^"\']+)["\']\s*,.*?function\s*\(',
37
+ re.MULTILINE | re.DOTALL,
38
+ )
39
+
40
+ FUNC_DEF_RE = re.compile(
41
+ # name <- function( ... ) { with multi-line args allowed
42
+ r'(?P<name>[A-Za-z.][\w.]*)\s*<-\s*function\s*\((?P<args>[^)]*)\)\s*\{',
43
+ re.MULTILINE,
44
+ )
45
+ S3_METHOD_RE = re.compile(
46
+ r'(?P<generic>[A-Za-z.][\w.]*)\.(?P<class>[A-Za-z.][\w.]*)\s*<-\s*function\s*\((?P<args>[^)]*)\)\s*\{',
47
+ re.MULTILINE,
48
+ )
49
+ R6_CLASS_RE = re.compile(
50
+ r'(?P<varname>[A-Za-z.][\w.]*)\s*<-\s*R6Class\s*\(\s*["\'](?P<classname>[^"\']+)["\']',
51
+ re.MULTILINE | re.DOTALL,
52
+ )
53
+ R6_METHOD_RE = re.compile(
54
+ r'(?P<mname>[A-Za-z.][\w.]*)\s*=\s*function\s*\((?P<args>[^)]*)\)\s*\{',
55
+ re.MULTILINE,
56
+ )
57
+ S4_CLASS_RE = re.compile(
58
+ r'setClass\s*\(\s*["\'](?P<classname>[^"\']+)["\']',
59
+ re.MULTILINE,
60
+ )
61
+ S4_METHOD_RE = re.compile(
62
+ r'setMethod\s*\(\s*["\'](?P<generic>[^"\']+)["\']\s*,.*?function\s*\((?P<args>[^)]*)\)\s*\{',
63
+ re.MULTILINE | re.DOTALL,
64
+ )
65
+ S4_SIG_CLASS_RE = re.compile(
66
+ r'signature\s*=\s*(?:list\s*\(|\()\s*(?:[^)]*class\s*=\s*["\'](?P<classname>[^"\']+)["\']|["\'](?P<classname2>[^"\']+)["\'])',
67
+ re.MULTILINE,
68
+ )
69
+ LIB_REQUIRE_RE = re.compile(
70
+ r'\b(?:library|require)\s*\(\s*([A-Za-z.][\w.]*)\s*\)',
71
+ re.MULTILINE,
72
+ )
73
+ NS_USE_RE = re.compile(
74
+ r'(?P<pkg>[A-Za-z.][\w.]*):::{0,2}(?P<sym>[A-Za-z.][\w.]*)',
75
+ re.MULTILINE,
76
+ )
77
+
78
+ def __init__(self, file_path: str):
79
+ self.file_path = file_path
80
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
81
+ self.text = f.read()
82
+ self.lines = self.text.splitlines()
83
+ self._brace_map = self._build_brace_map_safely() # FIX: ignore comments/strings
84
+
85
+ # ---------------- Public API ----------------
86
+
87
+ def get_functions_and_classes(self) -> List[Tuple[str, Optional[str], int, int, Optional[str], List[str]]]:
88
+ items: List[RSymbol] = []
89
+ items.extend(self._parse_functions())
90
+ items.extend(self._parse_s3_methods())
91
+ items.extend(self._parse_r6())
92
+ items.extend(self._parse_s4())
93
+ items.sort(key=lambda s: (s.start_line, s.end_line))
94
+ return [(i.name, i.parent, i.start_line, i.end_line, i.docstring, i.params) for i in items]
95
+
96
+ def get_imports(self) -> List[str]:
97
+ pkgs = set(self.LIB_REQUIRE_RE.findall(self.text))
98
+ for m in self.NS_USE_RE.finditer(self.text):
99
+ pkgs.add(m.group('pkg'))
100
+ return sorted(pkgs)
101
+
102
+ # ---------------- Parsers ----------------
103
+
104
+ def _parse_functions(self) -> List[RSymbol]:
105
+ syms: List[RSymbol] = []
106
+ for m in self.FUNC_DEF_HEAD_RE.finditer(self.text):
107
+ name = m.group('name')
108
+ open_paren = m.end() - 1 # points at '('
109
+ close_paren = self._matching_paren_pos_global(open_paren)
110
+ if close_paren is None:
111
+ continue
112
+ args_text = self.text[open_paren + 1: close_paren]
113
+ args = self._parse_params(args_text)
114
+
115
+ block_open = self._find_next_code_brace_after(close_paren + 1)
116
+ if block_open is None:
117
+ continue
118
+ block_close = self._matching_brace_pos(block_open)
119
+
120
+ start_line = self._pos_to_line(block_open)
121
+ end_line = self._pos_to_line(block_close)
122
+ doc = self._roxygen_before(m.start())
123
+
124
+ syms.append(RSymbol(name=name, parent=None,
125
+ start_line=start_line, end_line=end_line,
126
+ docstring=doc, params=args))
127
+
128
+ # nested
129
+ syms.extend(self._parse_nested_functions(block_open, block_close, parent=name))
130
+ return syms
131
+
132
+ def _parse_nested_functions(self, abs_start: int, abs_end: int, parent: str) -> List[RSymbol]:
133
+ sub = self.text[abs_start:abs_end+1]
134
+ syms: List[RSymbol] = []
135
+ for m in self.FUNC_DEF_HEAD_RE.finditer(sub):
136
+ open_rel = m.end() - 1
137
+ close_rel = self._matching_paren_pos_in_text(sub, open_rel)
138
+ if close_rel is None:
139
+ continue
140
+ args_text = sub[open_rel + 1: close_rel]
141
+ args = self._parse_params(args_text)
142
+
143
+ # brace after ')' within the slice
144
+ func_open_rel = self._find_next_char_in_text(sub, '{', close_rel + 1)
145
+ if func_open_rel is None:
146
+ continue
147
+ func_close_rel = self._matching_brace_pos_in_text(sub, func_open_rel)
148
+ if func_close_rel is None:
149
+ continue
150
+
151
+ block_open = abs_start + func_open_rel
152
+ block_close = abs_start + func_close_rel
153
+ name = m.group('name')
154
+ doc = self._roxygen_before(block_open)
155
+ syms.append(RSymbol(
156
+ name=name, parent=parent,
157
+ start_line=self._pos_to_line(block_open),
158
+ end_line=self._pos_to_line(block_close),
159
+ docstring=doc, params=args
160
+ ))
161
+ return syms
162
+
163
+
164
+ def _parse_s3_methods(self) -> List[RSymbol]:
165
+ syms: List[RSymbol] = []
166
+ for m in self.S3_METHOD_HEAD_RE.finditer(self.text):
167
+ generic = m.group('generic')
168
+ clazz = m.group('class')
169
+ name = f"{generic}.{clazz}"
170
+
171
+ open_paren = m.end() - 1
172
+ close_paren = self._matching_paren_pos_global(open_paren)
173
+ if close_paren is None:
174
+ continue
175
+ args_text = self.text[open_paren + 1: close_paren]
176
+ args = self._parse_params(args_text)
177
+
178
+ block_open = self._find_next_code_brace_after(close_paren + 1)
179
+ if block_open is None:
180
+ continue
181
+ block_close = self._matching_brace_pos(block_open)
182
+
183
+ syms.append(RSymbol(
184
+ name=name, parent=generic,
185
+ start_line=self._pos_to_line(block_open),
186
+ end_line=self._pos_to_line(block_close),
187
+ docstring=self._roxygen_before(m.start()),
188
+ params=args
189
+ ))
190
+ return syms
191
+
192
+
193
+ def _parse_r6(self) -> List[RSymbol]:
194
+ syms: List[RSymbol] = []
195
+ for m in self.R6_CLASS_RE.finditer(self.text):
196
+ classname = m.group('classname')
197
+ # Find the first '{' after R6Class( — it's the class call's body brace
198
+ first_brace = self._find_next_code_brace_after(m.end())
199
+ if first_brace is None:
200
+ continue
201
+ class_end = self._matching_brace_pos(first_brace)
202
+ syms.append(RSymbol(
203
+ name=classname, parent=None,
204
+ start_line=self._pos_to_line(first_brace),
205
+ end_line=self._pos_to_line(class_end),
206
+ docstring=self._roxygen_before(m.start()),
207
+ params=[]
208
+ ))
209
+ # Methods within public/private/active lists
210
+ class_text = self.text[m.start():class_end+1]
211
+ base = m.start()
212
+ for sect in ('public', 'private', 'active'):
213
+ for meth in self._parse_r6_section_methods(class_text, base, sect, classname):
214
+ syms.append(meth)
215
+ return syms
216
+
217
+ def _parse_r6_section_methods(self, class_text: str, base: int, section: str, parent_class: str) -> List[RSymbol]:
218
+ syms: List[RSymbol] = []
219
+ for sec in re.finditer(rf'{section}\s*=\s*list\s*\(', class_text):
220
+ lst_open = sec.end() - 1
221
+ lst_close = self._matching_paren_pos_in_text(class_text, lst_open)
222
+ if lst_close is None:
223
+ continue
224
+ list_text = class_text[lst_open:lst_close+1]
225
+ for m in self.R6_METHOD_HEAD_RE.finditer(list_text):
226
+ open_rel = m.end() - 1
227
+ close_rel = self._matching_paren_pos_in_text(list_text, open_rel)
228
+ if close_rel is None:
229
+ continue
230
+ args_text = list_text[open_rel + 1: close_rel]
231
+ args = self._parse_params(args_text)
232
+
233
+ func_open_rel = self._find_next_char_in_text(list_text, '{', close_rel + 1)
234
+ if func_open_rel is None:
235
+ continue
236
+ func_close_rel = self._matching_brace_pos_in_text(list_text, func_open_rel)
237
+ if func_close_rel is None:
238
+ continue
239
+
240
+ block_open = base + lst_open + func_open_rel
241
+ block_close = base + lst_open + func_close_rel
242
+
243
+ syms.append(RSymbol(
244
+ name=f"{parent_class}${m.group('mname')}",
245
+ parent=parent_class,
246
+ start_line=self._pos_to_line(block_open),
247
+ end_line=self._pos_to_line(block_close),
248
+ docstring=self._roxygen_before(block_open),
249
+ params=args
250
+ ))
251
+ return syms
252
+
253
+
254
+ def _parse_s4(self) -> List[RSymbol]:
255
+ syms: List[RSymbol] = []
256
+ for m in self.S4_CLASS_RE.finditer(self.text):
257
+ syms.append(RSymbol(
258
+ name=m.group('classname'), parent=None,
259
+ start_line=self._pos_to_line(m.start()),
260
+ end_line=self._pos_to_line(m.start()),
261
+ docstring=self._roxygen_before(m.start()),
262
+ params=[]
263
+ ))
264
+ for m in self.S4_METHOD_HEAD_RE.finditer(self.text):
265
+ generic = m.group('generic')
266
+
267
+ open_paren = m.end() - 1
268
+ close_paren = self._matching_paren_pos_global(open_paren)
269
+ if close_paren is None:
270
+ continue
271
+ args_text = self.text[open_paren + 1: close_paren]
272
+ args = self._parse_params(args_text)
273
+
274
+ block_open = self._find_next_code_brace_after(close_paren + 1)
275
+ block_close = self._matching_brace_pos(block_open) if block_open is not None else m.end()
276
+
277
+ sig_slice = self.text[m.start(): block_open or m.end()]
278
+ cm = self.S4_SIG_CLASS_RE.search(sig_slice)
279
+ clazz = cm.group('classname') if cm and cm.group('classname') else (cm.group('classname2') if cm else None)
280
+ name = f"{generic}{'<' + clazz + '>' if clazz else ''}"
281
+
282
+ syms.append(RSymbol(
283
+ name=name, parent=generic,
284
+ start_line=self._pos_to_line(block_open if block_open is not None else m.start()),
285
+ end_line=self._pos_to_line(block_close),
286
+ docstring=self._roxygen_before(m.start()),
287
+ params=args
288
+ ))
289
+
290
+ return syms
291
+
292
+ # ---------------- Utilities ----------------
293
+
294
+ def _parse_params(self, arg_str: str) -> List[str]:
295
+ params = []
296
+ depth = 0
297
+ token = []
298
+ in_s: Optional[str] = None
299
+ escape = False
300
+ for ch in arg_str:
301
+ if in_s:
302
+ token.append(ch)
303
+ if escape:
304
+ escape = False
305
+ elif ch == '\\':
306
+ escape = True
307
+ elif ch == in_s:
308
+ in_s = None
309
+ continue
310
+ if ch in ('"', "'"):
311
+ in_s = ch
312
+ token.append(ch)
313
+ continue
314
+ if ch in '([{':
315
+ depth += 1
316
+ token.append(ch)
317
+ elif ch in ')]}':
318
+ depth -= 1
319
+ token.append(ch)
320
+ elif ch == ',' and depth == 0:
321
+ params.append(''.join(token).strip())
322
+ token = []
323
+ else:
324
+ token.append(ch)
325
+ if token:
326
+ params.append(''.join(token).strip())
327
+
328
+ cleaned = []
329
+ for p in params:
330
+ p = p.strip()
331
+ if not p:
332
+ continue
333
+ if p == '...':
334
+ cleaned.append('...')
335
+ continue
336
+ name = p.split('=')[0].strip()
337
+ if name:
338
+ cleaned.append(name)
339
+ return cleaned
340
+
341
+ def _roxygen_before(self, pos: int) -> Optional[str]:
342
+ line_idx = self._pos_to_line(pos) - 2
343
+ if line_idx < 0:
344
+ return None
345
+ buf = []
346
+ while line_idx >= 0:
347
+ line = self.lines[line_idx]
348
+ s = line.lstrip()
349
+ if s.startswith("#'"):
350
+ buf.append(s[2:].lstrip())
351
+ elif s.strip() == "":
352
+ pass
353
+ else:
354
+ # stop at first non-roxygen line (don’t cross blank + NULL padding blocks)
355
+ break
356
+ line_idx -= 1
357
+ if not buf:
358
+ return None
359
+ buf.reverse()
360
+ return '\n'.join(buf).strip() or None
361
+
362
+ # -------- Position / brace helpers (comment/string aware) --------
363
+
364
+ def _build_brace_map_safely(self):
365
+ """
366
+ Build a map of '{' -> matching '}' while ignoring braces inside:
367
+ - comments starting with '#'
368
+ - single- and double-quoted strings with escapes
369
+ """
370
+ stack = []
371
+ pairs = {}
372
+ in_string: Optional[str] = None
373
+ escape = False
374
+ in_comment = False
375
+
376
+ for i, ch in enumerate(self.text):
377
+ if in_comment:
378
+ if ch == '\n':
379
+ in_comment = False
380
+ continue
381
+
382
+ if in_string:
383
+ if escape:
384
+ escape = False
385
+ continue
386
+ if ch == '\\':
387
+ escape = True
388
+ continue
389
+ if ch == in_string:
390
+ in_string = None
391
+ continue
392
+
393
+ # not in string/comment
394
+ if ch == '#':
395
+ in_comment = True
396
+ continue
397
+ if ch == '"' or ch == "'":
398
+ in_string = ch
399
+ continue
400
+
401
+ if ch == '{':
402
+ stack.append(i)
403
+ elif ch == '}':
404
+ if stack:
405
+ open_i = stack.pop()
406
+ pairs[open_i] = i
407
+ return pairs
408
+
409
+ def _matching_brace_pos(self, open_brace_pos: int) -> int:
410
+ return self._brace_map.get(open_brace_pos, len(self.text) - 1)
411
+
412
+ def _find_next_code_brace_after(self, start: int) -> Optional[int]:
413
+ """Find next '{' after start, skipping ones in comments/strings by scanning forward again."""
414
+ in_string: Optional[str] = None
415
+ escape = False
416
+ in_comment = False
417
+ for i in range(start, len(self.text)):
418
+ ch = self.text[i]
419
+ if in_comment:
420
+ if ch == '\n':
421
+ in_comment = False
422
+ continue
423
+ if in_string:
424
+ if escape:
425
+ escape = False
426
+ continue
427
+ if ch == '\\':
428
+ escape = True
429
+ continue
430
+ if ch == in_string:
431
+ in_string = None
432
+ continue
433
+ if ch == '#':
434
+ in_comment = True
435
+ continue
436
+ if ch == '"' or ch == "'":
437
+ in_string = ch
438
+ continue
439
+ if ch == '{':
440
+ return i
441
+ return None
442
+
443
+ def _pos_to_line(self, pos: int) -> int:
444
+ return self.text.count('\n', 0, max(0, pos)) + 1
445
+
446
+ def _find_next_char_in_text(self, text: str, ch: str, start: int) -> Optional[int]:
447
+ idx = text.find(ch, start)
448
+ return idx if idx != -1 else None
449
+
450
+ # For nested parsing on a slice (already delimited correctly)
451
+ def _matching_brace_pos_in_text(self, text: str, open_idx: int) -> Optional[int]:
452
+ in_string: Optional[str] = None
453
+ escape = False
454
+ in_comment = False
455
+ depth = 0
456
+ for i in range(open_idx, len(text)):
457
+ ch = text[i]
458
+ if in_comment:
459
+ if ch == '\n':
460
+ in_comment = False
461
+ continue
462
+ if in_string:
463
+ if escape:
464
+ escape = False
465
+ elif ch == '\\':
466
+ escape = True
467
+ elif ch == in_string:
468
+ in_string = None
469
+ continue
470
+ if ch == '#':
471
+ in_comment = True
472
+ continue
473
+ if ch == '"' or ch == "'":
474
+ in_string = ch
475
+ continue
476
+ if ch == '{':
477
+ depth += 1
478
+ elif ch == '}':
479
+ depth -= 1
480
+ if depth == 0:
481
+ return i
482
+ return None
483
+
484
+ def _matching_paren_pos_in_text(self, text: str, open_idx: int) -> Optional[int]:
485
+ in_string: Optional[str] = None
486
+ escape = False
487
+ in_comment = False
488
+ depth = 0
489
+ for i in range(open_idx, len(text)):
490
+ ch = text[i]
491
+ if in_comment:
492
+ if ch == '\n':
493
+ in_comment = False
494
+ continue
495
+ if in_string:
496
+ if escape:
497
+ escape = False
498
+ elif ch == '\\':
499
+ escape = True
500
+ elif ch == in_string:
501
+ in_string = None
502
+ continue
503
+ if ch == '#':
504
+ in_comment = True
505
+ continue
506
+ if ch == '"' or ch == "'":
507
+ in_string = ch
508
+ continue
509
+ if ch == '(':
510
+ depth += 1
511
+ elif ch == ')':
512
+ depth -= 1
513
+ if depth == 0:
514
+ return i
515
+ return None
516
+
517
+ def _matching_paren_pos_global(self, open_idx: int) -> Optional[int]:
518
+ """Given an index of '(' in self.text, return the matching ')' index,
519
+ ignoring parentheses inside strings/comments."""
520
+ in_string: Optional[str] = None
521
+ escape = False
522
+ in_comment = False
523
+ depth = 0
524
+ for i in range(open_idx, len(self.text)):
525
+ ch = self.text[i]
526
+ if in_comment:
527
+ if ch == '\n':
528
+ in_comment = False
529
+ continue
530
+ if in_string:
531
+ if escape:
532
+ escape = False
533
+ elif ch == '\\':
534
+ escape = True
535
+ elif ch == in_string:
536
+ in_string = None
537
+ continue
538
+ if ch == '#':
539
+ in_comment = True
540
+ continue
541
+ if ch == '"' or ch == "'":
542
+ in_string = ch
543
+ continue
544
+ if ch == '(':
545
+ depth += 1
546
+ elif ch == ')':
547
+ depth -= 1
548
+ if depth == 0:
549
+ return i
550
+ return None
551
+