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,876 @@
1
+ import dataclasses
2
+ import collections.abc
3
+ import hashlib
4
+ import re
5
+ import libcst as cst
6
+
7
+ from meltygui.core.rendering.core_decoration import exclude
8
+ from meltygui.core.rendering.core_decoration import defaults
9
+
10
+ # ==============================
11
+ # Formatting & whitespace constants
12
+ # ==============================
13
+
14
+ # no space '=' for keyword args
15
+ TIGHT_EQ = cst.AssignEqual(
16
+ whitespace_before=cst.SimpleWhitespace(""),
17
+ whitespace_after=cst.SimpleWhitespace("")
18
+ )
19
+
20
+ # ==============================
21
+ # Trivia-insensitive subtree hashing
22
+ # ==============================
23
+
24
+ _TRIVIA_FIELD_NAMES = {
25
+ "whitespace", "leading_lines", "trailing_whitespace",
26
+ "lpar", "rpar", "star", "comma", "semicolon",
27
+ "header", "footer", "default_newline", "default_indent",
28
+ "encoding", "has_trailing_newline", "trailing_comma",
29
+ }
30
+
31
+ _TRIVIA_NODE_TYPES = (
32
+ cst.EmptyLine,
33
+ cst.TrailingWhitespace,
34
+ cst.SimpleWhitespace,
35
+ cst.Newline,
36
+ cst.ParenthesizedWhitespace,
37
+ )
38
+
39
+
40
+ def _is_trivia_node(n: cst.CSTNode) -> bool:
41
+ return isinstance(n, _TRIVIA_NODE_TYPES)
42
+
43
+
44
+ def _fp_primitive(x):
45
+ if x is None: return ("none",)
46
+ if x is True: return ("bool", 1)
47
+ if x is False: return ("bool", 0)
48
+ if isinstance(x, (int, float)): return ("num", repr(x))
49
+ if isinstance(x, str): return ("str", x)
50
+ return ("repr", repr(x))
51
+
52
+
53
+ def _last_stmt(mapping) -> cst.BaseStatement | None:
54
+ last = None
55
+ for v in mapping.values():
56
+ n = v.node if hasattr(v, "node") else v
57
+ if isinstance(n, cst.BaseStatement):
58
+ last = n
59
+ return last
60
+
61
+
62
+
63
+ def _expr_placeholder_like_last_stmt(stmt: cst.BaseStatement | None) -> cst.BaseExpression:
64
+ """
65
+ If the last statement is a simple constant expression, mirror its *type*.
66
+ Otherwise fall back to NAME('PLACEHOLDER').
67
+ """
68
+ if isinstance(stmt, cst.SimpleStatementLine) and stmt.body:
69
+ first = stmt.body[0]
70
+ if isinstance(first, cst.Expr):
71
+ v = first.value
72
+ if isinstance(v, (cst.Integer, cst.Float, cst.Imaginary, cst.SimpleString,
73
+ cst.Name, cst.List, cst.Tuple, cst.Set, cst.Dict,
74
+ cst.ConcatenatedString, cst.FormattedString)):
75
+ # reuse your existing shape logic
76
+ like = _placeholder_like(v)
77
+ # _placeholder_like returns a CSTNode; check BaseExpression
78
+ if isinstance(like, cst.BaseExpression):
79
+ return like
80
+ # default
81
+ return cst.Name("PLACEHOLDER")
82
+
83
+
84
+ def _make_const_line_like(last_stmt: cst.BaseStatement | None) -> cst.SimpleStatementLine:
85
+ """
86
+ Produce a one-line statement with a constant-like expression, inferred from last_stmt.
87
+ This renders with its own newline automatically.
88
+ """
89
+ expr = _expr_placeholder_like_last_stmt(last_stmt)
90
+ return cst.SimpleStatementLine([cst.Expr(value=expr)])
91
+
92
+
93
+ def _make_pass_line() -> cst.SimpleStatementLine:
94
+ return cst.SimpleStatementLine([cst.Pass()])
95
+
96
+ def _fingerprint_struct(obj):
97
+ # unwrap proxy
98
+ if isinstance(obj, CSTProxy):
99
+ obj = obj.node
100
+
101
+ if isinstance(obj, cst.CSTNode) and dataclasses.is_dataclass(obj):
102
+ typ = type(obj).__name__
103
+ # obj_hash = hash(obj)
104
+ parts = []
105
+ for f in dataclasses.fields(obj):
106
+ name = f.name
107
+ if name in _TRIVIA_FIELD_NAMES:
108
+ continue
109
+ val = getattr(obj, name)
110
+
111
+ if isinstance(val, cst.CSTNode) and _is_trivia_node(val):
112
+ continue
113
+
114
+ if isinstance(val, collections.abc.Sequence) and not isinstance(val, str):
115
+ seq_elems = []
116
+ for e in val:
117
+ if isinstance(e, cst.CSTNode) and _is_trivia_node(e):
118
+ continue
119
+ seq_elems.append(_fingerprint_struct(e))
120
+ parts.append((name, ("seq", tuple(seq_elems))))
121
+ else:
122
+ parts.append((name, _fingerprint_struct(val)))
123
+ return ("cst", typ, tuple(parts))
124
+
125
+ if isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str):
126
+ return ("seq", tuple(_fingerprint_struct(e) for e in obj))
127
+
128
+ return _fp_primitive(obj)
129
+
130
+
131
+ def _hash_key_for_elem(elem, digest_size=3) -> str:
132
+ """
133
+ Stable short hex id for an element based on its trivia-insensitive structure.
134
+ digest_size=12 -> 24 hex chars.
135
+ """
136
+ struct = _fingerprint_struct(elem)
137
+ payload = repr(struct).encode("utf-8")
138
+ h = hashlib.blake2b(payload, digest_size=digest_size)
139
+ return h.hexdigest()
140
+
141
+
142
+ def _dedupe_key(base: str, mapping: collections.abc.Mapping) -> str:
143
+ key = base
144
+ i = 1
145
+ while key in mapping:
146
+ i += 1
147
+ key = f"{base}~{i}"
148
+ return key
149
+
150
+
151
+ _CONST_NAME_RE = re.compile(r"^CONSTANT_(\d+)$")
152
+
153
+
154
+ def _next_constant_index_in_body(mapping) -> int:
155
+ """
156
+ Scan a CSTDictProxy representing a .body field and find the next CONSTANT_N index.
157
+ """
158
+ max_idx = -1
159
+ for v in mapping.values():
160
+ n = v.node if hasattr(v, "node") else v
161
+ if isinstance(n, cst.SimpleStatementLine):
162
+ for small in n.body:
163
+ if isinstance(small, cst.Assign):
164
+ # consider only the `Name = ...` targets
165
+ for tgt in small.targets:
166
+ t = tgt.target
167
+ if isinstance(t, cst.Name):
168
+ m = _CONST_NAME_RE.match(t.value)
169
+ if m:
170
+ try:
171
+ idx = int(m.group(1))
172
+ if idx > max_idx:
173
+ max_idx = idx
174
+ except ValueError:
175
+ pass
176
+ return max_idx + 1
177
+
178
+
179
+ def _make_constant_assignment_line(name: str,
180
+ value_expr: cst.BaseExpression | None = None,
181
+ tight_equals: bool = True) -> cst.SimpleStatementLine:
182
+ if value_expr is None:
183
+ # default requested by you
184
+ value_expr = cst.SimpleString('"default_val"')
185
+ assign = cst.Assign(
186
+ targets=[cst.AssignTarget(target=cst.Name(name))],
187
+ value=value_expr
188
+ )
189
+ return cst.SimpleStatementLine([assign])
190
+
191
+ # ==============================
192
+ # Converters (primitive <-> LibCST)
193
+ # ==============================
194
+
195
+ def _lexeme(py):
196
+ if isinstance(py, bool): return "True" if py else "False"
197
+ if py is None: return "None"
198
+ if isinstance(py, float): return repr(py)
199
+ if isinstance(py, int): return str(py)
200
+ if isinstance(py, str): return py
201
+ return str(py)
202
+
203
+
204
+ def _autobox_expr(py, hint: cst.CSTNode | None):
205
+ if isinstance(py, cst.CSTNode): return py
206
+ if isinstance(py, CSTProxy): return py.node
207
+ if isinstance(py, bool): return cst.Name("True" if py else "False")
208
+ if py is None: return cst.Name("None")
209
+ if isinstance(py, int): return cst.Integer(_lexeme(py))
210
+ if isinstance(py, float): return cst.Float(_lexeme(py))
211
+ if isinstance(py, str):
212
+ if isinstance(hint, cst.Name): return cst.Name(py)
213
+ if isinstance(hint, cst.SimpleString): return cst.SimpleString(repr(py))
214
+ return cst.SimpleString(repr(py))
215
+ return py
216
+
217
+
218
+ def _to_libcst(value, orig_field):
219
+ if isinstance(value, CSTProxy): return value.node
220
+ if isinstance(value, cst.CSTNode): return value
221
+
222
+ if isinstance(orig_field, collections.abc.Sequence) and not isinstance(orig_field, str):
223
+ elems = value.values() if isinstance(value, collections.abc.Mapping) else value
224
+ hint_elem = next((e for e in orig_field if isinstance(e, cst.CSTNode)), None)
225
+ out = []
226
+ for x in elems:
227
+ if isinstance(x, CSTProxy):
228
+ out.append(x.node)
229
+ elif isinstance(x, cst.CSTNode):
230
+ out.append(x)
231
+ else:
232
+ out.append(_autobox_expr(x, hint_elem))
233
+ return out
234
+
235
+ if isinstance(orig_field, str):
236
+ return _lexeme(value)
237
+
238
+ if isinstance(orig_field, cst.CSTNode):
239
+ return _autobox_expr(value, orig_field)
240
+
241
+ return value
242
+
243
+
244
+ # ==============================
245
+ # Naming helpers & placeholders
246
+ # ==============================
247
+
248
+ def _first_hint_elem(seq):
249
+ for e in seq:
250
+ if isinstance(e, cst.CSTNode):
251
+ return e
252
+ return None
253
+
254
+
255
+ def _default_placeholder_for_hint(hint: cst.CSTNode | None) -> cst.CSTNode:
256
+ if isinstance(hint, cst.Arg) or hint is None:
257
+ return cst.Arg(value=cst.Name("PLACEHOLDER"))
258
+ if isinstance(hint, cst.BaseExpression):
259
+ return cst.Name("PLACEHOLDER")
260
+ if isinstance(hint, cst.Param):
261
+ return cst.Param(name=cst.Name("placeholder"))
262
+ if isinstance(hint, cst.Element):
263
+ return cst.Element(value=cst.Name("PLACEHOLDER"))
264
+ if isinstance(hint, cst.DictElement):
265
+ return cst.DictElement(key=cst.SimpleString("'key'"), value=cst.Name("PLACEHOLDER"))
266
+ if isinstance(hint, cst.BaseStatement):
267
+ return cst.Pass()
268
+ return cst.Name("PLACEHOLDER")
269
+
270
+
271
+ def _placeholder_like(expr: cst.CSTNode) -> cst.CSTNode:
272
+ if isinstance(expr, cst.Integer): return cst.Integer("0")
273
+ if isinstance(expr, cst.Float): return cst.Float("0.0")
274
+ if isinstance(expr, cst.Imaginary): return cst.Imaginary("0j")
275
+ if isinstance(expr, cst.SimpleString): return cst.SimpleString("''")
276
+ if isinstance(expr, cst.Name):
277
+ if expr.value in ("True", "False"): return cst.Name("False")
278
+ if expr.value == "None": return cst.Name("None")
279
+ return cst.Name("PLACEHOLDER")
280
+ if isinstance(expr, cst.List): return cst.List([])
281
+ if isinstance(expr, cst.Tuple): return cst.Tuple([])
282
+ if isinstance(expr, cst.Set): return cst.Set([])
283
+ if isinstance(expr, cst.Dict): return cst.Dict([])
284
+ if isinstance(expr, (cst.ConcatenatedString, cst.FormattedString)):
285
+ return cst.SimpleString("''")
286
+ return cst.Name("PLACEHOLDER")
287
+
288
+
289
+ def _next_indexed_name(prefix: str, taken: set[int]) -> str:
290
+ n = 0
291
+ while n in taken:
292
+ n += 1
293
+ return f"{prefix}_{n}"
294
+
295
+
296
+ def _collect_taken_indices_from_names(names: list[str], prefix: str) -> set[int]:
297
+ pat = re.compile(rf"^{re.escape(prefix)}_(\d+)$")
298
+ out = set()
299
+ for s in names:
300
+ m = pat.match(s)
301
+ if m:
302
+ out.add(int(m.group(1)))
303
+ return out
304
+
305
+
306
+ def _last_arg_value(args_mapping) -> cst.CSTNode | None:
307
+ last_val = None
308
+ for v in args_mapping.values():
309
+ n = v.node if hasattr(v, "node") else v
310
+ if isinstance(n, cst.Arg):
311
+ last_val = n.value
312
+ return last_val
313
+
314
+
315
+ # ==============================
316
+ # Error state
317
+ # ==============================
318
+
319
+ @dataclasses.dataclass
320
+ class ErrorState:
321
+ message: str
322
+ field: str | None = None
323
+ detail: str | None = None
324
+
325
+
326
+ # ==============================
327
+ # DictProxy for sequence fields
328
+ # ==============================
329
+
330
+ class CSTDictProxy(dict):
331
+ """
332
+ Ordered, dict-like proxy for sequence fields. Keys are stable subtree-hash IDs.
333
+ Any mutation bubbles dirty to the parent CSTProxy.
334
+ """
335
+
336
+ def reorder(self, keys: list[str]) -> None:
337
+ """
338
+ Reorder this mapping without changing values.
339
+ Unknown keys are ignored; missing existing keys are appended in original order.
340
+ """
341
+ # Snapshot of in in current order
342
+ current = dict(self) # plain dict: preserves order since 3.7+
343
+ new = {}
344
+
345
+ # First, take keys in the requested order
346
+ for k in keys:
347
+ if k in current:
348
+ new[k] = current.pop(k)
349
+
350
+ # Then append any leftovers in their original relative order
351
+ for k, v in current.items():
352
+ new[k] = v
353
+
354
+ # Apply atomically via base methods (avoid double _touch)
355
+ super().clear()
356
+ super().update(new)
357
+ self._touch()
358
+
359
+ def __init__(self, items_kv, parent: "CSTProxy", field_name: str):
360
+ super().__init__()
361
+ self._parent = parent
362
+ self._field = field_name
363
+ for k, v in items_kv:
364
+ w = cst_wrap(v)
365
+ if isinstance(w, CSTProxy):
366
+ w._set_parent(parent, field_name)
367
+ super().__setitem__(k, w)
368
+
369
+ # @property
370
+ # def unique_id(self):
371
+ # return str(self.id)
372
+
373
+ # --- helpers ---
374
+ def _touch(self):
375
+ self._parent._mark_dirty_up()
376
+
377
+ # --- mutators ---
378
+ def __setitem__(self, k, v):
379
+ w = cst_wrap(v)
380
+ if isinstance(w, CSTProxy):
381
+ w._set_parent(self._parent, self._field)
382
+ super().__setitem__(k, w)
383
+ self._touch()
384
+
385
+ def __delitem__(self, k):
386
+ super().__delitem__(k)
387
+ self._touch()
388
+
389
+ def clear(self):
390
+ super().clear()
391
+ self._touch()
392
+
393
+ def pop(self, k, *default):
394
+ val = super().pop(k, *default) if default else super().pop(k)
395
+ self._touch()
396
+ return val
397
+
398
+ def popitem(self):
399
+ kv = super().popitem()
400
+ self._touch()
401
+ return kv
402
+
403
+ def setdefault(self, k, default=None):
404
+ if k in self:
405
+ return super().get(k)
406
+ w = cst_wrap(default)
407
+ if isinstance(w, CSTProxy):
408
+ w._set_parent(self._parent, self._field)
409
+ super().__setitem__(k, w)
410
+ self._touch()
411
+ return w
412
+
413
+ def update(self, other=None, /, **kw):
414
+ if other is None:
415
+ other = {}
416
+ it = other.items() if hasattr(other, "items") else other
417
+ changed = False
418
+ for k, v in it:
419
+ w = cst_wrap(v)
420
+ if isinstance(w, CSTProxy):
421
+ w._set_parent(self._parent, self._field)
422
+ super().__setitem__(k, w);
423
+ changed = True
424
+ for k, v in kw.items():
425
+ w = cst_wrap(v)
426
+ if isinstance(w, CSTProxy):
427
+ w._set_parent(self._parent, self._field)
428
+ super().__setitem__(k, w);
429
+ changed = True
430
+ if changed:
431
+ self._touch()
432
+
433
+ def __ior__(self, other):
434
+ self.update(other)
435
+ return self
436
+
437
+ # --- ordering helpers ---
438
+ def insert_before(self, key_to_insert, before_key):
439
+ if key_to_insert not in self or before_key not in self:
440
+ return
441
+ val = super().pop(key_to_insert)
442
+ new = {}
443
+ for k, v in self.items():
444
+ if k == before_key:
445
+ new[key_to_insert] = val
446
+ new[k] = v
447
+ super().clear();
448
+ super().update(new);
449
+ self._touch()
450
+
451
+ def insert_after(self, key_to_insert, after_key):
452
+ if key_to_insert not in self or after_key not in self:
453
+ return
454
+ val = super().pop(key_to_insert)
455
+ new = {}
456
+ for k, v in self.items():
457
+ new[k] = v
458
+ if k == after_key:
459
+ new[key_to_insert] = val
460
+ super().clear();
461
+ super().update(new);
462
+ self._touch()
463
+
464
+ # --- auto append that infers kind/value from context ---
465
+ def append_to(self, value=None, *, factory=None, key: str | None = None) -> str:
466
+ """
467
+ Append an element to the end of this dict-backed sequence.
468
+
469
+ Context-aware defaults:
470
+ - Call.args:
471
+ * infer positional/keyword legality from existing args
472
+ * infer default value "shape" from last arg's value (e.g., int->0, str->'')
473
+ * keyword names: arg_0, arg_1, ...
474
+ * keyword '=' tight formatting
475
+ - Parameters.{params,posonly_params,kwonly_params}: param_0, param_1, ...
476
+ - Dict.elements: 'key_0', 'key_1', ...
477
+ - List/Tuple.elements: Element(Name('arg_N'))
478
+ - Module/IndentedBlock.body: Pass()
479
+ - Fallback: type-shaped placeholder from hint
480
+ """
481
+ parent_px = self._parent
482
+ parent_node = object.__getattribute__(parent_px, "_node")
483
+ orig_seq = getattr(parent_node, self._field)
484
+ hint_elem = _first_hint_elem(orig_seq)
485
+
486
+ if value is None and not callable(factory):
487
+ # ----------- Call.args: ----------
488
+ if isinstance(parent_node, cst.Call) and self._field == "args":
489
+ any_keyword = False
490
+ keyword_names = []
491
+ last_arg = None
492
+ last_is_positional = False
493
+ for v in self.values():
494
+ n = v.node if hasattr(v, "node") else v
495
+ if isinstance(n, cst.Arg):
496
+ last_arg = n
497
+ if n.keyword:
498
+ any_keyword = True
499
+ if isinstance(n.keyword, cst.Name):
500
+ keyword_names.append(n.keyword.value)
501
+ if isinstance(last_arg, cst.Arg):
502
+ last_is_positional = (last_arg.keyword is None)
503
+
504
+ exemplar_val = _last_arg_value(self) # last existing arg value
505
+ inferred_default = (
506
+ _placeholder_like(exemplar_val)
507
+ if exemplar_val is not None
508
+ else cst.Name("PLACEHOLDER")
509
+ )
510
+
511
+ if any_keyword or not last_is_positional:
512
+ taken = _collect_taken_indices_from_names(keyword_names, "arg")
513
+ name = _next_indexed_name("arg", taken)
514
+ value = cst.Arg(keyword=cst.Name(name), equal=TIGHT_EQ, value=inferred_default)
515
+ else:
516
+ value = cst.Arg(value=inferred_default)
517
+
518
+ # ----------- Parameters -----------
519
+ elif isinstance(parent_node, cst.Parameters) and self._field in {"params", "posonly_params",
520
+ "kwonly_params"}:
521
+ existing = []
522
+ for v in self.values():
523
+ n = v.node if hasattr(v, "node") else v
524
+ if isinstance(n, cst.Param) and isinstance(n.name, cst.Name):
525
+ existing.append(n.name.value)
526
+ prefix = "param" # you can split: po/kw/param if desired
527
+ taken = _collect_taken_indices_from_names(existing, prefix)
528
+ name = _next_indexed_name(prefix, taken)
529
+ value = cst.Param(name=cst.Name(name))
530
+
531
+ # ----------- Dict elements -----------
532
+ elif isinstance(parent_node, cst.Dict) and self._field == "elements":
533
+ existing = []
534
+ for v in self.values():
535
+ n = v.node if hasattr(v, "node") else v
536
+ if isinstance(n, cst.DictElement) and isinstance(n.key, cst.SimpleString):
537
+ try:
538
+ existing.append(eval(n.key.value))
539
+ except Exception:
540
+ pass
541
+ taken = _collect_taken_indices_from_names(existing, "key")
542
+ key_name = _next_indexed_name("key", taken)
543
+ value = cst.DictElement(key=cst.SimpleString(repr(key_name)),
544
+ value=cst.Name("PLACEHOLDER"))
545
+
546
+ # ----------- List/Tuple elements -----------
547
+ elif isinstance(parent_node, (cst.List, cst.Tuple)) and self._field == "elements":
548
+ existing = []
549
+ for v in self.values():
550
+ n = v.node if hasattr(v, "node") else v
551
+ if isinstance(n, cst.Element) and isinstance(n.value, cst.Name):
552
+ existing.append(n.value.value)
553
+ taken = _collect_taken_indices_from_names(existing, "arg")
554
+ name = _next_indexed_name("arg", taken)
555
+ value = cst.Element(value=cst.Name(name))
556
+
557
+ # ----------- Bodies & fallback -----------
558
+ elif isinstance(parent_node, (cst.Module, cst.IndentedBlock)) and self._field == "body":
559
+ # Build CONSTANT_N="default_val" as a full statement line (renders with newline)
560
+ next_idx = _next_constant_index_in_body(self)
561
+ const_name = f"CONSTANT_{next_idx}"
562
+ value = _make_constant_assignment_line(const_name)
563
+
564
+ else:
565
+ value = _default_placeholder_for_hint(hint_elem)
566
+
567
+ if value is None and callable(factory):
568
+ value = factory(hint_elem)
569
+
570
+ # wrap & wire
571
+ w = cst_wrap(value)
572
+ if isinstance(w, CSTProxy):
573
+ w._set_parent(parent_px, self._field)
574
+
575
+ # key: use hash (dedupe)
576
+ if key is None:
577
+ base = _hash_key_for_elem(w)
578
+ key = _dedupe_key(base, self)
579
+
580
+ self[key] = w # go through override so _touch() runs # __setitem__ bubbles dirty
581
+ return key
582
+
583
+
584
+ # ==============================
585
+ # CSTProxy with error handling
586
+ # ==============================
587
+
588
+ @exclude(["star", "header", "lpar", "rpar", "footer", "comma", "lbracket", "rbracket", "default_newline", "default_indent", "encoding", "has_trailing_newline"])
589
+ class CSTProxy:
590
+
591
+ def __init__(self, node: cst.CSTNode):
592
+ object.__setattr__(self, "_node", node)
593
+ object.__setattr__(self, "_dirty", False)
594
+ object.__setattr__(self, "_parent", None)
595
+ object.__setattr__(self, "_parent_field", None)
596
+ object.__setattr__(self, "_error", None) # ErrorState | None
597
+ self._refresh_fields_from(node)
598
+
599
+ # class spoofing so type-based routing works
600
+ @property
601
+ def __class__(self):
602
+ return type(self._node)
603
+
604
+ # make hashable
605
+ def __eq__(self, other):
606
+ if not isinstance(other, CSTProxy):
607
+ return False
608
+ return self.node == other.node
609
+
610
+
611
+ # ---- error handling ----
612
+ @property
613
+ def has_error(self) -> bool:
614
+ return object.__getattribute__(self, "_error") is not None
615
+
616
+ @property
617
+ def error(self) -> ErrorState | None:
618
+ return object.__getattribute__(self, "_error")
619
+
620
+ def clear_error(self):
621
+ object.__setattr__(self, "_error", None)
622
+
623
+ # materialize node safely
624
+ @property
625
+ def node(self) -> cst.CSTNode:
626
+ if self._dirty:
627
+ self._rebuild()
628
+ return object.__getattribute__(self, "_node")
629
+
630
+ # To string
631
+ # def __str__(self) -> str:
632
+ # # fingerprint
633
+ # finger_print = _hash_key_for_elem(self.node)
634
+ # return str(finger_print)
635
+ # def __hash__(self):
636
+ # return hash(_hash_node_struct(self.node))
637
+
638
+ def flush(self):
639
+ if self._dirty:
640
+ self._rebuild()
641
+
642
+ # --- parent plumbing & dirtiness bubbling ---
643
+ def _set_parent(self, parent: "CSTProxy|None", field_name: str | None):
644
+ object.__setattr__(self, "_parent", parent)
645
+ object.__setattr__(self, "_parent_field", field_name)
646
+
647
+ def _mark_dirty_up(self, depth=0):
648
+ object.__setattr__(self, "_dirty", True)
649
+ p = object.__getattribute__(self, "_parent")
650
+ if depth > 100:
651
+ return
652
+
653
+ if p is not None:
654
+ p._mark_dirty_up(depth + 1)
655
+
656
+ # --- field reflection & rebuild ---
657
+ def _refresh_fields_from(self, node):
658
+ if not dataclasses.is_dataclass(node):
659
+ object.__setattr__(self, "_field_names", ())
660
+ return
661
+ fns = tuple(f.name for f in dataclasses.fields(node))
662
+ object.__setattr__(self, "_field_names", fns)
663
+
664
+ keep = {k: v for k, v in self.__dict__.items() if k not in fns}
665
+ self.__dict__.clear()
666
+ self.__dict__.update(keep)
667
+
668
+ for name in fns:
669
+ val = getattr(node, name)
670
+ if isinstance(val, collections.abc.Sequence) and not isinstance(val, str):
671
+ self.__dict__[name] = _make_sequence_proxy(self, name, list(val))
672
+ else:
673
+ child = cst_wrap(val)
674
+ if isinstance(child, CSTProxy):
675
+ child._set_parent(self, name)
676
+ self.__dict__[name] = child
677
+
678
+ def _rebuild(self):
679
+ node = object.__getattribute__(self, "_node")
680
+ try:
681
+ kwargs = {}
682
+ for name in self._field_names:
683
+ current = self.__dict__[name]
684
+ original = getattr(node, name)
685
+ kwargs[name] = _to_libcst(current, original)
686
+ new_node = node.with_changes(**kwargs)
687
+ except Exception as e:
688
+ object.__setattr__(self, "_error", ErrorState(
689
+ message=f"rebuild failed on {type(node).__name__}",
690
+ field=None,
691
+ detail=repr(e),
692
+ ))
693
+ # keep old node; leave _ = True so caller/UI can decide to auto-resolve
694
+ return
695
+
696
+ object.__setattr__(self, "_node", new_node)
697
+ object.__setattr__(self, "_dirty", False)
698
+ object.__setattr__(self, "_error", None)
699
+ self._refresh_fields_from(new_node)
700
+
701
+ def auto_resolve_error(self) -> bool:
702
+ """
703
+ Attempt to repair bad field shapes in-place.
704
+ Returns True if a subsequent rebuild succeeds.
705
+ """
706
+ node = object.__getattribute__(self, "_node")
707
+ repaired_any = False
708
+
709
+ for name in self._field_names:
710
+ original = getattr(node, name)
711
+ current = self.__dict__[name]
712
+
713
+ # 1) Sequence field as mapping (CSTDictProxy)
714
+ if isinstance(original, collections.abc.Sequence) and not isinstance(original, str):
715
+ hint = _first_hint_elem(original)
716
+
717
+ # If not a mapping, coerce into mapping
718
+ if not isinstance(current, collections.abc.Mapping):
719
+ seq = current if isinstance(current, collections.abc.Sequence) else [current]
720
+ fixed = []
721
+ for elem in seq:
722
+ if isinstance(elem, CSTProxy):
723
+ fixed.append(elem.node)
724
+ elif isinstance(elem, cst.CSTNode):
725
+ fixed.append(elem)
726
+ else:
727
+ fixed.append(_autobox_expr(elem, hint) if hint else _default_placeholder_for_hint(hint))
728
+ self.__dict__[name] = _make_sequence_proxy(self, name, fixed)
729
+ repaired_any = True
730
+ else:
731
+ # validate each mapping value
732
+ mapping = current
733
+ new_values = []
734
+ for _, v in mapping.items():
735
+ n = v.node if isinstance(v, CSTProxy) else v
736
+ if isinstance(n, cst.CSTNode):
737
+ new_values.append(n)
738
+ else:
739
+ try:
740
+ new_values.append(
741
+ _autobox_expr(n, hint) if hint else _default_placeholder_for_hint(hint))
742
+ except Exception:
743
+ new_values.append(_default_placeholder_for_hint(hint))
744
+ repaired_any = True
745
+
746
+ # Special legality for Call.args: no positional after keyword
747
+ if isinstance(node, cst.Call) and name == "args":
748
+ vals = new_values
749
+ # normalize order legality
750
+ seen_kw = False
751
+ kw_names = [a.keyword.value for a in vals if
752
+ isinstance(a, cst.Arg) and isinstance(a.keyword, cst.Name)]
753
+ taken = _collect_taken_indices_from_names(kw_names, "arg")
754
+
755
+ coerced = []
756
+ for a in vals:
757
+ if isinstance(a, cst.Arg):
758
+ if a.keyword is not None:
759
+ seen_kw = True
760
+ coerced.append(a)
761
+ else:
762
+ if seen_kw:
763
+ idx_name = _next_indexed_name("arg", taken)
764
+ taken.add(int(idx_name.split("_")[-1]))
765
+ coerced.append(
766
+ cst.Arg(keyword=cst.Name(idx_name), equal=TIGHT_EQ, value=a.value))
767
+ else:
768
+ coerced.append(a)
769
+ else:
770
+ coerced.append(cst.Arg(value=cst.Name("PLACEHOLDER")))
771
+ new_values = coerced
772
+
773
+ self.__dict__[name] = _make_sequence_proxy(self, name, new_values)
774
+ repaired_any = True
775
+
776
+ # 2) Leaf string field
777
+ elif isinstance(original, str):
778
+ try:
779
+ self.__dict__[name] = _lexeme(current)
780
+ except Exception:
781
+ self.__dict__[name] = _lexeme("")
782
+ repaired_any = True
783
+
784
+ # 3) Node-typed field
785
+ elif isinstance(original, cst.CSTNode):
786
+ v = current
787
+ n = v.node if isinstance(v, CSTProxy) else v
788
+ if not isinstance(n, cst.CSTNode):
789
+ try:
790
+ self.__dict__[name] = cst_wrap(_autobox_expr(n, original))
791
+ except Exception:
792
+ self.__dict__[name] = cst_wrap(_placeholder_like(original))
793
+ repaired_any = True
794
+
795
+ # 4) Other scalars: rely on _to_libcst during rebuild
796
+
797
+ pre = self._error
798
+ self._rebuild()
799
+ return (not self.has_error) and (repaired_any or pre is not None)
800
+
801
+ # generic append API working on any sequence proxy this proxy owns
802
+ def append_to(self,
803
+ field_name: str,
804
+ value=None,
805
+ *,
806
+ factory=None,
807
+ key: str | None = None) -> str:
808
+ seq = self.__dict__.get(field_name, None)
809
+ if not isinstance(seq, CSTDictProxy):
810
+ raise TypeError(f"Field '{field_name}' is not a sequence (CSTDictProxy); got {type(seq).__name__}")
811
+
812
+ # Decide placeholder if value=None
813
+ orig_seq = getattr(object.__getattribute__(self, "_node"), field_name)
814
+ hint_elem = _first_hint_elem(orig_seq)
815
+ if value is None:
816
+ if callable(factory):
817
+ value = factory(hint_elem)
818
+ else:
819
+ # Defer to the dict proxy's context-aware append_to (smarts live there)
820
+ return seq.append_to(value=None, factory=None, key=key)
821
+
822
+ # Wrap & ded parent if needed, then append via seq
823
+ w = cst_wrap(value)
824
+ if isinstance(w, CSTProxy):
825
+ w._set_parent(self, field_name)
826
+ if key is None:
827
+ base = _hash_key_for_elem(w)
828
+ key = _dedupe_key(base, seq)
829
+ seq[key] = w # bubbles dirty
830
+ return key
831
+
832
+ # Hash & repr
833
+ def __hash__(self):
834
+ return hash(_fingerprint_struct(self.node))
835
+
836
+ def __setattr__(self, name, value):
837
+ if name in getattr(self, "_field_names", ()):
838
+ w = cst_wrap(value)
839
+ if isinstance(w, CSTProxy):
840
+ w._set_parent(self, name)
841
+ self.__dict__[name] = w
842
+ self._mark_dirty_up()
843
+ else:
844
+ self.__dict__[name] = value
845
+
846
+ def __repr__(self):
847
+ return f"<{type(self._node).__name__}Proxy>"
848
+
849
+
850
+ # ==============================
851
+ # Sequence proxy factory
852
+ # ==============================
853
+
854
+ def _make_sequence_proxy(parent_px: CSTProxy, field_name: str, seq_values: list) -> CSTDictProxy:
855
+ used = {}
856
+ kv = []
857
+ for elem in seq_values:
858
+ base = _hash_key_for_elem(elem)
859
+ key = _dedupe_key(base, used)
860
+ used[key] = True
861
+ kv.append((key, elem))
862
+ return CSTDictProxy(kv, parent_px, field_name)
863
+
864
+
865
+ # ==============================
866
+ # Public wrapper
867
+ # ==============================
868
+
869
+ def cst_wrap(obj):
870
+ if isinstance(obj, CSTProxy): return obj
871
+ if isinstance(obj, cst.CSTNode) and dataclasses.is_dataclass(obj):
872
+ return CSTProxy(obj)
873
+ if isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str):
874
+ # bare sequences outside of fields become simple wrapped lists
875
+ return [cst_wrap(x) for x in obj]
876
+ return obj