meltygui 0.1.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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,2684 @@
1
+ """melty_scan — the standalone front half of core_syntax: text → syntax dict.
2
+
3
+ STANDALONE ON PURPOSE: stdlib only, no Melty / libcst imports, so it runs in a
4
+ 3.12 SUBINTERPRETER (its own GIL — the parse of a big file never stalls the
5
+ render thread) and its output crosses back as pickled plain data. Three parts:
6
+
7
+ scan(text) the "cst-lite" parser: `tokenize` (C) → statements →
8
+ ast-SHAPED nodes (same class names / fields as `ast`,
9
+ columns in CHARACTERS) for exactly the subset the dict
10
+ surfaces; anything else is an `Opaque` node with a
11
+ span. Purely functional over the token list.
12
+ Extractor the dict builder (moved here from core_syntax) — runs on
13
+ real `ast` nodes or scanner nodes alike (kind = class
14
+ name), emitting either the studio's parse classes
15
+ (in-process) or the neutral `N*` classes below (worker),
16
+ plus the Origin tables.
17
+ scan_extract(text) the worker entry: scan → extract (neutral) → validate
18
+ with ast.parse → picklable result.
19
+
20
+ The `ast` front end stays (core_syntax.parse_to_dict(frontend="ast")) as the
21
+ oracle: tests/test_core_syntax.py compares both on every file under src/.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import ast
27
+ import bisect
28
+ import codecs
29
+ import io
30
+ import keyword
31
+ import tokenize
32
+ from typing import Any
33
+
34
+ _SKIP_PARAMS = {"self", "cls"}
35
+ _SIMPLE_LITERAL_TYPES = (str, int, float, bool, type(None))
36
+ _DEF_KINDS = ("ClassDef", "FunctionDef", "AsyncFunctionDef")
37
+ _BLOCK_KINDS = ("If", "For", "AsyncFor", "Try", "TryStar")
38
+ UNRESOLVED = object()
39
+
40
+
41
+ def _k(node):
42
+ return type(node).__name__
43
+
44
+
45
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
46
+ # ║ Neutral data objects (what the worker emits; materialized on the main thread) ║
47
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
48
+
49
+ class NParse(dict):
50
+ def __init__(self, *args, source="", **kwargs):
51
+ super().__init__(*args, **kwargs)
52
+ self.source = source
53
+
54
+
55
+ class NGeneralParse(NParse):
56
+ pass
57
+
58
+
59
+ class NClassParse(NParse):
60
+ pass
61
+
62
+
63
+ class NEnumParse(NClassParse):
64
+ pass
65
+
66
+
67
+ class NFunctionParse(NParse):
68
+ pass
69
+
70
+
71
+ class NCallParse(NParse):
72
+ def __init__(self, *args, func_name=None, **kwargs):
73
+ super().__init__(*args, **kwargs)
74
+ self.func_name = func_name
75
+
76
+
77
+ class NDecorationParse(NCallParse):
78
+ pass
79
+
80
+
81
+ class NConditional(dict):
82
+ def __init__(self, *args, condition=None, **kwargs):
83
+ super().__init__(*args, **kwargs)
84
+ self.condition = condition
85
+
86
+
87
+ class NLoop(dict):
88
+ def __init__(self, *args, target=None, iter=None, **kwargs):
89
+ super().__init__(*args, **kwargs)
90
+ self.target = target
91
+ self.iter = iter
92
+
93
+
94
+ class NTry(dict):
95
+ def __init__(self, *args, header=None, **kwargs):
96
+ super().__init__(*args, **kwargs)
97
+ self.header = header
98
+
99
+
100
+ class NExcept(dict):
101
+ def __init__(self, *args, header=None, **kwargs):
102
+ super().__init__(*args, **kwargs)
103
+ self.header = header
104
+
105
+
106
+ class NComment(str):
107
+ def __new__(cls, text, inline=None):
108
+ instance = super().__new__(cls, text)
109
+ instance.inline = inline
110
+ return instance
111
+
112
+ def __eq__(self, other):
113
+ return isinstance(other, NComment) and str(self) == str(other) and self.inline == other.inline
114
+
115
+ def __ne__(self, other):
116
+ return not self.__eq__(other)
117
+
118
+ def __hash__(self):
119
+ return hash(("__comment__", str(self), self.inline))
120
+
121
+ def __reduce__(self):
122
+ return (NComment, (str(self), self.inline))
123
+
124
+
125
+ class NCodeLine(str):
126
+ pass
127
+
128
+
129
+ class NameRef(str):
130
+ """A dotted name the worker can't resolve (that needs the live src scope):
131
+ materialize turns it into the callable / enum member or a CodeLine."""
132
+ def __new__(cls, text, parts):
133
+ instance = super().__new__(cls, text)
134
+ instance.parts = list(parts)
135
+ return instance
136
+
137
+ def __reduce__(self):
138
+ return (NameRef, (str(self), self.parts))
139
+
140
+
141
+ class NNoDefault:
142
+ def __repr__(self):
143
+ return "NO_DEFAULT"
144
+
145
+ def __reduce__(self):
146
+ return (NNoDefault, ()) # constructor form: the main side maps it to the NO_DEFAULT singleton
147
+
148
+
149
+ N_NO_DEFAULT = NNoDefault()
150
+
151
+
152
+ class NSpan:
153
+ """A span whose LINES are relative to a Base cell (the enclosing top-level
154
+ statement) — the main side maps it to core_syntax.RelSpan."""
155
+ __slots__ = ("base", "rel_start_line", "start_col", "rel_end_line", "end_col")
156
+
157
+ def __init__(self, base, rel_start_line, start_col, rel_end_line, end_col):
158
+ self.base = base
159
+ self.rel_start_line = rel_start_line
160
+ self.start_col = start_col
161
+ self.rel_end_line = rel_end_line
162
+ self.end_col = end_col
163
+
164
+ @property
165
+ def start_line(self):
166
+ return self.rel_start_line + self.base.line
167
+
168
+ @property
169
+ def end_line(self):
170
+ return self.rel_end_line + self.base.line
171
+
172
+ def __reduce__(self):
173
+ return (NSpan, (self.base, self.rel_start_line, self.start_col, self.rel_end_line, self.end_col))
174
+
175
+ def __repr__(self):
176
+ return f"Span({self.start_line}:{self.start_col}–{self.end_line}:{self.end_col})"
177
+
178
+
179
+ class Types:
180
+ """The output vocabulary of the Extractor. `resolve(parts)` → live object or
181
+ UNRESOLVED (None = defer as NameRef); `positional_names(parts)` → the
182
+ callee's positional parameter names or None (None hook = defer)."""
183
+
184
+ def __init__(self, *, GeneralParse, ClassParse, EnumParse, FunctionParse, CallParse,
185
+ DecorationParse, Comment, CodeLine, Conditional, Loop, Try, Except,
186
+ NO_DEFAULT, Span, resolve=None, positional_names=None):
187
+ self.GeneralParse = GeneralParse
188
+ self.ClassParse = ClassParse
189
+ self.EnumParse = EnumParse
190
+ self.FunctionParse = FunctionParse
191
+ self.CallParse = CallParse
192
+ self.DecorationParse = DecorationParse
193
+ self.Comment = Comment
194
+ self.CodeLine = CodeLine
195
+ self.Conditional = Conditional
196
+ self.Loop = Loop
197
+ self.Try = Try
198
+ self.Except = Except
199
+ self.NO_DEFAULT = NO_DEFAULT
200
+ self.Span = Span
201
+ self.resolve = resolve
202
+ self.positional_names = positional_names
203
+
204
+
205
+ NEUTRAL_TYPES = Types(
206
+ GeneralParse=NGeneralParse, ClassParse=NClassParse, EnumParse=NEnumParse,
207
+ FunctionParse=NFunctionParse, CallParse=NCallParse, DecorationParse=NDecorationParse,
208
+ Comment=NComment, CodeLine=NCodeLine, Conditional=NConditional, Loop=NLoop, Try=NTry,
209
+ Except=NExcept, NO_DEFAULT=N_NO_DEFAULT, Span=NSpan)
210
+
211
+
212
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
213
+ # ║ Override comments & block keys (copies of libcst_conversion's helpers) ║
214
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
215
+
216
+ def parse_override_comment(text):
217
+ """'# [k=v, ...]' → dict, else None. Same rules as libcst_conversion."""
218
+ if not isinstance(text, str):
219
+ return None
220
+ body = " ".join(ln.strip().lstrip("#").strip() for ln in text.split("\n")).strip()
221
+ if not (body.startswith("[") and body.endswith("]")):
222
+ return None
223
+ inner = body[1:-1].strip()
224
+ if not inner:
225
+ return None
226
+ try:
227
+ call = ast.parse(f"dict({inner})", mode="eval").body
228
+ if not isinstance(call, ast.Call) or call.args:
229
+ return None
230
+ parsed = {}
231
+ for kw in call.keywords:
232
+ if kw.arg is None:
233
+ return None
234
+ if kw.arg == "view_func" and isinstance(kw.value, (ast.Name, ast.Attribute)):
235
+ reference = ast.unparse(kw.value)
236
+ if not all(part.isidentifier() for part in reference.split(".")):
237
+ return None
238
+ parsed[kw.arg] = reference
239
+ else:
240
+ parsed[kw.arg] = ast.literal_eval(kw.value)
241
+ return parsed or None
242
+ except (SyntaxError, ValueError, TypeError):
243
+ return None
244
+
245
+
246
+ def occ_key(base, occ_counter):
247
+ n = occ_counter.get(base, 0)
248
+ occ_counter[base] = n + 1
249
+ return base if n == 0 else f"{base}##{n}"
250
+
251
+
252
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
253
+ # ║ Source text ║
254
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
255
+
256
+ class _Src:
257
+ """The parsed text with a line-start table. `ast` reports columns in UTF-8
258
+ BYTES; scanner nodes carry CHARACTER columns (`_charcols`); every span here
259
+ is in characters."""
260
+
261
+ __slots__ = ("text", "line_starts", "newline")
262
+
263
+ def __init__(self, text):
264
+ self.text = text
265
+ starts = [0]
266
+ find = text.find
267
+ i = find("\n")
268
+ while i != -1:
269
+ starts.append(i + 1)
270
+ i = find("\n", i + 1)
271
+ self.line_starts = starts
272
+ crlf = text.count("\r\n")
273
+ self.newline = "\r\n" if crlf and crlf * 2 > text.count("\n") else "\n"
274
+
275
+ @classmethod
276
+ def spliced(cls, old, new_text, rs, re_, delta, region_src):
277
+ """The line table of `new_text` from `old`'s, when only [rs, re_) was
278
+ replaced (by a region whose own table is `region_src`): lines before
279
+ stay, the region's are re-based, lines after shift by `delta`."""
280
+ self = cls.__new__(cls)
281
+ self.text = new_text
282
+ starts = old.line_starts
283
+ lo = bisect.bisect_right(starts, rs) # lines starting at or before rs stay (rs is a line start)
284
+ hi = bisect.bisect_left(starts, re_) # first line start at/after re_
285
+ region = [rs + x for x in region_src.line_starts[1:]]
286
+ self.line_starts = starts[:lo] + region + [x + delta for x in starts[hi:]]
287
+ self.newline = old.newline
288
+ return self
289
+
290
+ @property
291
+ def line_count(self):
292
+ return len(self.line_starts)
293
+
294
+ def line_start(self, lineno):
295
+ return self.line_starts[lineno - 1]
296
+
297
+ def next_line_start(self, lineno):
298
+ if lineno <= 0:
299
+ return 0
300
+ if lineno < len(self.line_starts):
301
+ return self.line_starts[lineno]
302
+ return len(self.text)
303
+
304
+ def line_end(self, lineno):
305
+ if lineno < len(self.line_starts):
306
+ e = self.line_starts[lineno] - 1
307
+ if e > 0 and self.text[e - 1] == "\r":
308
+ e -= 1
309
+ return e
310
+ return len(self.text)
311
+
312
+ def line_text(self, lineno):
313
+ return self.text[self.line_start(lineno):self.line_end(lineno)]
314
+
315
+ def offset(self, lineno, col_bytes):
316
+ start = self.line_start(lineno)
317
+ line = self.text[start:self.next_line_start(lineno)]
318
+ if line.isascii():
319
+ return start + col_bytes
320
+ return start + len(line.encode("utf-8")[:col_bytes].decode("utf-8", "ignore"))
321
+
322
+ def node_span(self, node):
323
+ if getattr(node, "_charcols", False):
324
+ return (self.line_starts[node.lineno - 1] + node.col_offset,
325
+ self.line_starts[node.end_lineno - 1] + node.end_col_offset)
326
+ return (self.offset(node.lineno, node.col_offset),
327
+ self.offset(node.end_lineno, node.end_col_offset))
328
+
329
+ def linecol(self, offset):
330
+ lineno = bisect.bisect_right(self.line_starts, offset)
331
+ return lineno, offset - self.line_starts[lineno - 1]
332
+
333
+ def indent_of_line(self, lineno):
334
+ line = self.line_text(lineno)
335
+ return line[:len(line) - len(line.lstrip())]
336
+
337
+ def is_blank(self, lineno):
338
+ return not self.line_text(lineno).strip()
339
+
340
+
341
+ def scan_comments(text):
342
+ """{lineno: (col, text)} for standalone comment lines and for trailing
343
+ (same-line-as-code) comments — the `ast` front end's comment source."""
344
+ standalone, trailing = {}, {}
345
+ try:
346
+ for tok in tokenize.generate_tokens(io.StringIO(text).readline):
347
+ if tok.type != tokenize.COMMENT:
348
+ continue
349
+ line, col = tok.start
350
+ if tok.line[:col].strip() == "":
351
+ standalone[line] = (col, tok.string)
352
+ else:
353
+ trailing[line] = (col, tok.string)
354
+ except (tokenize.TokenError, IndentationError, SyntaxError):
355
+ pass
356
+ return standalone, trailing
357
+
358
+
359
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
360
+ # ║ Origin tables ║
361
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
362
+
363
+ class Base:
364
+ """The anchor of one TOP-LEVEL statement: the char offset and 1-based line
365
+ where its extent starts. Every Item / Seq inside the statement stores its
366
+ positions RELATIVE to its Base, so an edit above the statement moves it
367
+ by touching this one cell — the incremental reparse shifts ~hundreds of
368
+ Bases instead of ~ten thousand items."""
369
+ __slots__ = ("offset", "line")
370
+
371
+ def __init__(self, offset=0, line=1):
372
+ self.offset = offset
373
+ self.line = line
374
+
375
+ def __reduce__(self):
376
+ return (Base, (self.offset, self.line))
377
+
378
+ def __repr__(self):
379
+ return f"Base({self.offset}@{self.line})"
380
+
381
+
382
+ ZERO_BASE = Base(0, 1)
383
+
384
+
385
+ class Item:
386
+ """One surfaced site. `extent` is what a delete removes (leading comment
387
+ lines and blank lines above, the statement, its trailing comment and
388
+ newline); `core` is what a reorder moves (extent minus the blank lines
389
+ above it); `value_span` is the editable expression, None when there is
390
+ none (a def, a block, a parameter without a default — `slot` then says
391
+ where a value would be inserted). Positions read and write ABSOLUTE
392
+ offsets; they are stored relative to `base`."""
393
+ __slots__ = ("base", "path", "key", "kind", "_extent", "_core", "_value_span", "orig", "indent",
394
+ "_slot", "_code_end", "seq", "comment_key", "shadowed")
395
+
396
+ def __init__(self, base, path, key, kind, extent, core, value_span, orig, indent="", slot=None,
397
+ code_end=None, seq=None, comment_key=None, shadowed=False):
398
+ self.base = base
399
+ self.path = path
400
+ self.key = key
401
+ self.kind = kind # value | param | kwarg | element | pair | def | block | header | comment | trailing | override | decorator | pseudo
402
+ self.extent = extent
403
+ self.core = core
404
+ self.value_span = value_span
405
+ self.orig = orig
406
+ self.indent = indent
407
+ self.slot = slot
408
+ self.code_end = code_end # end of the statement's code (trailing comments append here)
409
+ self.seq = seq
410
+ self.comment_key = comment_key # override items: the comment key that shares their span
411
+ self.shadowed = shadowed # a later statement re-bound this key: fixed in place, never diffed
412
+
413
+ @property
414
+ def extent(self):
415
+ o = self.base.offset
416
+ return (self._extent[0] + o, self._extent[1] + o)
417
+
418
+ @extent.setter
419
+ def extent(self, v):
420
+ o = self.base.offset
421
+ self._extent = (v[0] - o, v[1] - o)
422
+
423
+ @property
424
+ def core(self):
425
+ o = self.base.offset
426
+ return (self._core[0] + o, self._core[1] + o)
427
+
428
+ @core.setter
429
+ def core(self, v):
430
+ o = self.base.offset
431
+ self._core = (v[0] - o, v[1] - o)
432
+
433
+ @property
434
+ def value_span(self):
435
+ if self._value_span is None:
436
+ return None
437
+ o = self.base.offset
438
+ return (self._value_span[0] + o, self._value_span[1] + o)
439
+
440
+ @value_span.setter
441
+ def value_span(self, v):
442
+ if v is None:
443
+ self._value_span = None
444
+ else:
445
+ o = self.base.offset
446
+ self._value_span = (v[0] - o, v[1] - o)
447
+
448
+ @property
449
+ def slot(self):
450
+ return None if self._slot is None else self._slot + self.base.offset
451
+
452
+ @slot.setter
453
+ def slot(self, v):
454
+ self._slot = None if v is None else v - self.base.offset
455
+
456
+ @property
457
+ def code_end(self):
458
+ return None if self._code_end is None else self._code_end + self.base.offset
459
+
460
+ @code_end.setter
461
+ def code_end(self, v):
462
+ self._code_end = None if v is None else v - self.base.offset
463
+
464
+ def __reduce__(self):
465
+ return (Item, (self.base, self.path, self.key, self.kind, self.extent, self.core, self.value_span,
466
+ self.orig, self.indent, self.slot, self.code_end, self.seq, self.comment_key,
467
+ self.shadowed))
468
+
469
+ def __repr__(self):
470
+ return (f"Item({self.path!r}, {self.kind}, extent={self.extent}, value_span={self.value_span}"
471
+ f"{', shadowed' if self.shadowed else ''})")
472
+
473
+
474
+ class Seq:
475
+ """An ordered container of items in the source. `region` = [first core
476
+ start, last core end) (or the insertion point when empty); `insert_at` is
477
+ stored relative to `base`."""
478
+ __slots__ = ("id", "owner", "kind", "items", "base", "_insert_at", "indent", "sep")
479
+
480
+ def __init__(self, id, owner, kind, items=None, base=ZERO_BASE, insert_at=0, indent="", sep=", "):
481
+ self.id = id
482
+ self.owner = owner # node path whose keys this Seq holds
483
+ self.kind = kind # body | params | args | elements | pairs | decorators
484
+ self.items = items if items is not None else []
485
+ self.base = base
486
+ self.insert_at = insert_at
487
+ self.indent = indent
488
+ self.sep = sep
489
+
490
+ @property
491
+ def insert_at(self):
492
+ return self._insert_at + self.base.offset
493
+
494
+ @insert_at.setter
495
+ def insert_at(self, v):
496
+ self._insert_at = v - self.base.offset
497
+
498
+ @property
499
+ def region(self):
500
+ if self.items:
501
+ return (self.items[0].core[0], self.items[-1].core[1])
502
+ return (self.insert_at, self.insert_at)
503
+
504
+ def __reduce__(self):
505
+ return (Seq, (self.id, self.owner, self.kind, self.items, self.base, self.insert_at, self.indent, self.sep))
506
+
507
+ def __repr__(self):
508
+ return f"Seq(#{self.id} {self.kind} owner={self.owner!r} keys={[it.key for it in self.items]})"
509
+
510
+
511
+ class Origin:
512
+ """The residual: the parsed text plus the flat site tables."""
513
+
514
+ def __init__(self, text):
515
+ self.text = text
516
+ self.src = _Src(text)
517
+ self.items: dict[tuple, Item] = {}
518
+ self.seqs: dict[int, Seq] = {}
519
+ self.default_seq: dict[tuple, int] = {} # node path → seq new items of that node join
520
+ self.owned: dict[tuple, list] = {} # node path → seq ids whose deletes it answers
521
+ self.loose: dict[tuple, list] = {} # node path → items outside any Seq (comments, etc)
522
+ # Every TOP-LEVEL statement, surfaced or not, as (Base, relative extent
523
+ # end, key-or-None) in source order - the extents within the module body -
524
+ # the incremental reparse re-scans exactly the statements an edit touches
525
+ # and re-bases the rest. `top_extents()` gives these absolute.
526
+ self.top_stmts: list = []
527
+ self.generation = 0
528
+ self.file_path = None
529
+ self.line_offset = 0
530
+ self._next_seq_id = 0
531
+
532
+ def top_extents(self):
533
+ return [(b.offset, b.offset + rel_end, key) for b, rel_end, key in self.top_stmts]
534
+
535
+ def new_seq(self, owner, kind, *, base=ZERO_BASE, default=True, indent="", insert_at=0, sep=", "):
536
+ seq = Seq(self._next_seq_id, owner, kind, base=base, indent=indent, insert_at=insert_at, sep=sep)
537
+ self._next_seq_id += 1 # never len(seqs): ids must survive drop_seq
538
+ self.seqs[seq.id] = seq
539
+ self.owned.setdefault(owner, []).append(seq.id)
540
+ if default:
541
+ self.default_seq[owner] = seq.id
542
+ return seq
543
+
544
+ def drop_seq(self, seq):
545
+ self.seqs.pop(seq.id, None)
546
+ owned = self.owned.get(seq.owner)
547
+ if owned and seq.id in owned:
548
+ owned.remove(seq.id)
549
+ if owned is not None and not owned:
550
+ del self.owned[seq.owner]
551
+ if self.default_seq.get(seq.owner) == seq.id:
552
+ del self.default_seq[seq.owner]
553
+
554
+ def add(self, item, seq=None):
555
+ if seq is not None:
556
+ item.seq = seq.id
557
+ seq.items.append(item)
558
+ else:
559
+ self.loose.setdefault(item.path[:-1], []).append(item)
560
+ prev = self.items.get(item.path)
561
+ if prev is not None:
562
+ prev.shadowed = True
563
+ self.items[item.path] = item
564
+ return item
565
+
566
+ def shadow(self, path):
567
+ """A key is being re-bound: the earlier binding stays a fixed slot in its
568
+ Seq (never diffed) and everything nested under it is unreachable from
569
+ the dict, so its tables go. Call BEFORE extracting the new value."""
570
+ prev = self.items.get(path)
571
+ if prev is None:
572
+ return
573
+ prev.shadowed = True
574
+ n = len(path)
575
+ for p in [p for p in self.items if len(p) > n and p[:n] == path]:
576
+ del self.items[p]
577
+ for sid in [sid for sid, sq in self.seqs.items() if len(sq.owner) >= n and sq.owner[:n] == path]:
578
+ self.drop_seq(self.seqs[sid])
579
+ for p in [p for p in self.loose if len(p) >= n and p[:n] == path]:
580
+ del self.loose[p]
581
+
582
+ def seal_seq(self, seq):
583
+ pass # `region` derives from the items / insert point
584
+
585
+
586
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
587
+ # ║ Scanner: tokenize → ast-shaped nodes ║
588
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
589
+
590
+ class _Node:
591
+ """Base of the scanner's nodes: the `ast` field names, CHARACTER columns."""
592
+ __slots__ = ("lineno", "col_offset", "end_lineno", "end_col_offset")
593
+ _charcols = True
594
+
595
+ def _pos(self, first, last):
596
+ self.lineno, self.col_offset = first.start
597
+ self.end_lineno, self.end_col_offset = last.end
598
+ return self
599
+
600
+ def __repr__(self):
601
+ return f"<{_k(self)} {self.lineno}:{self.col_offset}-{self.end_lineno}:{self.end_col_offset}>"
602
+
603
+
604
+ class Module(_Node):
605
+ __slots__ = ("body",)
606
+
607
+ def __init__(self, body):
608
+ self.body = body
609
+ self.lineno = self.col_offset = 0
610
+ self.end_lineno = self.end_col_offset = 0
611
+
612
+
613
+ class Opaque(_Node):
614
+ """A statement / expression the dict doesn't model. `body` (compound
615
+ statements) is consumed but never surfaced."""
616
+ __slots__ = ("body",)
617
+
618
+ def __init__(self, body=None):
619
+ self.body = body or []
620
+
621
+
622
+ class alias(_Node):
623
+ """`name [as asname]` of an import statement (ast.alias shape)."""
624
+ __slots__ = ("name", "asname")
625
+
626
+ def __init__(self, name, asname=None):
627
+ self.name = name
628
+ self.asname = asname
629
+
630
+
631
+ class Import(_Node):
632
+ """`import a.b [as c], d` — ast.Import shape. Surfaced for the file
633
+ import graph (view/playground/file_graph.py); the dict ignores it."""
634
+ __slots__ = ("names",)
635
+
636
+ def __init__(self, names):
637
+ self.names = names
638
+
639
+
640
+ class ImportFrom(_Node):
641
+ """`from [..]mod import x [as y], (…)` / `import *` — ast.ImportFrom
642
+ shape: `module` None for a bare relative import, `level` = leading dots."""
643
+ __slots__ = ("module", "names", "level")
644
+
645
+ def __init__(self, module, names, level):
646
+ self.module = module
647
+ self.names = names
648
+ self.level = level
649
+
650
+
651
+ class ClassDef(_Node):
652
+ __slots__ = ("name", "bases", "keywords", "body", "decorator_list")
653
+
654
+ def __init__(self, name, bases, keywords, body, decorator_list):
655
+ self.name, self.bases, self.keywords = name, bases, keywords
656
+ self.body, self.decorator_list = body, decorator_list
657
+
658
+
659
+ class FunctionDef(_Node):
660
+ __slots__ = ("name", "args", "body", "decorator_list", "returns")
661
+
662
+ def __init__(self, name, args, body, decorator_list, returns=None):
663
+ self.name, self.args, self.body = name, args, body
664
+ self.decorator_list, self.returns = decorator_list, returns
665
+
666
+
667
+ class AsyncFunctionDef(FunctionDef):
668
+ __slots__ = ()
669
+
670
+
671
+ class arguments:
672
+ __slots__ = ("posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults")
673
+
674
+ def __init__(self):
675
+ self.posonlyargs, self.args, self.kwonlyargs = [], [], []
676
+ self.kw_defaults, self.defaults = [], []
677
+ self.vararg = self.kwarg = None
678
+
679
+
680
+ class arg(_Node):
681
+ __slots__ = ("arg", "annotation")
682
+
683
+ def __init__(self, name, annotation=None):
684
+ self.arg, self.annotation = name, annotation
685
+
686
+
687
+ class Assign(_Node):
688
+ __slots__ = ("targets", "value")
689
+
690
+ def __init__(self, targets, value):
691
+ self.targets, self.value = targets, value
692
+
693
+
694
+ class AnnAssign(_Node):
695
+ __slots__ = ("target", "annotation", "value")
696
+
697
+ def __init__(self, target, annotation, value):
698
+ self.target, self.annotation, self.value = target, annotation, value
699
+
700
+
701
+ class Expr(_Node):
702
+ __slots__ = ("value",)
703
+
704
+ def __init__(self, value):
705
+ self.value = value
706
+
707
+
708
+ class If(_Node):
709
+ __slots__ = ("test", "body", "orelse")
710
+
711
+ def __init__(self, test, body, orelse):
712
+ self.test, self.body, self.orelse = test, body, orelse
713
+
714
+
715
+ class For(_Node):
716
+ __slots__ = ("target", "iter", "body", "orelse")
717
+
718
+ def __init__(self, target, iter, body, orelse):
719
+ self.target, self.iter, self.body, self.orelse = target, iter, body, orelse
720
+
721
+
722
+ class AsyncFor(For):
723
+ __slots__ = ()
724
+
725
+
726
+ class Try(_Node):
727
+ __slots__ = ("body", "handlers", "orelse", "finalbody")
728
+
729
+ def __init__(self, body, handlers, orelse, finalbody):
730
+ self.body, self.handlers, self.orelse, self.finalbody = body, handlers, orelse, finalbody
731
+
732
+
733
+ class TryStar(Try):
734
+ __slots__ = ()
735
+
736
+
737
+ class ExceptHandler(_Node):
738
+ __slots__ = ("type", "name", "body")
739
+
740
+ def __init__(self, type, name, body):
741
+ self.type, self.name, self.body = type, name, body
742
+
743
+
744
+ # expressions
745
+ class Name(_Node):
746
+ __slots__ = ("id",)
747
+
748
+ def __init__(self, id):
749
+ self.id = id
750
+
751
+
752
+ class Attribute(_Node):
753
+ __slots__ = ("value", "attr")
754
+
755
+ def __init__(self, value, attr):
756
+ self.value, self.attr = value, attr
757
+
758
+
759
+ class Constant(_Node):
760
+ __slots__ = ("value",)
761
+
762
+ def __init__(self, value):
763
+ self.value = value
764
+
765
+
766
+ class USub:
767
+ pass
768
+
769
+
770
+ class UAdd:
771
+ pass
772
+
773
+
774
+ class UnaryOp(_Node):
775
+ __slots__ = ("op", "operand")
776
+
777
+ def __init__(self, op, operand):
778
+ self.op, self.operand = op, operand
779
+
780
+
781
+ class Call(_Node):
782
+ __slots__ = ("func", "args", "keywords")
783
+
784
+ def __init__(self, func, args, keywords):
785
+ self.func, self.args, self.keywords = func, args, keywords
786
+
787
+
788
+ class keyword_(_Node):
789
+ __slots__ = ("arg", "value")
790
+
791
+ def __init__(self, arg, value):
792
+ self.arg, self.value = arg, value
793
+
794
+
795
+ keyword_.__name__ = "keyword"
796
+
797
+
798
+ class Starred(_Node):
799
+ __slots__ = ("value",)
800
+
801
+ def __init__(self, value):
802
+ self.value = value
803
+
804
+
805
+ class Tuple(_Node):
806
+ __slots__ = ("elts",)
807
+
808
+ def __init__(self, elts):
809
+ self.elts = elts
810
+
811
+
812
+ class List(_Node):
813
+ __slots__ = ("elts",)
814
+
815
+ def __init__(self, elts):
816
+ self.elts = elts
817
+
818
+
819
+ class Set(_Node):
820
+ __slots__ = ("elts",)
821
+
822
+ def __init__(self, elts):
823
+ self.elts = elts
824
+
825
+
826
+ class Dict(_Node):
827
+ __slots__ = ("keys", "values")
828
+
829
+ def __init__(self, keys, values):
830
+ self.keys, self.values = keys, values
831
+
832
+
833
+ class Subscript(_Node):
834
+ __slots__ = ("value", "slice")
835
+
836
+ def __init__(self, value, slice):
837
+ self.value, self.slice = value, slice
838
+
839
+
840
+ _OP, _NAME, _NUMBER, _STRING = tokenize.OP, tokenize.NAME, tokenize.NUMBER, tokenize.STRING
841
+ _NEWLINE, _INDENT, _DEDENT, _ENDMARKER = tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT, tokenize.ENDMARKER
842
+ _FSTRING_START = getattr(tokenize, "FSTRING_START", -1)
843
+ _FSTRING_END = getattr(tokenize, "FSTRING_END", -2)
844
+ _OPEN = {"(": ")", "[": "]", "{": "}"}
845
+ _CLOSE = {")", "]", "}"}
846
+ # The keywords that start a statement the dict never does (`return`,
847
+ # `import`, ...). Soft keywords (`match`, `case`, `type`, `_`) are allowed
848
+ # unless the header rule in compound_or_simple says otherwise.
849
+ _STMT_KEYWORDS = set(keyword.kwlist) - {"True", "False", "None", "lambda", "not", "await"}
850
+ _UNARY_NUM = {"-": USub, "+": UAdd}
851
+
852
+
853
+ class ScanError(SyntaxError):
854
+ pass
855
+
856
+
857
+ def _number(s):
858
+ try:
859
+ if s[-1] in "jJ":
860
+ return complex(s)
861
+ return int(s, 0) if not any(c in s for c in ".eE") or s[:2].lower() == "0x" else float(s)
862
+ except ValueError:
863
+ try:
864
+ return float(s)
865
+ except ValueError:
866
+ return UNRESOLVED
867
+
868
+
869
+ def _string(tok):
870
+ """The value of one STRING token (no f-strings here: those are FSTRING_*
871
+ tokens). Raw strings pass through; bytes decode their own escapes."""
872
+ i = 0
873
+ while i < len(tok) and tok[i] in "rRbBuU":
874
+ i += 1
875
+ prefix = tok[:i].lower()
876
+ body = tok[i:]
877
+ q = body[:3] if body[:3] in ('"""', "'''") else body[:1]
878
+ inner = body[len(q):-len(q)]
879
+ raw = "r" in prefix
880
+ if "b" in prefix:
881
+ if raw:
882
+ return inner.encode("latin-1", "backslashreplace")
883
+ return codecs.escape_decode(inner.encode("latin-1", "backslashreplace"))[0]
884
+ if raw or "\\" not in inner:
885
+ return inner
886
+ # unicode_escape reads latin-1 bytes; non-latin-1 chars are round-tripped as
887
+ # \\uXXXX, so they survive the trip.
888
+ return inner.encode("latin-1", "backslashreplace").decode("unicode_escape")
889
+
890
+
891
+ def _is_whole_fstring(tok):
892
+ """Tokenizers before Python 3.12 give a whole f-string as ONE STRING token
893
+ (no FSTRING_* tokens): it is opaque code, never a constant."""
894
+ return tok.type == _STRING and "f" in tok.string[:tok.string.index(tok.string[-1])].lower()
895
+
896
+
897
+ def _has_depth0(toks, string, tok_type=_OP):
898
+ """Index of the first `string` token at bracket depth 0 (lambda parameter
899
+ lists excluded, their `=` / `:` are not the statement's), else None."""
900
+ depth = 0
901
+ lam = 0
902
+ for i, t in enumerate(toks):
903
+ if t.type == _OP:
904
+ if t.string in _OPEN:
905
+ depth += 1
906
+ elif t.string in _CLOSE:
907
+ depth -= 1
908
+ elif depth == 0:
909
+ if lam and t.string == ":":
910
+ lam -= 1
911
+ continue
912
+ if not lam and t.type == tok_type and t.string == string:
913
+ return i
914
+ elif t.type == _NAME and t.string == "lambda" and depth == 0:
915
+ lam += 1
916
+ elif depth == 0 and not lam and t.type == tok_type and t.string == string:
917
+ return i
918
+ return None
919
+
920
+
921
+ def _split_depth0(toks, string):
922
+ """Split at depth-0 `string` tokens. A depth-0 `lambda … :` is one unit:
923
+ its parameter commas / `=` defaults never split (`f(lambda a, b=1: a)`)."""
924
+ parts, cur, depth, lam = [], [], 0, 0
925
+ for t in toks:
926
+ if t.type == _OP and t.string in _OPEN:
927
+ depth += 1
928
+ elif t.type == _OP and t.string in _CLOSE:
929
+ depth -= 1
930
+ elif depth == 0 and t.type == _NAME and t.string == "lambda":
931
+ lam += 1
932
+ elif depth == 0 and lam and t.type == _OP and t.string == ":":
933
+ lam -= 1
934
+ cur.append(t)
935
+ continue
936
+ if depth == 0 and not lam and t.type == _OP and t.string == string:
937
+ parts.append(cur)
938
+ cur = []
939
+ else:
940
+ cur.append(t)
941
+ parts.append(cur)
942
+ return parts
943
+
944
+
945
+ def _match(toks, i):
946
+ """Index of the bracket closing toks[i], else None."""
947
+ depth = 0
948
+ for j in range(i, len(toks)):
949
+ t = toks[j]
950
+ if t.type == _OP:
951
+ if t.string in _OPEN:
952
+ depth += 1
953
+ elif t.string in _CLOSE:
954
+ depth -= 1
955
+ if depth == 0:
956
+ return j
957
+ return None
958
+
959
+
960
+ class _Parser:
961
+ def __init__(self, toks):
962
+ self.toks = toks
963
+ self.i = 0
964
+
965
+ def peek(self):
966
+ return self.toks[self.i]
967
+
968
+ def logical_line(self):
969
+ """Tokens of the next logical line (NEWLINE consumed, not returned)."""
970
+ out = []
971
+ toks = self.toks
972
+ n = len(toks)
973
+ while self.i < n:
974
+ t = toks[self.i]
975
+ self.i += 1
976
+ if t.type == _NEWLINE:
977
+ break
978
+ if t.type in (_INDENT, _DEDENT, _ENDMARKER):
979
+ self.i -= 1
980
+ break
981
+ out.append(t)
982
+ return out
983
+
984
+ # ── blocks ───────────────────────────────────────────────────────────────
985
+
986
+ def block(self):
987
+ stmts = []
988
+ while True:
989
+ t = self.peek()
990
+ if t.type in (_DEDENT, _ENDMARKER):
991
+ return stmts
992
+ if t.type == _INDENT:
993
+ raise ScanError("unexpected indent", ("<text>", t.start[0], t.start[1] + 1, t.line))
994
+ stmts.extend(self.statement())
995
+
996
+ def indented_block(self, after_tok):
997
+ t = self.peek()
998
+ if t.type != _INDENT:
999
+ raise ScanError("expected an indented block", ("<text>", after_tok.end[0], after_tok.end[1] + 1, after_tok.line))
1000
+ self.i += 1
1001
+ stmts = self.block()
1002
+ if self.peek().type == _DEDENT:
1003
+ self.i += 1
1004
+ return stmts
1005
+
1006
+ def body_after(self, toks, colon):
1007
+ """The body of a compound header: inline statements after the colon, or
1008
+ the indented block that follows."""
1009
+ rest = toks[colon + 1:]
1010
+ if rest:
1011
+ return self.simple_statements(rest)
1012
+ return self.indented_block(toks[colon])
1013
+
1014
+ # ── statements ───────────────────────────────────────────────────────────
1015
+
1016
+ def statement(self):
1017
+ toks = self.logical_line()
1018
+ if not toks:
1019
+ return []
1020
+ t0 = toks[0]
1021
+ if t0.type == _OP and t0.string == "@":
1022
+ decorators = [self.expr(toks[1:], t0)]
1023
+ while True:
1024
+ toks = self.logical_line()
1025
+ if toks and toks[0].type == _OP and toks[0].string == "@":
1026
+ decorators.append(self.expr(toks[1:], toks[0]))
1027
+ continue
1028
+ break
1029
+ stmts = self.compound_or_simple(toks)
1030
+ if stmts and _k(stmts[0]) in _DEF_KINDS:
1031
+ stmts[0].decorator_list = decorators
1032
+ return stmts
1033
+ return self.compound_or_simple(toks)
1034
+
1035
+ def compound_or_simple(self, toks):
1036
+ t0 = toks[0]
1037
+ if t0.type != _NAME:
1038
+ return self.simple_statements(toks)
1039
+ kw = t0.string
1040
+ if kw == "async" and len(toks) > 1 and toks[1].type == _NAME and toks[1].string in ("def", "for", "with"):
1041
+ kw = "async " + toks[1].string
1042
+ if kw in ("def", "async def"):
1043
+ return [self.funcdef(toks, kw == "async def")]
1044
+ if kw == "class":
1045
+ return [self.classdef(toks)]
1046
+ if kw == "if":
1047
+ return [self.if_stmt(toks)]
1048
+ if kw in ("for", "async for"):
1049
+ return [self.for_stmt(toks, kw == "async for")]
1050
+ if kw == "try":
1051
+ return [self.try_stmt(toks)]
1052
+ if kw in ("while", "with", "async with"):
1053
+ return [self.opaque_compound(toks)]
1054
+ if kw in ("match", "case") and toks[-1].type == _OP and toks[-1].string == ":" and self.peek().type == _INDENT:
1055
+ return [self.opaque_compound(toks)]
1056
+ return self.simple_statements(toks)
1057
+
1058
+ def header_colon(self, toks, start=1):
1059
+ c = _has_depth0(toks[start:], ":")
1060
+ if c is None:
1061
+ raise ScanError("expected ':'", ("<text>", toks[0].start[0], toks[0].start[1] + 1, toks[0].line))
1062
+ return c + start
1063
+
1064
+ def opaque_compound(self, toks):
1065
+ colon = self.header_colon(toks, 0)
1066
+ body = self.body_after(toks, colon)
1067
+ node = Opaque(body)
1068
+ last = self._last_tok(body) or toks[-1]
1069
+ node._pos(toks[0], last)
1070
+ # while/for-else on an opaque loop: consume the else block too.
1071
+ if toks[0].string == "while" and self.peek().type == _NAME and self.peek().string == "else":
1072
+ etoks = self.logical_line()
1073
+ ebody = self.body_after(etoks, self.header_colon(etoks, 0))
1074
+ last = self._last_tok(ebody) or etoks[-1]
1075
+ node.end_lineno, node.end_col_offset = last.end
1076
+ return node
1077
+
1078
+ @staticmethod
1079
+ def _last_tok(stmts):
1080
+ """A pseudo token for the end of the last statement of a body."""
1081
+ if not stmts:
1082
+ return None
1083
+ s = stmts[-1]
1084
+ return _EndTok(s.end_lineno, s.end_col_offset)
1085
+
1086
+ def funcdef(self, toks, is_async):
1087
+ start = toks[0]
1088
+ j = 1 if not is_async else 2
1089
+ name_tok = toks[j]
1090
+ open_idx = j + 1
1091
+ close = _match(toks, open_idx)
1092
+ if close is None or toks[open_idx].string != "(":
1093
+ raise ScanError("bad def", ("<text>", start.start[0], start.start[1] + 1, start.line))
1094
+ args = self.params(toks[open_idx + 1:close])
1095
+ colon = self.header_colon(toks, close + 1)
1096
+ returns = self.expr(toks[close + 2:colon], toks[close]) if colon > close + 2 else None
1097
+ body = self.body_after(toks, colon)
1098
+ node = (AsyncFunctionDef if is_async else FunctionDef)(name_tok.string, args, body, [], returns)
1099
+ node._pos(start, self._last_tok(body) or toks[colon])
1100
+ return node
1101
+
1102
+ def classdef(self, toks):
1103
+ start = toks[0]
1104
+ name_tok = toks[1]
1105
+ bases, keywords = [], []
1106
+ colon = self.header_colon(toks, 2)
1107
+ if colon > 2 and toks[2].type == _OP and toks[2].string == "(":
1108
+ close = _match(toks, 2)
1109
+ for part in _split_depth0(toks[3:close], ","):
1110
+ if not part:
1111
+ continue
1112
+ eq = _has_depth0(part, "=")
1113
+ if eq is not None and eq == 1 and part[0].type == _NAME:
1114
+ keywords.append(keyword_(part[0].string, self.expr(part[2:], part[1]))._pos(part[0], part[-1]))
1115
+ else:
1116
+ bases.append(self.expr(part, part[0]))
1117
+ body = self.body_after(toks, colon)
1118
+ node = ClassDef(name_tok.string, bases, keywords, body, [])
1119
+ node._pos(start, self._last_tok(body) or toks[colon])
1120
+ return node
1121
+
1122
+ def if_stmt(self, toks):
1123
+ start = toks[0]
1124
+ colon = self.header_colon(toks)
1125
+ test = self.expr(toks[1:colon], toks[0])
1126
+ body = self.body_after(toks, colon)
1127
+ node = If(test, body, [])
1128
+ node._pos(start, self._last_tok(body) or toks[colon])
1129
+ nxt = self.peek()
1130
+ if nxt.type == _NAME and nxt.string == "elif":
1131
+ etoks = self.logical_line()
1132
+ sub = self.if_stmt(etoks)
1133
+ node.orelse = [sub]
1134
+ node.end_lineno, node.end_col_offset = sub.end_lineno, sub.end_col_offset
1135
+ elif nxt.type == _NAME and nxt.string == "else":
1136
+ etoks = self.logical_line()
1137
+ node.orelse = self.body_after(etoks, self.header_colon(etoks, 0))
1138
+ last = self._last_tok(node.orelse) or etoks[-1]
1139
+ node.end_lineno, node.end_col_offset = last.end
1140
+ return node
1141
+
1142
+ def for_stmt(self, toks, is_async):
1143
+ start = toks[0]
1144
+ j = 2 if is_async else 1
1145
+ colon = self.header_colon(toks, j)
1146
+ in_idx = _has_depth0(toks[j:colon], "in", _NAME)
1147
+ if in_idx is None:
1148
+ raise ScanError("bad for", ("<text>", start.start[0], start.start[1] + 1, start.line))
1149
+ in_idx += j
1150
+ target = self.expr(toks[j:in_idx], toks[j - 1])
1151
+ it = self.expr(toks[in_idx + 1:colon], toks[in_idx])
1152
+ body = self.body_after(toks, colon)
1153
+ node = (AsyncFor if is_async else For)(target, it, body, [])
1154
+ node._pos(start, self._last_tok(body) or toks[colon])
1155
+ nxt = self.peek()
1156
+ if nxt.type == _NAME and nxt.string == "else":
1157
+ etoks = self.logical_line()
1158
+ node.orelse = self.body_after(etoks, self.header_colon(etoks, 0))
1159
+ last = self._last_tok(node.orelse) or etoks[-1]
1160
+ node.end_lineno, node.end_col_offset = last.end
1161
+ return node
1162
+
1163
+ def try_stmt(self, toks):
1164
+ start = toks[0]
1165
+ body = self.body_after(toks, self.header_colon(toks, 0))
1166
+ handlers, orelse, finalbody = [], [], []
1167
+ star = False
1168
+ last = self._last_tok(body) or toks[-1]
1169
+ while True:
1170
+ nxt = self.peek()
1171
+ if nxt.type != _NAME or nxt.string not in ("except", "else", "finally"):
1172
+ break
1173
+ htoks = self.logical_line()
1174
+ colon = self.header_colon(htoks, 0)
1175
+ if htoks[0].string == "except":
1176
+ j = 1
1177
+ if j < colon and htoks[j].type == _OP and htoks[j].string == "*":
1178
+ star = True
1179
+ j += 1
1180
+ as_idx = _has_depth0(htoks[j:colon], "as", _NAME)
1181
+ typ = name = None
1182
+ if as_idx is not None:
1183
+ as_idx += j
1184
+ typ = self.expr(htoks[j:as_idx], htoks[j - 1]) if as_idx > j else None
1185
+ name = htoks[as_idx + 1].string
1186
+ elif colon > j:
1187
+ typ = self.expr(htoks[j:colon], htoks[j - 1])
1188
+ hbody = self.body_after(htoks, colon)
1189
+ h = ExceptHandler(typ, name, hbody)
1190
+ h._pos(htoks[0], self._last_tok(hbody) or htoks[colon])
1191
+ handlers.append(h)
1192
+ last = self._last_tok(hbody) or htoks[colon]
1193
+ elif htoks[0].string == "else":
1194
+ orelse = self.body_after(htoks, colon)
1195
+ last = self._last_tok(orelse) or htoks[colon]
1196
+ else:
1197
+ finalbody = self.body_after(htoks, colon)
1198
+ last = self._last_tok(finalbody) or htoks[colon]
1199
+ node = (TryStar if star else Try)(body, handlers, orelse, finalbody)
1200
+ node._pos(start, last)
1201
+ return node
1202
+
1203
+ def params(self, toks):
1204
+ a = arguments()
1205
+ posonly_seen = False
1206
+ kwonly = False
1207
+ for part in _split_depth0(toks, ","):
1208
+ if not part:
1209
+ continue
1210
+ t0 = part[0]
1211
+ if t0.type == _OP and t0.string == "/":
1212
+ a.posonlyargs, a.args = a.args, []
1213
+ posonly_seen = True
1214
+ continue
1215
+ if t0.type == _OP and t0.string == "*":
1216
+ if len(part) == 1:
1217
+ kwonly = True
1218
+ else:
1219
+ a.vararg = self._arg(part[1:])
1220
+ kwonly = True
1221
+ continue
1222
+ if t0.type == _OP and t0.string == "**":
1223
+ a.kwarg = self._arg(part[1:])
1224
+ continue
1225
+ eq = _has_depth0(part, "=")
1226
+ if eq is None:
1227
+ p = self._arg(part)
1228
+ default = None
1229
+ else:
1230
+ p = self._arg(part[:eq])
1231
+ default = self.expr(part[eq + 1:], part[eq])
1232
+ if kwonly:
1233
+ a.kwonlyargs.append(p)
1234
+ a.kw_defaults.append(default)
1235
+ else:
1236
+ a.args.append(p)
1237
+ if default is not None:
1238
+ a.defaults.append(default)
1239
+ return a
1240
+
1241
+ def _arg(self, toks):
1242
+ name = toks[0]
1243
+ colon = _has_depth0(toks, ":")
1244
+ ann = self.expr(toks[colon + 1:], toks[colon]) if colon is not None and colon + 1 < len(toks) else None
1245
+ node = arg(name.string, ann)
1246
+ node._pos(name, toks[-1] if ann is not None else name)
1247
+ return node
1248
+
1249
+ def simple_statements(self, toks):
1250
+ out = []
1251
+ for part in _split_depth0(toks, ";"):
1252
+ if part:
1253
+ out.append(self.simple(part))
1254
+ return out
1255
+
1256
+ def simple(self, toks):
1257
+ t0 = toks[0]
1258
+ if t0.type == _NAME and t0.string in ("import", "from"):
1259
+ node = self.import_stmt(toks)
1260
+ if node is not None:
1261
+ return node
1262
+ if t0.type == _NAME and t0.string in _STMT_KEYWORDS:
1263
+ return Opaque()._pos(t0, toks[-1])
1264
+ eq = _has_depth0(toks, "=")
1265
+ if eq is None:
1266
+ colon = _has_depth0(toks, ":")
1267
+ if colon == 1 and t0.type == _NAME:
1268
+ node = AnnAssign(Name(t0.string)._pos(t0, t0), self.expr(toks[2:], toks[1]), None)
1269
+ return node._pos(t0, toks[-1])
1270
+ return Expr(self.expr(toks, t0))._pos(t0, toks[-1])
1271
+ lhs, rhs = toks[:eq], toks[eq + 1:]
1272
+ if not lhs or not rhs:
1273
+ return Opaque()._pos(t0, toks[-1])
1274
+ eq2 = _has_depth0(rhs, "=")
1275
+ if eq2 is not None:
1276
+ # chained assignment: two targets → never surfaced, like ast's Assign with 2+ targets
1277
+ value = self.expr(rhs[eq2 + 1:], rhs[eq2])
1278
+ node = Assign([self.target(lhs), self.target(rhs[:eq2])], value)
1279
+ return node._pos(t0, toks[-1])
1280
+ colon = _has_depth0(lhs, ":")
1281
+ if colon is not None:
1282
+ node = AnnAssign(self.target(lhs[:colon]), self.expr(lhs[colon + 1:], lhs[colon]), self.expr(rhs, toks[eq]))
1283
+ return node._pos(t0, toks[-1])
1284
+ node = Assign([self.target(lhs)], self.expr(rhs, toks[eq]))
1285
+ return node._pos(t0, toks[-1])
1286
+
1287
+ # ── imports ──────────────────────────────────────────────────────────────
1288
+
1289
+ def import_stmt(self, toks):
1290
+ return parse_import(toks)
1291
+
1292
+ @staticmethod
1293
+ def _import_aliases(toks):
1294
+ return _import_aliases(toks)
1295
+
1296
+
1297
+ def target(self, toks):
1298
+ if len(toks) == 1 and toks[0].type == _NAME:
1299
+ return Name(toks[0].string)._pos(toks[0], toks[0])
1300
+ chain = self.postfix(toks)
1301
+ if chain is not None and _k(chain) in ("Attribute", "Subscript"):
1302
+ return chain
1303
+ return Opaque()._pos(toks[0], toks[-1])
1304
+
1305
+ # ── expressions ──────────────────────────────────────────────────────────
1306
+
1307
+ def expr(self, toks, anchor):
1308
+ if not toks:
1309
+ return Opaque()._pos(anchor, anchor)
1310
+ t0, tl = toks[0], toks[-1]
1311
+ n = len(toks)
1312
+ if n == 1:
1313
+ if t0.type == _NUMBER:
1314
+ v = _number(t0.string)
1315
+ return (Constant(v) if v is not UNRESOLVED else Opaque())._pos(t0, t0)
1316
+ if t0.type == _STRING:
1317
+ if _is_whole_fstring(t0):
1318
+ return Opaque()._pos(t0, t0)
1319
+ return Constant(_string(t0.string))._pos(t0, t0)
1320
+ if t0.type == _NAME:
1321
+ if t0.string in ("True", "False", "None"):
1322
+ return Constant({"True": True, "False": False, "None": None}[t0.string])._pos(t0, t0)
1323
+ if t0.string not in _STMT_KEYWORDS and t0.string not in ("lambda", "not", "await"):
1324
+ return Name(t0.string)._pos(t0, t0)
1325
+ return Opaque()._pos(t0, t0)
1326
+ if t0.type == _OP and t0.string == "...":
1327
+ return Constant(Ellipsis)._pos(t0, t0)
1328
+ return Opaque()._pos(t0, t0)
1329
+ if n == 2 and t0.type == _OP and t0.string in _UNARY_NUM and tl.type == _NUMBER:
1330
+ v = _number(tl.string)
1331
+ if v is UNRESOLVED:
1332
+ return Opaque()._pos(t0, tl)
1333
+ return UnaryOp(_UNARY_NUM[t0.string](), Constant(v)._pos(tl, tl))._pos(t0, tl)
1334
+ if all(t.type == _STRING for t in toks):
1335
+ if any(_is_whole_fstring(t) for t in toks):
1336
+ return Opaque()._pos(t0, tl)
1337
+ try:
1338
+ parts = [_string(t.string) for t in toks]
1339
+ if all(isinstance(p, str) for p in parts) or all(isinstance(p, bytes) for p in parts):
1340
+ return Constant(parts[0][:0].join(parts))._pos(t0, tl)
1341
+ except (UnicodeDecodeError, ValueError):
1342
+ pass
1343
+ return Opaque()._pos(t0, tl)
1344
+ if _has_depth0(toks, ",") is not None and not (t0.type == _OP and t0.string in _OPEN and _match(toks, 0) == n - 1):
1345
+ elts = self.elements(_split_depth0(toks, ","))
1346
+ if elts is None:
1347
+ return Opaque()._pos(t0, tl)
1348
+ return Tuple(elts)._pos(t0, tl)
1349
+ chain = self.postfix(toks)
1350
+ if chain is not None:
1351
+ return chain
1352
+ return Opaque()._pos(t0, tl)
1353
+
1354
+ def elements(self, parts):
1355
+ """Element nodes of a display; None when a part is a comprehension /
1356
+ yield (the whole display then stays opaque)."""
1357
+ out = []
1358
+ for part in parts:
1359
+ if not part:
1360
+ continue
1361
+ if any(t.type == _NAME and t.string in ("for", "yield") for t in part
1362
+ if _has_depth0([t], t.string, _NAME) is not None):
1363
+ pass
1364
+ if _has_depth0(part, "for", _NAME) is not None or _has_depth0(part, "yield", _NAME) is not None:
1365
+ return None
1366
+ if part[0].type == _OP and part[0].string == "*":
1367
+ out.append(Starred(self.expr(part[1:], part[0]))._pos(part[0], part[-1]))
1368
+ else:
1369
+ out.append(self.expr(part, part[0]))
1370
+ return out
1371
+
1372
+ def atom(self, toks):
1373
+ """(node, next index) for the atom at toks[0], or None."""
1374
+ t0 = toks[0]
1375
+ if t0.type == _OP and t0.string in _OPEN:
1376
+ j = _match(toks, 0)
1377
+ if j is None:
1378
+ return None
1379
+ inner = toks[1:j]
1380
+ node = self.display(t0.string, inner, t0, toks[j])
1381
+ return (node, j + 1) if node is not None else None
1382
+ if t0.type == _NAME:
1383
+ if t0.string in ("True", "False", "None"):
1384
+ return Constant({"True": True, "False": False, "None": None}[t0.string])._pos(t0, t0), 1
1385
+ if t0.string in _STMT_KEYWORDS or t0.string in ("lambda", "not", "await"):
1386
+ return None
1387
+ return Name(t0.string)._pos(t0, t0), 1
1388
+ if t0.type == _NUMBER:
1389
+ v = _number(t0.string)
1390
+ return (Constant(v)._pos(t0, t0), 1) if v is not UNRESOLVED else None
1391
+ if t0.type == _STRING or t0.type == _FSTRING_START:
1392
+ # A run of string parts: plain STRING tokens and/or whole f-strings
1393
+ # (FSTRING_START ... FSTRING_END, which nest). All plain → Constant;
1394
+ # any f-string → an opaque JoinedStr with the run's span.
1395
+ k, depth, fstr = 0, 0, False
1396
+ n = len(toks)
1397
+ while k < n:
1398
+ t = toks[k]
1399
+ if t.type == _FSTRING_START:
1400
+ depth += 1
1401
+ fstr = True
1402
+ elif t.type == _FSTRING_END:
1403
+ depth -= 1
1404
+ elif depth == 0 and t.type != _STRING:
1405
+ break
1406
+ elif depth == 0 and _is_whole_fstring(t):
1407
+ fstr = True
1408
+ k += 1
1409
+ if depth != 0:
1410
+ return None
1411
+ if fstr:
1412
+ return Opaque()._pos(t0, toks[k - 1]), k
1413
+ node = self.expr(toks[:k], t0)
1414
+ return (node, k) if _k(node) == "Constant" else None
1415
+ return None
1416
+
1417
+ def display(self, opener, inner, open_tok, close_tok):
1418
+ if opener == "(":
1419
+ if not inner:
1420
+ return Tuple([])._pos(open_tok, close_tok)
1421
+ if _has_depth0(inner, ",") is not None:
1422
+ if _has_depth0(inner, "for", _NAME) is not None or _has_depth0(inner, "yield", _NAME) is not None:
1423
+ return Opaque()._pos(open_tok, close_tok)
1424
+ elts = self.elements(_split_depth0(inner, ","))
1425
+ if elts is None:
1426
+ return Opaque()._pos(open_tok, close_tok)
1427
+ return Tuple(elts)._pos(open_tok, close_tok)
1428
+ if _has_depth0(inner, "for", _NAME) is not None or _has_depth0(inner, "yield", _NAME) is not None:
1429
+ return Opaque()._pos(open_tok, close_tok)
1430
+ return self.expr(inner, open_tok) # (x): the inner node, its original span
1431
+ if opener == "[":
1432
+ if _has_depth0(inner, "for", _NAME) is not None:
1433
+ return Opaque()._pos(open_tok, close_tok)
1434
+ elts = self.elements(_split_depth0(inner, ",")) if inner else []
1435
+ if elts is None:
1436
+ return Opaque()._pos(open_tok, close_tok)
1437
+ return List(elts)._pos(open_tok, close_tok)
1438
+ # {
1439
+ if not inner:
1440
+ return Dict([], [])._pos(open_tok, close_tok)
1441
+ if _has_depth0(inner, "for", _NAME) is not None:
1442
+ return Opaque()._pos(open_tok, close_tok)
1443
+ parts = [p for p in _split_depth0(inner, ",") if p]
1444
+ is_dict = any((p[0].type == _OP and p[0].string == "**") or _has_depth0(p, ":") is not None for p in parts)
1445
+ if is_dict:
1446
+ keys, values = [], []
1447
+ for p in parts:
1448
+ if p[0].type == _OP and p[0].string == "**":
1449
+ keys.append(None)
1450
+ values.append(self.expr(p[1:], p[0]))
1451
+ continue
1452
+ c = _has_depth0(p, ":")
1453
+ if c is None:
1454
+ return Opaque()._pos(open_tok, close_tok)
1455
+ keys.append(self.expr(p[:c], p[0]))
1456
+ values.append(self.expr(p[c + 1:], p[c]))
1457
+ return Dict(keys, values)._pos(open_tok, close_tok)
1458
+ elts = self.elements(parts)
1459
+ if elts is None:
1460
+ return Opaque()._pos(open_tok, close_tok)
1461
+ return Set(elts)._pos(open_tok, close_tok)
1462
+
1463
+ def postfix(self, toks):
1464
+ """atom ('.' NAME | '(' args ')' | '[' ... ']')* covering ALL of toks, else None."""
1465
+ got = self.atom(toks)
1466
+ if got is None:
1467
+ return None
1468
+ node, i = got
1469
+ n = len(toks)
1470
+ while i < n:
1471
+ t = toks[i]
1472
+ if t.type == _OP and t.string == "." and i + 1 < n and toks[i + 1].type == _NAME:
1473
+ node = Attribute(node, toks[i + 1].string)._pos(toks[0], toks[i + 1])
1474
+ i += 2
1475
+ elif t.type == _OP and t.string == "(":
1476
+ j = _match(toks, i)
1477
+ if j is None:
1478
+ return None
1479
+ args, keywords = self.call_args(toks[i + 1:j], toks[i], toks[j])
1480
+ node = Call(node, args, keywords)._pos(toks[0], toks[j])
1481
+ i = j + 1
1482
+ elif t.type == _OP and t.string == "[":
1483
+ j = _match(toks, i)
1484
+ if j is None:
1485
+ return None
1486
+ node = Subscript(node, Opaque()._pos(toks[i], toks[j]))._pos(toks[0], toks[j])
1487
+ i = j + 1
1488
+ else:
1489
+ return None
1490
+ return node
1491
+
1492
+ def call_args(self, toks, open_tok, close_tok):
1493
+ args, keywords = [], []
1494
+ if toks and _has_depth0(toks, "for", _NAME) is not None:
1495
+ # A depth-0 `for` inside call parentheses can only be a SOLE generator
1496
+ # argument (`f(x for x in y)`, `"".join(t for _, t in run)`; its own
1497
+ # commas do not split it); ast spans it from `(` to `)`.
1498
+ return [Opaque()._pos(open_tok, close_tok)], []
1499
+ parts = _split_depth0(toks, ",")
1500
+ for part in parts:
1501
+ if not part:
1502
+ continue
1503
+ p0 = part[0]
1504
+ if p0.type == _OP and p0.string == "*":
1505
+ args.append(Starred(self.expr(part[1:], p0))._pos(p0, part[-1]))
1506
+ continue
1507
+ if p0.type == _OP and p0.string == "**":
1508
+ keywords.append(keyword_(None, self.expr(part[1:], p0))._pos(p0, part[-1]))
1509
+ continue
1510
+ if len(part) > 2 and p0.type == _NAME and part[1].type == _OP and part[1].string == "=":
1511
+ keywords.append(keyword_(p0.string, self.expr(part[2:], part[1]))._pos(p0, part[-1]))
1512
+ continue
1513
+ if _has_depth0(part, "for", _NAME) is not None or _has_depth0(part, ":=") is not None:
1514
+ args.append(Opaque()._pos(p0, part[-1]))
1515
+ continue
1516
+ args.append(self.expr(part, p0))
1517
+ return args, keywords
1518
+
1519
+
1520
+ class _EndTok:
1521
+ """A position-only stand-in for 'the end of a nested body'."""
1522
+ __slots__ = ("start", "end", "line", "type", "string")
1523
+
1524
+ def __init__(self, line, col):
1525
+ self.start = self.end = (line, col)
1526
+ self.line = ""
1527
+ self.type = -1
1528
+ self.string = ""
1529
+
1530
+
1531
+ def parse_import(toks):
1532
+ """`import …` / `from … import …` → Import / ImportFrom, or None when
1533
+ the tokens don't read as one (the caller falls back to Opaque). A
1534
+ module-level function so scan_imports can run it straight off the
1535
+ token stream without building a tree."""
1536
+ t0 = toks[0]
1537
+ if t0.string == "import":
1538
+ names = _import_aliases(toks[1:])
1539
+ return Import(names)._pos(t0, toks[-1]) if names else None
1540
+ # from [dots] [module] import names
1541
+ level, i = 0, 1
1542
+ while i < len(toks) and toks[i].type == _OP and toks[i].string in (".", "..."):
1543
+ level += len(toks[i].string)
1544
+ i += 1
1545
+ module_parts = []
1546
+ while i < len(toks) and not (toks[i].type == _NAME and toks[i].string == "import"):
1547
+ t = toks[i]
1548
+ if t.type == _NAME or (t.type == _OP and t.string == "."):
1549
+ module_parts.append(t.string)
1550
+ else:
1551
+ return None
1552
+ i += 1
1553
+ if i >= len(toks) or (not module_parts and level == 0):
1554
+ return None
1555
+ rest = toks[i + 1:]
1556
+ if rest and rest[0].type == _OP and rest[0].string == "(":
1557
+ close = _match(rest, 0)
1558
+ rest = rest[1:close] if close is not None else rest[1:]
1559
+ if len(rest) == 1 and rest[0].type == _OP and rest[0].string == "*":
1560
+ names = [alias("*")._pos(rest[0], rest[0])]
1561
+ else:
1562
+ names = _import_aliases(rest)
1563
+ if not names:
1564
+ return None
1565
+ module = "".join(module_parts) or None
1566
+ return ImportFrom(module, names, level)._pos(t0, toks[-1])
1567
+
1568
+
1569
+ def _import_aliases(toks):
1570
+ """`a.b [as c], d [as e]` → [alias]; None on anything unexpected."""
1571
+ out = []
1572
+ for part in _split_depth0(toks, ","):
1573
+ if not part:
1574
+ continue
1575
+ parts, asname, j = [], None, 0
1576
+ while j < len(part):
1577
+ t = part[j]
1578
+ if t.type == _NAME and t.string == "as":
1579
+ if j + 1 != len(part) - 1 or part[j + 1].type != _NAME:
1580
+ return None
1581
+ asname = part[j + 1].string
1582
+ break
1583
+ if t.type == _NAME or (t.type == _OP and t.string == "."):
1584
+ parts.append(t.string)
1585
+ else:
1586
+ return None
1587
+ j += 1
1588
+ if not parts:
1589
+ return None
1590
+ out.append(alias("".join(parts), asname)._pos(part[0], part[-1]))
1591
+ return out
1592
+
1593
+
1594
+ _BODY_FIELDS = ("body", "orelse", "handlers", "finalbody")
1595
+
1596
+
1597
+ def _token_scan_error(text, error):
1598
+ """Tokenizers before Python 3.12 report an unclosed bracket or string at the
1599
+ END of the text; the compiler names the line that opened it."""
1600
+ msg, (line, col) = error.args[0], error.args[1] if len(error.args) > 1 else (0, 0)
1601
+ try:
1602
+ compile(text, "<text>", "exec")
1603
+ except SyntaxError as located:
1604
+ return ScanError(located.msg, ("<text>", located.lineno or line, located.offset or col + 1, ""))
1605
+ except ValueError:
1606
+ pass
1607
+ return ScanError(msg, ("<text>", line, col + 1, ""))
1608
+
1609
+
1610
+ def scan_imports(text):
1611
+ """Every Import / ImportFrom of `text`, in source order, WITHOUT building
1612
+ the tree — the file import graph's path (file_graph.py). The token
1613
+ stream alone decides what is a statement start (after NEWLINE / INDENT
1614
+ / DEDENT / a depth-0 `;`), so imports inside strings and comments are
1615
+ never seen, exactly like scan(); each `import` / `from` statement's
1616
+ tokens go through the same parse_import. ~6× cheaper than scan() on
1617
+ the src tree (tokenize is the whole cost). Raises ScanError like scan()."""
1618
+ out = []
1619
+ stmt = None # tokens of the import statement being collected
1620
+ at_start = True
1621
+ try:
1622
+ for t in tokenize.generate_tokens(io.StringIO(text).readline):
1623
+ tt = t.type
1624
+ if tt in (tokenize.COMMENT, tokenize.NL, tokenize.ENCODING):
1625
+ continue
1626
+ if stmt is not None:
1627
+ if tt == _NEWLINE or tt == _ENDMARKER or (tt == _OP and t.string == ";"):
1628
+ node = parse_import(stmt)
1629
+ if node is not None:
1630
+ out.append(node)
1631
+ stmt = None
1632
+ at_start = True
1633
+ else:
1634
+ stmt.append(t)
1635
+ continue
1636
+ if tt in (_NEWLINE, _INDENT, _DEDENT):
1637
+ at_start = True
1638
+ continue
1639
+ if tt == _OP and t.string == ";":
1640
+ at_start = True
1641
+ continue
1642
+ if at_start and tt == _NAME and t.string in ("import", "from"):
1643
+ stmt = [t]
1644
+ at_start = False
1645
+ except tokenize.TokenError as e:
1646
+ raise _token_scan_error(text, e) from None
1647
+ except (IndentationError, SyntaxError) as e:
1648
+ raise ScanError(str(e), ("<text>", getattr(e, "lineno", 0) or 0, getattr(e, "offset", 0) or 0, "")) from None
1649
+ if stmt is not None:
1650
+ node = parse_import(stmt)
1651
+ if node is not None:
1652
+ out.append(node)
1653
+ return out
1654
+
1655
+
1656
+ def iter_imports(node):
1657
+ """Every Import / ImportFrom node under `node` (a scan() Module or any
1658
+ scanner node), in source order, at any nesting — function bodies,
1659
+ if/try blocks and opaque compounds included."""
1660
+ stack = [node]
1661
+ while stack:
1662
+ n = stack.pop()
1663
+ k = _k(n)
1664
+ if k in ("Import", "ImportFrom"):
1665
+ yield n
1666
+ continue
1667
+ children = []
1668
+ for field in _BODY_FIELDS:
1669
+ body = getattr(n, field, None)
1670
+ if isinstance(body, list):
1671
+ children.extend(body)
1672
+ stack.extend(reversed(children))
1673
+
1674
+
1675
+ def scan(text):
1676
+ """text → (Module, standalone comments, trailing comments). Raises
1677
+ SyntaxError (a ScanError) for what the tokenizer / block structure can't
1678
+ take; anything else parses — validation against `ast` is the caller's."""
1679
+ standalone, trailing = {}, {}
1680
+ sig = []
1681
+ try:
1682
+ for t in tokenize.generate_tokens(io.StringIO(text).readline):
1683
+ tt = t.type
1684
+ if tt == tokenize.COMMENT:
1685
+ line, col = t.start
1686
+ if t.line[:col].strip() == "":
1687
+ standalone[line] = (col, t.string)
1688
+ else:
1689
+ trailing[line] = (col, t.string)
1690
+ elif tt in (tokenize.NL, tokenize.ENCODING):
1691
+ continue
1692
+ else:
1693
+ sig.append(t)
1694
+ except tokenize.TokenError as e:
1695
+ raise _token_scan_error(text, e) from None
1696
+ except (IndentationError, SyntaxError) as e:
1697
+ raise ScanError(str(e), ("<text>", getattr(e, "lineno", 0) or 0, getattr(e, "offset", 0) or 0, "")) from None
1698
+ if not sig or sig[-1].type != _ENDMARKER:
1699
+ sig.append(_EndTok(text.count("\n") + 2, 0))
1700
+ sig[-1].type = _ENDMARKER
1701
+ p = _Parser(sig)
1702
+ body = p.block()
1703
+ return Module(body), standalone, trailing
1704
+
1705
+
1706
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
1707
+ # ║ Node helpers used by both front ends ║
1708
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
1709
+
1710
+ def _dotted_parts(node):
1711
+ parts = []
1712
+ while _k(node) == "Attribute":
1713
+ parts.append(node.attr)
1714
+ node = node.value
1715
+ if _k(node) == "Name":
1716
+ parts.append(node.id)
1717
+ parts.reverse()
1718
+ return parts
1719
+ return None
1720
+
1721
+
1722
+ def _call_func_name(call):
1723
+ func = call.func
1724
+ k = _k(func)
1725
+ if k == "Name":
1726
+ return func.id
1727
+ if k == "Attribute":
1728
+ parts = _dotted_parts(func)
1729
+ return ".".join(parts) if parts is not None else func.attr
1730
+ return None
1731
+
1732
+
1733
+ def _assign_target_name(stmt):
1734
+ k = _k(stmt)
1735
+ if k == "Assign" and len(stmt.targets) == 1 and _k(stmt.targets[0]) == "Name":
1736
+ return stmt.targets[0].id
1737
+ if k == "AnnAssign" and _k(stmt.target) == "Name" and stmt.value is not None:
1738
+ return stmt.target.id
1739
+ return None
1740
+
1741
+
1742
+ def _stmt_call(stmt, nonname_targets):
1743
+ k = _k(stmt)
1744
+ if k == "Expr" and _k(stmt.value) == "Call":
1745
+ return stmt.value
1746
+ if not nonname_targets:
1747
+ return None
1748
+ if k == "Assign" and _k(stmt.value) == "Call":
1749
+ if not (len(stmt.targets) == 1 and _k(stmt.targets[0]) == "Name"):
1750
+ return stmt.value
1751
+ if k == "AnnAssign" and stmt.value is not None and _k(stmt.value) == "Call":
1752
+ if _k(stmt.target) != "Name":
1753
+ return stmt.value
1754
+ return None
1755
+
1756
+
1757
+ def _is_enum_classdef(node):
1758
+ for base in list(node.bases) + [kw.value for kw in node.keywords if kw.arg == "metaclass"]:
1759
+ parts = _dotted_parts(base)
1760
+ last = parts[-1] if parts else None
1761
+ if last and (last.endswith("Enum") or last.endswith("Flag")):
1762
+ return True
1763
+ return False
1764
+
1765
+
1766
+ def _param_names(funcdef):
1767
+ a = funcdef.args
1768
+ names = []
1769
+ for p in list(a.posonlyargs) + list(a.args):
1770
+ if not names and p.arg in _SKIP_PARAMS:
1771
+ continue
1772
+ names.append(p.arg)
1773
+ return names
1774
+
1775
+
1776
+ def _local_signatures(stmts):
1777
+ return {s.name: _param_names(s) for s in stmts if _k(s) in ("FunctionDef", "AsyncFunctionDef")}
1778
+
1779
+
1780
+ def _const_value(node):
1781
+ """The value of a literal-only expression (dict keys), else raise ValueError."""
1782
+ k = _k(node)
1783
+ if k == "Constant":
1784
+ return node.value
1785
+ if k == "UnaryOp" and _k(node.op) in ("USub", "UAdd") and _k(node.operand) == "Constant" \
1786
+ and isinstance(node.operand.value, (int, float, complex)) and not isinstance(node.operand.value, bool):
1787
+ return -node.operand.value if _k(node.op) == "USub" else node.operand.value
1788
+ if k == "Tuple":
1789
+ return tuple(_const_value(e) for e in node.elts)
1790
+ raise ValueError("not a literal")
1791
+
1792
+
1793
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
1794
+ # ║ Extractor: nodes → dict + Origin ║
1795
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
1796
+
1797
+ class Extractor:
1798
+ def __init__(self, origin, types, comments, module_header=True):
1799
+ self.origin = origin
1800
+ self.src = origin.src
1801
+ self.T = types
1802
+ self.standalone, self.trailing = comments
1803
+ # False for a REGION parse (an incremental reparse of statements that
1804
+ # don't start the file): an override comment above its first
1805
+ # statement is that statement's, not a module-header override.
1806
+ self.module_header = module_header
1807
+ self._base = ZERO_BASE # the current TOP-LEVEL statement's Base cell
1808
+ self.cursor = 0 # last consumed line (1-based); 0 = nothing yet
1809
+ # >0 while extracting call arguments / container elements: a CallParse
1810
+ # statement there gets no .span (libcst's span map only covers a
1811
+ # statement's direct value call), so LineMap depth matches.
1812
+ self._nested = 0
1813
+
1814
+ # ── spans on the dict (the consumers' view: Span / _child_spans) ─────────
1815
+
1816
+ def span_obj(self, start, end):
1817
+ (sl, sc), (el, ec) = self.src.linecol(start), self.src.linecol(end)
1818
+ b = self._base
1819
+ return self.T.Span(b, sl - b.line, sc, el - b.line, ec)
1820
+
1821
+ def _stamp(self, obj, start, end):
1822
+ try:
1823
+ obj.span = self.span_obj(start, end)
1824
+ except AttributeError:
1825
+ pass
1826
+ return obj
1827
+
1828
+ def _record_child(self, container, key, value, start, end):
1829
+ if isinstance(value, dict) and getattr(value, "span", None) is not None:
1830
+ return
1831
+ cs = getattr(container, "_child_spans", None)
1832
+ if cs is None:
1833
+ cs = {}
1834
+ try:
1835
+ container._child_spans = cs
1836
+ except AttributeError:
1837
+ return
1838
+ cs[key] = self.span_obj(start, end)
1839
+
1840
+ # ── comments ─────────────────────────────────────────────────────────────
1841
+
1842
+ def _leading_groups(self, upto_line):
1843
+ runs, run = [], []
1844
+ for ln in range(self.cursor + 1, upto_line):
1845
+ c = self.standalone.get(ln)
1846
+ if c is None:
1847
+ if run:
1848
+ runs.append(run)
1849
+ run = []
1850
+ continue
1851
+ run.append((ln, c[1]))
1852
+ if run:
1853
+ runs.append(run)
1854
+ groups = []
1855
+ for run in runs:
1856
+ k = plain = 0
1857
+ while k < len(run):
1858
+ ov_end = self._override_run_end(run, k)
1859
+ if ov_end is None:
1860
+ k += 1
1861
+ continue
1862
+ if plain < k:
1863
+ groups.append(self._group(run[plain:k], False))
1864
+ groups.append(self._group(run[k:ov_end], True))
1865
+ k = plain = ov_end
1866
+ if plain < len(run):
1867
+ groups.append(self._group(run[plain:], False))
1868
+ return groups
1869
+
1870
+ @staticmethod
1871
+ def _group(run, is_override):
1872
+ return (run[0][0], run[-1][0], [t for _, t in run], is_override)
1873
+
1874
+ @staticmethod
1875
+ def _override_run_end(run, i):
1876
+ if not run[i][1].lstrip("#").strip().startswith("["):
1877
+ return None
1878
+ for j in range(i, len(run)):
1879
+ if run[j][1].rstrip().endswith("]"):
1880
+ joined = "\n".join(t for _, t in run[i:j + 1])
1881
+ if parse_override_comment(joined) is not None:
1882
+ return j + 1
1883
+ return None
1884
+
1885
+ def _comment_extent(self, first_line, last_line):
1886
+ return self.src.line_start(first_line), self.src.next_line_start(last_line)
1887
+
1888
+ def _comment_span(self, first_line, last_line):
1889
+ col = self.standalone[first_line][0]
1890
+ return self.src.line_start(first_line) + col, self.src.line_end(last_line)
1891
+
1892
+ def _surface_comment(self, out, path, group):
1893
+ first, last, texts, _ = group
1894
+ text = "\n".join(texts)
1895
+ c = self.T.Comment(text)
1896
+ out[c] = c
1897
+ extent = self._comment_extent(first, last)
1898
+ self.origin.add(Item(self._base, path + (c,), c, "comment", extent, extent,
1899
+ self._comment_span(first, last), text,
1900
+ indent=self.src.indent_of_line(first)))
1901
+ return c
1902
+
1903
+ def _merge_override(self, comment, out, path, span, indent):
1904
+ if isinstance(out.get("__overrides__"), dict):
1905
+ return
1906
+ parsed = parse_override_comment(str(comment))
1907
+ if parsed:
1908
+ out["__overrides__"] = parsed
1909
+ self.origin.add(Item(self._base, path + ("__overrides__",), "__overrides__", "override",
1910
+ span, span, span, dict(parsed), indent=indent,
1911
+ comment_key=comment))
1912
+
1913
+ def _trailing_comment(self, stmt, out, path, key):
1914
+ tc = self.trailing.get(stmt.end_lineno)
1915
+ if tc is None:
1916
+ return
1917
+ col, text = tc
1918
+ code_end = self.src.node_span(stmt)[1]
1919
+ c = self.T.Comment(text, inline=key)
1920
+ out[c] = c
1921
+ start = self.src.line_start(stmt.end_lineno) + col
1922
+ end = self.src.line_end(stmt.end_lineno)
1923
+ self.origin.add(Item(self._base, path + (c,), c, "trailing", (code_end, end), (code_end, end),
1924
+ (start, end), text, indent=self.src.indent_of_line(stmt.end_lineno)))
1925
+ self._merge_override(c, out, path, (start, end), self.src.indent_of_line(stmt.end_lineno))
1926
+
1927
+ def _consume_footer(self, body_indent_len):
1928
+ ln = self.cursor + 1
1929
+ last_taken = self.cursor
1930
+ while ln <= self.src.line_count:
1931
+ c = self.standalone.get(ln)
1932
+ if c is not None:
1933
+ if c[0] < body_indent_len:
1934
+ break
1935
+ last_taken = ln
1936
+ elif not self.src.is_blank(ln):
1937
+ break
1938
+ ln += 1
1939
+ self.cursor = last_taken
1940
+
1941
+ # ── statement extents ────────────────────────────────────────────────────
1942
+
1943
+ def _stmt_first_line(self, stmt):
1944
+ decs = getattr(stmt, "decorator_list", None)
1945
+ if decs:
1946
+ return min(stmt.lineno, decs[0].lineno)
1947
+ return stmt.lineno
1948
+
1949
+ def _header_end_line(self, stmt):
1950
+ body = stmt.body
1951
+ ln = self._stmt_first_line(body[0]) - 1
1952
+ first = self._stmt_first_line(stmt)
1953
+ while ln > first and (self.src.is_blank(ln) or ln in self.standalone):
1954
+ ln -= 1
1955
+ return ln
1956
+
1957
+ def _keyword_line(self, body_stmts, low):
1958
+ ln = self._stmt_first_line(body_stmts[0]) - 1
1959
+ while ln > low and (self.src.is_blank(ln) or ln in self.standalone):
1960
+ ln -= 1
1961
+ return ln
1962
+
1963
+ # ── module ───────────────────────────────────────────────────────────────
1964
+
1965
+ def module(self, tree):
1966
+ gp = self.T.GeneralParse(source=self.origin.text)
1967
+ seq = self.origin.new_seq((), "body", base=ZERO_BASE, indent="", insert_at=0)
1968
+ self._body(tree.body, gp, (), seq, scope="module")
1969
+ self.origin.seal_seq(seq)
1970
+ return gp
1971
+
1972
+ # ── bodies ───────────────────────────────────────────────────────────────
1973
+
1974
+ def _body(self, stmts, out, path, seq, *, scope, block_state=None):
1975
+ T = self.T
1976
+ is_function = scope == "function"
1977
+ nonname_calls = scope != "class"
1978
+ local_sigs = _local_signatures(stmts)
1979
+ call_seen: dict[str, int] = {}
1980
+ counts: dict[str, int] = {}
1981
+ seen: dict[str, int] = {}
1982
+ if is_function:
1983
+ for s in stmts:
1984
+ n = _assign_target_name(s)
1985
+ if n is not None:
1986
+ counts[n] = counts.get(n, 0) + 1
1987
+ if block_state is None:
1988
+ block_state = ({"if": 0, "elif": 0, "else": 0}, {})
1989
+ cond_counters, block_occ = block_state
1990
+
1991
+ for stmt in stmts:
1992
+ kind = _k(stmt)
1993
+ first_line = self._stmt_first_line(stmt)
1994
+ same_line = first_line <= self.cursor # `a = 1; b = 2`
1995
+ gap_start = self.src.next_line_start(self.cursor) if not same_line else self.src.node_span(stmt)[0]
1996
+ if scope == "module":
1997
+ self._base = Base(gap_start, self.src.linecol(gap_start)[0])
1998
+ groups = [] if same_line else self._leading_groups(first_line)
1999
+ core_start = self.src.line_start(groups[0][0]) if groups else (
2000
+ self.src.line_start(first_line) if not same_line else gap_start)
2001
+ indent = self.src.indent_of_line(first_line)
2002
+ is_def = kind in _DEF_KINDS
2003
+ is_block = kind in _BLOCK_KINDS
2004
+ field_override = child_override = None
2005
+
2006
+ for g in groups:
2007
+ if g[3]: # override comment
2008
+ if is_def:
2009
+ if child_override is None:
2010
+ child_override = g
2011
+ elif is_block and is_function:
2012
+ c = self._surface_comment(out, path, g)
2013
+ self._merge_override(c, out, path, self._comment_span(g[0], g[1]),
2014
+ self.src.indent_of_line(g[0]))
2015
+ elif scope == "module" and self.module_header and self.cursor == 0 and not out:
2016
+ c = self._surface_comment(out, path, g)
2017
+ self._merge_override(c, out, path, self._comment_span(g[0], g[1]),
2018
+ self.src.indent_of_line(g[0]))
2019
+ elif field_override is None:
2020
+ field_override = g
2021
+ else:
2022
+ self._surface_comment(out, path, g)
2023
+
2024
+ key = None
2025
+ if is_def:
2026
+ key = self._def(stmt, out, path, seq, scope, child_override, gap_start, core_start)
2027
+ elif kind in ("Assign", "AnnAssign") and _assign_target_name(stmt) is not None:
2028
+ name = _assign_target_name(stmt)
2029
+ if is_function:
2030
+ occ = seen.get(name, 0)
2031
+ seen[name] = occ + 1
2032
+ key = name if (counts[name] == 1 or occ == 0) else f"{name}#{occ}"
2033
+ else:
2034
+ key = name
2035
+ self.origin.shadow(path + (key,))
2036
+ value = self._value(stmt.value, path + (key,))
2037
+ out[key] = value
2038
+ vs = self.src.node_span(stmt.value)
2039
+ self._record_child(out, key, value, *vs)
2040
+ self.origin.add(Item(self._base, path + (key,), key, "value", (gap_start, 0), (core_start, 0), vs, value,
2041
+ indent=indent, code_end=self.src.node_span(stmt)[1]), seq)
2042
+ elif _stmt_call(stmt, nonname_calls) is not None:
2043
+ call = _stmt_call(stmt, nonname_calls)
2044
+ fname = _call_func_name(call) or "call"
2045
+ occ = call_seen.get(fname, 0)
2046
+ call_seen[fname] = occ + 1
2047
+ key = f"{fname}()" if occ == 0 else f"{fname}()#{occ}"
2048
+ self.origin.shadow(path + (key,))
2049
+ parsed = self._call(call, path + (key,), T.CallParse, local_sigs.get(fname), allow_empty=True)
2050
+ if parsed is None:
2051
+ key = None
2052
+ else:
2053
+ out[key] = parsed
2054
+ cs = self.src.node_span(call)
2055
+ self.origin.add(Item(self._base, path + (key,), key, "value", (gap_start, 0), (core_start, 0), cs, parsed,
2056
+ indent=indent, code_end=self.src.node_span(stmt)[1]), seq)
2057
+ elif is_function and kind == "If":
2058
+ self._if_chain(stmt, out, path, seq, cond_counters, block_occ, gap_start, core_start, indent)
2059
+ elif is_function and kind in ("For", "AsyncFor"):
2060
+ self._for_loop(stmt, out, path, seq, block_occ, gap_start, core_start, indent)
2061
+ elif is_function and kind in ("Try", "TryStar"):
2062
+ self._try_block(stmt, out, path, seq, block_occ, gap_start, core_start, indent)
2063
+
2064
+ if key is not None and not is_def and not same_line:
2065
+ self._trailing_comment(stmt, out, path, key)
2066
+ if field_override is not None:
2067
+ self._attach_field_override(field_override, out, path, key)
2068
+
2069
+ if not is_def and not is_block:
2070
+ self.cursor = max(self.cursor, stmt.end_lineno)
2071
+ end = self.src.next_line_start(self.cursor) if not same_line else self.src.node_span(stmt)[1]
2072
+ if key is not None:
2073
+ item = self.origin.items[path + (key,)]
2074
+ item.extent = (item.extent[0], end)
2075
+ item.core = (item.core[0], end)
2076
+ if scope == "module":
2077
+ self.origin.top_stmts.append((self._base, end - gap_start, key))
2078
+
2079
+ def _attach_field_override(self, group, out, path, key):
2080
+ first, last, texts, _ = group
2081
+ parsed = parse_override_comment("\n".join(texts))
2082
+ if not parsed:
2083
+ return
2084
+ overrides = out.get("__overrides__")
2085
+ if not isinstance(overrides, dict):
2086
+ overrides = {}
2087
+ out["__overrides__"] = overrides
2088
+ slot = f"__{key}__"
2089
+ if slot in overrides:
2090
+ return
2091
+ overrides[slot] = parsed
2092
+ span = self._comment_span(first, last)
2093
+ self.origin.add(Item(self._base, path + ("__overrides__", slot), slot, "override",
2094
+ self._comment_extent(first, last), span, span, dict(parsed),
2095
+ indent=self.src.indent_of_line(first)))
2096
+
2097
+ # ── definitions ─────────────────────────────────────────────────────────────────
2098
+
2099
+ def _def(self, stmt, out, path, seq, scope, child_override, gap_start, core_start):
2100
+ name = stmt.name
2101
+ child_path = path + (name,)
2102
+ first_line = self._stmt_first_line(stmt)
2103
+ indent = self.src.indent_of_line(first_line)
2104
+ is_init = (scope == "class" and _k(stmt) in ("FunctionDef", "AsyncFunctionDef") and name == "__init__")
2105
+ if is_init:
2106
+ self.cursor = max(self.cursor, stmt.end_lineno)
2107
+ return None
2108
+ code_span = (self.src.line_start(first_line) + len(indent), self.src.node_span(stmt)[1])
2109
+ self.origin.shadow(child_path)
2110
+ if _k(stmt) == "ClassDef":
2111
+ child = self._class(stmt, child_path)
2112
+ else:
2113
+ child = self._function(stmt, child_path)
2114
+ out[name] = child
2115
+ self.origin.add(Item(self._base, child_path, name, "def", (gap_start, 0), (core_start, 0), None, None,
2116
+ indent=indent, code_end=code_span[1]), seq)
2117
+ if child_override is not None:
2118
+ parsed = parse_override_comment("\n".join(child_override[2]))
2119
+ if parsed:
2120
+ existing = child.get("__overrides__")
2121
+ if isinstance(existing, dict):
2122
+ for k, v in parsed.items():
2123
+ existing.setdefault(k, v)
2124
+ else:
2125
+ child["__overrides__"] = parsed
2126
+ span = self._comment_span(child_override[0], child_override[1])
2127
+ self.origin.add(Item(self._base, child_path + ("__overrides__",), "__overrides__", "override",
2128
+ self._comment_extent(child_override[0], child_override[1]), span, span,
2129
+ dict(parsed), indent=self.src.indent_of_line(child_override[0])))
2130
+ return name
2131
+
2132
+ def _class(self, node, path):
2133
+ T = self.T
2134
+ cls = T.EnumParse if _is_enum_classdef(node) else T.ClassParse
2135
+ first_line = self._stmt_first_line(node)
2136
+ start = self.src.line_start(first_line) + len(self.src.indent_of_line(first_line))
2137
+ end = self.src.node_span(node)[1]
2138
+ readable = cls(source=self.origin.text[start:end])
2139
+ readable.def_name = node.name
2140
+ self._stamp(readable, self.src.node_span(node)[0], end)
2141
+ decorators = self._decorators(node, path)
2142
+ if decorators:
2143
+ readable["decorators"] = decorators
2144
+ self.cursor = self._header_end_line(node)
2145
+ body_indent = self.src.indent_of_line(node.body[0].lineno)
2146
+ seq = self.origin.new_seq(path, "body", base=self._base, indent=body_indent,
2147
+ insert_at=self.src.line_start(node.body[0].lineno))
2148
+ self._body(node.body, readable, path, seq, scope="class")
2149
+ self._consume_footer(len(body_indent))
2150
+ self.origin.seal_seq(seq)
2151
+ self._init_fields(node, readable, path)
2152
+ return readable
2153
+
2154
+ def _init_fields(self, node, readable, path):
2155
+ init = next((s for s in node.body if _k(s) in ("FunctionDef", "AsyncFunctionDef")
2156
+ and s.name == "__init__"), None)
2157
+ if init is None:
2158
+ return
2159
+ body_indent = self.src.indent_of_line(init.body[0].lineno)
2160
+ seq = self.origin.new_seq(path, "body", base=self._base, default=False, indent=body_indent,
2161
+ insert_at=self.src.line_start(init.body[0].lineno))
2162
+ for stmt in init.body:
2163
+ target = value = None
2164
+ k = _k(stmt)
2165
+ if k == "Assign" and len(stmt.targets) == 1:
2166
+ target, value = stmt.targets[0], stmt.value
2167
+ elif k == "AnnAssign":
2168
+ target, value = stmt.target, stmt.value
2169
+ if (target is None or value is None or _k(target) != "Attribute"
2170
+ or _k(target.value) != "Name" or target.value.id != "self"):
2171
+ continue
2172
+ attr = target.attr
2173
+ if attr in readable:
2174
+ continue
2175
+ py = self._value(value, path + (attr,))
2176
+ readable[attr] = py
2177
+ vs = self.src.node_span(value)
2178
+ self._record_child(readable, attr, py, *vs)
2179
+ extent = (self.src.line_start(stmt.lineno), self.src.next_line_start(stmt.end_lineno))
2180
+ self.origin.add(Item(self._base, path + (attr,), attr, "value", extent, extent, vs, py,
2181
+ indent=self.src.indent_of_line(stmt.lineno),
2182
+ code_end=self.src.node_span(stmt)[1]), seq)
2183
+ self.origin.seal_seq(seq)
2184
+
2185
+ def _function(self, node, path):
2186
+ T = self.T
2187
+ first_line = self._stmt_first_line(node)
2188
+ start = self.src.line_start(first_line) + len(self.src.indent_of_line(first_line))
2189
+ end = self.src.node_span(node)[1]
2190
+ readable = T.FunctionParse(source=self.origin.text[start:end])
2191
+ readable.def_name = node.name
2192
+ self._stamp(readable, self.src.node_span(node)[0], end)
2193
+ decorators = self._decorators(node, path)
2194
+ if decorators:
2195
+ readable["decorators"] = decorators
2196
+ params = self._params(node, path + ("parameters",))
2197
+ if params:
2198
+ readable["parameters"] = params
2199
+ self.origin.add(Item(ZERO_BASE, path + ("parameters",), "parameters", "pseudo", (0, 0), (0, 0), None, params))
2200
+ self.cursor = self._header_end_line(node)
2201
+ body_indent = self.src.indent_of_line(node.body[0].lineno)
2202
+ locals_ = T.GeneralParse(source="")
2203
+ seq = self.origin.new_seq(path + ("locals",), "body", base=self._base, indent=body_indent,
2204
+ insert_at=self.src.line_start(node.body[0].lineno))
2205
+ self._body(node.body, locals_, path + ("locals",), seq, scope="function")
2206
+ self._consume_footer(len(body_indent))
2207
+ self.origin.seal_seq(seq)
2208
+ if locals_:
2209
+ bs, be = self.src.node_span(node.body[0])[0], self.src.node_span(node.body[-1])[1]
2210
+ self._stamp(locals_, bs, be)
2211
+ readable["locals"] = locals_
2212
+ self.origin.add(Item(ZERO_BASE, path + ("locals",), "locals", "pseudo", (0, 0), (0, 0), None, locals_))
2213
+ else:
2214
+ self.origin.drop_seq(seq)
2215
+ return readable
2216
+
2217
+ def _decorators(self, node, path):
2218
+ T = self.T
2219
+ if not node.decorator_list:
2220
+ return {}
2221
+ result = {}
2222
+ dpath = path + ("decorators",)
2223
+ seq = self.origin.new_seq(dpath, "decorators", base=self._base,
2224
+ indent=self.src.indent_of_line(node.decorator_list[0].lineno),
2225
+ insert_at=self.src.line_start(node.decorator_list[0].lineno))
2226
+ for dec in node.decorator_list:
2227
+ extent = (self.src.line_start(dec.lineno), self.src.next_line_start(dec.end_lineno))
2228
+ if _k(dec) == "Call":
2229
+ name = _call_func_name(dec)
2230
+ if name is None:
2231
+ continue
2232
+ self.origin.shadow(dpath + (name,))
2233
+ self._nested += 1
2234
+ try:
2235
+ parsed = self._call(dec, dpath + (name,), T.DecorationParse, None, allow_empty=True)
2236
+ finally:
2237
+ self._nested -= 1
2238
+ if parsed is None:
2239
+ parsed = T.CodeLine(self._text(dec))
2240
+ result[name] = parsed
2241
+ self.origin.add(Item(self._base, dpath + (name,), name, "decorator", extent, extent,
2242
+ self.src.node_span(dec), parsed,
2243
+ indent=self.src.indent_of_line(dec.lineno)), seq)
2244
+ else:
2245
+ code = self._text(dec)
2246
+ result[code] = code
2247
+ self.origin.add(Item(self._base, dpath + (code,), code, "decorator", extent, extent,
2248
+ self.src.node_span(dec), code,
2249
+ indent=self.src.indent_of_line(dec.lineno)), seq)
2250
+ self.origin.seal_seq(seq)
2251
+ if result:
2252
+ self.origin.add(Item(ZERO_BASE, dpath, "decorators", "pseudo", (0, 0), (0, 0), None, result))
2253
+ else:
2254
+ self.origin.drop_seq(seq)
2255
+ return result
2256
+
2257
+ def _params(self, node, path):
2258
+ T = self.T
2259
+ a = node.args
2260
+ n_pos = len(a.posonlyargs) + len(a.args)
2261
+ defaults = [None] * (n_pos - len(a.defaults)) + list(a.defaults)
2262
+ regular = [(p, defaults[len(a.posonlyargs) + i]) for i, p in enumerate(a.args)]
2263
+ posonly = [(p, defaults[i]) for i, p in enumerate(a.posonlyargs)]
2264
+ kwonly = [(p, a.kw_defaults[i]) for i, p in enumerate(a.kwonlyargs)]
2265
+ ordered = regular + posonly + kwonly
2266
+ if not ordered:
2267
+ return None
2268
+ result = T.GeneralParse(source="")
2269
+ all_params = [p for p, _ in ordered]
2270
+ seq = self.origin.new_seq(path, "params", base=self._base, sep=", ")
2271
+ first = True
2272
+ for p, default in ordered:
2273
+ if p.arg in _SKIP_PARAMS:
2274
+ continue
2275
+ ps = self.src.node_span(p)
2276
+ if first:
2277
+ seq.insert_at = ps[0]
2278
+ first = False
2279
+ if default is not None:
2280
+ value = self._value(default, path + (p.arg,))
2281
+ vs = self.src.node_span(default)
2282
+ extent = (ps[0], vs[1])
2283
+ else:
2284
+ value = T.NO_DEFAULT
2285
+ vs = None
2286
+ extent = ps
2287
+ result[p.arg] = value
2288
+ self.origin.add(Item(self._base, path + (p.arg,), p.arg, "param", extent, extent, vs, value,
2289
+ slot=ps[1]), seq)
2290
+ if all_params:
2291
+ self._stamp(result, self.src.node_span(all_params[0])[0],
2292
+ max(self.src.node_span(p)[1] for p in all_params))
2293
+ if len(seq.items) >= 2:
2294
+ seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
2295
+ self.origin.seal_seq(seq)
2296
+ if not result:
2297
+ self.origin.drop_seq(seq)
2298
+ return None
2299
+ return result
2300
+
2301
+ # ── blocks (function body) ─────────────────────────────────────────────
2302
+
2303
+ def _block_body(self, stmts, out, path, header_line, block_state):
2304
+ self.cursor = header_line
2305
+ body_indent = self.src.indent_of_line(stmts[0].lineno)
2306
+ seq = self.origin.new_seq(path, "body", base=self._base, indent=body_indent,
2307
+ insert_at=self.src.line_start(stmts[0].lineno))
2308
+ self._body(stmts, out, path, seq, scope="function")
2309
+ self._consume_footer(len(body_indent))
2310
+ self.origin.seal_seq(seq)
2311
+ return seq
2312
+
2313
+ def _finish_block_item(self, key, path, seq, gap_start, core_start, indent, kind="block"):
2314
+ end = self.src.next_line_start(self.cursor)
2315
+ self.origin.add(Item(self._base, path + (key,), key, kind, (gap_start, end), (core_start, end), None, None,
2316
+ indent=indent), seq)
2317
+
2318
+ def _condition(self, test, branch_path, keyword):
2319
+ T = self.T
2320
+ sub_call = _k(test) == "Subscript" and _k(test.value) == "Call"
2321
+ call = test.value if sub_call else test
2322
+ if _k(call) == "Call":
2323
+ cond_key = f"{_call_func_name(call) or 'call'}()##{keyword}"
2324
+ else:
2325
+ cond_key = f"##{keyword}"
2326
+ if sub_call:
2327
+ inner = self._call(test.value, branch_path + (cond_key,), T.CallParse, None)
2328
+ value = inner if inner is not None else T.CodeLine(self._text(test))
2329
+ else:
2330
+ value = self._value(test, branch_path + (cond_key,))
2331
+ return cond_key, value
2332
+
2333
+ def _if_chain(self, node, out, path, seq, cond_counters, block_occ, gap_start, core_start, indent):
2334
+ T = self.T
2335
+ first = True
2336
+ while True:
2337
+ keyword = "if" if first else "elif"
2338
+ idx = cond_counters[keyword]
2339
+ cond_counters[keyword] += 1
2340
+ key = f"{keyword}##{idx}"
2341
+ branch_path = path + (key,)
2342
+ branch = T.Conditional(condition=key)
2343
+ cond_key, cond_value = self._condition(node.test, branch_path, keyword)
2344
+ branch[cond_key] = cond_value
2345
+ ts = self.src.node_span(node.test)
2346
+ self._record_child(branch, cond_key, cond_value, *ts)
2347
+ self.origin.add(Item(self._base, branch_path + (cond_key,), cond_key, "header", ts, ts, ts, cond_value))
2348
+ self._block_body(node.body, branch, branch_path, node.test.end_lineno, (cond_counters, block_occ))
2349
+ self._stamp(branch, ts[0], self.src.node_span(node.body[-1])[1])
2350
+ out[key] = branch
2351
+ self._finish_block_item(key, path, seq, gap_start, core_start, indent)
2352
+ first = False
2353
+ orelse = node.orelse
2354
+ if not orelse:
2355
+ return
2356
+ if (len(orelse) == 1 and _k(orelse[0]) == "If"
2357
+ and self.src.line_text(orelse[0].lineno).lstrip().startswith("elif")):
2358
+ node = orelse[0]
2359
+ gap_start = core_start = self.src.line_start(node.lineno)
2360
+ indent = self.src.indent_of_line(node.lineno)
2361
+ continue
2362
+ idx = cond_counters["else"]
2363
+ cond_counters["else"] += 1
2364
+ key = f"else##{idx}"
2365
+ branch_path = path + (key,)
2366
+ branch = T.Conditional(condition=key)
2367
+ kw_line = self._keyword_line(orelse, self.cursor)
2368
+ else_seq = self._block_body(orelse, branch, branch_path, kw_line, (cond_counters, block_occ))
2369
+ if branch:
2370
+ self._stamp(branch, self.src.node_span(orelse[0])[0], self.src.node_span(orelse[-1])[1])
2371
+ out[key] = branch
2372
+ gs = self.src.line_start(kw_line)
2373
+ self._finish_block_item(key, path, seq, gs, gs, self.src.indent_of_line(kw_line))
2374
+ else:
2375
+ self.origin.drop_seq(else_seq)
2376
+ return
2377
+
2378
+ def _for_loop(self, node, out, path, seq, block_occ, gap_start, core_start, indent):
2379
+ T = self.T
2380
+ target = self._text(node.target)
2381
+ it = self._text(node.iter)
2382
+ key = occ_key(f"for {target} in {it}", block_occ)
2383
+ loop_path = path + (key,)
2384
+ loop = T.Loop(target=target, iter=it)
2385
+ self._block_body(node.body, loop, loop_path, node.iter.end_lineno, None)
2386
+ range_args = self._range_args(node.iter, loop_path + ("range",))
2387
+ if range_args is not None:
2388
+ loop["range"] = range_args
2389
+ rs = (self.src.node_span(node.iter.args[0])[0], self.src.node_span(node.iter.args[-1])[1])
2390
+ self._record_child(loop, "range", range_args, *rs)
2391
+ self.origin.add(Item(self._base, loop_path + ("range",), "range", "value", rs, rs, rs, range_args))
2392
+ self._stamp(loop, *self.src.node_span(node))
2393
+ out[key] = loop
2394
+ self._finish_block_item(key, path, seq, gap_start, core_start, indent)
2395
+ if node.orelse:
2396
+ branch = T.Conditional(condition="else")
2397
+ kw_line = self._keyword_line(node.orelse, self.cursor)
2398
+ else_seq = self._block_body(node.orelse, branch, path + (f"{key} else",), kw_line, None)
2399
+ if branch:
2400
+ self._stamp(branch, self.src.node_span(node.orelse[0])[0], self.src.node_span(node.orelse[-1])[1])
2401
+ out[f"{key} else"] = branch
2402
+ gs = self.src.line_start(kw_line)
2403
+ self._finish_block_item(f"{key} else", path, seq, gs, gs, self.src.indent_of_line(kw_line))
2404
+ else:
2405
+ self.origin.drop_seq(else_seq)
2406
+
2407
+ def _range_args(self, iter_node, path):
2408
+ if not (_k(iter_node) == "Call" and _k(iter_node.func) == "Name"
2409
+ and iter_node.func.id == "range" and iter_node.args and not iter_node.keywords):
2410
+ return None
2411
+ args = []
2412
+ seq = self.origin.new_seq(path, "elements", base=self._base, sep=", ",
2413
+ insert_at=self.src.node_span(iter_node.args[0])[0])
2414
+ for i, a in enumerate(iter_node.args):
2415
+ v = self._literal(a)
2416
+ if v is UNRESOLVED:
2417
+ self.origin.drop_seq(seq)
2418
+ for j in range(i):
2419
+ self.origin.items.pop(path + (j,), None)
2420
+ return None
2421
+ args.append(v)
2422
+ sp = self.src.node_span(a)
2423
+ self.origin.add(Item(self._base, path + (i,), i, "element", sp, sp, sp, v), seq)
2424
+ self.origin.seal_seq(seq)
2425
+ return args
2426
+
2427
+ def _try_block(self, node, out, path, seq, block_occ, gap_start, core_start, indent):
2428
+ T = self.T
2429
+ try_key = occ_key("try", block_occ)
2430
+ body = T.Try(header="try")
2431
+ tseq = self._block_body(node.body, body, path + (try_key,), node.lineno, None)
2432
+ if body:
2433
+ self._stamp(body, self.src.node_span(node.body[0])[0], self.src.node_span(node.body[-1])[1])
2434
+ out[try_key] = body
2435
+ self._finish_block_item(try_key, path, seq, gap_start, core_start, indent)
2436
+ else:
2437
+ self.origin.drop_seq(tseq)
2438
+ star = _k(node) == "TryStar"
2439
+ for handler in node.handlers:
2440
+ parts = ["except*" if star else "except"]
2441
+ if handler.type is not None:
2442
+ parts.append(self._text(handler.type))
2443
+ if handler.name is not None:
2444
+ parts += ["as", handler.name]
2445
+ header = " ".join(parts)
2446
+ hkey = occ_key(header, block_occ)
2447
+ hbody = T.Except(header=header)
2448
+ hline = handler.type.end_lineno if handler.type is not None else handler.lineno
2449
+ hseq = self._block_body(handler.body, hbody, path + (hkey,), hline, None)
2450
+ if hbody:
2451
+ self._stamp(hbody, *self.src.node_span(handler))
2452
+ out[hkey] = hbody
2453
+ gs = self.src.line_start(handler.lineno)
2454
+ self._finish_block_item(hkey, path, seq, gs, gs, self.src.indent_of_line(handler.lineno))
2455
+ else:
2456
+ self.origin.drop_seq(hseq)
2457
+ if node.orelse:
2458
+ ekey = occ_key("try else", block_occ)
2459
+ ebody = T.Try(header="try else")
2460
+ kw_line = self._keyword_line(node.orelse, self.cursor)
2461
+ eseq = self._block_body(node.orelse, ebody, path + (ekey,), kw_line, None)
2462
+ if ebody:
2463
+ self._stamp(ebody, self.src.node_span(node.orelse[0])[0], self.src.node_span(node.orelse[-1])[1])
2464
+ out[ekey] = ebody
2465
+ gs = self.src.line_start(kw_line)
2466
+ self._finish_block_item(ekey, path, seq, gs, gs, self.src.indent_of_line(kw_line))
2467
+ else:
2468
+ self.origin.drop_seq(eseq)
2469
+ if node.finalbody:
2470
+ fkey = occ_key("finally", block_occ)
2471
+ fbody = T.Try(header="finally")
2472
+ kw_line = self._keyword_line(node.finalbody, self.cursor)
2473
+ fseq = self._block_body(node.finalbody, fbody, path + (fkey,), kw_line, None)
2474
+ if fbody:
2475
+ self._stamp(fbody, self.src.node_span(node.finalbody[0])[0], self.src.node_span(node.finalbody[-1])[1])
2476
+ out[fkey] = fbody
2477
+ gs = self.src.line_start(kw_line)
2478
+ self._finish_block_item(fkey, path, seq, gs, gs, self.src.indent_of_line(kw_line))
2479
+ else:
2480
+ self.origin.drop_seq(fseq)
2481
+
2482
+ # ── values ───────────────────────────────────────────────────────────────
2483
+
2484
+ def _text(self, node):
2485
+ s, e = self.src.node_span(node)
2486
+ return self.origin.text[s:e]
2487
+
2488
+ def _literal(self, node):
2489
+ k = _k(node)
2490
+ if k == "Constant" and isinstance(node.value, _SIMPLE_LITERAL_TYPES):
2491
+ return node.value
2492
+ if (k == "UnaryOp" and _k(node.op) in ("USub", "UAdd") and _k(node.operand) == "Constant"
2493
+ and isinstance(node.operand.value, (int, float)) and not isinstance(node.operand.value, bool)):
2494
+ return -node.operand.value if _k(node.op) == "USub" else node.operand.value
2495
+ return UNRESOLVED
2496
+
2497
+ def _value(self, node, path):
2498
+ T = self.T
2499
+ lit = self._literal(node)
2500
+ if lit is not UNRESOLVED:
2501
+ return lit
2502
+ k = _k(node)
2503
+ if k == "Subscript" and _k(node.value) == "Call":
2504
+ inner = self._call(node.value, path, T.CallParse, None)
2505
+ if inner is not None:
2506
+ return inner
2507
+ return T.CodeLine(self._text(node))
2508
+ if k == "Call":
2509
+ parsed = self._call(node, path, T.CallParse, None)
2510
+ return parsed if parsed is not None else T.CodeLine(self._text(node))
2511
+ if k in ("Tuple", "List"):
2512
+ if any(_k(e) == "Starred" for e in node.elts):
2513
+ return T.CodeLine(self._text(node))
2514
+ cstart = self.src.node_span(node)[0]
2515
+ seq = self.origin.new_seq(path, "elements", base=self._base, sep=", ",
2516
+ insert_at=cstart + (1 if self.origin.text[cstart] in "([{" else 0))
2517
+ values = []
2518
+ self._nested += 1
2519
+ for i, e in enumerate(node.elts):
2520
+ v = self._value(e, path + (i,))
2521
+ values.append(v)
2522
+ sp = self.src.node_span(e)
2523
+ self.origin.add(Item(self._base, path + (i,), i, "element", sp, sp, sp, v), seq)
2524
+ self._nested -= 1
2525
+ if len(seq.items) >= 2:
2526
+ seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
2527
+ if seq.items:
2528
+ self.origin.seal_seq(seq)
2529
+ else:
2530
+ self.origin.drop_seq(seq)
2531
+ return tuple(values) if k == "Tuple" else values
2532
+ if k == "Dict":
2533
+ if any(kn is None for kn in node.keys):
2534
+ return T.CodeLine(self._text(node))
2535
+ keys = []
2536
+ for kn in node.keys:
2537
+ try:
2538
+ kv = _const_value(kn)
2539
+ hash(kv)
2540
+ except Exception:
2541
+ # Mode dictionaries use type expressions as keys. Keep
2542
+ # those expressions unevaluated while exposing their
2543
+ # nested kwargs to the same editable source tree.
2544
+ kv = T.CodeLine(self._text(kn))
2545
+ keys.append(kv)
2546
+ seq = self.origin.new_seq(path, "pairs", base=self._base, sep=", ",
2547
+ insert_at=self.src.node_span(node)[0] + 1)
2548
+ result = {}
2549
+ self._nested += 1
2550
+ for kv, knode, vnode in zip(keys, node.keys, node.values):
2551
+ self.origin.shadow(path + (kv,))
2552
+ v = self._value(vnode, path + (kv,))
2553
+ result[kv] = v
2554
+ ks, ke = self.src.node_span(knode)
2555
+ vs = self.src.node_span(vnode)
2556
+ self.origin.add(Item(self._base, path + (kv,), kv, "pair", (ks, vs[1]), (ks, vs[1]), vs, v), seq)
2557
+ self._nested -= 1
2558
+ if len(seq.items) >= 2:
2559
+ seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
2560
+ if seq.items:
2561
+ self.origin.seal_seq(seq)
2562
+ else:
2563
+ self.origin.drop_seq(seq)
2564
+ return result
2565
+ if k == "Set":
2566
+ elements = []
2567
+ for e in node.elts:
2568
+ v = self._literal(e)
2569
+ if v is UNRESOLVED:
2570
+ return T.CodeLine(self._text(node))
2571
+ elements.append(v)
2572
+ return set(elements)
2573
+ if k in ("Name", "Attribute"):
2574
+ parts = _dotted_parts(node)
2575
+ if parts is not None:
2576
+ if T.resolve is None:
2577
+ return NameRef(self._text(node), parts) # worker: the main side resolves
2578
+ resolved = T.resolve(parts)
2579
+ if resolved is not UNRESOLVED:
2580
+ return resolved
2581
+ return T.CodeLine(self._text(node))
2582
+
2583
+ def _call(self, call, path, result_cls, pos_names_override, allow_empty=False):
2584
+ T = self.T
2585
+ readable = result_cls(source=self._text(call), func_name=_call_func_name(call))
2586
+ positional = [a for a in call.args if _k(a) != "Starred"]
2587
+ pending = None
2588
+ if pos_names_override is not None:
2589
+ pos_names = pos_names_override
2590
+ elif positional:
2591
+ parts = _dotted_parts(call.func)
2592
+ pos_names = None
2593
+ if parts is not None:
2594
+ if T.positional_names is None:
2595
+ pending = parts # worker: the main side binds the runtime signature
2596
+ else:
2597
+ pos_names = T.positional_names(parts)
2598
+ if pos_names is None:
2599
+ pos_names = [f"arg{i}" for i in range(len(positional))]
2600
+ if any(n in {kw.arg for kw in call.keywords} for n in pos_names):
2601
+ pos_names = None
2602
+ pending = None
2603
+ else:
2604
+ pos_names = None
2605
+ seq = self.origin.new_seq(path, "args", base=self._base, sep=", ")
2606
+ pos_idx = 0
2607
+ nested = self._nested > 0
2608
+ self._nested += 1
2609
+ for a in call.args:
2610
+ if _k(a) == "Starred":
2611
+ continue
2612
+ if pos_names is not None and pos_idx < len(pos_names):
2613
+ key = pos_names[pos_idx]
2614
+ v = self._value(a, path + (key,))
2615
+ readable[key] = v
2616
+ sp = self.src.node_span(a)
2617
+ self.origin.add(Item(self._base, path + (key,), key, "element", sp, sp, sp, v), seq)
2618
+ pos_idx += 1
2619
+ for kw in call.keywords:
2620
+ if kw.arg is None:
2621
+ continue
2622
+ v = self._value(kw.value, path + (kw.arg,))
2623
+ readable[kw.arg] = v
2624
+ vs = self.src.node_span(kw.value)
2625
+ ks = self.src.node_span(kw)[0]
2626
+ self.origin.add(Item(self._base, path + (kw.arg,), kw.arg, "kwarg", (ks, vs[1]), (ks, vs[1]), vs, v), seq)
2627
+ self._nested -= 1
2628
+ if len(seq.items) >= 2:
2629
+ seq.sep = self.origin.text[seq.items[0].extent[1]:seq.items[1].extent[0]]
2630
+ # Always anchored just inside the closing paren: the main side may drop
2631
+ # positionals a runtime signature doesn't name (`print(x)`) and re-seal.
2632
+ seq.insert_at = self.src.node_span(call)[1] - 1
2633
+ self.origin.seal_seq(seq)
2634
+ if not readable and not allow_empty:
2635
+ self.origin.drop_seq(seq)
2636
+ return None
2637
+ callee_span = self.src.node_span(call.func)
2638
+ self.origin.add(Item(self._base, path + ("__callee__",), "__callee__", "callee",
2639
+ callee_span, callee_span, callee_span, self._text(call.func)))
2640
+ if pos_names:
2641
+ readable["__pos_names__"] = list(pos_names)
2642
+ if pending is not None:
2643
+ readable._pos_pending = pending
2644
+ if not nested:
2645
+ self._stamp(readable, *self.src.node_span(call))
2646
+ return readable
2647
+
2648
+
2649
+ # ╔══════════════════════════════════════════════════════════════════════════════╗
2650
+ # ║ Worker entry ║
2651
+ # ╚══════════════════════════════════════════════════════════════════════════════╝
2652
+
2653
+ def extract(text, *, frontend, types, file_path=None, line_offset=0, module_header=True):
2654
+ """(gp, origin) for `text` through one front end: "scan" (the tokenizer
2655
+ parser) or "ast" (Python's parser, the oracle)."""
2656
+ if frontend == "ast":
2657
+ tree = ast.parse(text)
2658
+ comments = scan_comments(text)
2659
+ else:
2660
+ tree, standalone, trailing = scan(text)
2661
+ comments = (standalone, trailing)
2662
+ origin = Origin(text)
2663
+ origin.file_path = file_path
2664
+ origin.line_offset = line_offset
2665
+ gp = Extractor(origin, types, comments, module_header=module_header).module(tree)
2666
+ return gp, origin
2667
+
2668
+
2669
+ def scan_extract(text, *, validate=False):
2670
+ """The worker's job: scanner front end, neutral types, validated by
2671
+ ast.parse (whose SyntaxError is reported as data — nothing raises across
2672
+ the interpreter boundary). Returns ("ok", gp, origin) or
2673
+ ("error", message, lineno, offset)."""
2674
+ if validate:
2675
+ try:
2676
+ ast.parse(text)
2677
+ except SyntaxError as e:
2678
+ return ("error", e.msg, e.lineno or 0, e.offset or 0)
2679
+ try:
2680
+ gp, origin = extract(text, frontend="scan", types=NEUTRAL_TYPES)
2681
+ except SyntaxError as e:
2682
+ return ("error", str(e.msg if hasattr(e, "msg") else e), getattr(e, "lineno", 0) or 0,
2683
+ getattr(e, "offset", 0) or 0)
2684
+ return ("ok", gp, origin)