chartflow 0.1__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 (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. chartflow-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,523 @@
1
+ """
2
+ 代码折叠系统
3
+ 支持代码块的折叠和展开
4
+ """
5
+
6
+ from PyQt6.QtCore import QObject, pyqtSignal, QRect, Qt, QPoint
7
+ from PyQt6.QtGui import QPainter, QColor, QFont, QIcon, QPolygon
8
+ from PyQt6.QtWidgets import QStyle, QStyleOption
9
+ from typing import List, Dict, Set, Tuple, Optional
10
+ import ast
11
+ import re
12
+
13
+
14
+ class FoldRegion:
15
+ """折叠区域"""
16
+ def __init__(self, start_line: int, end_line: int,
17
+ fold_type: str = 'block', indent: int = 0):
18
+ self.start_line = start_line # 开始行(1-based)
19
+ self.end_line = end_line # 结束行(1-based)
20
+ self.fold_type = fold_type # 类型:class, function, if, for, while, with, try
21
+ self.indent = indent # 缩进级别
22
+ self.collapsed = False # 是否折叠
23
+ self.marker_rect: Optional[QRect] = None # 折叠标记矩形
24
+
25
+ def __repr__(self):
26
+ return f"FoldRegion({self.start_line}-{self.end_line}, {self.fold_type})"
27
+
28
+ def contains(self, line: int) -> bool:
29
+ """检查行是否在折叠区域内"""
30
+ return self.start_line < line <= self.end_line
31
+
32
+ def toggle(self):
33
+ """切换折叠状态"""
34
+ self.collapsed = not self.collapsed
35
+ return self.collapsed
36
+
37
+
38
+ class CodeFolder(QObject):
39
+ """代码折叠管理器"""
40
+
41
+ fold_changed = pyqtSignal(int, bool) # 折叠状态改变信号 (line, collapsed)
42
+ regions_changed = pyqtSignal() # 折叠区域改变信号
43
+
44
+ def __init__(self, parent=None):
45
+ super().__init__(parent)
46
+ self._regions: List[FoldRegion] = []
47
+ self._collapsed_lines: Set[int] = set() # 被折叠的行
48
+ self._code: str = ""
49
+
50
+ def analyze_code(self, code: str):
51
+ """分析代码,找出可折叠区域"""
52
+ self._code = code
53
+ self._regions = []
54
+
55
+ try:
56
+ tree = ast.parse(code)
57
+ # 遍历模块的所有顶层节点
58
+ for node in tree.body:
59
+ self._analyze_node(node, 0)
60
+ except SyntaxError:
61
+ # 语法错误时使用简单缩进分析
62
+ self._analyze_indentation(code)
63
+
64
+ # 按开始行排序
65
+ self._regions.sort(key=lambda r: r.start_line)
66
+
67
+ self.regions_changed.emit()
68
+
69
+ def _analyze_node(self, node: ast.AST, indent: int):
70
+ """递归分析AST节点"""
71
+ if isinstance(node, ast.ClassDef):
72
+ end_line = self._get_end_line(node)
73
+ if end_line > node.lineno:
74
+ self._regions.append(FoldRegion(
75
+ start_line=node.lineno,
76
+ end_line=end_line,
77
+ fold_type='class',
78
+ indent=indent
79
+ ))
80
+ for item in node.body:
81
+ self._analyze_node(item, indent + 1)
82
+
83
+ elif isinstance(node, ast.FunctionDef):
84
+ end_line = self._get_end_line(node)
85
+ if end_line > node.lineno:
86
+ self._regions.append(FoldRegion(
87
+ start_line=node.lineno,
88
+ end_line=end_line,
89
+ fold_type='function',
90
+ indent=indent
91
+ ))
92
+ for item in node.body:
93
+ self._analyze_node(item, indent + 1)
94
+
95
+ elif isinstance(node, ast.AsyncFunctionDef):
96
+ end_line = self._get_end_line(node)
97
+ if end_line > node.lineno:
98
+ self._regions.append(FoldRegion(
99
+ start_line=node.lineno,
100
+ end_line=end_line,
101
+ fold_type='function',
102
+ indent=indent
103
+ ))
104
+ for item in node.body:
105
+ self._analyze_node(item, indent + 1)
106
+
107
+ elif isinstance(node, ast.If):
108
+ end_line = self._get_end_line(node)
109
+ if end_line > node.lineno:
110
+ self._regions.append(FoldRegion(
111
+ start_line=node.lineno,
112
+ end_line=end_line,
113
+ fold_type='if',
114
+ indent=indent
115
+ ))
116
+ for item in node.body:
117
+ self._analyze_node(item, indent + 1)
118
+ for item in node.orelse:
119
+ self._analyze_node(item, indent + 1)
120
+
121
+ elif isinstance(node, ast.For):
122
+ end_line = self._get_end_line(node)
123
+ if end_line > node.lineno:
124
+ self._regions.append(FoldRegion(
125
+ start_line=node.lineno,
126
+ end_line=end_line,
127
+ fold_type='for',
128
+ indent=indent
129
+ ))
130
+ for item in node.body:
131
+ self._analyze_node(item, indent + 1)
132
+
133
+ elif isinstance(node, ast.While):
134
+ end_line = self._get_end_line(node)
135
+ if end_line > node.lineno:
136
+ self._regions.append(FoldRegion(
137
+ start_line=node.lineno,
138
+ end_line=end_line,
139
+ fold_type='while',
140
+ indent=indent
141
+ ))
142
+ for item in node.body:
143
+ self._analyze_node(item, indent + 1)
144
+
145
+ elif isinstance(node, ast.With):
146
+ end_line = self._get_end_line(node)
147
+ if end_line > node.lineno:
148
+ self._regions.append(FoldRegion(
149
+ start_line=node.lineno,
150
+ end_line=end_line,
151
+ fold_type='with',
152
+ indent=indent
153
+ ))
154
+ for item in node.body:
155
+ self._analyze_node(item, indent + 1)
156
+
157
+ elif isinstance(node, ast.Try):
158
+ end_line = self._get_end_line(node)
159
+ if end_line > node.lineno:
160
+ self._regions.append(FoldRegion(
161
+ start_line=node.lineno,
162
+ end_line=end_line,
163
+ fold_type='try',
164
+ indent=indent
165
+ ))
166
+ for item in node.body:
167
+ self._analyze_node(item, indent + 1)
168
+ for handler in node.handlers:
169
+ for item in handler.body:
170
+ self._analyze_node(item, indent + 1)
171
+ for item in node.orelse:
172
+ self._analyze_node(item, indent + 1)
173
+ for item in node.finalbody:
174
+ self._analyze_node(item, indent + 1)
175
+
176
+ def _get_end_line(self, node: ast.AST) -> int:
177
+ """获取节点的结束行号"""
178
+ max_line = getattr(node, 'lineno', 0)
179
+ for child in ast.walk(node):
180
+ if hasattr(child, 'lineno'):
181
+ max_line = max(max_line, child.lineno)
182
+ if hasattr(child, 'end_lineno') and child.end_lineno:
183
+ max_line = max(max_line, child.end_lineno)
184
+ return max_line
185
+
186
+ def _analyze_indentation(self, code: str):
187
+ """使用缩进分析代码块(语法错误时的回退方案)
188
+
189
+ 改进的折叠逻辑:
190
+ 1. 只有行末带有 ':' 的语句才显示折叠按钮(排除行末空白字符干扰)
191
+ 2. 通过缩进分析找到代码块的结束位置
192
+ 3. 如果用户代码正确(有缩进的子块),才显示折叠按钮
193
+ """
194
+ lines = code.split('\n')
195
+
196
+ # 匹配可能的代码块开始语句(关键字在行首,以冒号结尾)
197
+ block_patterns = [
198
+ (r'^\s*(class|def)\s+\w+', 'block'),
199
+ (r'^\s*(async\s+def)\s+\w+', 'block'),
200
+ (r'^\s*if\s+[^:]+:', 'if'),
201
+ (r'^\s*elif\s+[^:]+:', 'elif'),
202
+ (r'^\s*else\s*:', 'else'),
203
+ (r'^\s*for\s+[^:]+:', 'for'),
204
+ (r'^\s*while\s+[^:]+:', 'while'),
205
+ (r'^\s*with\s+[^:]+:', 'with'),
206
+ (r'^\s*try\s*:', 'try'),
207
+ (r'^\s*except\s*', 'except'),
208
+ (r'^\s*finally\s*:', 'finally'),
209
+ (r'^\s*match\s+[^:]+:', 'match'),
210
+ (r'^\s*case\s+[^:]+:', 'case'),
211
+ ]
212
+
213
+ def get_indent(line: str) -> int:
214
+ """获取行的缩进级别"""
215
+ return len(line) - len(line.lstrip())
216
+
217
+ def is_block_start(line: str) -> tuple:
218
+ """检查是否是代码块开始
219
+ 返回: (是否是块开始, 块类型)
220
+ """
221
+ # 去除行末空白后检查是否以冒号结尾
222
+ stripped_end = line.rstrip()
223
+ if not stripped_end.endswith(':'):
224
+ return False, ''
225
+
226
+ for pattern, block_type in block_patterns:
227
+ if re.match(pattern, line):
228
+ return True, block_type
229
+ return False, ''
230
+
231
+ i = 0
232
+ while i < len(lines):
233
+ line = lines[i]
234
+
235
+ # 检查是否是块开始
236
+ is_start, block_type = is_block_start(line)
237
+ if is_start:
238
+ start_indent = get_indent(line)
239
+ start_line = i + 1
240
+
241
+ # 查找块的结束位置
242
+ # 块的结束是下一个相同缩进或更少缩进的非空行
243
+ j = i + 1
244
+ while j < len(lines):
245
+ next_line = lines[j]
246
+ next_line_stripped = next_line.strip()
247
+
248
+ if next_line_stripped: # 非空行
249
+ next_indent = get_indent(next_line)
250
+
251
+ # 如果缩进小于等于起始缩进,说明块结束了
252
+ if next_indent <= start_indent:
253
+ break
254
+
255
+ # 检查是否是同一缩进级别的 elif/else/case(这些应该属于同一个 if/match 块,而不是新块)
256
+ if next_indent == start_indent + 4:
257
+ next_is_start, next_type = is_block_start(next_line)
258
+ if next_is_start and next_type in ('elif', 'else', 'case'):
259
+ # 这些是分支语句,不终止当前块
260
+ pass
261
+
262
+ j += 1
263
+
264
+ end_line = j
265
+ # 只有当块有实际内容(至少有一行缩进的子代码)时才显示折叠按钮
266
+ if end_line > start_line + 1:
267
+ # 检查中间是否有缩进的代码
268
+ has_indented_content = False
269
+ for k in range(start_line, end_line):
270
+ content_line = lines[k]
271
+ if content_line.strip():
272
+ content_indent = get_indent(content_line)
273
+ if content_indent > start_indent:
274
+ has_indented_content = True
275
+ break
276
+
277
+ if has_indented_content:
278
+ region = FoldRegion(
279
+ start_line=start_line,
280
+ end_line=end_line - 1, # 结束于最后一个子代码行
281
+ fold_type=block_type,
282
+ indent=start_indent // 4
283
+ )
284
+ self._regions.append(region)
285
+
286
+ i += 1
287
+
288
+ def get_regions(self) -> List[FoldRegion]:
289
+ """获取所有折叠区域"""
290
+ return self._regions
291
+
292
+ def get_region_at_line(self, line: int) -> Optional[FoldRegion]:
293
+ """获取指定行的折叠区域"""
294
+ for region in self._regions:
295
+ if region.start_line == line:
296
+ return region
297
+ return None
298
+
299
+ def get_region_by_pos(self, x: int, y: int, line_height: int,
300
+ offset_y: int = 0) -> Optional[FoldRegion]:
301
+ """根据鼠标位置获取折叠区域"""
302
+ for region in self._regions:
303
+ if region.marker_rect and region.marker_rect.contains(x, y):
304
+ return region
305
+ return None
306
+
307
+ def toggle_fold(self, line: int) -> bool:
308
+ """切换指定行的折叠状态"""
309
+ region = self.get_region_at_line(line)
310
+ if region:
311
+ collapsed = region.toggle()
312
+ self._update_collapsed_lines()
313
+ self.fold_changed.emit(line, collapsed)
314
+ return collapsed
315
+ return False
316
+
317
+ def fold_region(self, line: int):
318
+ """折叠指定区域"""
319
+ region = self.get_region_at_line(line)
320
+ if region and not region.collapsed:
321
+ region.collapsed = True
322
+ self._update_collapsed_lines()
323
+ self.fold_changed.emit(line, True)
324
+
325
+ def unfold_region(self, line: int):
326
+ """展开指定区域"""
327
+ region = self.get_region_at_line(line)
328
+ if region and region.collapsed:
329
+ region.collapsed = False
330
+ self._update_collapsed_lines()
331
+ self.fold_changed.emit(line, False)
332
+
333
+ def fold_all(self):
334
+ """折叠所有区域"""
335
+ for region in self._regions:
336
+ region.collapsed = True
337
+ self._update_collapsed_lines()
338
+ self.regions_changed.emit()
339
+
340
+ def unfold_all(self):
341
+ """展开所有区域"""
342
+ for region in self._regions:
343
+ region.collapsed = False
344
+ self._update_collapsed_lines()
345
+ self.regions_changed.emit()
346
+
347
+ def fold_at_level(self, level: int):
348
+ """折叠指定级别的区域"""
349
+ for region in self._regions:
350
+ if region.indent == level:
351
+ region.collapsed = True
352
+ self._update_collapsed_lines()
353
+ self.regions_changed.emit()
354
+
355
+ def _update_collapsed_lines(self):
356
+ """更新被折叠的行集合"""
357
+ self._collapsed_lines.clear()
358
+ for region in self._regions:
359
+ if region.collapsed:
360
+ for line in range(region.start_line + 1, region.end_line + 1):
361
+ self._collapsed_lines.add(line)
362
+
363
+ def is_line_collapsed(self, line: int) -> bool:
364
+ """检查行是否被折叠"""
365
+ return line in self._collapsed_lines
366
+
367
+ def get_visible_line_count(self) -> int:
368
+ """获取可见行数"""
369
+ if not self._code:
370
+ return 0
371
+ total_lines = len(self._code.split('\n'))
372
+ return total_lines - len(self._collapsed_lines)
373
+
374
+ def map_visible_to_real(self, visible_line: int) -> int:
375
+ """将可见行号映射到实际行号"""
376
+ real_line = 0
377
+ visible_count = 0
378
+
379
+ while visible_count < visible_line:
380
+ real_line += 1
381
+ if real_line not in self._collapsed_lines:
382
+ visible_count += 1
383
+
384
+ return real_line
385
+
386
+ def map_real_to_visible(self, real_line: int) -> int:
387
+ """将实际行号映射到可见行号"""
388
+ visible_line = 0
389
+ for line in range(1, real_line + 1):
390
+ if line not in self._collapsed_lines:
391
+ visible_line += 1
392
+ return visible_line
393
+
394
+ def get_fold_marker_rect(self, line: int, x: int, y: int,
395
+ size: int = 12) -> QRect:
396
+ """获取折叠标记的矩形区域"""
397
+ margin = (line_height - size) // 2 if 'line_height' in dir() else 2
398
+ return QRect(x, y + margin, size, size)
399
+
400
+
401
+ class FoldMarkerPainter:
402
+ """折叠标记绘制器"""
403
+
404
+ MARKER_SIZE = 12
405
+ MARKER_MARGIN = 2
406
+
407
+ def __init__(self, folder: CodeFolder):
408
+ self._folder = folder
409
+
410
+ def paint_marker(self, painter: QPainter, region: FoldRegion,
411
+ x: int, y: int, line_height: int):
412
+ """绘制折叠标记"""
413
+ # 计算标记位置
414
+ marker_x = x + 2
415
+ marker_y = int(y + (line_height - self.MARKER_SIZE) // 2)
416
+
417
+ # 更新区域的标记矩形
418
+ region.marker_rect = QRect(marker_x, marker_y,
419
+ self.MARKER_SIZE, self.MARKER_SIZE)
420
+
421
+ # 绘制背景
422
+ painter.setPen(Qt.PenStyle.NoPen)
423
+ painter.setBrush(QColor('#e0e0e0'))
424
+ painter.drawRect(marker_x, marker_y, self.MARKER_SIZE, self.MARKER_SIZE)
425
+
426
+ # 绘制边框
427
+ painter.setPen(QColor('#999999'))
428
+ painter.setBrush(Qt.BrushStyle.NoBrush)
429
+ painter.drawRect(marker_x, marker_y, self.MARKER_SIZE, self.MARKER_SIZE)
430
+
431
+ # 绘制展开/折叠图标
432
+ painter.setPen(QColor('#333333'))
433
+ painter.setBrush(QColor('#333333'))
434
+
435
+ center_x = marker_x + self.MARKER_SIZE // 2
436
+ center_y = marker_y + self.MARKER_SIZE // 2
437
+
438
+ if region.collapsed:
439
+ # 折叠状态:绘制右向三角形
440
+ polygon = QPolygon([
441
+ QPoint(marker_x + 3, marker_y + 3),
442
+ QPoint(marker_x + 3, marker_y + self.MARKER_SIZE - 3),
443
+ QPoint(marker_x + self.MARKER_SIZE - 3, marker_y + self.MARKER_SIZE // 2),
444
+ ])
445
+ painter.drawPolygon(polygon)
446
+ else:
447
+ # 展开状态:绘制下向三角形
448
+ polygon = QPolygon([
449
+ QPoint(marker_x + 3, marker_y + 3),
450
+ QPoint(marker_x + self.MARKER_SIZE - 3, marker_y + 3),
451
+ QPoint(marker_x + self.MARKER_SIZE // 2, marker_y + self.MARKER_SIZE - 3),
452
+ ])
453
+ painter.drawPolygon(polygon)
454
+
455
+ def paint_guides(self, painter: QPainter, regions: List[FoldRegion],
456
+ x: int, first_line: int, last_line: int,
457
+ line_height: int, scroll_y: int):
458
+ """绘制折叠引导线"""
459
+ painter.setPen(QColor('#d0d0d0'))
460
+
461
+ for region in regions:
462
+ if region.end_line < first_line or region.start_line > last_line:
463
+ continue
464
+
465
+ # 计算引导线位置
466
+ start_y = (region.start_line - 1) * line_height - scroll_y + line_height // 2
467
+ end_y = (region.end_line - 1) * line_height - scroll_y + line_height // 2
468
+
469
+ # 限制在可见区域内
470
+ start_y = max(start_y, 0)
471
+ end_y = min(end_y, (last_line - first_line + 1) * line_height)
472
+
473
+ # 绘制垂直引导线
474
+ guide_x = x + self.MARKER_SIZE // 2
475
+ painter.drawLine(guide_x, start_y, guide_x, end_y)
476
+
477
+ # 绘制水平引导线(到代码开始)
478
+ if not region.collapsed:
479
+ painter.drawLine(guide_x, end_y, x + 20, end_y)
480
+
481
+
482
+ def get_fold_indicator(line: str) -> Tuple[bool, str]:
483
+ """获取行的折叠指示器信息
484
+
485
+ 改进:检查行末是否有冒号(排除行末空白字符干扰)
486
+ """
487
+ # 去除行末空白后检查是否以冒号结尾
488
+ stripped_end = line.rstrip()
489
+ if not stripped_end.endswith(':'):
490
+ return False, ''
491
+
492
+ stripped = line.strip()
493
+
494
+ if stripped.startswith('class '):
495
+ return True, 'class'
496
+ elif stripped.startswith('def '):
497
+ return True, 'def'
498
+ elif stripped.startswith('async def '):
499
+ return True, 'async'
500
+ elif stripped.startswith('if '):
501
+ return True, 'if'
502
+ elif stripped.startswith('for '):
503
+ return True, 'for'
504
+ elif stripped.startswith('while '):
505
+ return True, 'while'
506
+ elif stripped.startswith('with '):
507
+ return True, 'with'
508
+ elif stripped.startswith('try:'):
509
+ return True, 'try'
510
+ elif stripped.startswith('elif '):
511
+ return True, 'elif'
512
+ elif stripped.startswith('else:'):
513
+ return True, 'else'
514
+ elif stripped.startswith('except'):
515
+ return True, 'except'
516
+ elif stripped.startswith('finally:'):
517
+ return True, 'finally'
518
+ elif stripped.startswith('match '):
519
+ return True, 'match'
520
+ elif stripped.startswith('case '):
521
+ return True, 'case'
522
+
523
+ return False, ''