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 @@
|
|
|
1
|
+
"""Shared framework conversion machinery."""
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bubbling proxies — make a nested mutation notify a root object.
|
|
3
|
+
|
|
4
|
+
A RenderHost only intercepts writes to its OWN keys. A mutation to a nested
|
|
5
|
+
container — `host['cfg']['rank'] = 16`, or editing a `GeneralParse` you grabbed a
|
|
6
|
+
reference to — runs through THAT child's methods, which the host never sees. So a
|
|
7
|
+
deep edit silently fails to mark the host dirty, and its file-IO / state handling
|
|
8
|
+
doesn't react.
|
|
9
|
+
|
|
10
|
+
`install_bubbling(value, root)` walks a value tree and upgrades every mutable
|
|
11
|
+
container so a mutation at ANY depth calls `root._mark_changed()`. With it, the
|
|
12
|
+
host reflects (and an editor like code_file_io saves) deep edits with no manual
|
|
13
|
+
signal — the stateful handling becomes invisible.
|
|
14
|
+
|
|
15
|
+
Two type-preserving upgrade paths:
|
|
16
|
+
|
|
17
|
+
• A dict / list SUBCLASS that carries a __dict__ (GeneralParse, Conditional, Loop,
|
|
18
|
+
CallParse, …) is upgraded IN PLACE via `__class__` reassignment to a generated
|
|
19
|
+
`(mixin, base)` subclass. Same object, same identity, same attributes, and
|
|
20
|
+
`isinstance(x, GeneralParse)` still holds — so a reference grabbed BEFORE
|
|
21
|
+
adoption bubbles too, and the converters / renderers (all isinstance/MRO based)
|
|
22
|
+
keep working.
|
|
23
|
+
|
|
24
|
+
• A PLAIN dict / list (no __dict__, so `__class__` can't be reassigned) is replaced
|
|
25
|
+
by a constructed bubbling copy, written back into its parent.
|
|
26
|
+
|
|
27
|
+
The framework resolves `@defaults` kwargs by EXACT type (not MRO — see
|
|
28
|
+
core_render.py's `default_kwargs_by_type[type(input_value)]`), so a generated
|
|
29
|
+
subclass would otherwise lose its base's tint / included / disable_scroll defaults.
|
|
30
|
+
Each generated subclass therefore mirrors the base's per-class registry entries.
|
|
31
|
+
|
|
32
|
+
Cycle- and idempotency-safe: re-installing an already-bubbling tree just re-points
|
|
33
|
+
the root and recurses into any fresh subtrees.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from collections import deque
|
|
37
|
+
|
|
38
|
+
from meltygui.core.melty import Melty
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Lazy deep attribute traversal ──────────────────────────────────────────────
|
|
42
|
+
#
|
|
43
|
+
# A converted value tree is full of redundant wrapper rungs:
|
|
44
|
+
#
|
|
45
|
+
# render_func_dict["value"]["draw_collection"]["decorators"]["render_func"]
|
|
46
|
+
#
|
|
47
|
+
# Only "decorators" and "render_func" carry meaning - "value" and "draw_collection"
|
|
48
|
+
# are bookkeeping the converter had to spell out and guard (`if "value" in d: ...`).
|
|
49
|
+
# The `.deep` accessor lets you name ONLY the rungs you care about and finds the rest:
|
|
50
|
+
#
|
|
51
|
+
# func_dict.deep.decorators.render_func() # == the line above, without guards
|
|
52
|
+
#
|
|
53
|
+
# It is reached via an EXPLICIT `.deep` property - NOT a blanket __getattr__ on
|
|
54
|
+
# the container. A blanket __getattr__ would answer EVERY missing-attribute probe the
|
|
55
|
+
# framework makes (`hasattr(d, 'children')`, `getattr(d, 'x', default)`, copy/pickle
|
|
56
|
+
# dunders) with a stand-in object instead of the AttributeError those call sites
|
|
57
|
+
# expect - so MISSING/proxy values leak everywhere. `.deep` is opt-in: normal
|
|
58
|
+
# attribute access on the container is still completely unaffected.
|
|
59
|
+
#
|
|
60
|
+
# `.deep.a.b` accumulates the path lazily; calling it (`()`) resolves the WHOLE path
|
|
61
|
+
# at once with backtracking, so a name that matches a dead-end branch (one lacking the
|
|
62
|
+
# next rung) doesn't terminate the lookup - it tries the next candidate. Resolution
|
|
63
|
+
# returns the real value (ready to hand to draw_collection), or MISSING.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Missing:
|
|
67
|
+
"""The result of a deep lookup that found nothing.
|
|
68
|
+
|
|
69
|
+
A null object so a resolved chain is easy to test: it's falsy, iterates empty,
|
|
70
|
+
has len 0, and any further attr/index access returns itself. Underscore names
|
|
71
|
+
still raise AttributeError so it can't masquerade as having dunder/protocol
|
|
72
|
+
methods (copy/pickle/etc.)."""
|
|
73
|
+
|
|
74
|
+
__slots__ = ()
|
|
75
|
+
|
|
76
|
+
def __getattr__(self, name):
|
|
77
|
+
if name.startswith("_"):
|
|
78
|
+
raise AttributeError(name)
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __getitem__(self, key):
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __call__(self, *args, **kwargs):
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def __iter__(self):
|
|
88
|
+
return iter(())
|
|
89
|
+
|
|
90
|
+
def __len__(self):
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
def __bool__(self):
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def __contains__(self, item):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def __repr__(self):
|
|
100
|
+
return "<missing>"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
MISSING = _Missing()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _iter_matches(node, name):
|
|
107
|
+
"""Yield every value stored under key `name` at or below `node`, breadth-first
|
|
108
|
+
(shallowest first), descending through nested dicts/lists. Internal `__…` keys
|
|
109
|
+
are neither matched nor descended."""
|
|
110
|
+
queue = deque([node])
|
|
111
|
+
seen = set()
|
|
112
|
+
while queue:
|
|
113
|
+
cur = queue.popleft()
|
|
114
|
+
cid = id(cur)
|
|
115
|
+
if cid in seen:
|
|
116
|
+
continue
|
|
117
|
+
seen.add(cid)
|
|
118
|
+
if isinstance(cur, dict):
|
|
119
|
+
if name in cur and not _is_internal_key(name):
|
|
120
|
+
yield dict.__getitem__(cur, name)
|
|
121
|
+
for k, v in cur.items():
|
|
122
|
+
if not _is_internal_key(k) and isinstance(v, (dict, list)):
|
|
123
|
+
queue.append(v)
|
|
124
|
+
elif isinstance(cur, list):
|
|
125
|
+
for v in cur:
|
|
126
|
+
if isinstance(v, (dict, list)):
|
|
127
|
+
queue.append(v)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _resolve_chain(node, names):
|
|
131
|
+
"""Resolve `names` from `node` with backtracking. For each rung, try every place
|
|
132
|
+
the name matches (shallowest first); recurse for the rest, and only accept a
|
|
133
|
+
candidate whose subtree can satisfy the REMAINING rungs. So a repeated key on a
|
|
134
|
+
branch that lacks the next rung is skipped rather than dead-ending the lookup.
|
|
135
|
+
Returns the matched value, or MISSING when no full path exists."""
|
|
136
|
+
if not names:
|
|
137
|
+
return node
|
|
138
|
+
first, rest = names[0], names[1:]
|
|
139
|
+
for cand in _iter_matches(node, first):
|
|
140
|
+
result = _resolve_chain(cand, rest)
|
|
141
|
+
if result is not MISSING:
|
|
142
|
+
return result
|
|
143
|
+
return MISSING
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def deep_get(container, *names):
|
|
147
|
+
"""Resolve a path of `names` through `container`, skipping redundant wrapper rungs
|
|
148
|
+
and backtracking past dead-end branches (see `_resolve_chain`). The function form
|
|
149
|
+
of the `.deep` accessor: `deep_get(host, "decorators", "render_func")`."""
|
|
150
|
+
return _resolve_chain(container, names)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _resolve_all(node, names, out, seen):
|
|
154
|
+
"""Collect EVERY value reachable by `names` from `node` (all branches, not just the
|
|
155
|
+
first backtracked hit), deduped by identity. Empty `names` → `node` itself."""
|
|
156
|
+
if not names:
|
|
157
|
+
if id(node) not in seen:
|
|
158
|
+
seen.add(id(node))
|
|
159
|
+
out.append(node)
|
|
160
|
+
return
|
|
161
|
+
first, rest = names[0], names[1:]
|
|
162
|
+
for cand in _iter_matches(node, first):
|
|
163
|
+
_resolve_all(cand, rest, out, seen)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _iter_leaves(node, prefix, seen):
|
|
167
|
+
"""Yield `(path_tuple, value)` for every LEAF (non-dict/list) at or below `node`,
|
|
168
|
+
descending dicts/lists. Internal `__…` keys are skipped. Cycle-safe."""
|
|
169
|
+
nid = id(node)
|
|
170
|
+
if nid in seen:
|
|
171
|
+
return
|
|
172
|
+
if isinstance(node, dict):
|
|
173
|
+
seen.add(nid)
|
|
174
|
+
for k, v in node.items():
|
|
175
|
+
if _is_internal_key(k):
|
|
176
|
+
continue
|
|
177
|
+
if isinstance(v, (dict, list)):
|
|
178
|
+
yield from _iter_leaves(v, prefix + (k,), seen)
|
|
179
|
+
else:
|
|
180
|
+
yield prefix + (k,), v
|
|
181
|
+
elif isinstance(node, list):
|
|
182
|
+
seen.add(nid)
|
|
183
|
+
for i, v in enumerate(node):
|
|
184
|
+
if isinstance(v, (dict, list)):
|
|
185
|
+
yield from _iter_leaves(v, prefix + (i,), seen)
|
|
186
|
+
else:
|
|
187
|
+
yield prefix + (i,), v
|
|
188
|
+
else:
|
|
189
|
+
yield prefix, node
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def deep_all(container, *names):
|
|
193
|
+
"""Every value matching `names` across ALL branches (the multi-hit form of
|
|
194
|
+
`deep_get`). With no names → every leaf value in the whole tree. Function form of
|
|
195
|
+
`host.deep.all()` / `host.deep.<names>.all()`."""
|
|
196
|
+
if not names:
|
|
197
|
+
return [v for _, v in _iter_leaves(container, (), set())]
|
|
198
|
+
out = []
|
|
199
|
+
_resolve_all(container, names, out, set())
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class _DeepPath:
|
|
204
|
+
"""Lazy, backtracking path builder returned by `.deep`. Each `.name` / `[name]`
|
|
205
|
+
appends a rung WITHOUT resolving; calling it (`path()`) resolves the whole chain
|
|
206
|
+
at once against the root, so backtracking can see the full path. Underscore names
|
|
207
|
+
raise AttributeError so it stays invisible to attribute/copy/pickle probes."""
|
|
208
|
+
|
|
209
|
+
__slots__ = ("_root", "_names")
|
|
210
|
+
|
|
211
|
+
def __init__(self, root, names=()):
|
|
212
|
+
object.__setattr__(self, "_root", root)
|
|
213
|
+
object.__setattr__(self, "_names", names)
|
|
214
|
+
|
|
215
|
+
def __getattr__(self, name):
|
|
216
|
+
if name.startswith("_"):
|
|
217
|
+
raise AttributeError(name)
|
|
218
|
+
return _DeepPath(self._root, self._names + (name,))
|
|
219
|
+
|
|
220
|
+
def __getitem__(self, key):
|
|
221
|
+
return _DeepPath(self._root, self._names + (key,))
|
|
222
|
+
|
|
223
|
+
def __call__(self):
|
|
224
|
+
return _resolve_chain(self._root, self._names)
|
|
225
|
+
|
|
226
|
+
def all(self):
|
|
227
|
+
"""Every value matching the accumulated path, across ALL branches — the
|
|
228
|
+
multi-hit counterpart to `()` (which returns just the first). With NO path
|
|
229
|
+
(`host.deep.all()`) → every leaf value in the whole tree: "I don't know the
|
|
230
|
+
path, give me everything". `host.deep.params.all()` → every `params` anywhere."""
|
|
231
|
+
return deep_all(self._root, *self._names)
|
|
232
|
+
|
|
233
|
+
def unwrap(self):
|
|
234
|
+
"""The structure-preserving "everything": descend past redundant single-entry
|
|
235
|
+
wrapper rungs (`value`, the fn-name level, …) and return the first MEANINGFUL
|
|
236
|
+
container — the dict/list that actually holds the content — with its KEYS
|
|
237
|
+
intact, ready for draw_collection. Use this (not `.all()`) to render a whole
|
|
238
|
+
subtree when you don't know the path: `.all()` flattens to a list of leaf
|
|
239
|
+
values (so a labelled `tint=(...)` becomes a positional list entry); `.unwrap()`
|
|
240
|
+
keeps it a dict. Stops at the first branch (>1 child) or leaf-bearing level."""
|
|
241
|
+
node = self.__call__()
|
|
242
|
+
seen = set()
|
|
243
|
+
while isinstance(node, dict) and id(node) not in seen:
|
|
244
|
+
seen.add(id(node))
|
|
245
|
+
keys = [k for k in node.keys() if not _is_internal_key(k)]
|
|
246
|
+
if len(keys) != 1:
|
|
247
|
+
break # branch or empty → this is the content
|
|
248
|
+
child = dict.__getitem__(node, keys[0])
|
|
249
|
+
if not isinstance(child, (dict, list)):
|
|
250
|
+
break # single child is a leaf → keep the dict
|
|
251
|
+
node = child
|
|
252
|
+
return node
|
|
253
|
+
|
|
254
|
+
def items(self):
|
|
255
|
+
"""Every leaf as `(path_tuple, value)` so you can see WHERE each came from.
|
|
256
|
+
Spans all branches the path resolves to (the whole tree for an empty path)."""
|
|
257
|
+
roots = self.all() if self._names else [self._root]
|
|
258
|
+
out = []
|
|
259
|
+
for node in roots:
|
|
260
|
+
if node is MISSING:
|
|
261
|
+
continue
|
|
262
|
+
out.extend(_iter_leaves(node, (), set()))
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
def __bool__(self):
|
|
266
|
+
# `if host.a.b:` resolves and tests the result - no explicit call needed. Also
|
|
267
|
+
# the safety net for a bad probe that leaks here: an unresolved/empty chain
|
|
268
|
+
# reads as falsy, so framework code treats a leaked proxy like None.
|
|
269
|
+
result = self.__call__()
|
|
270
|
+
return result is not MISSING and bool(result)
|
|
271
|
+
|
|
272
|
+
def __iter__(self):
|
|
273
|
+
result = self.__call__()
|
|
274
|
+
return iter(result) if result is not MISSING else iter(())
|
|
275
|
+
|
|
276
|
+
def __len__(self):
|
|
277
|
+
result = self.__call__()
|
|
278
|
+
try:
|
|
279
|
+
return len(result) if result is not MISSING else 0
|
|
280
|
+
except TypeError:
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
def __contains__(self, item):
|
|
284
|
+
result = self.__call__()
|
|
285
|
+
try:
|
|
286
|
+
return result is not MISSING and item in result
|
|
287
|
+
except TypeError:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def __repr__(self):
|
|
291
|
+
return f"<deep {'.'.join(map(str, self._names))} -> {self.__call__()!r}>"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class _DeepAttrMixin:
|
|
295
|
+
"""Gives a dict/list container a `.deep` entry point for safe path traversal.
|
|
296
|
+
|
|
297
|
+
`.deep` is a real attribute (a property), so it never interferes with normal
|
|
298
|
+
attribute lookup, `getattr(x, name, default)`, `hasattr`, or copy/pickle — those
|
|
299
|
+
all behave exactly as before. Only `host.deep.<names>()` runs the deep search."""
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def deep(self):
|
|
303
|
+
return _DeepPath(self)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# base type -> generated bubbling subclass (one per concrete type, cached)
|
|
307
|
+
_BUBBLING_DICT_CLASSES = {}
|
|
308
|
+
_BUBBLING_LIST_CLASSES = {}
|
|
309
|
+
|
|
310
|
+
# Melty registries looked up by EXACT class (no MRO lookup), so a bubbling subclass
|
|
311
|
+
# must inherit the base's entries or it loses the base's @defaults / annotations.
|
|
312
|
+
_CLASS_KEYED_REGISTRIES = (
|
|
313
|
+
"default_kwargs_by_type",
|
|
314
|
+
"default_kwargs_by_attrib_type",
|
|
315
|
+
"default_funcs_by_name_type",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _mirror_registries(base, bubbling):
|
|
320
|
+
"""Point a generated bubbling subclass at the SAME per-class registry entries as
|
|
321
|
+
its base, so exact-type lookups (@defaults kwargs, per-attr overrides) still
|
|
322
|
+
resolve. Share the references — base entries are import-time and don't change."""
|
|
323
|
+
for reg_name in _CLASS_KEYED_REGISTRIES:
|
|
324
|
+
reg = getattr(Melty, reg_name, None)
|
|
325
|
+
if reg is not None and base in reg: # `in` on a registry: no autocreate
|
|
326
|
+
reg[bubbling] = reg[base]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def base_of_bubbling(t):
|
|
330
|
+
"""A generated bubbling subclass (`Bubbling_<Base>`, made by `type(...)` at runtime)
|
|
331
|
+
has NO source of its own, so `inspect.getfile`/`getsourcelines` on it raises "could
|
|
332
|
+
not find class definition". It's an implementation detail that should behave like its
|
|
333
|
+
base everywhere — the registry mirroring already does this for `@defaults`; this does
|
|
334
|
+
it for SOURCE resolution. Return the real base (the non-mixin entry of `__bases__`,
|
|
335
|
+
since the subclass is `(mixin, base)`); pass non-bubbling types through unchanged."""
|
|
336
|
+
if isinstance(t, type) and issubclass(t, (_BubblingDictMixin, _BubblingListMixin)):
|
|
337
|
+
for b in t.__bases__:
|
|
338
|
+
if not (isinstance(b, type) and issubclass(b, (_BubblingDictMixin, _BubblingListMixin))):
|
|
339
|
+
return b
|
|
340
|
+
return t
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _notify(node):
|
|
344
|
+
root = getattr(node, "_bubble_root", None)
|
|
345
|
+
if root is not None:
|
|
346
|
+
root._mark_changed()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# Immutable SCALAR leaf types. imgui widgets hand back a FRESH object each frame for
|
|
350
|
+
# these (a slider returns a new int/float; ints outside CPython's -5..256 cache aren't
|
|
351
|
+
# interned), so an identity (`is`) check sees a spurious "change" and re-fires notify -
|
|
352
|
+
# which, through the proxy round-trip, becomes a Tree↔String save feedback loop. An `==`
|
|
353
|
+
# on a scalar leaf is O(1) and BOUNDED (its own size, the rendering cost paid), so
|
|
354
|
+
# it's safe here - unlike a deep `==` over a whole container/GeneralParse tree, which is
|
|
355
|
+
# what doesn't scale. Containers fall through to identity (they're edited in place).
|
|
356
|
+
_SCALAR_TYPES = frozenset({int, float, complex, bool, str, bytes, type(None)})
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _unchanged_leaf(cur, value):
|
|
360
|
+
"""True if writing `value` over `cur` is a no-op: the SAME object, or an equal
|
|
361
|
+
immutable scalar (a reconstructed-but-equal leaf). Containers → identity only."""
|
|
362
|
+
return cur is value or (type(value) in _SCALAR_TYPES and cur == value)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _is_internal_key(key):
|
|
366
|
+
"""`__…` keys are framework bookkeeping, NOT user data — the editor already
|
|
367
|
+
excludes them (`excluded=["__cst__"]`, draw skips `__`-prefixed). Writing them
|
|
368
|
+
must NOT mark the host dirty: dict_to_cst_module's `leave_ClassDef` caches the
|
|
369
|
+
rebuilt node as `edit_dict["__cst__"] = node` while SERIALIZING the tree, and if
|
|
370
|
+
that re-dirties the proxy, every save triggers a serialize that triggers another
|
|
371
|
+
save — an infinite loop. So bubbling sets these raw and never notifies."""
|
|
372
|
+
return isinstance(key, str) and key.startswith("__")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ── Mixins (front of the MRO) - bubble every mutation, keep new children bubbling ──
|
|
376
|
+
class _BubblingDictMixin(_DeepAttrMixin):
|
|
377
|
+
_bubble_root = None
|
|
378
|
+
|
|
379
|
+
def __setitem__(self, key, value):
|
|
380
|
+
if _is_internal_key(key):
|
|
381
|
+
super().__setitem__(key, value) # bookkeeping write, no bubble
|
|
382
|
+
return
|
|
383
|
+
# Re-assigning an unchanged child is not an edit (draw_collection writes the
|
|
384
|
+
# rendered child back every frame) - store it, but don't notify / loop. Identity
|
|
385
|
+
# for containers (edited in place → same object), O(1) `==` for scalar leaves
|
|
386
|
+
# (imgui reconstructs them, so `is` would spuriously fire). See _unchanged_leaf.
|
|
387
|
+
unchanged = key in self and _unchanged_leaf(dict.__getitem__(self, key), value)
|
|
388
|
+
root = self._bubble_root
|
|
389
|
+
if root is not None:
|
|
390
|
+
value = install_bubbling(value, root)
|
|
391
|
+
super().__setitem__(key, value)
|
|
392
|
+
if not unchanged:
|
|
393
|
+
_notify(self)
|
|
394
|
+
|
|
395
|
+
def __delitem__(self, key):
|
|
396
|
+
print("DEL", key)
|
|
397
|
+
internal = _is_internal_key(key)
|
|
398
|
+
super().__delitem__(key)
|
|
399
|
+
if not internal:
|
|
400
|
+
_notify(self)
|
|
401
|
+
|
|
402
|
+
def update(self, *args, **kwargs):
|
|
403
|
+
super().update(*args, **kwargs)
|
|
404
|
+
root = self._bubble_root
|
|
405
|
+
if root is not None:
|
|
406
|
+
_reinstall_children(self, root)
|
|
407
|
+
_notify(self)
|
|
408
|
+
|
|
409
|
+
def setdefault(self, key, default=None):
|
|
410
|
+
if key in self:
|
|
411
|
+
return super().__getitem__(key)
|
|
412
|
+
root = self._bubble_root
|
|
413
|
+
if root is not None:
|
|
414
|
+
default = install_bubbling(default, root)
|
|
415
|
+
super().__setitem__(key, default)
|
|
416
|
+
_notify(self)
|
|
417
|
+
return default
|
|
418
|
+
|
|
419
|
+
def pop(self, *args):
|
|
420
|
+
had = (not args) or args[0] in self
|
|
421
|
+
result = super().pop(*args)
|
|
422
|
+
if had:
|
|
423
|
+
_notify(self)
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
def popitem(self):
|
|
427
|
+
result = super().popitem()
|
|
428
|
+
_notify(self)
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
def clear(self):
|
|
432
|
+
had = bool(self)
|
|
433
|
+
super().clear()
|
|
434
|
+
if had:
|
|
435
|
+
_notify(self)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class _BubblingListMixin(_DeepAttrMixin):
|
|
439
|
+
_bubble_root = None
|
|
440
|
+
|
|
441
|
+
def __setitem__(self, idx, value):
|
|
442
|
+
# Unchanged element is not an edit → store but don't notify. Identity for containers,
|
|
443
|
+
# O(1) `==` for scalar leaves (imgui hands back a new int/float each frame, so a
|
|
444
|
+
# `is` check spuriously fires → the list-element feedback loop). See _unchanged_leaf.
|
|
445
|
+
unchanged = not isinstance(idx, slice) and _unchanged_leaf(list.__getitem__(self, idx), value)
|
|
446
|
+
root = self._bubble_root
|
|
447
|
+
if root is not None:
|
|
448
|
+
value = ([install_bubbling(v, root) for v in value]
|
|
449
|
+
if isinstance(idx, slice) else install_bubbling(value, root))
|
|
450
|
+
super().__setitem__(idx, value)
|
|
451
|
+
if not unchanged:
|
|
452
|
+
_notify(self)
|
|
453
|
+
|
|
454
|
+
def __delitem__(self, idx):
|
|
455
|
+
super().__delitem__(idx)
|
|
456
|
+
_notify(self)
|
|
457
|
+
|
|
458
|
+
def append(self, value):
|
|
459
|
+
root = self._bubble_root
|
|
460
|
+
if root is not None:
|
|
461
|
+
value = install_bubbling(value, root)
|
|
462
|
+
super().append(value)
|
|
463
|
+
_notify(self)
|
|
464
|
+
|
|
465
|
+
def extend(self, iterable):
|
|
466
|
+
root = self._bubble_root
|
|
467
|
+
if root is not None:
|
|
468
|
+
iterable = [install_bubbling(v, root) for v in iterable]
|
|
469
|
+
super().extend(iterable)
|
|
470
|
+
_notify(self)
|
|
471
|
+
|
|
472
|
+
def insert(self, idx, value):
|
|
473
|
+
root = self._bubble_root
|
|
474
|
+
if root is not None:
|
|
475
|
+
value = install_bubbling(value, root)
|
|
476
|
+
super().insert(idx, value)
|
|
477
|
+
_notify(self)
|
|
478
|
+
|
|
479
|
+
def pop(self, *args):
|
|
480
|
+
result = super().pop(*args)
|
|
481
|
+
_notify(self)
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
def remove(self, value):
|
|
485
|
+
super().remove(value)
|
|
486
|
+
_notify(self)
|
|
487
|
+
|
|
488
|
+
def clear(self):
|
|
489
|
+
had = bool(self)
|
|
490
|
+
super().clear()
|
|
491
|
+
if had:
|
|
492
|
+
_notify(self)
|
|
493
|
+
|
|
494
|
+
def __iadd__(self, other):
|
|
495
|
+
self.extend(other)
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class _BubblingDict(_BubblingDictMixin, dict):
|
|
500
|
+
"""Bubbling replacement for a PLAIN dict (which can't be upgraded in place)."""
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class _BubblingList(_BubblingListMixin, list):
|
|
504
|
+
"""Bubbling replacement for a PLAIN list."""
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _bubbling_dict_class_for(base):
|
|
508
|
+
cls = _BUBBLING_DICT_CLASSES.get(base)
|
|
509
|
+
if cls is None:
|
|
510
|
+
cls = type(f"Bubbling_{base.__name__}", (_BubblingDictMixin, base), {})
|
|
511
|
+
_BUBBLING_DICT_CLASSES[base] = cls
|
|
512
|
+
_mirror_registries(base, cls)
|
|
513
|
+
return cls
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _bubbling_list_class_for(base):
|
|
517
|
+
cls = _BUBBLING_LIST_CLASSES.get(base)
|
|
518
|
+
if cls is None:
|
|
519
|
+
cls = type(f"Bubbling_{base.__name__}", (_BubblingListMixin, base), {})
|
|
520
|
+
_BUBBLING_LIST_CLASSES[base] = cls
|
|
521
|
+
_mirror_registries(base, cls)
|
|
522
|
+
return cls
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _reinstall_children(node, root, _seen=None):
|
|
526
|
+
"""Upgrade every child of an already-bubbling container, writing replacements
|
|
527
|
+
back through the BASE methods (raw dict/list ops) so we don't re-fire notify."""
|
|
528
|
+
if _seen is None:
|
|
529
|
+
_seen = set()
|
|
530
|
+
if isinstance(node, dict):
|
|
531
|
+
for k in list(dict.keys(node)):
|
|
532
|
+
child = dict.__getitem__(node, k)
|
|
533
|
+
new = install_bubbling(child, root, _seen)
|
|
534
|
+
if new is not child:
|
|
535
|
+
dict.__setitem__(node, k, new)
|
|
536
|
+
elif isinstance(node, list):
|
|
537
|
+
for i in range(len(node)):
|
|
538
|
+
child = list.__getitem__(node, i)
|
|
539
|
+
new = install_bubbling(child, root, _seen)
|
|
540
|
+
if new is not child:
|
|
541
|
+
list.__setitem__(node, i, new)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def install_bubbling(value, root, _seen=None):
|
|
545
|
+
"""Upgrade `value` and everything nested under it to bubble mutations to `root`.
|
|
546
|
+
|
|
547
|
+
Returns the bubbling value: the SAME object when it can be upgraded in place (a
|
|
548
|
+
__dict__-bearing dict/list subclass), or a constructed copy for a plain
|
|
549
|
+
dict/list (whose parent then stores the replacement). Leaves immutable / opaque
|
|
550
|
+
leaves (str, int, tuple, libcst nodes, …) untouched. Idempotent and cycle-safe."""
|
|
551
|
+
if _seen is None:
|
|
552
|
+
_seen = set()
|
|
553
|
+
vid = id(value)
|
|
554
|
+
|
|
555
|
+
# Already bubbling - just (re)point the root and recurse for fresh subtrees.
|
|
556
|
+
if isinstance(value, (_BubblingDictMixin, _BubblingListMixin)):
|
|
557
|
+
value._bubble_root = root
|
|
558
|
+
if vid not in _seen:
|
|
559
|
+
_seen.add(vid)
|
|
560
|
+
_reinstall_children(value, root, _seen)
|
|
561
|
+
return value
|
|
562
|
+
|
|
563
|
+
if isinstance(value, dict):
|
|
564
|
+
if vid in _seen:
|
|
565
|
+
return value
|
|
566
|
+
_seen.add(vid)
|
|
567
|
+
if type(value) is dict: # plain dict: can't reclass, copy
|
|
568
|
+
target = _BubblingDict()
|
|
569
|
+
dict.update(target, value)
|
|
570
|
+
else: # subclass with __dict__: in-place
|
|
571
|
+
try:
|
|
572
|
+
value.__class__ = _bubbling_dict_class_for(type(value))
|
|
573
|
+
target = value
|
|
574
|
+
except TypeError: # exotic dict that refuses reclass
|
|
575
|
+
target = _BubblingDict()
|
|
576
|
+
dict.update(target, value)
|
|
577
|
+
target._bubble_root = root
|
|
578
|
+
_reinstall_children(target, root, _seen)
|
|
579
|
+
return target
|
|
580
|
+
|
|
581
|
+
if isinstance(value, list):
|
|
582
|
+
if vid in _seen:
|
|
583
|
+
return value
|
|
584
|
+
_seen.add(vid)
|
|
585
|
+
if type(value) is list:
|
|
586
|
+
target = _BubblingList()
|
|
587
|
+
list.extend(target, value)
|
|
588
|
+
else:
|
|
589
|
+
try:
|
|
590
|
+
value.__class__ = _bubbling_list_class_for(type(value))
|
|
591
|
+
target = value
|
|
592
|
+
except TypeError:
|
|
593
|
+
target = _BubblingList()
|
|
594
|
+
list.extend(target, value)
|
|
595
|
+
target._bubble_root = root
|
|
596
|
+
_reinstall_children(target, root, _seen)
|
|
597
|
+
return target
|
|
598
|
+
|
|
599
|
+
return value
|