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,1815 @@
1
+ import importlib
2
+ import inspect
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ import time
8
+ import weakref
9
+ from ast import literal_eval
10
+ from copy import copy
11
+ from enum import Enum
12
+ from typing import Any, Dict, Optional, Union, List, Tuple
13
+
14
+
15
+
16
+ def _loaded_class(module, name):
17
+ """A class from an optional heavy module (torch, transformers), or None if that module was
18
+ never imported. Used for isinstance() checks: a value can only be an instance of the class if
19
+ its module is already loaded, so this never triggers the import."""
20
+ mod = sys.modules.get(module)
21
+ return getattr(mod, name, None) if mod is not None else None
22
+
23
+
24
+ from meltygui.state.core_markers import FieldMeta
25
+ from meltygui.core.conversion.dict_conversion_util import ClassUtility
26
+ from meltygui.state.core_enums import generate_id
27
+ from meltygui.state.model_enums import RelaxedEnum
28
+ from meltygui.core.rendering.core_decoration import exclude
29
+ from meltygui.core.rendering.core_decoration import deep_refresh
30
+ from meltygui.core.cache.invalidation_decoration import live
31
+
32
+ _SEGMENT_RE = re.compile(
33
+ r'(?:[^.\[]+|\[[^\]]*\])+') # matches a segment like: attr, attr[0], attr["a.b"][1], [0], ...
34
+ _BRACKET_RE = re.compile(r'\[([^\]]*)\]') # extracts inner text of each [...] in a segment
35
+ # [tint=(0.022, 0.103, 0.356), show_tint=True]
36
+ @exclude(["tint", "hash", "id", "name", "prev_mouse_y", "prev_mouse_x", "pending_invalidate"])
37
+ @deep_refresh("tint")
38
+ @live
39
+ class DictConversion(metaclass=FieldMeta):
40
+
41
+ _instances: weakref.WeakSet = weakref.WeakSet()
42
+ hash = None
43
+ is_class_dict = True
44
+ outliner_expanded_h = False
45
+
46
+ def __init_subclass__(cls, **kw):
47
+ super().__init_subclass__(**kw)
48
+ cls._instances = weakref.WeakSet()
49
+
50
+ def __init__(self):
51
+ # Using weak references to avoid circular references
52
+ self.__post_init__()
53
+ self.__class__._instances.add(self)
54
+
55
+ # Check on the class ITSELF (`__dict__`, not hasattr): a subclass
56
+ # inherits its base's default through hasattr and would never set its
57
+ # own, so load_save_v2.py rebuilt it from the base's template and
58
+ # its extra fields (ContextMenuItemsState.open_at) came back missing.
59
+ if 'default_instance' not in self.__class__.__dict__:
60
+ self.__class__.default_instance = None
61
+ self.__class__.default_instance = self.__class__()
62
+
63
+ def __post_init__(self):
64
+ self.id = generate_id()
65
+ self.hash = None
66
+ self._parent: Optional[weakref.ReferenceType] = None
67
+ self._parent_key: Optional[Union[str, int]] = None
68
+ self._children: Dict[Union[str, int], 'DictConversion'] = {}
69
+ self._exclude_attrs = {'_history_manager', '_exclude_attrs', '_parameters',
70
+ '_buffers', '_modules', 'training'}
71
+ self._obj_path = None
72
+ self._path_updated = None
73
+ self.name = ""
74
+
75
+ self.tint = (0, 0, 0) # Default black tint
76
+ # self.child_collapsed = set()
77
+ # self._history_manager = GlobalUndoRedoManager.get_instance()
78
+
79
+ def from_dict(self, object_dict, excluded=None, class_root=None, vis=None):
80
+ # ---- fast refs
81
+ DictConv = DictConversion
82
+ EnumType = Enum
83
+
84
+ # ---- fast membership
85
+ base_excluded = {
86
+ "class_names", "_parent", "_parent_key", "_children", "hash",
87
+ "outliner_expanded_h", "modules_imported"
88
+ }
89
+ if excluded is None:
90
+ excluded_set = set(base_excluded)
91
+ else:
92
+ # keep the user's exclusions but make O(1) lookups
93
+ excluded_set = set(excluded)
94
+ excluded_set |= base_excluded
95
+
96
+ if class_root is not None:
97
+ ClassUtility().initialize_class_names(class_root)
98
+
99
+ # We read root without mutating the huge input dict
100
+ root_id = object_dict.get("root", None)
101
+ if root_id is None or root_id not in object_dict:
102
+ raise ValueError("Saved object graph has no valid root; refusing to discard session data")
103
+
104
+ instantiated_objects = {}
105
+
106
+ # ---------- pass 1: instantiate all objects (so cross-refs resolve)
107
+ start_time = time.time()
108
+ for okey, ovalue in object_dict.items():
109
+ if okey == "root":
110
+ continue
111
+ class_path = ovalue["type"]
112
+
113
+ try:
114
+ instance = DictConv.instantiate_from_class_path(class_path)
115
+ except Exception as error:
116
+ print(f"Cannot resolve saved class {class_path}: {error}; preserving its state")
117
+ instance = None
118
+ if instance is None:
119
+ from meltygui.core.conversion.missing_saved_class import missing_saved_class
120
+ instance = missing_saved_class(class_path)()
121
+ instantiated_objects[okey] = instance
122
+
123
+ end_time = time.time()
124
+ elapsed = end_time - start_time
125
+ print(f"instantiate all objects took {elapsed:.4f} seconds")
126
+
127
+ # ---------- parser (no deepcopies of user input)
128
+ def update_instance(unset_value, new_value, _excluded_unused):
129
+ # fast outs
130
+ if unset_value is None and new_value is None:
131
+ return None
132
+
133
+ # object id refs: tuple where first element is in instantiated map
134
+ if isinstance(new_value, tuple) and new_value and new_value[0] in instantiated_objects:
135
+ return instantiated_objects[new_value[0]]
136
+
137
+ if isinstance(new_value, tuple) and len(new_value) == 2 and isinstance(new_value[1], str) and "src.lsd" in new_value[1]:
138
+ return None
139
+
140
+ # Enums
141
+ if isinstance(unset_value, EnumType) or (
142
+ isinstance(new_value, tuple) and len(new_value) > 2 and new_value[2] == "Enum"
143
+ ):
144
+ if isinstance(new_value, tuple):
145
+ # (name_or_value, enum_type_name, "Enum", optional_module)
146
+ name_or_val = new_value[0]
147
+ enum_type_name = new_value[1]
148
+ mod = new_value[3] if len(new_value) > 3 else None
149
+ if isinstance(name_or_val, int):
150
+ # old path: int indicates "leave unset_value as-is"
151
+ return unset_value
152
+ return DictConv.get_enum_value(enum_type_name, name_or_val, mod)
153
+ if isinstance(new_value, str):
154
+ return type(unset_value)[new_value]
155
+ if isinstance(new_value, EnumType):
156
+ return new_value
157
+ return unset_value
158
+
159
+ # Function references: (name_or_qualname, module, "Function")
160
+ if (isinstance(new_value, tuple) and len(new_value) >= 3
161
+ and new_value[2] == DictConv.FUNCTION_TAG):
162
+ return DictConv.resolve_callable(new_value)
163
+
164
+ # Tuple passthrough (non-ref)
165
+ if isinstance(unset_value, tuple):
166
+ # if tuple was a latent ref, it was handled above
167
+ return new_value
168
+
169
+ # Lists
170
+ if isinstance(unset_value, list) and isinstance(new_value, list):
171
+ unset_value.clear()
172
+ for item in new_value:
173
+ # Avoid mutating the input by creating a fresh container as an "unset" prototype
174
+ if isinstance(item, dict):
175
+ proto = {}
176
+ elif isinstance(item, list):
177
+ proto = []
178
+ else:
179
+ proto = None
180
+ unset_value.append(update_instance(proto, item, _excluded_unused))
181
+ return unset_value
182
+
183
+ # Dicts
184
+ if isinstance(unset_value, dict) and isinstance(new_value, dict):
185
+ if "parse_direct" in new_value:
186
+ return new_value
187
+ unset_value.clear()
188
+ for a_key, a_value in new_value.items():
189
+ if isinstance(a_value, dict):
190
+ proto = {}
191
+ elif isinstance(a_value, list):
192
+ proto = []
193
+ else:
194
+ proto = None
195
+ unset_value[a_key] = update_instance(proto, a_value, _excluded_unused)
196
+ return unset_value
197
+
198
+ # Scalars / everything else
199
+ return new_value
200
+
201
+ # ---------- pass 2: materialize fields
202
+ start_time = time.time()
203
+ for okey, ovalue in object_dict.items():
204
+ if okey == "root":
205
+ continue
206
+
207
+ instance = instantiated_objects[okey]
208
+ missing_class = getattr(type(instance), "__missing_saved_path__", None)
209
+ for key, new_value in ovalue.items():
210
+ if missing_class and key in {"type", "is_root"}:
211
+ continue
212
+ if key == "subviews":
213
+ pass
214
+ if key in excluded_set:
215
+ continue
216
+ unset_value = getattr(instance, key, None)
217
+ if missing_class and unset_value is None:
218
+ unset_value = {} if isinstance(new_value, dict) else [] if isinstance(new_value, list) else None
219
+ try:
220
+ parsed = update_instance(unset_value, new_value, excluded_set)
221
+ if missing_class or self.has_valid_attr(instance, key):
222
+ setattr(instance, key, parsed)
223
+ except (KeyError, AttributeError):
224
+ # Keep the original behavior and message
225
+ print(
226
+ f"KeyError: {key} not found in instance {instance}. "
227
+ f"Should not name attributes \"type\""
228
+ )
229
+ continue
230
+
231
+ root = instantiated_objects[root_id] if root_id in instantiated_objects else None
232
+ setattr(root, '_instantiated_objects', instantiated_objects)
233
+ end_time = time.time()
234
+ elapsed = end_time - start_time
235
+ print(f"materialize fields took {elapsed:.4f} seconds")
236
+
237
+ # Run callbacks
238
+ start_time = time.time()
239
+ for obj_instance in instantiated_objects.values():
240
+ cb = getattr(obj_instance, "on_load", None)
241
+ if callable(cb):
242
+ cb(vis=vis, root=root)
243
+ end_time = time.time()
244
+ elapsed = end_time - start_time
245
+ print(f"on_load callbacks took {elapsed:.4f} seconds")
246
+
247
+
248
+
249
+ return root
250
+
251
+ def to_dict(self, excluded=None, objects=None, shallow=False, use_references=False):
252
+ """
253
+ Generic method to convert any class instance to a dictionary.
254
+ Handles nested objects, enums, and basic types.
255
+ """
256
+ if hasattr(self, 'excluded'):
257
+ if excluded is None:
258
+ excluded = self.excluded
259
+ else:
260
+ if isinstance(excluded, list):
261
+ excluded = set(excluded)
262
+
263
+ if self.excluded is not None:
264
+ excluded = excluded.union(self.excluded)
265
+
266
+ if hasattr(self, '__no_save__'):
267
+ if excluded is None:
268
+ excluded = []
269
+ excluded = list(excluded)[:]
270
+ if excluded is None:
271
+ excluded = self.__no_save__
272
+ else:
273
+ excluded += self.__no_save__
274
+
275
+ result = {}
276
+ is_root = False
277
+ if objects is None:
278
+ objects = {}
279
+ is_root = True
280
+
281
+ if not shallow:
282
+ shallow_parse = self.to_dict(excluded=excluded, objects=objects, shallow=True, use_references=False)
283
+ if is_root:
284
+ shallow_parse["is_root"] = is_root
285
+ from meltygui.core.conversion.dynamic_obj import DynamicObj
286
+ if isinstance(self, DynamicObj):
287
+ pass
288
+
289
+ if 'dlt_count' in shallow_parse:
290
+ if shallow_parse['dlt_count'] <= 0:
291
+ # print(f"Skipping object due delete_countdown. id: {self.id}")
292
+ return None
293
+ shallow_parse['dlt_count'] = shallow_parse['dlt_count'] - 1
294
+
295
+ # Class path
296
+ if hasattr(self, 'id'):
297
+ self.id = self.id[0:8]
298
+ object_id = self.id
299
+ else:
300
+ object_id = id(self)
301
+
302
+ classtype = DictConversion.get_full_class_path(self)
303
+ shallow_parse["type"] = classtype
304
+ if classtype == "MouseState":
305
+ pass
306
+ if is_root:
307
+ objects["root"] = object_id
308
+
309
+ if shallow_parse is not None:
310
+ objects[object_id] = shallow_parse
311
+
312
+
313
+ if hasattr(self.__class__, 'default_instance') and self.__class__.default_instance is not None:
314
+ default_instance = self.__class__.default_instance
315
+ else:
316
+ default_instance = self.__class__()
317
+ # Get all attributes that don't start with '_'
318
+ saved_keys = (self.__dict__.keys() if getattr(type(self), "__missing_saved_path__", None)
319
+ else default_instance.__dict__.keys())
320
+ for key in saved_keys:
321
+ value = getattr(self, key, None)
322
+ key = str(key)
323
+ if key.startswith('_') or (excluded and key in excluded):
324
+ continue
325
+
326
+ if key != "dlt_count":
327
+ if isinstance(value, (int, float, str, bool, bytes, Enum, RelaxedEnum, tuple, type(None))):
328
+ if hasattr(default_instance, key):
329
+ default_value = getattr(default_instance, key)
330
+ if value == default_value:
331
+ continue
332
+ if isinstance(value, (dict, list, set)):
333
+ if hasattr(default_instance, key):
334
+ default_value = getattr(default_instance, key)
335
+ if value == default_value and len(default_value) == 0:
336
+ continue
337
+ if isinstance(value, (DictConversion)):
338
+ if hasattr(default_instance, key):
339
+ default_value = getattr(default_instance, key)
340
+ if id(value) == id(default_value):
341
+ continue
342
+
343
+ #
344
+ # if hasattr(value, 'unused_obj') and value.unused_obj:
345
+ # print(f"Skipping unused object for key: {key}")
346
+ # continue
347
+ if not shallow:
348
+ parsed = self.parse_value(result, objects, key, value, excluded)
349
+ result[key] = parsed
350
+ else:
351
+ shallow_parse = self.parse_value(result, objects, key, value, excluded, shallow=True, depth = 0)
352
+ result[key] = shallow_parse
353
+
354
+
355
+ if is_root:
356
+ result["objects"] = objects
357
+
358
+ if use_references:
359
+ return objects
360
+
361
+ return result
362
+
363
+ def reset(self):
364
+
365
+ # Get type of the current instance
366
+ instance_type = type(self)
367
+ # Create a new instance of the same type
368
+ new_instance = instance_type()
369
+ # Copy attributes from the current instance to the new instance
370
+ for key, value in self.__dict__.items():
371
+ key = str(key)
372
+ if key.startswith('_') or key == 'id':
373
+ continue
374
+ setattr(self, key, new_instance.__dict__.get(key, None))
375
+
376
+ def save(self, save_file: str):
377
+ view_dict = self.to_dict()
378
+ path_dir = os.path.dirname(save_file)
379
+ os.makedirs(path_dir, exist_ok=True)
380
+ with open(save_file, "w") as f:
381
+ f.write(str(view_dict["objects"]))
382
+ #
383
+ # def __eq__(self, other):
384
+ # class_name = self.__class__.__name__
385
+ # other_class_name = other.__class__.__name__
386
+ # if class_name == other_class_name:
387
+ # return True
388
+ # return super().__eq__(other)
389
+
390
+ # Make hashable
391
+ def __hash__(self):
392
+ return hash(self.id)
393
+
394
+ @staticmethod
395
+ def load(cls, path: str):
396
+ """
397
+ Load a DictConversion object from a file.
398
+
399
+ example usage: DictConversion.load(ServerModel, "/server/lsd-server.ini")
400
+ """
401
+ ClassUtility().initialize_class_names(ClassUtility().root)
402
+
403
+ # Add cls to the class names if not already present
404
+ # This is to ensure that the class can be found in the class_names dictionary
405
+ if cls.__name__ not in ClassUtility().class_names:
406
+ ClassUtility().class_names[cls.__name__] = cls.__module__ + "." + cls.__name__
407
+
408
+ # Create instance of the dict conversion class
409
+ # and load the class names from the server
410
+
411
+ # Check if the class is a subclass of DictConversion
412
+ if not issubclass(cls, DictConversion):
413
+ raise TypeError(f"{cls.__name__} is not a subclass of DictConversion")
414
+ instance = cls()
415
+
416
+ save_file = f'{path}'
417
+
418
+ if os.path.exists(save_file):
419
+ with open(save_file, "r") as f:
420
+ view_dict = f.read()
421
+ loaded_dict = literal_eval(view_dict)
422
+ server_model = instance.from_dict(loaded_dict)
423
+ return server_model
424
+ else:
425
+ print(f"Error: {save_file} does not exist")
426
+ return None
427
+
428
+ @staticmethod
429
+ def compute_hash(self, exclude=None, memo=None, depth=0, do_print=False, include_hidden=False):
430
+ """
431
+ Create a hash of the instance's content with custom attribute exclusions.
432
+ Recursively handles DictConversion objects, collections, and primitive types.
433
+
434
+ Args:
435
+ exclude: List of attribute names to exclude from hashing.
436
+ memo: Dictionary of already-processed objects to avoid infinite recursion.
437
+ depth: Current recursion depth for debugging.
438
+ do_print: Whether to print debug information.
439
+
440
+ Returns:
441
+ A 16-bit float value representing the instance's content.
442
+ """
443
+ if exclude is None:
444
+ exclude = {}
445
+ import hashlib
446
+
447
+ if exclude is None:
448
+ exclude = set()
449
+
450
+ if memo is None:
451
+ memo = {}
452
+
453
+ # Check if self is already in memo to avoid infinite recursion
454
+ if isinstance(self, (int, float, str, bool)):
455
+ return str(self)
456
+
457
+ try:
458
+ if id(self) in memo:
459
+ if memo[id(self)] != "processing":
460
+ return memo[id(self)]
461
+ except Exception as e:
462
+ print(f"Error checking memo for id(self): {e}")
463
+ return None
464
+
465
+ # Add self to memo temporarily with a temporary value
466
+ # This is crucial to break recursion cycles
467
+ memo[id(self)] = "processing" # Temporary value
468
+
469
+ # Create a string builder for this object
470
+ content_str = f"{self.__class__.__name__}:"
471
+
472
+ # Create set of attributes to exclude
473
+ excluded_attrs = {'outliner_expanded_h', 'expanded', 'hash', '_parent', '_children', 'kwargs', "tensor", "tensor_b", "tensor_c", 'buffer', 'ctx',
474
+ 'texture', "texture3D", "cuda_buffer", "xy_renderer", "xyz_renderer",
475
+ 'previous_mouse_x', 'previous_mouse_y', 'last_mouse_x', 'last_mouse_y'}
476
+ if exclude:
477
+ for excl in exclude:
478
+ excluded_attrs.add(excl)
479
+
480
+
481
+ # Add all non-excluded attributes to the string representation
482
+ if hasattr(self, '__dict__'):
483
+ for key, value in self.__dict__.items():
484
+ # Skip private attributes (starting with underscore)
485
+ key = str(key)
486
+
487
+ if not include_hidden:
488
+ if key.startswith('_'):
489
+ continue
490
+ if key in excluded_attrs or value is None:
491
+ continue
492
+ # Get string representation of the value
493
+ value_str = DictConversion._hash_value_to_str(value, exclude, memo, depth, do_print, include_hidden=include_hidden)
494
+ content_str += f"{value_str}"
495
+ else:
496
+ content_str = DictConversion._hash_value_to_str(self, exclude, memo, depth, do_print, include_hidden=include_hidden)
497
+
498
+ # Calculate hash
499
+ hash_result = hashlib.sha256(content_str.encode('utf-8')).hexdigest()
500
+
501
+ # Convert the hash to a 16-bit float (Float16)
502
+ # Take the first 4 hex chars (16 bits) and convert to integer, then normalize to float16 range
503
+ hash_int = int(hash_result[:4], 16)
504
+
505
+ # Float16 has 1 sign bit, 5 exponent bits, and 10 mantissa bits
506
+ # We'll use the range -65504 to +65504 (max range for float16)
507
+ float_value = (hash_int / 0xFFFF) * 65504 * 2 - 65504
508
+
509
+ # Update the memo with the final value
510
+ memo[id(self)] = float_value
511
+
512
+ if do_print:
513
+ print(
514
+ f"Depth: {depth}, Class: {self.__class__.__name__}, Hash: {hash_result[:8]}..., Float16: {float_value}")
515
+
516
+ if hasattr(self, 'hash'):
517
+ self.hash = float_value
518
+ return float_value
519
+
520
+ @staticmethod
521
+ def _hash_value_to_str(value, exclude=None, memo=None, depth=0, do_print=False, include_hidden=False):
522
+ """
523
+ Helper method to convert a value to a string representation based on its type.
524
+
525
+ Args:
526
+ value: The value to convert to string
527
+ exclude: List of attribute names to exclude from hashing
528
+ memo: Dictionary of already-processed objects
529
+ depth: Current recursion depth for debugging
530
+ do_print: Whether to print debug information
531
+
532
+ Returns:
533
+ A string representation of the value
534
+ """
535
+ if exclude is None:
536
+ exclude = {}
537
+ depth += 1
538
+
539
+ if memo is None:
540
+ memo = {}
541
+
542
+ # Check if hashable first
543
+ try:
544
+ standard_hash = hash(value)
545
+ return str(standard_hash)
546
+ except TypeError:
547
+ pass
548
+
549
+ if do_print:
550
+ import torch
551
+ if hasattr(torch, 'cuda') and torch.cuda.is_available():
552
+ mem_str = ""
553
+ for i in range(torch.cuda.device_count()):
554
+ mem_alloc = torch.cuda.memory_allocated(i) / 1024 ** 3
555
+ mem_str += f"GPU {i}: {mem_alloc:.2f} GB\n"
556
+ print(f"Depth: {depth}, Value: {value}, Type: {type(value)}, Memory: {mem_str}")
557
+ else:
558
+ print(f"Depth: {depth}, Value: {value}, Type: {type(value)}")
559
+
560
+ # Check if value is already in memo - crucial for avoiding infinite recursion
561
+ if id(value) in memo:
562
+ return f"ref:{value}" # Return a reference indicator instead of recursing
563
+
564
+ # Handle None
565
+ if value is None:
566
+ return ""
567
+
568
+ # Handle tensors and other special objects by returning their type and shape/identity
569
+ if hasattr(value, "__class__") and value.__class__.__name__ == "Tensor":
570
+ try:
571
+ tensor_repr = f"Tensor:shape={list(value.shape)}:dtype={value.dtype}"
572
+ except:
573
+ tensor_repr = f"Tensor:{id(value)}"
574
+ memo[id(value)] = tensor_repr
575
+ return tensor_repr
576
+
577
+ if hasattr(value, "__class__") and "PreTrainedTokenizerBase" in str(value.__class__.__mro__):
578
+ tokenizer_repr = f"Tokenizer:{value.__class__.__name__}"
579
+ memo[id(value)] = tokenizer_repr
580
+ return tokenizer_repr
581
+
582
+ if hasattr(value, "__class__") and "Module" in str(value.__class__.__mro__):
583
+ module_repr = f"Module:{value.__class__.__name__}"
584
+ memo[id(value)] = module_repr
585
+ return module_repr
586
+
587
+ # Handle DictConversion objects - pass the depth parameter correctly
588
+ if hasattr(value, "compute_hash") and isinstance(value, DictConversion):
589
+ memo[id(value)] = "processing" # Add immediately to avoid recursion
590
+ result = DictConversion.compute_hash(self=value, exclude=exclude, memo=memo, depth=depth,
591
+ do_print=do_print, include_hidden=include_hidden)
592
+ memo[id(value)] = result # Update with actual result
593
+ return str(result)
594
+
595
+ # Handle Enums
596
+ if hasattr(value, "__class__") and hasattr(value.__class__,
597
+ "__module__") and "enum" in value.__class__.__module__:
598
+ try:
599
+ enum_repr = f"Enum:{value.__class__.__name__}.{value.name}"
600
+ except:
601
+ enum_repr = f"Enum:{value.__class__.__name__}"
602
+ memo[id(value)] = enum_repr
603
+ return enum_repr
604
+
605
+ # Handle lists
606
+ if isinstance(value, list):
607
+ memo[id(value)] = "list:processing" # Add immediately to avoid recursion
608
+ items_str = "["
609
+ for item in value:
610
+ items_str += DictConversion._hash_value_to_str(value=item, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
611
+ include_hidden=include_hidden) + ","
612
+ items_str += "]"
613
+ memo[id(value)] = items_str
614
+ return items_str
615
+
616
+ # Handle tuples
617
+ if isinstance(value, tuple):
618
+ memo[id(value)] = "tuple:processing" # Add immediately to avoid recursion
619
+ items_str = "("
620
+ for item in value:
621
+ items_str += DictConversion._hash_value_to_str(value=item, exclude=exclude, memo=memo, depth=depth,
622
+ do_print=do_print, include_hidden=include_hidden) + ","
623
+ items_str += ")"
624
+ memo[id(value)] = items_str
625
+ return items_str
626
+
627
+ # Handle dictionaries
628
+ if isinstance(value, dict):
629
+ memo[id(value)] = "dict:processing" # Add immediately to avoid recursion
630
+ items_str = "{"
631
+ for k, v in value.items():
632
+ k = str(k)
633
+ if not include_hidden and k.startswith('_'):
634
+ continue
635
+ if k in exclude:
636
+ continue
637
+ # Convert the key to string representation
638
+ # Get value string representation
639
+ val_str = DictConversion._hash_value_to_str(value=v, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
640
+ include_hidden=include_hidden)
641
+ items_str += f"{val_str}"
642
+ items_str += "}"
643
+ memo[id(value)] = items_str
644
+ return items_str
645
+
646
+ if isinstance(value, set):
647
+ memo[id(value)] = "set:processing" # Add immediately to avoid recursion
648
+ items_str = "{"
649
+ for item in value:
650
+ items_str += str(item) + ","
651
+ items_str += "}"
652
+ memo[id(value)] = items_str
653
+ return items_str
654
+
655
+ # Handle primitive types (int, float, str, bool)
656
+ if isinstance(value, (int, float, str, bool)):
657
+ result = str(value)
658
+ memo[id(value)] = result
659
+ return result
660
+
661
+ # Any other types - use their string representation
662
+ other_repr = f"{str(type(value).__name__)}" # Just use type to prevent recursion
663
+ memo[id(value)] = other_repr
664
+ return other_repr
665
+
666
+ def deepcopy(self, do_print=False, max_depth=20):
667
+ """
668
+ Create a deep copy of the instance, excluding certain attributes.
669
+ Recursively handles DictConversion objects, collections, and primitive types.
670
+
671
+ Returns:
672
+ A new instance with deep-copied attributes.
673
+ """
674
+ return self.deepcopy_exclude(exclude=None, memo=None, depth=0, do_print=do_print, max_depth=max_depth)
675
+
676
+ def deepcopy_exclude(self, exclude=None, include=None, memo=None, depth=0, do_print=False, max_depth=30):
677
+ """
678
+ Create a deep copy of the instance with custom attribute exclusions.
679
+ Recursively handles DictConversion objects, collections, and primitive types.
680
+
681
+ Args:
682
+ exclude: List of attribute names to exclude from copying.
683
+ memo: Dictionary of already-copied objects to avoid infinite recursion.
684
+
685
+ Returns:
686
+ A new instance with deep-copied attributes except for excluded ones.
687
+ """
688
+ if memo is None or isinstance(memo, int):
689
+ memo = {}
690
+
691
+ # Check if self is already in memo to avoid infinite recursion
692
+ if id(self) in memo:
693
+ return memo[id(self)]
694
+
695
+ # Create a new instance of the same class
696
+ result = self.__class__.__new__(self.__class__)
697
+
698
+ # Add the new object to memo to avoid infinite recursion
699
+ memo[id(self)] = result
700
+
701
+ if depth > max_depth:
702
+ return self
703
+
704
+ # Initialize new parent and children tracking attributes
705
+ if result is not None:
706
+ if hasattr(result, '_parent'):
707
+ result._parent = None
708
+ result._parent_key = None
709
+ result._children = {}
710
+
711
+ # Create set of attributes to exclude
712
+ excluded_attrs = {'class_names', '_parent', '_children', "tensor", "tensor_b", "tensor_c", 'buffer', 'ctx',
713
+ 'texture', "texture3D", "cuda_buffer", "xy_renderer", "xyz_renderer", "parents", 'attr_settings',
714
+ 'input_value', 'parent', 'value_type', 'settings', '_settings', '_children', '_history_manager', 'selected',
715
+ 'selected_object', 'default_value', '_view_parent', '_attr_size', '_attr_pos', '_job_queue', '_result_queue'}
716
+ if exclude:
717
+ excluded_attrs.update(exclude)
718
+
719
+ # Copy all attributes except excluded ones
720
+ if hasattr(self, '__dict__'):
721
+ for key, value in self.__dict__.items():
722
+ if include is not None:
723
+ if key in include:
724
+ # Deep copy the value with appropriate handling based on type
725
+ if do_print:
726
+ print(f"Copying attribute: {key}, Value: {value}, Type: {type(value)}")
727
+ copied_value = self._deepcopy_value(value, exclude=exclude, memo=memo, do_print=do_print, depth=depth, max_depth=max_depth)
728
+ setattr(result, key, copied_value)
729
+ else:
730
+ setattr(result, key, value)
731
+ else:
732
+ if key not in excluded_attrs:
733
+ # Deep copy the value with appropriate handling based on type
734
+ if do_print:
735
+ print(f"Copying attribute: {key}, Value: {value}, Type: {type(value)}")
736
+ copied_value = self._deepcopy_value(value, exclude=exclude, memo=memo, do_print=do_print, depth=depth, max_depth=max_depth)
737
+ setattr(result, key, copied_value)
738
+ else:
739
+ # For excluded attributes, just set them to None
740
+ setattr(result, key, value)
741
+
742
+
743
+ return result
744
+
745
+ def _deepcopy_value(self, input_value, exclude=None, memo=None, depth=0, do_print=False, max_depth=30):
746
+ """
747
+ Helper method to deep copy a value based on its type.
748
+
749
+ Args:
750
+ input_value: The value to deep copy
751
+ exclude: List of attribute names to exclude from copying
752
+ memo: Dictionary of already-copied objects
753
+
754
+ Returns:
755
+ A deep copy of the value
756
+ """
757
+ if memo is None:
758
+ memo = {}
759
+
760
+ if depth > max_depth:
761
+ return input_value
762
+
763
+ if do_print:
764
+ print(f"Depth: {depth}, Value: {input_value}, Type: {type(input_value)}")
765
+ mem_str = ""
766
+ import torch
767
+ for i in range(torch.cuda.device_count()):
768
+ mem_alloc = torch.cuda.memory_allocated(i) / 1024 ** 3
769
+ mem_str += f"GPU {i}: {mem_alloc:.2f} GB\n"
770
+
771
+ print(f"Depth: {depth}, Value: {input_value}, Type: {type(input_value)}, Memory: {mem_str}")
772
+
773
+ # Check if value is already in memo
774
+ if id(input_value) in memo:
775
+ return memo[id(input_value)]
776
+
777
+ # Handle None
778
+ if input_value is None:
779
+ return None
780
+
781
+ # Pass heavy ML objects through untouched. Looked up via sys.modules so a GUI that never
782
+ # imported torch/transformers doesn't break them here.
783
+ for module, name in (('torch', 'Tensor'), ('transformers', 'LlamaTokenizerFast'),
784
+ ('transformers', 'PreTrainedTokenizerBase'), ('torch.nn', 'Module')):
785
+ cls = _loaded_class(module, name)
786
+ if cls is not None and isinstance(input_value, cls):
787
+ return input_value
788
+
789
+ # Handle DictConversion objects
790
+ if hasattr(input_value, "deepcopy_exclude") and callable(getattr(input_value, "deepcopy_exclude")):
791
+ if not isinstance(input_value, (DictConversion, type)):
792
+ print(f"Warning: Encountered non-DictConversion with deepcopy_exclude method: {input_value.__class__.__name__}")
793
+
794
+ if isinstance(input_value, DictConversion):
795
+ return input_value.deepcopy_exclude(exclude=exclude, memo=memo, depth=depth + 1, do_print=do_print, max_depth=max_depth)
796
+
797
+ if input_value.__class__.__name__ == "ObjectRef":
798
+ return input_value
799
+
800
+ # Handle Enums (should be copied by value, not deep copied)
801
+ if isinstance(input_value, Enum):
802
+ return input_value
803
+
804
+ # Handle lists
805
+ if isinstance(input_value, list):
806
+ new_list = []
807
+ memo[id(input_value)] = new_list
808
+ for item in input_value:
809
+ new_list.append(self._deepcopy_value(item, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
810
+ max_depth=max_depth))
811
+ return new_list
812
+
813
+ # Handle tuples
814
+ if isinstance(input_value, tuple):
815
+ items = [self._deepcopy_value(item, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
816
+ max_depth=max_depth) for item in input_value]
817
+ result = tuple(items)
818
+ memo[id(input_value)] = result
819
+ return result
820
+
821
+ # Handle dictionaries
822
+ if isinstance(input_value, dict):
823
+ new_dict = {}
824
+ memo[id(input_value)] = new_dict
825
+ for k, v in input_value.items():
826
+ # The keys are immutable, so we don't need to copy them
827
+ new_dict[k] = self._deepcopy_value(v, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
828
+ max_depth=max_depth)
829
+ return new_dict
830
+
831
+ if isinstance(input_value, set):
832
+ new_set = set()
833
+ for item in input_value:
834
+ new_set.add(self._deepcopy_value(item, exclude=exclude, memo=memo, depth=depth, do_print=do_print,
835
+ max_depth=max_depth))
836
+
837
+ # Handle primitive types (int, float, str, bool)
838
+ if isinstance(input_value, (int, float, str, bool)):
839
+ return input_value
840
+
841
+ # print(f"Should't get here: {value.__class__} {isinstance(value, DictConversion)}")
842
+
843
+ return input_value
844
+
845
+ # def __new__(cls, *args, **kwargs):
846
+ # instance = super().__new__(cls)
847
+ # # Initialize tracking attributes
848
+ # instance._history_manager = GlobalUndoRedoManager.get_instance()
849
+ # instance.hash = None
850
+ #
851
+ # instance._parent = None
852
+ # instance._parent_key = None
853
+ # instance._attributes = {}
854
+ # return instance
855
+
856
+ def get_attrib_path(self, attrib_name) -> str:
857
+ """
858
+ Returns the path to this object by walking up the parent chain.
859
+ """
860
+ new_path = self.get_path()
861
+
862
+ attrib_path = f"{new_path}.{attrib_name}" if new_path else attrib_name
863
+ return attrib_path
864
+
865
+ def get_path(self) -> str:
866
+ """
867
+ Returns the path to this object by walking up the parent chain.
868
+ """
869
+ if self._obj_path is not None and self._obj_path != "":
870
+ return self._obj_path
871
+
872
+ path_components = []
873
+ current = self
874
+
875
+ while current._parent is not None:
876
+ parent = current._parent()
877
+ if parent is None: # weak reference expired
878
+ raise ValueError("Parent reference expired")
879
+
880
+ path_components.append(current._parent_key)
881
+ current = parent
882
+
883
+ computed_path = ''.join(reversed([comp if comp.startswith('[') else f'.{comp}'
884
+ for comp in path_components])).lstrip('.')
885
+
886
+ self._obj_path = computed_path
887
+
888
+ return computed_path
889
+
890
+ # def __setattr__(self, name: str, value: Any) -> None:
891
+ #
892
+ # try:
893
+ # wrapped_value = value
894
+ # # Make the actual change
895
+ # super().__setattr__(name, wrapped_value)
896
+ #
897
+ # except Exception as e:
898
+ # # If something goes wrong, still make the change
899
+ # super().__setattr__(name, value)
900
+ # raise
901
+
902
+ # def __getitem__(self, key: Union[str, int]) -> Any:
903
+ # # First check if this key is directly in __dict__
904
+ # if isinstance(key, str) and hasattr(self, key):
905
+ # return getattr(self, key)
906
+ #
907
+ # # Then check if it's in __dict__ as a list/sequence attribute
908
+ # for attr_name, attr_value in self.__dict__.items():
909
+ # if isinstance(attr_value, (list, tuple)) and isinstance(key, int):
910
+ # if key < len(attr_value):
911
+ # return attr_value[key]
912
+ # raise IndexError(f"Index {key} out of range for sequence of length {len(attr_value)}")
913
+ # elif isinstance(attr_value, dict) and key in attr_value:
914
+ # return attr_value[key]
915
+ #
916
+ # raise TypeError(f"'{self.__class__.__name__}' object has no sequence or mapping with the key '{key}'")
917
+ #
918
+ # def __setitem__(self, key: Union[str, int], value: Any) -> None:
919
+ # if isinstance(value, DictConversion):
920
+ # value._parent = weakref.ref(self)
921
+ # value._parent_key = f"['{key}']" if isinstance(key, str) else f"[{key}]"
922
+ # self._children[key] = value
923
+ #
924
+ # # Try to find appropriate sequence/mapping to set the item
925
+ # for attr_name, attr_value in self.__dict__.items():
926
+ # if isinstance(attr_value, (list, tuple)) and isinstance(key, int):
927
+ # if isinstance(attr_value, tuple):
928
+ # # Convert tuple to list if needed
929
+ # setattr(self, attr_name, list(attr_value))
930
+ # attr_value = getattr(self, attr_name)
931
+ # if key < len(attr_value):
932
+ # attr_value[key] = value
933
+ # return
934
+ # elif isinstance(attr_value, dict) and key in attr_value:
935
+ # attr_value[key] = value
936
+ # return
937
+ #
938
+ # # If we didn't find a place to set it, treat it as a new attribute
939
+ # setattr(self, str(key), value)
940
+
941
+ def get(self, path: str) -> Any:
942
+ """
943
+ Retrieves a value using a path string.
944
+ Example paths: "attr1.attr2", "attr1[0]", "attr1['key']"
945
+ Splits on '.' outside brackets and then resolves bracket chains per segment.
946
+ """
947
+ # Precompiled patterns (compiled once at function def time)
948
+
949
+ _missing = object()
950
+
951
+ # Be defensive: convert string (avoid getattr TypeError on non-str input)
952
+ if not isinstance(path, str):
953
+ try:
954
+ path = path.decode() if isinstance(path, (bytes, bytearray)) else str(path)
955
+ except Exception:
956
+ return None
957
+
958
+ current = self
959
+ if not path:
960
+ return current
961
+
962
+ # Iterate by segments (no Python char-by-char loop)
963
+ for m in _SEGMENT_RE.finditer(path):
964
+ segment = m.group(0)
965
+ name_end = segment.find('[')
966
+ if name_end == -1:
967
+ name = segment.strip()
968
+ brackets_inner = []
969
+ else:
970
+ name = segment[:name_end].strip()
971
+ brackets_inner = [b.group(1).strip() for b in _BRACKET_RE.finditer(segment)]
972
+
973
+ # Attribute access (if any)
974
+ if name:
975
+ val = getattr(current, name, _missing)
976
+ if val is _missing:
977
+ return None
978
+ current = val
979
+
980
+ # Resolve any bracketed chains in order
981
+ for inner in brackets_inner:
982
+ # Strip matching quotes if present
983
+ if len(inner) >= 2 and inner[0] in ("'", '"') and inner[-1] == inner[0]:
984
+ key = inner[1:-1]
985
+ else:
986
+ key = int(inner) if inner.isdigit() else inner # from original: only digit-only becomes int
987
+
988
+ if isinstance(current, (list, tuple)):
989
+ if isinstance(key, int) and -len(current) <= key < len(current):
990
+ current = current[key]
991
+ else:
992
+ return None
993
+ elif isinstance(current, dict):
994
+ if key in current:
995
+ current = current[key]
996
+ else:
997
+ return None
998
+ else:
999
+ # Fallback for mapping/array-likes (e.g., some objects)
1000
+ try:
1001
+ current = current[key]
1002
+ except (TypeError, KeyError, IndexError):
1003
+ return None
1004
+
1005
+ # If the path had only dots or was malformed (e.g., "a..b"), nothing matched:
1006
+ # In that case, try to short-circuit to None to mirror "invalid key => None".
1007
+ if current is self and not _SEGMENT_RE.search(path):
1008
+ return None
1009
+
1010
+ return current
1011
+
1012
+ def set(self, path: str, value: Any) -> None:
1013
+ """
1014
+ Sets a value using a path string.
1015
+ Example paths: "attr1.attr2", "attr1[0]", "attr1['key']"
1016
+ """
1017
+ if not path:
1018
+ raise ValueError("Path cannot be empty")
1019
+
1020
+ # Split path into components while preserving nested structure
1021
+ parts = []
1022
+ current_part = ''
1023
+ brackets = 0
1024
+
1025
+ for char in path:
1026
+ if char == '[':
1027
+ brackets += 1
1028
+ if brackets == 1 and current_part:
1029
+ parts.append(current_part)
1030
+ current_part = '['
1031
+ else:
1032
+ current_part += char
1033
+ elif char == ']':
1034
+ brackets -= 1
1035
+ current_part += char
1036
+ if brackets == 0:
1037
+ parts.append(current_part)
1038
+ current_part = ''
1039
+ elif char == '.' and brackets == 0:
1040
+ if current_part:
1041
+ parts.append(current_part)
1042
+ current_part = ''
1043
+ else:
1044
+ current_part += char
1045
+
1046
+ if current_part:
1047
+ parts.append(current_part)
1048
+
1049
+ # Navigate to the parent of the target
1050
+ current = self
1051
+ for i, part in enumerate(parts[:-1]):
1052
+ if part.startswith('['):
1053
+ # Handle array/dict access
1054
+ idx = part[1:-1].strip("'\"") # Remove quotes if present
1055
+ try:
1056
+ current = current[int(idx) if idx.isdigit() else idx]
1057
+ except (TypeError, ValueError, KeyError, IndexError):
1058
+ raise AttributeError(f"Cannot access {part} in path {path}")
1059
+ else:
1060
+ # Handle attribute access
1061
+ try:
1062
+ current = getattr(current, part)
1063
+ except AttributeError:
1064
+ raise AttributeError(f"Cannot access attribute {part} in path {path}")
1065
+
1066
+ # Set the final value
1067
+ final_part = parts[-1]
1068
+ if final_part.startswith('['):
1069
+ # Handle array/dict assignment
1070
+ idx = final_part[1:-1].strip("'\"") # Remove quotes if present
1071
+ try:
1072
+ current[int(idx) if idx.isdigit() else idx] = value
1073
+ except (TypeError, ValueError, KeyError, IndexError) as e:
1074
+ raise AttributeError(f"Cannot set {final_part} in path {path}: {e}")
1075
+ else:
1076
+ # Handle attribute assignment
1077
+ try:
1078
+ setattr(current, final_part, value)
1079
+ except AttributeError as e:
1080
+ raise AttributeError(f"Cannot set attribute {final_part} in path {path}: {e}")
1081
+
1082
+ def get_path_of_attribute(self, attr_value: Any) -> str:
1083
+ """
1084
+ Returns the path to find an attribute value within the nested structure.
1085
+ Uses parent references for efficient path construction.
1086
+ """
1087
+ if not isinstance(attr_value, DictConversion):
1088
+ # Search immediate children first
1089
+ for name, value in self.__dict__.items():
1090
+ if not name.startswith('_'):
1091
+ if value is attr_value:
1092
+ return name
1093
+
1094
+ # Search in nested structures
1095
+ path = self._find_path(attr_value)
1096
+ if path is not None:
1097
+ return path
1098
+ raise ValueError("Attribute not found in nested structure")
1099
+
1100
+ # If attr_value is a DictConversion, build path from parent references
1101
+ path_components = []
1102
+ current = attr_value
1103
+
1104
+ while current is not None and current is not self:
1105
+ if current._parent is None:
1106
+ raise ValueError("Attribute not found in nested structure")
1107
+
1108
+ parent = current._parent()
1109
+ if parent is None: # weak reference expired
1110
+ raise ValueError("Parent reference expired")
1111
+
1112
+ path_components.append(current._parent_key)
1113
+ current = parent
1114
+
1115
+ if current is not self:
1116
+ raise ValueError("Attribute not found in nested structure")
1117
+
1118
+ return ''.join(reversed([comp if comp.startswith('[') else f'.{comp}'
1119
+ for comp in path_components])).lstrip('.')
1120
+
1121
+ def _find_path(self, target: Any, current_path: str = '') -> Optional[str]:
1122
+ """Helper method to find path for non-DictConversion values"""
1123
+ # Search in sequences
1124
+ if isinstance(self, (list, tuple)):
1125
+ for i, value in enumerate(self):
1126
+ new_path = f"{current_path}[{i}]"
1127
+ if value is target:
1128
+ return new_path
1129
+ if isinstance(value, DictConversion):
1130
+ result = value._find_path(target, new_path)
1131
+ if result is not None:
1132
+ return result
1133
+
1134
+ # Search in dicts
1135
+ if isinstance(self, dict):
1136
+ for key, value in self.items():
1137
+ new_path = f"{current_path}['{key}']"
1138
+ if value is target:
1139
+ return new_path
1140
+ if isinstance(value, DictConversion):
1141
+ result = value._find_path(target, new_path)
1142
+ if result is not None:
1143
+ return result
1144
+
1145
+ # Search in object attributes
1146
+ for key, value in self.__dict__.items():
1147
+ if key.startswith('_'):
1148
+ continue
1149
+ new_path = f"{current_path}.{key}" if current_path else key
1150
+ if value is target:
1151
+ return new_path
1152
+ if isinstance(value, DictConversion):
1153
+ result = value._find_path(target, new_path)
1154
+ if result is not None:
1155
+ return result
1156
+
1157
+ return None
1158
+
1159
+ @classmethod
1160
+ def _is_dataclass(cls, obj: Any) -> bool:
1161
+ """Check if an object is a custom dataclass (has attributes)."""
1162
+ return hasattr(obj, '__dict__')
1163
+
1164
+ @classmethod
1165
+ def _is_enum(cls, obj: Any) -> bool:
1166
+ """Check if an object is an Enum."""
1167
+ return isinstance(obj, Enum)
1168
+
1169
+
1170
+
1171
+ @staticmethod
1172
+ def find_nested_classes(parent_class: type, parent_path: str) -> List[Tuple[str, type]]:
1173
+ """
1174
+ Recursively find all nested classes within a class.
1175
+
1176
+ Args:
1177
+ parent_class: The parent class to search in
1178
+ parent_path: The full path of the parent class
1179
+
1180
+ Returns:
1181
+ A list of tuples (full_class_path, class_object) for nested classes
1182
+ """
1183
+ nested_classes = []
1184
+
1185
+ # Check all attributes of the class
1186
+ for name, obj in parent_class.__dict__.items():
1187
+ # Skip special methods, private attributes, and non-classes
1188
+ if name.startswith('__'):
1189
+ continue
1190
+
1191
+ if not isinstance(obj, type):
1192
+ continue
1193
+
1194
+ # Build full path for the nested class
1195
+ class_path = f"{parent_path}.{name}"
1196
+ nested_classes.append((class_path, obj))
1197
+
1198
+ # Recursively find classes nested within this class
1199
+ inner_classes = DictConversion.find_nested_classes(obj, class_path)
1200
+ nested_classes.extend(inner_classes)
1201
+
1202
+ return nested_classes
1203
+
1204
+ @staticmethod
1205
+ def instantiate_from_class_path(class_path: str, last_try=False):
1206
+ from meltygui.core.module_names import canonical_name
1207
+ class_path = canonical_name(class_path)
1208
+ ClassUtility().initialize_class_names()
1209
+ parts = class_path.split('.')
1210
+ class_name = parts[-1]
1211
+ parent_name = parts[-2] if len(parts) >= 2 else ""
1212
+ combined_name = f"{parent_name}.{class_name}" if parent_name else class_name
1213
+
1214
+ if "." not in class_path and class_name in ClassUtility().class_names:
1215
+ class_path = ClassUtility().class_names[class_name]
1216
+ elif combined_name in ClassUtility().class_names:
1217
+ class_path = ClassUtility().class_names[combined_name]
1218
+ else:
1219
+ pass
1220
+
1221
+ parts = class_path.split('.')
1222
+
1223
+ # module_name = ".".join(parts[:-1])
1224
+ # module = importlib.import_module(module_name)
1225
+ # if module is None:
1226
+ # return None
1227
+ #
1228
+ # if class_name not in vars(module):
1229
+ # return None
1230
+ module = None
1231
+ i = 0
1232
+ for i in range(len(parts) - 1, 0, -1):
1233
+ try:
1234
+ module_path = '.'.join(parts[:i])
1235
+
1236
+ if module_path in sys.modules:
1237
+ module = sys.modules[module_path]
1238
+ else:
1239
+ module = importlib.import_module(module_path)
1240
+ break
1241
+ except ImportError:
1242
+ continue
1243
+
1244
+ if module is None:
1245
+ return None
1246
+
1247
+ try:
1248
+ obj = module
1249
+ for part in parts[i:]:
1250
+ obj = getattr(obj, part)
1251
+ # members = inspect.getmembers(module, inspect.isclass)
1252
+
1253
+ if not inspect.isclass(obj):
1254
+ print(f"{class_path} is not a class")
1255
+ return None
1256
+
1257
+ return obj()
1258
+ except Exception as e:
1259
+ print(f"Error instantiating {class_path}: {str(e)}")
1260
+
1261
+ # except Exception as e:
1262
+ # target_class_name = parts[-1]
1263
+ # target_class_parent = parts[-2]
1264
+ # target_class_combine = f"{target_class_parent}.{target_class_name}"
1265
+ # if last_try:
1266
+ # # Print stack trace for debugging
1267
+ # import traceback
1268
+ # traceback.print_exc()
1269
+ #
1270
+ # print(f"Error instantiating {class_path}: {str(e)}")
1271
+ # return None
1272
+
1273
+ # ClassUtility().initialize_class_names()
1274
+ #
1275
+ # if target_class_combine in ClassUtility().class_names:
1276
+ # found_class_path = ClassUtility().class_names[target_class_combine]
1277
+ # return DictConversion.instantiate_from_class_path(found_class_path, last_try=True)
1278
+ # elif target_class_name in ClassUtility().class_names:
1279
+ # found_class_path = ClassUtility().class_names[target_class_name]
1280
+ # return DictConversion.instantiate_from_class_path(found_class_path, last_try=True)
1281
+ # else:
1282
+ # print(f"Class {target_class_name} not found in known classes.")
1283
+
1284
+ # return None
1285
+
1286
+ @staticmethod
1287
+ def get_enum_value(class_path: str, value_name: str, value: Optional[int], last_try=False):
1288
+ from meltygui.core.module_names import canonical_name
1289
+ class_path = canonical_name(class_path)
1290
+ # First get the enum class
1291
+ parts = class_path.split('.')
1292
+ module = None
1293
+ class_name = parts[-1]
1294
+
1295
+ if len(parts) >= 2:
1296
+ parent_name = parts[-2]
1297
+ else:
1298
+ parent_name = ""
1299
+ combined_name = f"{parent_name}.{class_name}"
1300
+
1301
+ ClassUtility().initialize_class_names()
1302
+
1303
+ class_parent = f"{parent_name}.{class_name}"
1304
+
1305
+ if class_parent in ClassUtility().class_names:
1306
+ class_path = ClassUtility().class_names[class_parent]
1307
+ parts = class_path.split('.')
1308
+
1309
+ elif "." not in class_path and class_name in ClassUtility().class_names:
1310
+ class_path = ClassUtility().class_names[class_name]
1311
+ parts = class_path.split('.')
1312
+
1313
+ # for i in range(len(parts) - 1):
1314
+ # if parts[i] == "tensorview":
1315
+ # parts[i] = "model.app_model"
1316
+
1317
+ for i in range(len(parts) - 1, 0, -1):
1318
+ try:
1319
+
1320
+ module_path = '.'.join(parts[:i])
1321
+ module = sys.modules.get(module_path)
1322
+ if module is None:
1323
+ # Only import if it's not already imported
1324
+ module = importlib.import_module(module_path)
1325
+ break
1326
+ except ImportError:
1327
+ print(f"Error importing {module_path}")
1328
+ continue
1329
+
1330
+ if module is None:
1331
+ return None
1332
+ # Get the enum class
1333
+ obj = module
1334
+ for part in parts[i:]:
1335
+ obj = getattr(obj, part)
1336
+
1337
+ # Get the actual enum value - prefer name-based lookup (always works),
1338
+ # fall back to value-based only when name lookup fails.
1339
+ try:
1340
+ if issubclass(obj, Enum):
1341
+ try:
1342
+ return obj[value_name] # name-based lookup
1343
+ except KeyError:
1344
+ if value is not None:
1345
+ return obj(value) # value-based fallback
1346
+ raise
1347
+ else:
1348
+ raise ValueError(f"{class_path} is not an Enum class")
1349
+ except Exception as e:
1350
+ if last_try:
1351
+ # Print stack trace for debugging
1352
+ import traceback
1353
+ traceback.print_exc()
1354
+
1355
+ print(f"Error getting enum value for {class_path}: {str(e)}")
1356
+ return None
1357
+
1358
+ ClassUtility().initialize_class_names()
1359
+
1360
+ if value_name in ClassUtility().class_names:
1361
+ found_class_path = ClassUtility().class_names[value_name]
1362
+ return DictConversion.get_enum_value(found_class_path, value_name, value, last_try=True)
1363
+ elif combined_name in ClassUtility().class_names:
1364
+ found_class_path = ClassUtility().class_names[combined_name]
1365
+ return DictConversion.get_enum_value(found_class_path, combined_name, value, last_try=True)
1366
+
1367
+ def has_valid_attr(self, obj, attr_name: str) -> bool:
1368
+ return True
1369
+
1370
+ # ── Function-reference (de)serialization ──────────────────────────────────
1371
+ # Functions can't be stored directly (they're not primitives, so parse_value
1372
+ # would drop them to None). We store a reference - module path + qualified
1373
+ # name - and resolve it back at load time, mirroring the Enum marker tuple so
1374
+ # the load side matches on a "Function" tag.
1375
+
1376
+ FUNCTION_TAG = "Function"
1377
+ RENDER_FUNC_MODULE = "__render_func__"
1378
+
1379
+ @staticmethod
1380
+ def serialize_callable(value):
1381
+ """A function/callable -> a resolvable marker tuple, or None if it can't
1382
+ be referenced.
1383
+
1384
+ (qualname, module, "Function") for a normally-importable function (incl.
1385
+ @render_func wrappers, which carry the original module/qualname via
1386
+ @wraps). (name, "__render_func__", "Function") for handles only reachable
1387
+ through the render-func registry — e.g. a _LazyRenderFunc, which exposes
1388
+ __name__ but no __module__/__qualname__."""
1389
+ module = getattr(value, "__module__", None)
1390
+ qualname = getattr(value, "__qualname__", None)
1391
+ if module and qualname and "<locals>" not in qualname:
1392
+ return (qualname, module, DictConversion.FUNCTION_TAG)
1393
+ name = getattr(value, "__name__", None)
1394
+ if name:
1395
+ return (name, DictConversion.RENDER_FUNC_MODULE, DictConversion.FUNCTION_TAG)
1396
+ return None
1397
+
1398
+ @staticmethod
1399
+ def resolve_callable(ref):
1400
+ """Inverse of serialize_callable: marker tuple -> the live callable (or
1401
+ None if it can't be resolved)."""
1402
+ from meltygui.core.module_names import canonical_name
1403
+ name, module = ref[0], canonical_name(ref[1])
1404
+ if module == DictConversion.RENDER_FUNC_MODULE:
1405
+ # Registry handle - rely on RenderFuncs giving back a lazy handle
1406
+ # that resolves against the live @render_func registry at call time.
1407
+ from meltygui.core.rendering.render_funcs import RenderFuncs
1408
+ return getattr(RenderFuncs, name)
1409
+ try:
1410
+ obj = importlib.import_module(module)
1411
+ except Exception as e:
1412
+ print(f"resolve_callable: cannot import {module!r}: {e}")
1413
+ return None
1414
+ for part in name.split("."):
1415
+ obj = getattr(obj, part, None)
1416
+ if obj is None:
1417
+ print(f"resolve_callable: {module}.{name} not found")
1418
+ return None
1419
+ return obj
1420
+
1421
+ def on_load(self, vis, root):
1422
+ """
1423
+ Method to be called after the object is loaded from a dictionary.
1424
+ Can be overridden in subclasses to perform additional initialization.
1425
+ """
1426
+ pass
1427
+ # Loop over attribs
1428
+
1429
+
1430
+ @staticmethod
1431
+ def get_full_class_path(obj):
1432
+ cls = obj.__class__
1433
+ if getattr(cls, "__missing_saved_path__", None):
1434
+ return cls.__missing_saved_path__
1435
+ module = cls.__module__
1436
+ qualname = cls.__qualname__ # This already contains the full nested path
1437
+
1438
+ # Combine module with qualname so the class can be resolved by direct
1439
+ # lookup, without depending on ClassRegistry having scanned the module.
1440
+ return f"{module}.{qualname}"
1441
+ # cls = obj.__class__
1442
+ # module = cls.__module__d
1443
+ #
1444
+ # # Get the full class path by walking through any nested classes
1445
+ # try:
1446
+ # class_parts = []
1447
+ # while cls:
1448
+ # class_parts.append(cls.__name__)
1449
+ # # Get the enclosing class if it exists
1450
+ # cls = cls.__qualname__.rsplit('.', 1)[0] if '.' in cls.__qualname__ else None
1451
+ # if cls:
1452
+ # # Convert string class name to actual class
1453
+ # cls = getattr(sys.modules[module], cls)
1454
+ # except (AttributeError, KeyError):
1455
+ # print(f"Error getting full class path for {obj}")
1456
+ # print(f"Module: {module}, Class: {cls}")
1457
+ # return None
1458
+ #
1459
+ # # Reverse the parts since we collected them from inner to outer
1460
+ # class_path = '.'.join([module] + class_parts[::-1])
1461
+ # return class_path
1462
+
1463
+
1464
+ def is_primitive(self, value: Any) -> bool:
1465
+ """Check if a value is a primitive type."""
1466
+ primitive_types = (
1467
+ int, float, str, bool,
1468
+ type(None), # NoneType
1469
+ )
1470
+ # Check direct primitive types
1471
+ if isinstance(value, primitive_types):
1472
+ return True
1473
+ # Check built-in collections with primitive contents
1474
+ if isinstance(value, (list, dict, set, tuple)):
1475
+ return True
1476
+ # Handle enums separately since they're technically classes
1477
+ if isinstance(value, Enum):
1478
+ return True
1479
+ return False
1480
+
1481
+ def is_class(self, value: Any) -> bool:
1482
+ """Check if a value is a class instance (non-primitive)."""
1483
+ return not self.is_primitive(value)
1484
+
1485
+ def parse_value(self, result, objects, key, value, excluded, shallow=False, depth=0):
1486
+ if depth > 10000:
1487
+ print(f"Max depth reached at key: {key} with value: {value}")
1488
+ return None
1489
+ # Handle None
1490
+ if value is None:
1491
+ return None
1492
+ # Handle enums
1493
+ if isinstance(value, tuple):
1494
+ new_list = []
1495
+ for item in value:
1496
+ if self.is_primitive(item):
1497
+ new_list.append(item)
1498
+ else:
1499
+ # print(f"dangerous tuple item found {value} {key} {type(item)}")
1500
+ # Escape to string
1501
+ new_list.append("Parse Failure")
1502
+ to_tuple = tuple(new_list)
1503
+ return to_tuple
1504
+ elif isinstance(value, Enum):
1505
+ classtype = DictConversion.get_full_class_path(value)
1506
+ enum_val = value.value
1507
+ # Only store the value if it's a simple literal type that survives
1508
+ # str/literal(str round-trip. Complex values (dicts with function refs,
1509
+ # dataclasses, etc.) are replaced with None - the name alone is sufficient.
1510
+ if not isinstance(enum_val, (int, float, str, bool, type(None))):
1511
+ enum_val = None
1512
+ results = (value.name, classtype, "Enum", enum_val)
1513
+ return results
1514
+ # Handle lists
1515
+ elif isinstance(value, Dict):
1516
+ # Loop through the dictionary and convert each item
1517
+ inner_dict = {}
1518
+ for sub_key, sub_value in value.items():
1519
+ if excluded and sub_key in excluded:
1520
+ continue
1521
+
1522
+ if sub_value is None:
1523
+ continue
1524
+
1525
+ inner_dict[sub_key] = self.parse_value(inner_dict, objects, sub_key, sub_value, excluded, shallow, depth=depth + 1)
1526
+
1527
+ if hasattr(inner_dict[sub_key], 'id') and inner_dict[sub_key].id is not None:
1528
+ if inner_dict[sub_key].id in sub_key and inner_dict[sub_key].id != sub_key:
1529
+ print(f"Warning: Key '{sub_key}' contains id '{inner_dict[sub_key].id}' {inner_dict[sub_key].__class__.__name__} but does not match exactly.")
1530
+
1531
+ return inner_dict
1532
+ elif isinstance(value, list):
1533
+ inner_list = []
1534
+ for item in value:
1535
+ inner_list.append(
1536
+ self.parse_value(result, objects, key, item, excluded, shallow, depth=depth + 1)
1537
+ )
1538
+ return inner_list
1539
+ # Handle nested objects with to_dict method
1540
+ elif hasattr(value, 'to_dict') and isinstance(value, DictConversion):
1541
+ if shallow:
1542
+ classtype = DictConversion.get_full_class_path(value)
1543
+ if hasattr(value, 'id'):
1544
+ value.id = value.id[0:8]
1545
+ results = (value.id, classtype)
1546
+ else:
1547
+ results = (id(value), classtype)
1548
+ else:
1549
+ if hasattr(value, 'id') and value.id not in objects:
1550
+ results = value.to_dict(excluded=excluded, objects=objects, shallow=False, use_references=False)
1551
+ else:
1552
+ results = None
1553
+ return results
1554
+ # Serialize function references (e.g. TabState.selected_tabs holding view
1555
+ # funcs). Not primitives, so without this they'd serialize to None.
1556
+ elif callable(value) and not isinstance(value, type):
1557
+ return DictConversion.serialize_callable(value)
1558
+ # Handle basic types
1559
+ # elif not isinstance(value, DictConversion):
1560
+ # return
1561
+ else:
1562
+ if self.is_primitive(value):
1563
+ return value
1564
+
1565
+ def update_from_dict_references(self, update_dict: dict, visited: set = None, excluded=None) -> None:
1566
+ """
1567
+ Update object attributes recursively from a dictionary, properly handling nested objects and enums.
1568
+ """
1569
+ # Create a new visited set for the root call
1570
+ is_root = visited is None
1571
+ if is_root:
1572
+ visited = set()
1573
+
1574
+ obj_id = id(self)
1575
+ if obj_id in visited or not update_dict:
1576
+ return
1577
+ visited.add(obj_id)
1578
+
1579
+ try:
1580
+ for key, new_value in update_dict.items():
1581
+ if excluded and key in excluded:
1582
+ continue
1583
+
1584
+ try:
1585
+ current_value = getattr(self, key, None)
1586
+
1587
+ # Handle Enums
1588
+ if isinstance(current_value, Enum):
1589
+ if isinstance(new_value, str):
1590
+ # Convert string to enum value
1591
+ enum_type = type(current_value)
1592
+ if self.has_valid_attr(self, key):
1593
+ setattr(self, key, enum_type[new_value])
1594
+ elif isinstance(new_value, Enum):
1595
+ if self.has_valid_attr(self, key):
1596
+ setattr(self, key, new_value)
1597
+ continue
1598
+
1599
+ # Handle nested objects
1600
+ if hasattr(current_value, 'update_from_dict') and isinstance(new_value, dict):
1601
+ current_value.update_from_dict(new_value, visited, excluded)
1602
+
1603
+ # Handle lists
1604
+ elif isinstance(current_value, list) and isinstance(new_value, list):
1605
+ self._update_list(current_value, [], visited, excluded)
1606
+
1607
+ # Handle dictionaries
1608
+ elif isinstance(current_value, dict) and isinstance(new_value, dict):
1609
+ self._update_dict(current_value, copy(new_value), visited, excluded)
1610
+ to_delete = []
1611
+ for a_key in current_value.keys():
1612
+ if a_key not in new_value:
1613
+ to_delete.append(a_key)
1614
+ for a_key in to_delete:
1615
+ del current_value[a_key]
1616
+
1617
+ # Direct update for non-container types
1618
+ else:
1619
+ if self.has_valid_attr(self, key):
1620
+ setattr(self, key, new_value)
1621
+
1622
+ except Exception as e:
1623
+ print(f"Error updating {key}: {str(e)}")
1624
+ # Print stack trace for debugging
1625
+ import traceback
1626
+ traceback.print_exc()
1627
+ finally:
1628
+ # Clean up visited set when we're done with the root update
1629
+ if is_root:
1630
+ visited.clear()
1631
+
1632
+ def update_from_dict(self, update_dict: dict, visited: set = None, excluded=None) -> None:
1633
+ """
1634
+ Update object attributes recursively from a dictionary, properly handling nested objects and enums.
1635
+ """
1636
+ # Create a new visited set on the root call
1637
+ is_root = visited is None
1638
+ if is_root:
1639
+ visited = set()
1640
+
1641
+ obj_id = id(self)
1642
+ if obj_id in visited or not update_dict:
1643
+ return
1644
+ visited.add(obj_id)
1645
+
1646
+ try:
1647
+ for key, new_value in update_dict.items():
1648
+ if excluded and key in excluded:
1649
+ continue
1650
+
1651
+ try:
1652
+ current_value = getattr(self, key, None)
1653
+
1654
+ # Handle Enums
1655
+ if isinstance(current_value, Enum):
1656
+ if isinstance(new_value, str):
1657
+ # Convert string to enum value
1658
+ enum_type = type(current_value)
1659
+ if self.has_valid_attr(self, key):
1660
+ setattr(self, key, enum_type[new_value])
1661
+ elif isinstance(new_value, Enum):
1662
+ if self.has_valid_attr(self, key):
1663
+ setattr(self, key, new_value)
1664
+ continue
1665
+
1666
+ # Handle nested objects
1667
+ if hasattr(current_value, 'update_from_dict') and isinstance(new_value, dict):
1668
+ current_value.update_from_dict(new_value, visited, excluded)
1669
+
1670
+ # Handle lists
1671
+ elif isinstance(current_value, list) and isinstance(new_value, list):
1672
+ self._update_list(current_value, copy(new_value), visited, excluded)
1673
+
1674
+ # Handle dictionaries
1675
+ elif isinstance(current_value, dict) and isinstance(new_value, dict):
1676
+ self._update_dict(current_value, copy(new_value), visited, excluded)
1677
+ to_delete = []
1678
+ for a_key in current_value.keys():
1679
+ if a_key not in new_value:
1680
+ to_delete.append(a_key)
1681
+ for a_key in to_delete:
1682
+ del current_value[a_key]
1683
+
1684
+ # Direct update for non-container values
1685
+ else:
1686
+ if self.has_valid_attr(self, key):
1687
+ setattr(self, key, new_value)
1688
+
1689
+ except Exception as e:
1690
+ print(f"Error updating {key}: {str(e)}")
1691
+ # Print stack trace for debugging
1692
+ import traceback
1693
+ traceback.print_exc()
1694
+ finally:
1695
+ # Clean up visited set when we're done with the root update
1696
+ if is_root:
1697
+ visited.clear()
1698
+
1699
+ def _update_list(self, current_list: list, new_list: list, visited: set, excluded=None) -> None:
1700
+ """Helper method to update list items recursively."""
1701
+ # Determine the length difference
1702
+ current_length = len(current_list)
1703
+ new_length = len(new_list)
1704
+
1705
+ # Update existing items
1706
+ for i in range(min(current_length, new_length)):
1707
+ current_item = current_list[i]
1708
+ new_item = new_list[i]
1709
+
1710
+ # Handle Enums
1711
+ if isinstance(current_item, Enum):
1712
+ if isinstance(new_item, str):
1713
+ enum_type = type(current_item)
1714
+ current_list[i] = enum_type[new_item]
1715
+ elif isinstance(new_item, Enum):
1716
+ current_list[i] = new_item
1717
+ continue
1718
+
1719
+ # Handle updatable objects
1720
+ if hasattr(current_item, 'update_from_dict') and isinstance(new_item, dict):
1721
+ current_item.update_from_dict(new_item, visited, excluded)
1722
+ # Handle nested lists
1723
+ elif isinstance(current_item, list) and isinstance(new_item, list):
1724
+ self._update_list(current_item, new_item, visited, excluded)
1725
+ # Handle nested dicts
1726
+ elif isinstance(current_item, dict) and isinstance(new_item, dict):
1727
+ self._update_dict(current_item, new_item, visited, excluded)
1728
+ # Direct update
1729
+ else:
1730
+ current_list[i] = new_item
1731
+
1732
+ # Handle any additional items in new_list
1733
+ if new_length > current_length:
1734
+ # Determine the type of items in the list if any exist
1735
+ item_type = None
1736
+ if current_list:
1737
+ for item in current_list:
1738
+ if hasattr(item, 'update_from_dict'):
1739
+ item_type = type(item)
1740
+ break
1741
+
1742
+ # Add new items
1743
+ for i in range(current_length, new_length):
1744
+ new_item = new_list[i]
1745
+ if isinstance(new_item, dict) and item_type is not None:
1746
+ try:
1747
+ # Create new instance without initialization
1748
+ new_instance = item_type.__new__(item_type)
1749
+ # Initialize with empty/default values
1750
+ if hasattr(new_instance, '__init__'):
1751
+ new_instance.__init__()
1752
+ # Then update with the dictionary values
1753
+ new_instance.update_from_dict(new_item, visited, excluded)
1754
+ current_list.append(new_instance)
1755
+ except Exception as e:
1756
+ raise Exception(f"Failed to create new instance of {item_type.__name__}: {str(e)}")
1757
+ else:
1758
+ current_list.append(new_item)
1759
+
1760
+ # Remove any extra items if new list is shorter
1761
+ while len(current_list) > new_length:
1762
+ current_list.pop()
1763
+
1764
+ def _update_dict(self, current_dict: dict, new_dict: dict, visited: set, excluded=None) -> None:
1765
+ """Helper method to update dictionary values recursively."""
1766
+ not_in_new_dict = set()
1767
+ for key, new_value in new_dict.items():
1768
+ if (excluded and key in excluded):
1769
+ continue
1770
+
1771
+ current_value = current_dict.get(key)
1772
+ if current_value is None:
1773
+ # Get any existing value to use as a template
1774
+ any_key = next(iter(current_dict.keys()), None)
1775
+ current_value = current_dict.get(any_key, None)
1776
+
1777
+
1778
+ # Handle Enums in dictionaries
1779
+ if isinstance(current_value, Enum):
1780
+ if isinstance(new_value, str):
1781
+ enum_type = type(current_value)
1782
+ current_dict[key] = enum_type[new_value]
1783
+ elif isinstance(new_value, Enum):
1784
+ current_dict[key] = new_value
1785
+ continue
1786
+
1787
+ # Handle updatable objects
1788
+ if hasattr(current_value, 'update_from_dict') and isinstance(new_value, dict):
1789
+ item_type = None
1790
+ try:
1791
+ item_type = type(current_value)
1792
+
1793
+ if item_type is not None:
1794
+ # Create new instance without initialization
1795
+ new_instance = item_type.__new__(item_type)
1796
+ # Initialize with empty/default values
1797
+ if hasattr(new_instance, '__init__'):
1798
+ new_instance.__init__()
1799
+ # Then update with the dictionary values
1800
+ new_instance.update_from_dict(new_value, visited, excluded)
1801
+ current_dict[key] = new_instance
1802
+ except Exception as e:
1803
+ raise Exception(f"Failed to create new instance of {item_type.__name__}: {str(e)}")
1804
+
1805
+ # Handle nested lists
1806
+ elif isinstance(current_value, list) and isinstance(new_value, list):
1807
+ self._update_list(current_value, new_value, visited, excluded)
1808
+
1809
+ # Handle nested dicts
1810
+ elif isinstance(current_value, dict) and isinstance(new_value, dict):
1811
+ self._update_dict(current_value, new_value, visited, excluded)
1812
+
1813
+ # Direct update
1814
+ else:
1815
+ current_dict[key] = new_value