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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- 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
|