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,39 @@
1
+ """Keep saved objects readable and re-saveable after their class is deleted."""
2
+ import importlib
3
+ import logging
4
+ import types
5
+ from functools import lru_cache
6
+
7
+ from meltygui.core.conversion.dict_conversion import DictConversion
8
+
9
+
10
+ @lru_cache(maxsize=None)
11
+ def missing_saved_class(class_path):
12
+ logging.getLogger("load_save_v2").warning(
13
+ "Saved class %s is unavailable; preserving its state in a placeholder", class_path)
14
+ return type(class_path.rsplit(".", 1)[-1], (DictConversion,), {
15
+ "__missing_saved_path__": class_path,
16
+ "default_instance": None,
17
+ })
18
+
19
+
20
+ def restore_saved_class(class_path):
21
+ # The unpickler's find_class also resolves function globals. A missing
22
+ # renderer can therefore have been preserved as this placeholder too.
23
+ # Recover its original function when available, rather than keeping a
24
+ # class that crashes when the renderer is called with view arguments.
25
+ from meltygui.core.module_names import canonical_name
26
+ class_path = canonical_name(class_path)
27
+ parts = class_path.split(".")
28
+ for boundary in range(len(parts) - 1, 0, -1):
29
+ try:
30
+ value = importlib.import_module(".".join(parts[:boundary]))
31
+ for part in parts[boundary:]:
32
+ value = getattr(value, part)
33
+ except Exception:
34
+ # A syntax error or import-time exception in today's code says
35
+ # nothing about the validity of the saved object's type.
36
+ continue
37
+ if isinstance(value, (type, types.FunctionType, types.BuiltinFunctionType)):
38
+ return value
39
+ return missing_saved_class(class_path)
@@ -0,0 +1,606 @@
1
+ """
2
+ Automatic converter chaining via shortest-path search.
3
+
4
+ Given a registry of direct A→B converters, this module finds multi-hop
5
+ paths (A→X→…→B) and composes them into a single callable. The shortest
6
+ path (fewest hops) is always chosen to minimise data loss.
7
+
8
+ Usage:
9
+ result = convert(my_obj, str, registry=Melty)
10
+ chain = find_chain(MyType, str, registry=Melty) # returns the composed fn
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import types
16
+ from collections import deque
17
+ from enum import Enum
18
+ from typing import Any, Callable, TypeVar
19
+
20
+ from meltygui.core.windowing.glfw_utils import print_stack_trace
21
+
22
+ T = TypeVar("T")
23
+
24
+ # Sentinel cached for type pairs with no conversion path,
25
+ # so we don't re-run BFS every frame.
26
+ _NO_PATH = object()
27
+ NO_VALUE = object()
28
+
29
+
30
+ class PendingState(Enum):
31
+ BACKGROUND = "background"
32
+ CONFIRM = "blocking"
33
+ BROKEN_PATH = "broken_path"
34
+ ERROR = "error"
35
+
36
+
37
+ # ── Pending wrapper ──────────────────────────────────────────────────────────
38
+
39
+ class Pending:
40
+ """Wraps a converter result that hasn't been fully applied yet.
41
+
42
+ Converters that support deferred execution (file I/O, network calls,
43
+ etc.) return Pending(partial_result) when apply=False. The UI can
44
+ inspect pending.wrapped for metadata, then call convert() again with
45
+ apply=True when ready.
46
+
47
+ When a Pending is encountered mid-chain, the chain stops immediately
48
+ and returns the Pending as-is. The caller can inspect the partial
49
+ result and re-run with apply=True to complete.
50
+
51
+ Usage:
52
+ result = convert(path, dict, registry=R, apply=False)
53
+ if isinstance(result, Pending):
54
+ print(result.wrapped["name"]) # metadata available
55
+ # later:
56
+ full = convert(path, dict, registry=R, apply=True)
57
+ """
58
+ __slots__ = ("wrapped", "status", "state", "originated")
59
+
60
+ def __init__(self, originated: types.FunctionType | type, wrapped=NO_VALUE, status="pending", state=PendingState.CONFIRM):
61
+ self.wrapped = wrapped
62
+ self.status = status
63
+ self.state = state
64
+ self.originated = originated
65
+
66
+ def __repr__(self):
67
+ return f"Pending({self.wrapped!r})"
68
+
69
+ def __bool__(self):
70
+ return self.wrapped is not None
71
+
72
+
73
+ # ── Apply introspection ──────────────────────────────────────────────────────
74
+
75
+ _accepts_apply_cache: dict[Callable, bool] = {}
76
+
77
+
78
+ def _accepts_apply(fn: Callable) -> bool:
79
+ """Check (cached) whether a converter function accepts an 'apply' kwarg."""
80
+ return True
81
+ # cached = _accepts_apply_cache.get(fn)
82
+ # if cached is not None:
83
+ # return cached
84
+ # import inspect
85
+ # try:
86
+ # sig = inspect.signature(fn)
87
+ # result = "apply" in sig.parameters
88
+ # except (ValueError, TypeError):
89
+ # result = False
90
+ # _accepts_apply_cache[fn] = result
91
+ # return result
92
+ #
93
+
94
+
95
+
96
+
97
+ def _is_path_type(item) -> bool:
98
+ """True if item is a type (waypoint in a path), False if callable (edge)."""
99
+ return isinstance(item, type)
100
+
101
+
102
+ def _find_converter(converters: dict, from_type: type, to_type: type) -> Callable | None:
103
+ """Look up a converter with MRO fallback."""
104
+ fn = converters.get((from_type, to_type))
105
+ if fn is None and hasattr(from_type, "__mro__"):
106
+ for ancestor in from_type.__mro__[1:]:
107
+ fn = converters.get((ancestor, to_type))
108
+ if fn is not None:
109
+ break
110
+ return fn
111
+
112
+
113
+ def convert(value: Any, target: type[T] = None, *, registry, path: list | None = None, apply: bool = False, cache_id=None) -> T:
114
+ """Convert *value* to *target* type using the registry.
115
+
116
+ If *path* is provided, follows it exactly. Path items can be:
117
+ - types (waypoints): looked up in the registry
118
+ - callables (edges): called directly on the current value
119
+
120
+ Examples:
121
+ # All types — registry lookup between each consecutive pair
122
+ convert(obj, dict, registry=R, path=[str, cst.Module, dict])
123
+
124
+ # All functions — called in sequence
125
+ convert(obj, registry=R, path=[my_parser, my_formatter])
126
+
127
+ # Mixed — types trigger registry lookup, functions called directly
128
+ convert(obj, dict, registry=R, path=[str, my_custom_parser, dict])
129
+
130
+ If *apply* is False (default), converters that support deferred
131
+ execution may return Pending(partial_result). Pass apply=True
132
+ to force full execution (e.g. file writes, network calls).
133
+
134
+ If any link in the chain returns Pending, the chain stops
135
+ immediately and returns that Pending. The caller can inspect
136
+ the partial result and re-run with apply=True to complete.
137
+
138
+ Otherwise, tries a direct converter first, then BFS for a multi-hop
139
+ path through intermediate types. Raises TypeError if no path exists.
140
+ """
141
+ if target is not None:
142
+ if isinstance(value, target):
143
+ return value # type: ignore[return-value]
144
+
145
+ if cache_id is None:
146
+ from meltygui.core.melty import Melty
147
+ cache_id = Melty.unique_stack[-1] if Melty.unique_stack else None
148
+
149
+ if path is not None:
150
+ return _run_explicit_path(value, target, cache_id=cache_id, path=path, registry=registry, apply=apply)
151
+
152
+
153
+
154
+ # Value-aware shortcut: dicts produced by object_to_dict carry a
155
+ # __meta__ key with the original class info. The graph can't know
156
+ # that dict→object will produce the right type at runtime, so we
157
+ # try it here before falling into BFS.
158
+ converters = getattr(registry, "_converters", {})
159
+ dict_to_obj = converters.get((dict, object))
160
+
161
+ if dict_to_obj is not None:
162
+ meta_dict = None
163
+
164
+ if isinstance(value, dict) and "__meta__" in value:
165
+ meta_dict = value
166
+ else:
167
+ # The value might be one hop away from a __meta__ dict
168
+ # (e.g. a list of pairs that returned from dict_to_list).
169
+ # Try converting to dict first and check.
170
+ try:
171
+ to_dict_fn = find_chain(type(value), dict, registry=registry)
172
+ candidate = to_dict_fn(value)
173
+ if isinstance(candidate, dict) and "__meta__" in candidate:
174
+ meta_dict = candidate
175
+ except (TypeError, Exception):
176
+ pass
177
+
178
+ if meta_dict is not None:
179
+ try:
180
+ result = dict_to_obj(meta_dict)
181
+ if isinstance(result, target):
182
+ return result # type: ignore[return-value]
183
+ # Result isn't the target yet, but it's more specific
184
+ # than `object` - try one more conversion from here.
185
+ if not isinstance(result, type(value)): # avoid loops
186
+ try:
187
+ final = convert(result, target, registry=registry)
188
+ return final
189
+ except (TypeError, ValueError):
190
+ pass
191
+ except Exception:
192
+ pass
193
+
194
+ chain_fn = find_chain(type(value), target, registry=registry)
195
+
196
+ # If we need to pass apply, decompose the chain and walk step by step
197
+ steps = getattr(chain_fn, "__converter_chain__", None)
198
+ if steps is not None:
199
+ return _run_chain_steps(value, steps,cache_id=cache_id, apply=apply)
200
+ # Direct call (single hop)
201
+ if getattr(chain_fn, '_accepts_cache_id', False):
202
+ return chain_fn(value, cache_id=cache_id, apply=apply)
203
+ return chain_fn(value)
204
+
205
+
206
+ def _run_explicit_path(value: Any, target: type, *, path: list, registry, apply: any, cache_id=None) -> Any:
207
+ """Follow an explicit path of types and/or converter functions.
208
+
209
+ Each path item is either:
210
+ - a type: a waypoint — if the value isn't already this type,
211
+ look up a converter from type(value) → this type
212
+ - a callable: an explicit edge — called directly on the value
213
+
214
+ Examples:
215
+ path=[str, cst.Module, dict]
216
+ → ensure value is str, convert str→Module, convert Module→dict
217
+
218
+ path=[my_parser, my_formatter]
219
+ → call my_parser(value), then my_formatter(result)
220
+
221
+ path=[str, my_custom_parser, dict]
222
+ → ensure value is str, call my_custom_parser, convert →dict
223
+
224
+ If any step returns Pending, the chain stops and returns it.
225
+ Raises TypeError if a type→type edge is missing from the registry.
226
+ """
227
+ converters = getattr(registry, "_converters", {})
228
+ result = value
229
+ last_fn = None
230
+ fn_name = "N/A"
231
+
232
+ for i, item in enumerate(path):
233
+
234
+
235
+ if _is_path_type(item):
236
+ # Type waypoint - skip if already there, otherwise look up
237
+ if isinstance(result, item):
238
+ continue
239
+
240
+ fn = _find_converter(converters, type(result), item)
241
+ if fn is None:
242
+ # print(last_fn.__name__ if last_fn else "N/A", item.__name__)
243
+ # raise TypeError(
244
+ # f"No converter registered for {type(result).__name__!r} → "
245
+ # f"{item.__name__!r} (step {i + 1} of explicit path)"
246
+ # )
247
+ print_stack_trace(watch=["path", "target", "result", "item", "apply", "value", "cache_id", "last_fn", "fn_name"])
248
+ print(f"Missing converter for {type(result).__name__!r} -> {item.__name__!r} at step {i + 1} of explicit path")
249
+
250
+ return Pending(originated=fn, state=PendingState.BROKEN_PATH, wrapped=item, status=f"Missing converter for {type(result).__name__!r} -> {item.__name__!r} at step {i + 1} of explicit path")
251
+
252
+ # if apply is not False and apply is not None:
253
+ # if fn.__name__ == apply.__name__:
254
+ # do_apply = True
255
+ # print(f"Applying at step {i + 1} of explicit path via {getattr(item, '__name__', repr(item))}")
256
+ # else:
257
+ # do_apply = False
258
+ # print(
259
+ # f"Not applying at step {i + 1} of explicit path: apply={apply}, item={fn} ")
260
+ # else:
261
+ # do_apply = False
262
+ last_fn = fn
263
+ if getattr(fn, '_accepts_cache_id', False):
264
+ result = fn(result, cache_id=cache_id, apply=apply)
265
+ else:
266
+ result = fn(result)
267
+ else:
268
+ # Callable edge - skip if already the target type
269
+ type_info = getattr(registry, "_converter_to_type", {}).get(item)
270
+ if type_info is not None:
271
+ _, to_type = type_info
272
+ if isinstance(result, to_type):
273
+ result = value
274
+ continue
275
+
276
+
277
+ if getattr(item, '_accepts_cache_id', False):
278
+ result = item(result, cache_id=cache_id, apply=apply)
279
+ else:
280
+ result = item(result)
281
+
282
+ if isinstance(result, Pending):
283
+ return result
284
+
285
+ return result
286
+
287
+
288
+ def _run_chain_steps(value: Any, steps: list[Callable], *, apply: bool, cache_id=None) -> Any:
289
+ """Walk a list of converter functions, passing apply and handling Pending.
290
+
291
+ If any step returns Pending, stops immediately and returns it.
292
+ """
293
+ result = value
294
+
295
+ for fn in steps:
296
+ if getattr(fn, '_accepts_cache_id', False):
297
+ result = fn(result, cache_id=cache_id, apply=apply)
298
+ else:
299
+ result = fn(result)
300
+ if isinstance(result, Pending):
301
+ return result
302
+
303
+ return result
304
+
305
+
306
+ def find_chain(source: type, target: type, *, registry) -> Callable:
307
+ """Return a single callable that converts *source* → *target*.
308
+
309
+ Builds a directed graph from every converter in the registry, runs
310
+ BFS to find the shortest path, composes the converters along that
311
+ path into a single function, and caches the result.
312
+
313
+ The graph and all discovered chains are cached on the registry itself.
314
+ Call invalidate_cache() after registering new converters at runtime.
315
+
316
+ Raises TypeError if no path can be found.
317
+ """
318
+ # ── Cache lookup ─────────────────────────────────────────────────
319
+
320
+ if not hasattr(registry, "_chain_cache"):
321
+ registry._chain_cache = {}
322
+ key = (source, target)
323
+ if key in registry._chain_cache:
324
+ cached = registry._chain_cache[key]
325
+ if cached is _NO_PATH:
326
+ raise TypeError(
327
+ f"No conversion path from {source.__name__!r} to {target.__name__!r} (cached)"
328
+ )
329
+ return cached
330
+
331
+ converters = getattr(registry, "_converters", {})
332
+
333
+ # ── Direct lookup with MRO fallback ──────────────────────────────
334
+ #
335
+ # Check for an exact (source, target) converter first, then walk the
336
+ # source's MRO so that e.g. ValueError finds Exception→dict.
337
+
338
+ direct = converters.get((source, target))
339
+ if direct is None and hasattr(source, "__mro__"):
340
+ for ancestor in source.__mro__[1:]:
341
+ direct = converters.get((ancestor, target))
342
+ if direct is not None:
343
+ break
344
+
345
+ if direct is not None:
346
+ registry._chain_cache[key] = direct
347
+ return direct
348
+
349
+ # ── Build the type graph ─────────────────────────────────────────
350
+ #
351
+ # Adjacency list built from every registered (from_type, to_type)
352
+ # pair. Cached on the registry so it's only built once.
353
+
354
+ if not hasattr(registry, "_graph_cache") or registry._graph_cache is None:
355
+ graph: dict[type, list[type]] = {}
356
+ for (from_type, to_type) in converters.keys():
357
+ graph.setdefault(from_type, []).append(to_type)
358
+ registry._graph_cache = graph
359
+ graph = registry._graph_cache
360
+
361
+ # ── Find starting types via MRO ───────────────────────────────
362
+ #
363
+ # If source isn't directly in the graph, we can still start from any
364
+ # registered ancestor type. Sort by MRO distance so BFS explores
365
+ # the most specific converters first (ValueError before Exception
366
+ # before object).
367
+
368
+ all_from_types = set(graph.keys())
369
+ start_entries: list[tuple[int, type]] = []
370
+ for rt in all_from_types:
371
+ if rt == source:
372
+ start_entries.append((0, rt))
373
+ elif isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
374
+ try:
375
+ distance = source.__mro__.index(rt)
376
+ except ValueError:
377
+ distance = 999
378
+ start_entries.append((distance, rt))
379
+ start_entries.sort(key=lambda x: x[0])
380
+ start_types = [t for _, t in start_entries] if start_entries else [source]
381
+
382
+ # ── BFS ──────────────────────────────────────────────────────────
383
+ #
384
+ # Standard breadth-first search over the type graph. Each queue
385
+ # entry is a full path (list of types) so we can reconstruct the
386
+ # chain when we hit the target.
387
+
388
+ queue: deque[list[type]] = deque()
389
+ visited: set[type] = {source}
390
+ for st in start_types:
391
+ # When starting from an MRO ancestor, begin the path there
392
+ # directly - the value IS an instance of the ancestor, so no
393
+ # converter is needed for that step.
394
+ queue.append([st])
395
+ visited.add(st)
396
+
397
+ path: list[type] | None = None
398
+ while queue:
399
+ current_path = queue.popleft()
400
+ current = current_path[-1]
401
+
402
+ if current == target or (isinstance(current, type) and issubclass(current, target)):
403
+ path = current_path
404
+ break
405
+
406
+ for neighbour in graph.get(current, []):
407
+ if neighbour not in visited:
408
+ visited.add(neighbour)
409
+ new_path = current_path + [neighbour]
410
+ if neighbour == target:
411
+ path = new_path
412
+ break
413
+ queue.append(new_path)
414
+ if path is not None:
415
+ break
416
+
417
+ if path is None:
418
+ registry._chain_cache[key] = _NO_PATH
419
+ raise TypeError(
420
+ f"No conversion path from {source.__name__!r} to {target.__name__!r}"
421
+ )
422
+
423
+ # ── Collect edge converters ──────────────────────────────────────
424
+
425
+ edge_fns: list[Callable] = []
426
+ for i in range(len(path) - 1):
427
+ step_from, step_to = path[i], path[i + 1]
428
+ fn = converters.get((step_from, step_to))
429
+ if fn is None and hasattr(step_from, "__mro__"):
430
+ for ancestor in step_from.__mro__[1:]:
431
+ fn = converters.get((ancestor, step_to))
432
+ if fn is not None:
433
+ break
434
+ assert fn is not None, f"BFS found edge {step_from} → {step_to} but lookup failed"
435
+ edge_fns.append(fn)
436
+
437
+ # ── Compose into a single callable ───────────────────────────────
438
+
439
+ if len(edge_fns) == 1:
440
+ registry._chain_cache[key] = edge_fns[0]
441
+ return edge_fns[0]
442
+
443
+ fns = list(edge_fns) # bind for closure
444
+
445
+ def composed(value: Any) -> Any:
446
+ result = value
447
+ for fn in fns:
448
+ result = fn(result)
449
+ return result
450
+
451
+ type_names = [source.__name__]
452
+ # If the path starts from an MRO ancestor, show the actual start
453
+ if path[0] != source:
454
+ type_names.append(path[0].__name__)
455
+ for fn in edge_fns:
456
+ ret = getattr(fn, "__annotations__", {}).get("return")
457
+ if isinstance(ret, type):
458
+ type_names.append(ret.__name__)
459
+ elif isinstance(ret, str):
460
+ type_names.append(ret)
461
+ else:
462
+ type_names.append("?")
463
+
464
+ chain_str = " → ".join(type_names)
465
+ composed.__name__ = f"chain[{chain_str}]"
466
+ composed.__qualname__ = composed.__name__
467
+ composed.__doc__ = (
468
+ f"Auto-composed converter: {chain_str}\n"
469
+ f"Hops: {len(edge_fns)}\n"
470
+ f"Steps: {', '.join(getattr(fn, '__name__', '?') for fn in edge_fns)}"
471
+ )
472
+ composed.__converter_chain__ = edge_fns # type: ignore[attr-defined]
473
+ composed.__converter_path__ = chain_str # type: ignore[attr-defined]
474
+
475
+ registry._chain_cache[key] = composed
476
+ return composed
477
+
478
+
479
+ def invalidate_cache(registry) -> None:
480
+ """Call this after registering new converters at runtime to rebuild
481
+ the graph and clear cached chains.
482
+ """
483
+ registry._chain_cache = {}
484
+ registry._graph_cache = None
485
+
486
+
487
+ def invert_path(path: list, *, registry) -> list:
488
+ """Return the reverse of a mixed path, swapping each callable for its inverse.
489
+
490
+ Types are kept as-is (just reversed). Callables are replaced with
491
+ their inverse via registry.converter_flags[fn].
492
+
493
+ Example:
494
+ path = [str, cst.Module, my_custom_fn, dict]
495
+ invert_path(path, registry=Melty)
496
+ → [dict, inverse_of_my_custom_fn, cst.Module, str]
497
+
498
+ Raises KeyError if a callable has no registered inverse.
499
+ """
500
+ flags = getattr(registry, "converter_flags", {})
501
+ result = []
502
+ for item in reversed(path):
503
+ if _is_path_type(item):
504
+ result.append(item)
505
+ else:
506
+ inverse = flags.get(item, {}).get("inverse_of", None)
507
+ if inverse is None:
508
+ raise KeyError(
509
+ f"No inverse registered for {getattr(item, '__name__', repr(item))}. "
510
+ f"Register with @converter(registry=..., inverse_of=...)"
511
+ )
512
+ result.append(inverse)
513
+ return result
514
+
515
+
516
+ def explain_chain(source: type, target: type, *, registry) -> str:
517
+ """Return a human-readable explanation of how source → target would
518
+ be converted, without actually performing the conversion.
519
+ """
520
+ if isinstance(source, type) and issubclass(source, target):
521
+ return f"{source.__name__} is already a subclass of {target.__name__}"
522
+
523
+ try:
524
+ chain_fn = find_chain(source, target, registry=registry)
525
+ except TypeError as e:
526
+ return str(e)
527
+
528
+ path = getattr(chain_fn, "__converter_path__", None)
529
+ steps = getattr(chain_fn, "__converter_chain__", [])
530
+
531
+ lines = [f"Path: {path or '(direct)'}"]
532
+ for i, fn in enumerate(steps, 1):
533
+ lines.append(f" Step {i}: {getattr(fn, '__name__', '?')}")
534
+ return "\n".join(lines)
535
+
536
+
537
+ def all_reachable_from(source: type, *, registry) -> set[type]:
538
+ """Return every type reachable from *source* through any chain of
539
+ converters. Walks the full graph via BFS from source (including
540
+ MRO-matched ancestors) and collects every type visited.
541
+ """
542
+ converters = getattr(registry, "_converters", {})
543
+
544
+ graph: dict[type, list[type]] = {}
545
+ for (from_type, to_type) in converters.keys():
546
+ graph.setdefault(from_type, []).append(to_type)
547
+
548
+ start_types = [source]
549
+ for rt in graph.keys():
550
+ if rt != source and isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
551
+ start_types.append(rt)
552
+
553
+ visited: set[type] = set(start_types)
554
+ visited.add(source)
555
+ queue = deque(start_types)
556
+
557
+ while queue:
558
+ current = queue.popleft()
559
+ for neighbour in graph.get(current, []):
560
+ if neighbour not in visited:
561
+ visited.add(neighbour)
562
+ queue.append(neighbour)
563
+
564
+ visited.discard(source)
565
+ return visited
566
+
567
+
568
+ def all_paths(source: type, target: type, *, registry, max_depth: int = 6) -> list[list[type]]:
569
+ """Return ALL paths from source → target up to max_depth hops,
570
+ sorted shortest-first. Useful for understanding why a particular
571
+ chain was chosen and what alternatives exist.
572
+ """
573
+ converters = getattr(registry, "_converters", {})
574
+
575
+ graph: dict[type, list[type]] = {}
576
+ for (from_type, to_type) in converters.keys():
577
+ graph.setdefault(from_type, []).append(to_type)
578
+
579
+ start_types = [source]
580
+ for rt in graph.keys():
581
+ if rt != source and isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
582
+ start_types.append(rt)
583
+
584
+ results: list[list[type]] = []
585
+
586
+ def dfs(path: list[type], visited: set[type]) -> None:
587
+ if len(path) > max_depth + 1:
588
+ return
589
+ current = path[-1]
590
+ if current == target and len(path) > 1:
591
+ results.append(list(path))
592
+ return
593
+ for neighbour in graph.get(current, []):
594
+ if neighbour not in visited:
595
+ visited.add(neighbour)
596
+ path.append(neighbour)
597
+ dfs(path, visited)
598
+ path.pop()
599
+ visited.discard(neighbour)
600
+
601
+ for st in start_types:
602
+ initial = [source] if st == source else [source, st]
603
+ dfs(initial, set(initial))
604
+
605
+ results.sort(key=len)
606
+ return results