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,2490 @@
|
|
|
1
|
+
"""live_view() — capture a running local and key it by its CST-dict address.
|
|
2
|
+
|
|
3
|
+
User code drops a bare `live_view()` after an assignment (or `live_view(expr)`
|
|
4
|
+
anywhere) in a function body. At runtime the call resolves its own site to the
|
|
5
|
+
SAME key the libcst→dict conversion gives that statement — `("if##0",
|
|
6
|
+
"live_view()")`, not a line number — reads the preceding local for the bare
|
|
7
|
+
form, and stores the value RAW on the OWNING FUNCTION object under
|
|
8
|
+
`__live_values__`. The editor reads the store off the object it is editing
|
|
9
|
+
(`live_values_for`) and renders each value as a nested window anchored to the
|
|
10
|
+
call's inline token, so the value and the code that produced it stay linked.
|
|
11
|
+
|
|
12
|
+
Why CST keys, not line numbers: the keys (`x`, `x#1`, `live_view()#N`,
|
|
13
|
+
`if##0`) are structural — they survive the line drift between code that is
|
|
14
|
+
RUNNING (an old frame in a training loop) and source that is being edited.
|
|
15
|
+
Keys are relative to the STORE OBJECT's scope (the segments after the owning
|
|
16
|
+
def's own "locals"), so a nested def's sites stay distinct on the outer
|
|
17
|
+
function they attach to, and the editor's span parse sees the same paths.
|
|
18
|
+
Statements the dict conversion doesn't surface (while/with/match bodies,
|
|
19
|
+
nested defs) fall back to a `line:N`-qualified key — still distinct per site,
|
|
20
|
+
just line-stable instead of edit-stable.
|
|
21
|
+
|
|
22
|
+
Why the function object: hotswap mutates `func.__code__` in place
|
|
23
|
+
(file_converters._recompile) and never rebinds the module var, so the function
|
|
24
|
+
object — and this store — keeps its identity across recompiles. The capture
|
|
25
|
+
side and the editor side converge on the same object through the same
|
|
26
|
+
resolver (`_enclosing_function`). Class-body and module-level sites attach to
|
|
27
|
+
the module instead (a class-body frame is not CO_OPTIMIZED — see
|
|
28
|
+
_resolve_site).
|
|
29
|
+
|
|
30
|
+
Two parses, both bounded: a C-speed `ast` parse of the file finds the
|
|
31
|
+
enclosing top-level statement and the bare form's preceding assignment (ast
|
|
32
|
+
sees aug-assigns, tuple unpacks, while/with bodies — everything the display
|
|
33
|
+
dict elides), then the libcst→dict pass runs on JUST that statement's span, so
|
|
34
|
+
a save of a large file costs O(function), not the whole-file position pass
|
|
35
|
+
that chain_converters measured at ~1s of GIL stall.
|
|
36
|
+
|
|
37
|
+
Capture must be hot-loop cheap: everything line-dependent is resolved ONCE per
|
|
38
|
+
(code object, line) and cached BY CODE OBJECT IDENTITY — the steady-state path
|
|
39
|
+
is two dict lookups, no stat / parse / sys.modules walk. A hotswap installs a
|
|
40
|
+
new code object, whose first live_view call re-resolves against the file; the
|
|
41
|
+
old code object's sites evict via weakref.finalize.
|
|
42
|
+
|
|
43
|
+
Known limit: a frame still executing OLD code after a hotswap-without-save
|
|
44
|
+
reports linenos that may not match the file on disk, so a site first resolved
|
|
45
|
+
after that point can mis-key until the function re-enters. Sites resolved
|
|
46
|
+
before the swap are cached and stay correct.
|
|
47
|
+
|
|
48
|
+
Loop sites ACCUMULATE: a site enclosed by for/while loops (statically known —
|
|
49
|
+
from the injected snapshot's loop context or the site resolution's own ast
|
|
50
|
+
walk) folds each publish into a per-key accumulator instead of overwriting.
|
|
51
|
+
Tensors/ndarrays append along a new leading dim per enclosing loop, auto-named
|
|
52
|
+
from the loop variable ('l_idx' for `for l_idx, layer in enumerate(…)`);
|
|
53
|
+
everything else appends to a list. See _accumulate for the growth-buffer,
|
|
54
|
+
rollover, and nested-grid mechanics, and __live_dim_names__ /
|
|
55
|
+
auto_dim_names_for for how the editor marker merges the auto names into the
|
|
56
|
+
value window's dim_names.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
import ast
|
|
60
|
+
import inspect
|
|
61
|
+
import itertools
|
|
62
|
+
import sys
|
|
63
|
+
import threading
|
|
64
|
+
import time
|
|
65
|
+
import types
|
|
66
|
+
import weakref
|
|
67
|
+
from contextlib import contextmanager
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
|
|
70
|
+
_MISSING = object()
|
|
71
|
+
|
|
72
|
+
# Guards _sites entry creation only. Parses run OUTSIDE any lock - two threads
|
|
73
|
+
# racing a first call may duplicate a parse (benign, identical results) instead
|
|
74
|
+
# of one file's parse blocking every other file's resolution.
|
|
75
|
+
_lock = threading.Lock()
|
|
76
|
+
|
|
77
|
+
# id(code object) -> {lineno: _Site or None}. Identity-keyed, NOT a
|
|
78
|
+
# WeakKeyDictionary - code objects compare equal across files (co_filename is
|
|
79
|
+
# excluded from code equality), so identity keying aliases same-source
|
|
80
|
+
# functions in different files onto one site. A weakref.finalize evicts the
|
|
81
|
+
# entry when the code object dies - hotswap gives the function a new __code__,
|
|
82
|
+
# the old one's sites evict themselves. (A None site marks a failed
|
|
83
|
+
# resolution, so it never re-parses per call.)
|
|
84
|
+
_sites = {}
|
|
85
|
+
|
|
86
|
+
# Every object that ever owned a live store (functions, the def-run path's
|
|
87
|
+
# parked twins, ...). Weak: a store owner that dies takes its store with it.
|
|
88
|
+
# The CUDA OOM responder (gc_manager.respond_to_cuda_oom) sweeps these -
|
|
89
|
+
# nothing else enumerates stores, they are found by resolution.
|
|
90
|
+
_store_owners = globals().get("_store_owners")
|
|
91
|
+
if _store_owners is None:
|
|
92
|
+
_store_owners = weakref.WeakSet()
|
|
93
|
+
|
|
94
|
+
# str(path) -> (mtime, ast.Module, source text). One C-speed parse per file
|
|
95
|
+
# version, shared by span lookup and previous-assignment resolution.
|
|
96
|
+
_asts = {}
|
|
97
|
+
|
|
98
|
+
# (str(path), span start line) -> (mtime, LineMap). One libcst→dict pass per
|
|
99
|
+
# top-level statement (def/class) version - function-size, latest mtime wins.
|
|
100
|
+
_linemaps = {}
|
|
101
|
+
|
|
102
|
+
# (str(path), lineno) -> (sig, (store_obj, key_path)). site_for_line memo -
|
|
103
|
+
# the editor overlay resolves every visible live_view token per repaint, and
|
|
104
|
+
# the chain under it (enclosing-def walk, file parse, linemap) is
|
|
105
|
+
# once-per-edit slow. Keyed on _ast_for's (mtime, pending_edit) sig; negative
|
|
106
|
+
# results cached too (a token with no surfaced site would otherwise re-walk
|
|
107
|
+
# per repaint).
|
|
108
|
+
_site_for_line_cache = {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _Site:
|
|
112
|
+
"""Everything line-dependent about one live_view call site, resolved once.
|
|
113
|
+
`key_path` is relative to `store_obj`'s scope; `var_name` labels (and for
|
|
114
|
+
the bare form, selects) the captured local; `arg_label` is the explicit
|
|
115
|
+
form's argument source text; `loop_dims` names the enclosing loops
|
|
116
|
+
(outermost first — see _loop_dims_at), which turns the site's publishes
|
|
117
|
+
into accumulation instead of overwrite."""
|
|
118
|
+
__slots__ = ("key_path", "var_name", "arg_label", "store_obj", "lineno",
|
|
119
|
+
"loop_dims")
|
|
120
|
+
|
|
121
|
+
def __init__(self, key_path, var_name, arg_label, store_obj, lineno,
|
|
122
|
+
loop_dims=()):
|
|
123
|
+
self.key_path = key_path
|
|
124
|
+
self.var_name = var_name
|
|
125
|
+
self.arg_label = arg_label
|
|
126
|
+
self.store_obj = store_obj
|
|
127
|
+
self.lineno = lineno
|
|
128
|
+
self.loop_dims = loop_dims
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def live_view(value=_MISSING, name=None):
|
|
132
|
+
"""Publish a live value for the editor, keyed by this call's CST address.
|
|
133
|
+
|
|
134
|
+
Bare form — `live_view()` — captures the variable assigned by the
|
|
135
|
+
PRECEDING statement (aug-assigns and block bodies included; resolved from
|
|
136
|
+
the ast, not the display dict). Explicit form — `live_view(expr)` —
|
|
137
|
+
captures expr and passes it through, so `y = live_view(f(x))` works
|
|
138
|
+
inline. A site inside a loop ACCUMULATES instead of overwriting — see
|
|
139
|
+
_accumulate; the loop variables' current values are read from the calling
|
|
140
|
+
frame so int-indexed loops key entries by iteration. Never raises into
|
|
141
|
+
the caller; an unresolvable site is recorded once and skipped
|
|
142
|
+
thereafter."""
|
|
143
|
+
frame = sys._getframe(1)
|
|
144
|
+
try:
|
|
145
|
+
site = _site_for(frame.f_code, frame.f_lineno)
|
|
146
|
+
if site is None or site.store_obj is None:
|
|
147
|
+
return None if value is _MISSING else value
|
|
148
|
+
bare = value is _MISSING
|
|
149
|
+
if bare:
|
|
150
|
+
if site.var_name is None:
|
|
151
|
+
return None
|
|
152
|
+
resolved = frame.f_locals.get(site.var_name, _MISSING)
|
|
153
|
+
if resolved is _MISSING:
|
|
154
|
+
return None
|
|
155
|
+
else:
|
|
156
|
+
resolved = value
|
|
157
|
+
dims = getattr(site, "loop_dims", ()) or None
|
|
158
|
+
idx = (tuple(frame.f_locals.get(d) for d in dims)
|
|
159
|
+
if dims else None)
|
|
160
|
+
_publish(site, resolved, name, bare, dims=dims, idx=idx)
|
|
161
|
+
return resolved
|
|
162
|
+
finally:
|
|
163
|
+
del frame
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def twin_snap(value, name=None, dims=None):
|
|
167
|
+
"""The instrumented twin's injected snapshot hook (`__lv_view__`) — the
|
|
168
|
+
FAST publish path. Synthesizes the same ``line:N#name`` keys the
|
|
169
|
+
frame-snapshot publisher uses (the overlay anchors line keys by line and
|
|
170
|
+
boxes by label), so a twin run does NO site resolution: the classic path
|
|
171
|
+
pays a libcst linemap per enclosing span (~240ms for a big function,
|
|
172
|
+
the whole CLASS for a method) on every fresh twin code object — i.e.
|
|
173
|
+
after every edit. Store resolution is the cached _enclosing_function
|
|
174
|
+
walk. Sharing the frame-snapshot key scheme also means a twin run and a
|
|
175
|
+
context-menu snapshot UPDATE THE SAME MARKERS instead of doubling them.
|
|
176
|
+
Manual live_view() calls in the body keep the structural-key path (their
|
|
177
|
+
markers anchor to the call token). Falls back to the classic resolved
|
|
178
|
+
path when the store can't be resolved.
|
|
179
|
+
|
|
180
|
+
`dims` is the instrumentation's STATIC loop context — the names of the
|
|
181
|
+
loops enclosing the snapped assignment, outermost first (see
|
|
182
|
+
live_instrument._inject_snaps). When present, the loop variables' current
|
|
183
|
+
values are read from the twin frame and the publish ACCUMULATES (stacked
|
|
184
|
+
tensors / lists) instead of overwriting — see _accumulate."""
|
|
185
|
+
frame = sys._getframe(1)
|
|
186
|
+
try:
|
|
187
|
+
code, lineno = frame.f_code, frame.f_lineno
|
|
188
|
+
idx = (tuple(frame.f_locals.get(d) for d in dims)
|
|
189
|
+
if dims else None)
|
|
190
|
+
finally:
|
|
191
|
+
del frame
|
|
192
|
+
dims = tuple(dims) if dims else None
|
|
193
|
+
try:
|
|
194
|
+
# The run's own store comes first (run_capture on this file, same
|
|
195
|
+
# file): the prune at run end and the accumulator's fresh-run reset
|
|
196
|
+
# both key on that object, so publishing anywhere else leaks.
|
|
197
|
+
fn = current_run_owner()
|
|
198
|
+
try:
|
|
199
|
+
same_file = (fn is not None and isinstance(fn, types.FunctionType)
|
|
200
|
+
and fn.__code__.co_filename == code.co_filename)
|
|
201
|
+
except Exception:
|
|
202
|
+
same_file = False
|
|
203
|
+
if not same_file:
|
|
204
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
205
|
+
fn = _enclosing_function(code.co_filename, lineno)
|
|
206
|
+
if (isinstance(fn, types.FunctionType)
|
|
207
|
+
and isinstance(name, str) and name.isidentifier()):
|
|
208
|
+
site = _Site((f"line:{lineno}#{name}",), None, None, fn, lineno)
|
|
209
|
+
_publish(site, value, name, bare=False, dims=dims, idx=idx)
|
|
210
|
+
return value
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
site = _site_for(code, lineno)
|
|
214
|
+
if site is not None and site.store_obj is not None:
|
|
215
|
+
_publish(site, value, name, bare=False, dims=dims, idx=idx)
|
|
216
|
+
return value
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def twin_ret(value=None):
|
|
220
|
+
"""The instrumented twin's return hook (`__lv_ret__`): every `return X`
|
|
221
|
+
in the twin compiles as `return __lv_ret__(X)` (bare `return` passes
|
|
222
|
+
None), so the calling frame's f_lineno IS the return statement's
|
|
223
|
+
original file line. Stamps `__live_return_line__` ((absolute 1-indexed
|
|
224
|
+
file line, run-time line text)) on the resolved store function and wakes the store-level
|
|
225
|
+
watchers, so the editor's snapshot overlay can wash that line green —
|
|
226
|
+
the success twin of the red error-line wash — the moment the run comes
|
|
227
|
+
out. run_instrumented clears the stamp at run start, so a raise (or an
|
|
228
|
+
edit that removes the return) never leaves a stale green line. Returns
|
|
229
|
+
the value unchanged."""
|
|
230
|
+
frame = sys._getframe(1)
|
|
231
|
+
try:
|
|
232
|
+
code, lineno = frame.f_code, frame.f_lineno
|
|
233
|
+
finally:
|
|
234
|
+
del frame
|
|
235
|
+
try:
|
|
236
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
237
|
+
fn = _enclosing_function(code.co_filename, lineno)
|
|
238
|
+
if isinstance(fn, types.FunctionType):
|
|
239
|
+
stamp_run_marker(fn, "__live_return_line__",
|
|
240
|
+
(lineno, _line_text_at(fn, lineno)))
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
return value
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _line_text_at(fn, lineno):
|
|
247
|
+
"""The run-time text of absolute file line `lineno` in fn's file — read
|
|
248
|
+
from the same cached in-memory build the twin compiles against (disk +
|
|
249
|
+
pending splices, via _ast_for), so it costs a dict hit per run. Stamped
|
|
250
|
+
alongside the exit-line numbers so the overlay can re-find the line by
|
|
251
|
+
CONTENT after later edits shift it — the wash follows its statement the
|
|
252
|
+
way line-keyed markers follow their symbols. Never raises; None when
|
|
253
|
+
unresolvable."""
|
|
254
|
+
try:
|
|
255
|
+
path = Path(inspect.unwrap(fn).__code__.co_filename).resolve()
|
|
256
|
+
_tree, text, _sig = _ast_for(path, path.stat().st_mtime)
|
|
257
|
+
lines = text.split("\n")
|
|
258
|
+
if 1 <= lineno <= len(lines):
|
|
259
|
+
return lines[lineno - 1]
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def stamp_run_marker(fn, attr, value):
|
|
266
|
+
"""Stamp a per-run marker attribute (`__live_return_line__`,
|
|
267
|
+
`__live_error_line__`) on a store function and wake its store-level
|
|
268
|
+
watchers (the snapshot editors) with the same throttled render wake the
|
|
269
|
+
publish path uses, so the editor washes appear without an unrelated
|
|
270
|
+
repaint. Safe from any thread; swallows everything — markers are
|
|
271
|
+
decoration, never worth breaking a run over."""
|
|
272
|
+
try:
|
|
273
|
+
vars(fn)[attr] = value
|
|
274
|
+
notified = False
|
|
275
|
+
try:
|
|
276
|
+
targets = tuple(
|
|
277
|
+
getattr(fn, "__live_store_watchers__", None) or ())
|
|
278
|
+
except RuntimeError:
|
|
279
|
+
targets = ()
|
|
280
|
+
for ds in targets:
|
|
281
|
+
try:
|
|
282
|
+
ds.invalidate()
|
|
283
|
+
notified = True
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
if notified:
|
|
287
|
+
global _last_wake
|
|
288
|
+
now = time.time()
|
|
289
|
+
if now - _last_wake > 0.033:
|
|
290
|
+
_last_wake = now
|
|
291
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
292
|
+
request_render()
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def call_with_body_capture(func, kwargs, on_captured=None):
|
|
298
|
+
"""Call ``func(**kwargs)`` and capture its body frame's locals at return,
|
|
299
|
+
handing them to the async frame-snapshot publisher.
|
|
300
|
+
|
|
301
|
+
This is the context menu's one-shot answer to "what are the TARGET's
|
|
302
|
+
mid-body locals": the wrapper's stack capture runs before the body
|
|
303
|
+
executes, so entry kwargs were all it could publish and the func tab
|
|
304
|
+
showed markers only on the signature. A per-thread profile hook watches
|
|
305
|
+
for the target code's outermost 'return' (depth-tracked, so a
|
|
306
|
+
self-recursive view captures the WIDGET's frame, not an inner one);
|
|
307
|
+
profiling covers only this one call's subtree, armed on menu-open only —
|
|
308
|
+
never steady-state. An exception unwind still fires the profile return
|
|
309
|
+
event, so a crashed body publishes its state at the raise."""
|
|
310
|
+
inner = inspect.unwrap(func)
|
|
311
|
+
target_code = getattr(inner, "__code__", None)
|
|
312
|
+
if target_code is None:
|
|
313
|
+
return func(**kwargs)
|
|
314
|
+
captured = {}
|
|
315
|
+
exit_line = [None]
|
|
316
|
+
depth = [0]
|
|
317
|
+
|
|
318
|
+
def _finish():
|
|
319
|
+
if captured:
|
|
320
|
+
# The GLOBAL half (return-line wash + frame-snapshot publish)
|
|
321
|
+
# stays behind the live-view toggle, exactly as before. The
|
|
322
|
+
# LOCAL half - `on_captured` (the context menu's CodeEditor
|
|
323
|
+
# adopting the body locals into its own stack copy) - always
|
|
324
|
+
# fires; it touches nothing global.
|
|
325
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
326
|
+
if Toggles.TextEditor.enable_live_view:
|
|
327
|
+
if exit_line[0] is not None:
|
|
328
|
+
stamp_run_marker(inner, "__live_return_line__",
|
|
329
|
+
(exit_line[0],
|
|
330
|
+
_line_text_at(inner, exit_line[0])))
|
|
331
|
+
publish_stack_locals((), extra_snapshots=[(inner, captured,
|
|
332
|
+
None)])
|
|
333
|
+
if on_captured is not None:
|
|
334
|
+
try:
|
|
335
|
+
on_captured(dict(captured))
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
# sys.monitoring with LOCAL events on just the target code object: the
|
|
340
|
+
# old sys.setprofile hook fired a Python callback for EVERY call/return
|
|
341
|
+
# in the whole call subtree, running a big view took under took
|
|
342
|
+
# SECONDS per capture (the 12s editor frames when the context menu's
|
|
343
|
+
# capture re-ran draw_text). Local events instrument only target_code,
|
|
344
|
+
# so the subtree runs at full speed and only pay one callback per
|
|
345
|
+
# entry/exit of the target itself.
|
|
346
|
+
def _grab(offset_frames):
|
|
347
|
+
try:
|
|
348
|
+
f = sys._getframe(offset_frames)
|
|
349
|
+
if f.f_code is target_code:
|
|
350
|
+
captured.update(f.f_locals)
|
|
351
|
+
# The frame's line AT the callback is the return statement
|
|
352
|
+
# itself (or the raise, on unwind) - the same
|
|
353
|
+
# __live_return_line__ the instrumented twin stamps.
|
|
354
|
+
exit_line[0] = f.f_lineno
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
mon = getattr(sys, "monitoring", None)
|
|
359
|
+
if mon is not None:
|
|
360
|
+
try:
|
|
361
|
+
mon.use_tool_id(mon.PROFILER_ID, "lsd-live-capture")
|
|
362
|
+
except Exception:
|
|
363
|
+
mon = None # tool slot busy - fall back to setprofile
|
|
364
|
+
if mon is not None:
|
|
365
|
+
def _on_start(code, off):
|
|
366
|
+
depth[0] += 1
|
|
367
|
+
|
|
368
|
+
def _on_return(code, off, retval):
|
|
369
|
+
depth[0] -= 1
|
|
370
|
+
if depth[0] <= 0 and not captured:
|
|
371
|
+
_grab(2) # callback ← returning target frame
|
|
372
|
+
|
|
373
|
+
# PY_UNWIND cannot be local to a code object (only PY_START/PY_RETURN
|
|
374
|
+
# are local-able events — 0x1005 is rejected), so an exception unwind
|
|
375
|
+
# publishes nothing here. That loses the old profile hook's
|
|
376
|
+
# capture-at-raise, which is acceptable: the instrumented-twin path
|
|
377
|
+
# stamps raise lines through its own machinery, and a crashed body
|
|
378
|
+
# capture just stays un-published.
|
|
379
|
+
try:
|
|
380
|
+
ev = mon.events
|
|
381
|
+
mon.register_callback(mon.PROFILER_ID, ev.PY_START, _on_start)
|
|
382
|
+
mon.register_callback(mon.PROFILER_ID, ev.PY_RETURN, _on_return)
|
|
383
|
+
mon.set_local_events(mon.PROFILER_ID, target_code,
|
|
384
|
+
ev.PY_START | ev.PY_RETURN)
|
|
385
|
+
return func(**kwargs)
|
|
386
|
+
finally:
|
|
387
|
+
try:
|
|
388
|
+
mon.set_local_events(mon.PROFILER_ID, target_code, 0)
|
|
389
|
+
for _e in (mon.events.PY_START, mon.events.PY_RETURN):
|
|
390
|
+
mon.register_callback(mon.PROFILER_ID, _e, None)
|
|
391
|
+
mon.free_tool_id(mon.PROFILER_ID)
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
_finish()
|
|
395
|
+
|
|
396
|
+
# Monitoring unavailable (tool id busy, e.g. a debugger owns it): return
|
|
397
|
+
# plainly rather than fall back to the old whole-subtree sys.setprofile
|
|
398
|
+
# hook, which cost SECONDS for a big view body. The capture (and its
|
|
399
|
+
# return-line wash) just doesn't happen for this call.
|
|
400
|
+
return func(**kwargs)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def live_values_for(obj):
|
|
404
|
+
"""A SNAPSHOT {key_path: value} of a function/module's store, or {}. The
|
|
405
|
+
store holds the captured values RAW — no record wrapper — so a reader
|
|
406
|
+
hands them straight to draw_any and the framework routes by type. Copied
|
|
407
|
+
so a render-thread iteration can't race a training-thread insert. Looks
|
|
408
|
+
through decorator wrappers — capture attaches to the UNWRAPPED function,
|
|
409
|
+
the object whose __code__ hotswap mutates."""
|
|
410
|
+
try:
|
|
411
|
+
obj = inspect.unwrap(obj)
|
|
412
|
+
except Exception:
|
|
413
|
+
pass
|
|
414
|
+
store = getattr(obj, "__live_values__", None)
|
|
415
|
+
return dict(store) if store else {}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def label_for(obj, key_path):
|
|
419
|
+
"""The display label published for a key (the explicit name= kwarg, else
|
|
420
|
+
the variable the bare form read, else the argument's source text), or
|
|
421
|
+
None. Labels ride the store object beside the values (the same
|
|
422
|
+
attach-to-object pattern as the watcher sets)."""
|
|
423
|
+
try:
|
|
424
|
+
obj = inspect.unwrap(obj)
|
|
425
|
+
except Exception:
|
|
426
|
+
pass
|
|
427
|
+
labels = getattr(obj, "__live_labels__", None)
|
|
428
|
+
return labels.get(key_path) if labels else None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def site_for_line(filename, lineno):
|
|
432
|
+
"""(store_obj, key_path) of the live_view call at a 1-indexed absolute
|
|
433
|
+
file line — the EDITOR-side mirror of capture resolution. Same caches,
|
|
434
|
+
same span parse, same relativization, so the widget anchoring a value and
|
|
435
|
+
the publisher writing it converge on one key by construction. (None, None)
|
|
436
|
+
when no live_view call is surfaced at that line — e.g. while/with bodies,
|
|
437
|
+
whose line-keyed fallback sites have no token node to anchor anyway.
|
|
438
|
+
|
|
439
|
+
Memoized per (path, line) against _ast_for's (mtime, pending-gen)
|
|
440
|
+
signature: the editor overlay calls this for every visible live_view
|
|
441
|
+
token on every repaint, and the resolver chain behind it (realpath,
|
|
442
|
+
enclosing-def walk, span parse, linemap) is once-per-edit work, not
|
|
443
|
+
per-frame work."""
|
|
444
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
445
|
+
from meltygui.code.chain_converters import _module_for_file
|
|
446
|
+
try:
|
|
447
|
+
path = Path(filename).resolve()
|
|
448
|
+
mtime = path.stat().st_mtime
|
|
449
|
+
tree, text, sig = _ast_for(path, mtime)
|
|
450
|
+
_sck = (str(path), lineno)
|
|
451
|
+
_sch = _site_for_line_cache.get(_sck)
|
|
452
|
+
if _sch is not None and _sch[0] == sig:
|
|
453
|
+
return _sch[1]
|
|
454
|
+
# Same stamp to line bridge as _resolve_site, anchored at the
|
|
455
|
+
# enclosing def's disk start when one resolves (module-level stamps
|
|
456
|
+
# anchor at the stamp - no own-span growth to mis-count there).
|
|
457
|
+
_fn = _enclosing_function(str(path), lineno)
|
|
458
|
+
_fc = getattr(_fn, "__code__", None)
|
|
459
|
+
line_p = lineno + _stamp_delta(
|
|
460
|
+
path, _fc.co_firstlineno if _fc is not None else lineno)
|
|
461
|
+
span = _top_level_span(tree, line_p)
|
|
462
|
+
lm = _linemap_for(path, sig, span, text)
|
|
463
|
+
ref = _live_view_ref(lm, line_p)
|
|
464
|
+
if ref is None:
|
|
465
|
+
_site_for_line_cache[_sck] = (sig, (None, None))
|
|
466
|
+
return None, None
|
|
467
|
+
# Function-frame detection, editor flavor: capture reads CO_OPTIMIZED
|
|
468
|
+
# off the frame; here the parse path crossing a `<name>, "def"`
|
|
469
|
+
# segment says the call sits in a def body.
|
|
470
|
+
store_obj = None
|
|
471
|
+
store_is_module = True
|
|
472
|
+
if _owning_def_name(ref.path) is not None:
|
|
473
|
+
store_obj = _fn # resolved above for the line bridge
|
|
474
|
+
store_is_module = store_obj is None
|
|
475
|
+
if store_obj is None:
|
|
476
|
+
store_obj = _module_for_file(path)
|
|
477
|
+
if store_obj is None:
|
|
478
|
+
_site_for_line_cache[_sck] = (sig, (None, None))
|
|
479
|
+
return None, None
|
|
480
|
+
key_path = _store_relative(ref.path, store_obj, store_is_module)
|
|
481
|
+
_res = (store_obj, (key_path or (f"line:{lineno}",)))
|
|
482
|
+
_site_for_line_cache[_sck] = (sig, _res)
|
|
483
|
+
return _res
|
|
484
|
+
except Exception as e:
|
|
485
|
+
print(f"live_view: site_for_line failed for {filename}:{lineno}: "
|
|
486
|
+
f"{e!r}", file=sys.stderr)
|
|
487
|
+
return None, None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def install_builtin(name="live_view"):
|
|
491
|
+
"""Make live_view() callable from ANY module with no import — the seamless
|
|
492
|
+
instrumentation path: type the call, hotswap, run. Mirrors Python's own
|
|
493
|
+
breakpoint(): a debugging entry point that shouldn't demand source changes
|
|
494
|
+
beyond the call itself. No-op if the name is already bound to something
|
|
495
|
+
else in builtins. Also taught to the static lint: code_checks freezes
|
|
496
|
+
dir(builtins) at ITS import, so patch its set when it loaded first."""
|
|
497
|
+
import builtins
|
|
498
|
+
existing = getattr(builtins, name, None)
|
|
499
|
+
if existing is not None and existing is not live_view:
|
|
500
|
+
return
|
|
501
|
+
setattr(builtins, name, live_view)
|
|
502
|
+
checks = sys.modules.get("meltygui.code.code_checks")
|
|
503
|
+
if checks is not None and hasattr(checks, "_BUILTIN_NAMES"):
|
|
504
|
+
checks._BUILTIN_NAMES = frozenset(checks._BUILTIN_NAMES) | {name}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def watch(store_obj, key_path, draw_state, first_only=False):
|
|
508
|
+
"""Register a draw_state to invalidate when `key_path` publishes on
|
|
509
|
+
`store_obj`. Watchers ride the store object (attach-to-object, like the
|
|
510
|
+
values) in a per-key WeakSet — a closed window's draw_state just drops
|
|
511
|
+
out. Re-registering every render is the idempotent norm.
|
|
512
|
+
|
|
513
|
+
first_only=True fires ONLY on a key's FIRST value: the
|
|
514
|
+
marker dot registers this way, so a freshly-typed live_view() flips green
|
|
515
|
+
and auto-opens the moment its code first runs — one editor re-render per
|
|
516
|
+
new key — without paying a full editor recomposite on every publish.
|
|
517
|
+
|
|
518
|
+
key_path=None registers a STORE-LEVEL watcher, fired when ANY brand-new
|
|
519
|
+
key appears on the object. The snapshot overlay registers the editor this
|
|
520
|
+
way: before a function's first instrumented run there are no keys, hence
|
|
521
|
+
no markers, hence no per-key watchers — without this the first run's
|
|
522
|
+
values would sit invisible until an unrelated editor repaint."""
|
|
523
|
+
try:
|
|
524
|
+
if key_path is None:
|
|
525
|
+
vars(store_obj).setdefault(
|
|
526
|
+
"__live_store_watchers__", weakref.WeakSet()).add(draw_state)
|
|
527
|
+
return
|
|
528
|
+
attr = "__live_first_watchers__" if first_only else "__live_watchers__"
|
|
529
|
+
watchers = vars(store_obj).setdefault(attr, {})
|
|
530
|
+
watchers.setdefault(key_path, weakref.WeakSet()).add(draw_state)
|
|
531
|
+
except (AttributeError, TypeError):
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
_PUBLISH_GEN = itertools.count(1)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _stamp_publish_gen(value):
|
|
539
|
+
"""Stamp a monotonic publish generation on a tensor-like value
|
|
540
|
+
(`_lv_pub`). Views that cache work per source (the voxel / line-graph
|
|
541
|
+
volume uploads) key on it next to `id()`/`_version`: a fresh run's
|
|
542
|
+
tensor is routinely allocated at the SAME id as the generation just
|
|
543
|
+
released at run start (`_release_store_generation` frees it first), and
|
|
544
|
+
`_version` starts at 0 for both — an identity-only key then hits the
|
|
545
|
+
cache and shows the previous run's texture. No invalidation involved:
|
|
546
|
+
the key changes exactly when a new value was published. Values that
|
|
547
|
+
refuse attributes (ndarrays, scalars) are left alone."""
|
|
548
|
+
try:
|
|
549
|
+
if hasattr(value, "shape") and hasattr(value, "__dict__"):
|
|
550
|
+
value._lv_pub = next(_PUBLISH_GEN)
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def publish_gen(value):
|
|
556
|
+
"""The `_lv_pub` stamp of a published value (0 when unstamped) — fold
|
|
557
|
+
into any per-source cache key beside id()/_version."""
|
|
558
|
+
try:
|
|
559
|
+
return int(getattr(value, "_lv_pub", 0) or 0)
|
|
560
|
+
except Exception:
|
|
561
|
+
return 0
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# Largest line distance a re-key may span: an edit shifts a site by the
|
|
565
|
+
# lines it inserted/deleted above it - a same-named assignment further away
|
|
566
|
+
# is a different site.
|
|
567
|
+
REKEY_MAX_SHIFT = 12
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def bump_keys_gen(store_obj):
|
|
571
|
+
"""Advance the store's KEY-SET generation (`__live_keys_gen__`) — called
|
|
572
|
+
wherever the set of store keys changes (first publish, re-key, prune).
|
|
573
|
+
The editor's view-name map is derived from the WHOLE key set (ordinal
|
|
574
|
+
disambiguation), so every consumer memoizes it on this generation and
|
|
575
|
+
they all agree within a frame — differently-stale maps minted the same
|
|
576
|
+
name for different keys (duplicate view IDs)."""
|
|
577
|
+
try:
|
|
578
|
+
d = vars(store_obj)
|
|
579
|
+
d["__live_keys_gen__"] = d.get("__live_keys_gen__", 0) + 1
|
|
580
|
+
except (AttributeError, TypeError):
|
|
581
|
+
pass
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _adopt_rekeyed(store_obj, key_path, store):
|
|
585
|
+
"""If `key_path` is new to the store while an UNTOUCHED (this run)
|
|
586
|
+
same-label `line:N#name` key sits within REKEY_MAX_SHIFT lines, that
|
|
587
|
+
key is this site's previous generation: move its entry — value,
|
|
588
|
+
accumulator, label, dim names, both watcher sets, frame-snapshot
|
|
589
|
+
membership — under `key_path`. Nearest line wins; only during an active
|
|
590
|
+
run (a touched set exists) so a second same-named site is never
|
|
591
|
+
mistaken for a re-key outside the publish sequence."""
|
|
592
|
+
if key_path in store:
|
|
593
|
+
return
|
|
594
|
+
label = _key_label(key_path)
|
|
595
|
+
if label is None:
|
|
596
|
+
return
|
|
597
|
+
try:
|
|
598
|
+
d = vars(store_obj)
|
|
599
|
+
except TypeError:
|
|
600
|
+
return
|
|
601
|
+
touched = d.get("__live_touched__")
|
|
602
|
+
if touched is None:
|
|
603
|
+
return
|
|
604
|
+
line = _key_line(key_path)
|
|
605
|
+
best = None
|
|
606
|
+
for k in tuple(store):
|
|
607
|
+
if k in touched or _key_label(k) != label or len(k) != len(key_path):
|
|
608
|
+
continue
|
|
609
|
+
kl = _key_line(k)
|
|
610
|
+
if kl is None or line is None:
|
|
611
|
+
continue
|
|
612
|
+
dist = abs(kl - line)
|
|
613
|
+
if dist > REKEY_MAX_SHIFT or (best is not None and dist >= best[0]):
|
|
614
|
+
continue
|
|
615
|
+
best = (dist, k)
|
|
616
|
+
if best is None:
|
|
617
|
+
return
|
|
618
|
+
old = best[1]
|
|
619
|
+
for attr in ("__live_values__", "__live_accum__", "__live_labels__",
|
|
620
|
+
"__live_dim_names__", "__live_watchers__",
|
|
621
|
+
"__live_first_watchers__"):
|
|
622
|
+
m = d.get(attr)
|
|
623
|
+
if not m or old not in m:
|
|
624
|
+
continue
|
|
625
|
+
try:
|
|
626
|
+
ent = m.pop(old)
|
|
627
|
+
except (KeyError, RuntimeError):
|
|
628
|
+
continue
|
|
629
|
+
if key_path in m and attr in ("__live_watchers__",
|
|
630
|
+
"__live_first_watchers__"):
|
|
631
|
+
try:
|
|
632
|
+
m[key_path].update(ent)
|
|
633
|
+
except Exception:
|
|
634
|
+
m[key_path] = ent
|
|
635
|
+
else:
|
|
636
|
+
m[key_path] = ent
|
|
637
|
+
snaps = d.get("__frame_snapshot_keys__")
|
|
638
|
+
if snaps and old in snaps:
|
|
639
|
+
snaps.discard(old)
|
|
640
|
+
snaps.add(key_path)
|
|
641
|
+
bump_keys_gen(store_obj)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _key_line(key_path):
|
|
645
|
+
"""The line of a `line:N#name` key path's last segment, else None."""
|
|
646
|
+
tail = key_path[-1] if isinstance(key_path, tuple) and key_path else key_path
|
|
647
|
+
if not isinstance(tail, str) or not tail.startswith("line:"):
|
|
648
|
+
return None
|
|
649
|
+
num = tail[5:].partition("#")[0]
|
|
650
|
+
return int(num) if num.isdigit() else None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class RerunHint(str):
|
|
654
|
+
"""The 'Rerun to visualize …' placeholder a released site holds. A str
|
|
655
|
+
subclass so every existing consumer (the value window's str view, tests)
|
|
656
|
+
keeps working, while views that treat plain strings specially can tell
|
|
657
|
+
it apart: the marker's INLINE label refuses it (it's an affordance, not
|
|
658
|
+
a captured value) and keeps the click/caret popover for it."""
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def rerun_hint(store_obj, key_path):
|
|
662
|
+
"""The placeholder a loop site holds while nothing shows it."""
|
|
663
|
+
label = (vars(store_obj).get("__live_labels__") or {}).get(key_path)
|
|
664
|
+
return RerunHint(f"Rerun to visualize {label or key_path[-1]}")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def park_rerun_hint(store_obj, key_path):
|
|
668
|
+
"""A value window just stopped showing: forget its accumulated stack and
|
|
669
|
+
swap the store value for the rerun hint, then sever every watcher pin
|
|
670
|
+
(marker + window draw_states, GL texture) so the tensor has no referrer
|
|
671
|
+
left — the VRAM comes back now, not at the next run. The key STAYS (the
|
|
672
|
+
marker keeps reading captured); the next run with the window open
|
|
673
|
+
accumulates again. Any thread; never raises."""
|
|
674
|
+
try:
|
|
675
|
+
d = vars(store_obj)
|
|
676
|
+
accums = d.get("__live_accum__")
|
|
677
|
+
if accums:
|
|
678
|
+
accums.pop(key_path, None)
|
|
679
|
+
store = d.get("__live_values__")
|
|
680
|
+
if store is not None and key_path in store:
|
|
681
|
+
store[key_path] = rerun_hint(store_obj, key_path)
|
|
682
|
+
except (AttributeError, TypeError):
|
|
683
|
+
return
|
|
684
|
+
_release_key_watchers(store_obj, key_path)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# Headless override for the accumulate-only-when-watched gate below: tests
|
|
688
|
+
# (and any widgetless driver) flip this on so loop sites stack without a
|
|
689
|
+
# window watching them. Patch via unittest.mock / direct assignment.
|
|
690
|
+
ACCUMULATE_UNWATCHED = False
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def has_visible_widget(store_obj, key_path):
|
|
694
|
+
"""True when some live widget for `key_path` currently SHOWS its value
|
|
695
|
+
window. The full per-publish watcher (`__live_watchers__`) is registered
|
|
696
|
+
only while a marker's window exists, but draw_states persist past an
|
|
697
|
+
X-close, so also require the watcher's window ds to be open. Any
|
|
698
|
+
thread; never raises."""
|
|
699
|
+
if ACCUMULATE_UNWATCHED:
|
|
700
|
+
return True
|
|
701
|
+
try:
|
|
702
|
+
watchers = vars(store_obj).get("__live_watchers__")
|
|
703
|
+
if not watchers:
|
|
704
|
+
return False
|
|
705
|
+
try:
|
|
706
|
+
targets = tuple(watchers.get(key_path) or ())
|
|
707
|
+
except RuntimeError: # concurrent registration resized the set
|
|
708
|
+
return True # benign, err on the side of accumulating
|
|
709
|
+
for ds in targets:
|
|
710
|
+
win = getattr(ds, "_lv_window_ds", None)
|
|
711
|
+
if win is not None and not getattr(win, "closed", False):
|
|
712
|
+
return True
|
|
713
|
+
# Re-keyed site: an edit above the line moved `line:N#name` to
|
|
714
|
+
# `line:M#name`, and the run publishes the new key before its
|
|
715
|
+
# (name-keyed, still open) marker has re-rendered and re-watched
|
|
716
|
+
# it. Nothing is registered under the new key yet, but the window
|
|
717
|
+
# is right there: treat an open window under any same-named key
|
|
718
|
+
# as watching (two same-named sites may over-accumulate for one
|
|
719
|
+
# run; the data loss the other way was the real cost).
|
|
720
|
+
label = _key_label(key_path)
|
|
721
|
+
if label is not None:
|
|
722
|
+
for k, targets in tuple(watchers.items()):
|
|
723
|
+
if k is key_path or _key_label(k) != label:
|
|
724
|
+
continue
|
|
725
|
+
try:
|
|
726
|
+
targets = tuple(targets or ())
|
|
727
|
+
except RuntimeError:
|
|
728
|
+
return True
|
|
729
|
+
for ds in targets:
|
|
730
|
+
win = getattr(ds, "_lv_window_ds", None)
|
|
731
|
+
if win is not None and not getattr(win, "closed", False):
|
|
732
|
+
return True
|
|
733
|
+
except (AttributeError, TypeError):
|
|
734
|
+
pass
|
|
735
|
+
return False
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _key_label(key_path):
|
|
739
|
+
"""The token name of a `line:N#name` key path (its last segment), else
|
|
740
|
+
None — the line-free identity a site keeps across re-keys."""
|
|
741
|
+
tail = key_path[-1] if isinstance(key_path, tuple) and key_path else key_path
|
|
742
|
+
if not isinstance(tail, str) or not tail.startswith("line:"):
|
|
743
|
+
return None
|
|
744
|
+
_, sep, name = tail.partition("#")
|
|
745
|
+
return name if sep else None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
_last_wake = 0.0
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _notify_watchers(store_obj, key_path, first):
|
|
752
|
+
"""Invalidate every draw_state watching this key (the terminal-reader
|
|
753
|
+
pattern: invalidate from the publishing thread, render loop repaints) and
|
|
754
|
+
wake the render loop at most ~30/s so a hot training loop can't spin it.
|
|
755
|
+
First publishes additionally fire the first-only watcher set."""
|
|
756
|
+
attrs = ("__live_watchers__", "__live_first_watchers__") if first else (
|
|
757
|
+
"__live_watchers__",)
|
|
758
|
+
notified = False
|
|
759
|
+
for attr in attrs:
|
|
760
|
+
watchers = getattr(store_obj, attr, None)
|
|
761
|
+
if not watchers:
|
|
762
|
+
continue
|
|
763
|
+
try:
|
|
764
|
+
targets = tuple(watchers.get(key_path) or ())
|
|
765
|
+
except RuntimeError: # concurrent registration resized the set
|
|
766
|
+
continue
|
|
767
|
+
for ds in targets:
|
|
768
|
+
try:
|
|
769
|
+
ds.invalidate()
|
|
770
|
+
notified = True
|
|
771
|
+
except Exception:
|
|
772
|
+
pass
|
|
773
|
+
# The open value WINDOW is a nested root - the marker's
|
|
774
|
+
# invalidate stops at its own tile + ancestors and never
|
|
775
|
+
# reaches the window tile, - a rerun's publish leaves the
|
|
776
|
+
# window blitting the stale value. Invalidate FORCE-down its
|
|
777
|
+
# subtree via the marker's window handle (the draw_function
|
|
778
|
+
# completion pattern - safe from the publishing thread).
|
|
779
|
+
win = getattr(ds, "_lv_window_ds", None)
|
|
780
|
+
if win is not None and not getattr(win, "closed", False):
|
|
781
|
+
try:
|
|
782
|
+
from meltygui.core.melty import Melty
|
|
783
|
+
Melty.cache.invalidate_up(win._tile_id, force=True,
|
|
784
|
+
max_depth=8)
|
|
785
|
+
notified = True
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
if first:
|
|
789
|
+
# A brand-new key: wake the store-level watchers (usually editors)
|
|
790
|
+
# so the overlay re-runs and materializes this key's marker.
|
|
791
|
+
try:
|
|
792
|
+
store_targets = tuple(
|
|
793
|
+
getattr(store_obj, "__live_store_watchers__", None) or ())
|
|
794
|
+
except RuntimeError:
|
|
795
|
+
store_targets = ()
|
|
796
|
+
for ds in store_targets:
|
|
797
|
+
try:
|
|
798
|
+
ds.invalidate()
|
|
799
|
+
notified = True
|
|
800
|
+
except Exception:
|
|
801
|
+
pass
|
|
802
|
+
if not notified:
|
|
803
|
+
return
|
|
804
|
+
_wake_render(throttle=True)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
_wake_timer = None
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _wake_render(throttle):
|
|
811
|
+
"""Wake the render loop. throttle=True rate-limits to ~30/s — but with a
|
|
812
|
+
TRAILING edge: a wake that falls inside the window arms a one-shot
|
|
813
|
+
timer, so the LAST publish of a burst always gets painted. Without it a
|
|
814
|
+
run's final publish landed inside the window of the one before, the
|
|
815
|
+
invalidation sat pending with the loop asleep, and the window stayed on
|
|
816
|
+
whatever intermediate layer the previous frame had caught."""
|
|
817
|
+
global _last_wake, _wake_timer
|
|
818
|
+
now = time.time()
|
|
819
|
+
if throttle and now - _last_wake <= 0.033:
|
|
820
|
+
if _wake_timer is None:
|
|
821
|
+
t = threading.Timer(0.04, _wake_render, kwargs={"throttle": False})
|
|
822
|
+
t.daemon = True
|
|
823
|
+
_wake_timer = t
|
|
824
|
+
t.start()
|
|
825
|
+
return
|
|
826
|
+
_last_wake = now
|
|
827
|
+
_wake_timer = None
|
|
828
|
+
try:
|
|
829
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
830
|
+
request_render()
|
|
831
|
+
except Exception:
|
|
832
|
+
pass # headless (tests) - nothing to wake
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def settle_store(store_obj, keys):
|
|
836
|
+
"""Run-end pass: re-invalidate every watcher of `keys` and wake the loop
|
|
837
|
+
UNTHROTTLED, so the state rendered after a run is the run's FINAL state —
|
|
838
|
+
intermediate frames during a long run are fine, landing on one is not."""
|
|
839
|
+
for key in tuple(keys or ()):
|
|
840
|
+
try:
|
|
841
|
+
_notify_watchers(store_obj, key, first=False)
|
|
842
|
+
except Exception:
|
|
843
|
+
pass
|
|
844
|
+
_wake_render(throttle=False)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# ── capture internals ─────────────────────────────────────────────────────────
|
|
848
|
+
|
|
849
|
+
def _site_for(code, lineno):
|
|
850
|
+
per_code = _sites.get(id(code))
|
|
851
|
+
if per_code is not None and lineno in per_code:
|
|
852
|
+
return per_code[lineno]
|
|
853
|
+
with _lock:
|
|
854
|
+
per_code = _sites.get(id(code))
|
|
855
|
+
if per_code is None:
|
|
856
|
+
per_code = {}
|
|
857
|
+
_sites[id(code)] = per_code
|
|
858
|
+
weakref.finalize(code, _sites.pop, id(code), None)
|
|
859
|
+
# Resolve under the lock; a racing duplicate resolution writes the same
|
|
860
|
+
# result twice.
|
|
861
|
+
if lineno not in per_code:
|
|
862
|
+
try:
|
|
863
|
+
site = _resolve_site(code, lineno)
|
|
864
|
+
except Exception as e:
|
|
865
|
+
print(f"live_view: site resolution failed for "
|
|
866
|
+
f"{code.co_filename}:{lineno}: {e!r}", file=sys.stderr)
|
|
867
|
+
site = None
|
|
868
|
+
per_code[lineno] = site
|
|
869
|
+
return per_code[lineno]
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _publish(site, value, name, bare, dims=None, idx=None):
|
|
873
|
+
try:
|
|
874
|
+
# setdefault on the object's __dict is atomic under the GIL, so two
|
|
875
|
+
# threads first-publishing to a function can't drop a store.
|
|
876
|
+
store = vars(site.store_obj).setdefault("__live_values__", {})
|
|
877
|
+
except (AttributeError, TypeError):
|
|
878
|
+
return
|
|
879
|
+
first = site.key_path not in store
|
|
880
|
+
# Register on EVERY publish, not just a key's first - the store rides
|
|
881
|
+
# the function object and outlives the session, so after a restart
|
|
882
|
+
# every key is already "known" and a first-only registration left the
|
|
883
|
+
# owner registry EMPTY - release_all_live_stores (cleanup, the OOM
|
|
884
|
+
# responder) then dropped nothing. WeakSet.add is O(1).
|
|
885
|
+
_register_store_owner(site.store_obj)
|
|
886
|
+
label = name or (site.var_name if bare else site.arg_label)
|
|
887
|
+
if label:
|
|
888
|
+
vars(site.store_obj).setdefault("__live_labels__", {})[
|
|
889
|
+
site.key_path] = label
|
|
890
|
+
# Re-keyed site (an edit above it moved `line:N#name` to `line:M#name`):
|
|
891
|
+
# rename the OLD key's entry in place before this first publish, so the
|
|
892
|
+
# two generations never coexist - the marker/window names (ordinal
|
|
893
|
+
# among same-label keys), the watcher sets, the accumulator and the
|
|
894
|
+
# value all carry over, the end-of-run pass sees nothing removed, and
|
|
895
|
+
# nothing needs invalidating beyond this key's own publish.
|
|
896
|
+
_adopt_rekeyed(site.store_obj, site.key_path, store)
|
|
897
|
+
# A loop site accumulates: the store holds the growing stack/list, the
|
|
898
|
+
# raw per-iteration value only feeds the type recorder below. Must run
|
|
899
|
+
# BEFORE the touched.add - "key not yet touched this run" is how the
|
|
900
|
+
# accumulator detects a fresh run and resets.
|
|
901
|
+
display = value
|
|
902
|
+
if dims and not has_visible_widget(site.store_obj, site.key_path):
|
|
903
|
+
# Loop site nobody is LOOKING at: don't stack (a 32-layer stack of
|
|
904
|
+
# hidden states is gigabytes PER key, repeated for every hidden
|
|
905
|
+
# widget). Drop any existing stack and park a hint in its place -
|
|
906
|
+
# opening the widget shows it, and the next run, with the window
|
|
907
|
+
# now watching, accumulates for real.
|
|
908
|
+
try:
|
|
909
|
+
vars(site.store_obj).get("__live_accum__", {}).pop(
|
|
910
|
+
site.key_path, None)
|
|
911
|
+
except Exception:
|
|
912
|
+
pass
|
|
913
|
+
display = rerun_hint(site.store_obj, site.key_path)
|
|
914
|
+
elif dims:
|
|
915
|
+
try:
|
|
916
|
+
display = _accumulate(site.store_obj, site.key_path, value,
|
|
917
|
+
tuple(dims), idx)
|
|
918
|
+
except Exception as e:
|
|
919
|
+
# Never break the publish; drop the entry so the next one restarts.
|
|
920
|
+
try:
|
|
921
|
+
vars(site.store_obj).get("__live_accum__", {}).pop(
|
|
922
|
+
site.key_path, None)
|
|
923
|
+
except Exception:
|
|
924
|
+
pass
|
|
925
|
+
print(f"live_view: accumulate failed for {site.key_path}: {e!r}",
|
|
926
|
+
file=sys.stderr)
|
|
927
|
+
display = value
|
|
928
|
+
touched = vars(site.store_obj).get("__live_touched__")
|
|
929
|
+
if touched is not None and site.key_path not in touched and not first:
|
|
930
|
+
# FIRST publish of this key in a fresh run: the store is about to
|
|
931
|
+
# drop the previous generation of value - make sure it actually
|
|
932
|
+
# dies. The window's marker / draw_states pin the value they just
|
|
933
|
+
# rendered (and the window's GL volume / cuda_view cache pins it
|
|
934
|
+
# too) until they re-render, which doesn't happen until the run is
|
|
935
|
+
# done publishing - so without this every visible view kept the OLD
|
|
936
|
+
# generation alive for the whole run and peak VRAM was 2× (views
|
|
937
|
+
# hidden → 1×, the tell). Release now; the window refills from the
|
|
938
|
+
# store on its next frame (the publish below notifies it).
|
|
939
|
+
_release_key_watchers(site.store_obj, site.key_path)
|
|
940
|
+
# The value is stored RAW - a single object assignment, atomic under the
|
|
941
|
+
# GIL, so the rendering thread always reads either the old or new value.
|
|
942
|
+
_stamp_publish_gen(display)
|
|
943
|
+
store[site.key_path] = display
|
|
944
|
+
if first:
|
|
945
|
+
bump_keys_gen(site.store_obj) # key set grew - view names re-rank
|
|
946
|
+
# Publish ORDER, one int per key: the inline USAGE labels resolve a
|
|
947
|
+
# usage to the binding above it that actually published LAST (the
|
|
948
|
+
# branch-not-taken rule - see live_usage.governing_key). Monotonic
|
|
949
|
+
# across the process via the shared generation counter.
|
|
950
|
+
try:
|
|
951
|
+
_sv = vars(site.store_obj)
|
|
952
|
+
_sv.setdefault("__live_pub_seq__", {})[
|
|
953
|
+
site.key_path] = next(_PUBLISH_GEN)
|
|
954
|
+
# Store-level publish generation: the usage marker memoizes each
|
|
955
|
+
# occurrence's governing key against it (live_view_views).
|
|
956
|
+
_sv["__live_pub_gen__"] = _sv.get("__live_pub_gen__", 0) + 1
|
|
957
|
+
except (AttributeError, TypeError):
|
|
958
|
+
pass
|
|
959
|
+
# Run-scope liveness: while a run_capture is active for this store,
|
|
960
|
+
# every published key is recorded so the run's exit can prune the rest
|
|
961
|
+
# (set.add - atomic under the GIL).
|
|
962
|
+
if touched is not None:
|
|
963
|
+
touched.add(site.key_path)
|
|
964
|
+
_record_scope_type(site, value, name, bare)
|
|
965
|
+
_notify_watchers(site.store_obj, site.key_path, first=first)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
# Sliding-window cap for a loop-site publishing outside any run_capture
|
|
969
|
+
# scope (a free-running training loop has no run boundary to stop at).
|
|
970
|
+
_ACCUM_CAP = 512
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _accumulate(store_obj, key_path, value, dims, idx):
|
|
974
|
+
"""Fold one LOOP-SITE publish into its per-key accumulator and return the
|
|
975
|
+
DISPLAY value the store should hold: tensors/ndarrays append along a new
|
|
976
|
+
leading dim (a doubling growth buffer — amortized O(1) per publish, no
|
|
977
|
+
per-iteration restack), everything else appends to a list.
|
|
978
|
+
|
|
979
|
+
`dims` is the static tuple of enclosing-loop names, outermost first
|
|
980
|
+
(`for l_idx, layer in enumerate(…)` → 'l_idx'; while → 'iter'); `idx` is
|
|
981
|
+
those loop variables' CURRENT values read from the publishing frame.
|
|
982
|
+
An all-int idx keys the entry by ITERATION INDEX, so a repeated pass
|
|
983
|
+
(an outer epoch loop, a re-called function) OVERWRITES in place instead
|
|
984
|
+
of growing — natural rollover — and a complete rectangular nested grid
|
|
985
|
+
reshapes into one named dim PER loop. Non-int idx (`for layer in
|
|
986
|
+
layers`, while bodies) appends flat in publish order; nested loops that
|
|
987
|
+
can't grid flatten under a composite ' × '-joined name.
|
|
988
|
+
|
|
989
|
+
The auto dim names land in __live_dim_names__[key_path] (read by the
|
|
990
|
+
editor marker, which prepends them to the site's user dim_names before
|
|
991
|
+
the value-window call). A run_capture scope resets a key's accumulator
|
|
992
|
+
on its first publish of the run — the store keeps showing the finished
|
|
993
|
+
stack between runs. Tensors are detached first: a graph-carrying stack
|
|
994
|
+
would pin autograd memory across the whole loop."""
|
|
995
|
+
try:
|
|
996
|
+
accums = vars(store_obj).setdefault("__live_accum__", {})
|
|
997
|
+
except (AttributeError, TypeError):
|
|
998
|
+
return value
|
|
999
|
+
kind = type(value).__name__
|
|
1000
|
+
stackable = (kind == "Tensor"
|
|
1001
|
+
or (kind == "ndarray" and value.dtype.kind in "fiub"))
|
|
1002
|
+
if kind == "Tensor":
|
|
1003
|
+
try:
|
|
1004
|
+
value = value.detach()
|
|
1005
|
+
except Exception:
|
|
1006
|
+
pass
|
|
1007
|
+
touched = vars(store_obj).get("__live_touched__")
|
|
1008
|
+
ent = accums.get(key_path)
|
|
1009
|
+
if (ent is None or ent["dims"] != dims
|
|
1010
|
+
or (touched is not None and key_path not in touched)):
|
|
1011
|
+
# A fresh run's stack almost always ends the same size as the last
|
|
1012
|
+
# one (same loop) - size the buffer to that at the start instead
|
|
1013
|
+
# of doubling up to it: each doubling step held the old + new
|
|
1014
|
+
# buffer (the 16→32 step on a 32-layer (32, 1413, 1413) stack is
|
|
1015
|
+
# a 2 GB + 4 GB transient per key) - on a card that's full, the
|
|
1016
|
+
# transient IS the OOM.
|
|
1017
|
+
hint = 0
|
|
1018
|
+
if ent is not None and ent["dims"] == dims:
|
|
1019
|
+
hint = int(ent["n"] or ent.get("hint", 0))
|
|
1020
|
+
ent = accums[key_path] = {
|
|
1021
|
+
"dims": dims, # static loop names, outermost first
|
|
1022
|
+
"slots": {}, # idx tuple | ('#', n) counter -> row/seq position
|
|
1023
|
+
"buf": None, # stack growth buffer (cap, *shape) | None
|
|
1024
|
+
"n": 0, # buffer rows in use
|
|
1025
|
+
"seq": None, # list fallback (non-stackable / ragged shapes)
|
|
1026
|
+
"hint": hint, # last run's final row count - the initial cap
|
|
1027
|
+
}
|
|
1028
|
+
by_index = (idx is not None and len(idx) == len(dims)
|
|
1029
|
+
and all(type(i) is int for i in idx))
|
|
1030
|
+
slot_key = tuple(idx) if by_index else ("#", len(ent["slots"]))
|
|
1031
|
+
|
|
1032
|
+
buf = ent["buf"]
|
|
1033
|
+
if buf is not None and (not stackable
|
|
1034
|
+
or (kind == "Tensor") != (type(buf).__name__
|
|
1035
|
+
== "Tensor")
|
|
1036
|
+
or tuple(buf.shape[1:]) != tuple(value.shape)):
|
|
1037
|
+
# Genuinely ragged (not the value's shape): give up the packed
|
|
1038
|
+
# buffer, keep the rows (views into it) and continue as a plain
|
|
1039
|
+
# list. Dtype/device drift is NOT a deopt - copy_ casts and crosses
|
|
1040
|
+
# devices below, so a sharded/offloaded model's per-layer values
|
|
1041
|
+
# still stack into the first layer's buffer.
|
|
1042
|
+
print(f"live_view: {key_path} accumulates as a list — "
|
|
1043
|
+
f"{getattr(value, 'shape', type(value).__name__)} doesn't "
|
|
1044
|
+
f"stack on buffer {tuple(buf.shape[1:])}", file=sys.stderr)
|
|
1045
|
+
ent["seq"] = [buf[i] for i in range(ent["n"])]
|
|
1046
|
+
ent["buf"] = buf = None
|
|
1047
|
+
|
|
1048
|
+
if not stackable or ent["seq"] is not None:
|
|
1049
|
+
seq = ent["seq"]
|
|
1050
|
+
if seq is None:
|
|
1051
|
+
seq = ent["seq"] = []
|
|
1052
|
+
pos = ent["slots"].get(slot_key)
|
|
1053
|
+
if pos is not None and pos < len(seq):
|
|
1054
|
+
seq[pos] = value
|
|
1055
|
+
else:
|
|
1056
|
+
ent["slots"][slot_key] = len(seq)
|
|
1057
|
+
seq.append(value)
|
|
1058
|
+
if len(seq) > _ACCUM_CAP:
|
|
1059
|
+
del seq[0]
|
|
1060
|
+
ent["slots"] = {("#", i): i for i in range(len(seq))}
|
|
1061
|
+
_stamp_auto_dims(store_obj, key_path, dims, nested=False)
|
|
1062
|
+
return seq
|
|
1063
|
+
|
|
1064
|
+
if buf is None:
|
|
1065
|
+
buf = ent["buf"] = _accum_alloc(
|
|
1066
|
+
value, max(4, min(_ACCUM_CAP, int(ent.get("hint", 0)))))
|
|
1067
|
+
pos = ent["slots"].get(slot_key)
|
|
1068
|
+
if pos is None:
|
|
1069
|
+
if ent["n"] >= _ACCUM_CAP:
|
|
1070
|
+
# Sliding window: pop off the oldest row. Index identity is
|
|
1071
|
+
# gone after a window, so slots degrade to flat counters.
|
|
1072
|
+
src = buf[1:ent["n"]]
|
|
1073
|
+
buf[:ent["n"] - 1] = (src.clone() if kind == "Tensor"
|
|
1074
|
+
else src.copy())
|
|
1075
|
+
pos = ent["n"] - 1
|
|
1076
|
+
ent["slots"] = {("#", i): i for i in range(pos + 1)}
|
|
1077
|
+
else:
|
|
1078
|
+
if ent["n"] >= buf.shape[0]:
|
|
1079
|
+
# Grow from the BUFFER's dtype/device (not the value's - a
|
|
1080
|
+
# drifting value must not silently re-home the whole stack).
|
|
1081
|
+
cap = min(_ACCUM_CAP, max(4, buf.shape[0] * 2))
|
|
1082
|
+
if kind == "Tensor":
|
|
1083
|
+
new = buf.new_empty((cap,) + tuple(buf.shape[1:]))
|
|
1084
|
+
else:
|
|
1085
|
+
import numpy as np
|
|
1086
|
+
new = np.empty((cap,) + tuple(buf.shape[1:]),
|
|
1087
|
+
dtype=buf.dtype)
|
|
1088
|
+
new[:ent["n"]] = buf[:ent["n"]]
|
|
1089
|
+
buf = ent["buf"] = new
|
|
1090
|
+
pos = ent["n"]
|
|
1091
|
+
ent["n"] += 1
|
|
1092
|
+
ent["slots"][slot_key] = pos
|
|
1093
|
+
if kind == "Tensor":
|
|
1094
|
+
# copy_ casts dtype and crosses devices - mixed-precision layers and
|
|
1095
|
+
# device_map-sharded models stack instead of deopting to a list.
|
|
1096
|
+
buf[pos].copy_(value)
|
|
1097
|
+
else:
|
|
1098
|
+
buf[pos] = value
|
|
1099
|
+
|
|
1100
|
+
flat = buf[:ent["n"]]
|
|
1101
|
+
nested = False
|
|
1102
|
+
if len(dims) > 1 and ent["n"]:
|
|
1103
|
+
ks = list(ent["slots"])
|
|
1104
|
+
if all(k[0] != "#" for k in ks) and ks == sorted(ks):
|
|
1105
|
+
extents = tuple(len({k[d] for k in ks})
|
|
1106
|
+
for d in range(len(dims)))
|
|
1107
|
+
total = 1
|
|
1108
|
+
for e in extents:
|
|
1109
|
+
total *= e
|
|
1110
|
+
if total == ent["n"]:
|
|
1111
|
+
flat = flat.reshape(extents + tuple(buf.shape[1:]))
|
|
1112
|
+
nested = True
|
|
1113
|
+
_stamp_auto_dims(store_obj, key_path, dims, nested)
|
|
1114
|
+
return flat
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _accum_alloc(value, cap):
|
|
1118
|
+
"""An uninitialized (cap, *value.shape) buffer matching value's dtype
|
|
1119
|
+
(and device, for torch tensors)."""
|
|
1120
|
+
if type(value).__name__ == "Tensor":
|
|
1121
|
+
return value.new_empty((cap,) + tuple(value.shape))
|
|
1122
|
+
import numpy as np
|
|
1123
|
+
return np.empty((cap,) + tuple(value.shape), dtype=value.dtype)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _stamp_auto_dims(store_obj, key_path, dims, nested):
|
|
1127
|
+
"""Record the accumulated dims' auto names for the editor marker: one
|
|
1128
|
+
name per loop when the stack is nested (or single-loop), a composite
|
|
1129
|
+
' × ' name for a flattened multi-loop stack."""
|
|
1130
|
+
names = tuple(dims) if (nested or len(dims) == 1) else (" × ".join(dims),)
|
|
1131
|
+
try:
|
|
1132
|
+
stamped = vars(store_obj).setdefault("__live_dim_names__", {})
|
|
1133
|
+
stamped[key_path] = names
|
|
1134
|
+
except (AttributeError, TypeError):
|
|
1135
|
+
pass
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def auto_dim_names_for(obj, key_path):
|
|
1139
|
+
"""The auto loop-dim names a key's accumulated value carries (leading
|
|
1140
|
+
dims, outermost first), or None. Editor-side reader — the marker
|
|
1141
|
+
prepends these to the site's user dim_names before the value-window
|
|
1142
|
+
call."""
|
|
1143
|
+
try:
|
|
1144
|
+
obj = inspect.unwrap(obj)
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1147
|
+
names = getattr(obj, "__live_dim_names__", None)
|
|
1148
|
+
return names.get(key_path) if names else None
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _record_scope_type(site, value, name, bare):
|
|
1152
|
+
"""Feed the published value's runtime type into FuncsMetadata, keyed by the
|
|
1153
|
+
owning function — so an editor on that function's source autocompletes
|
|
1154
|
+
`x.` against the LIVE type of x, for every local a live_view / snapshot
|
|
1155
|
+
run has seen. This is the mid-body complement to the context menu's stack
|
|
1156
|
+
capture (which only sees callers' frames and the target's entry kwargs).
|
|
1157
|
+
|
|
1158
|
+
The recorded NAME must be a real local: the bare form's resolved
|
|
1159
|
+
assignment target, or an explicit call's arg source when it is a plain
|
|
1160
|
+
identifier (`live_view(attn_weights, ...)`); expression args (`x[0].w`)
|
|
1161
|
+
and display-only `name=` labels are skipped. One exception: a snapshot
|
|
1162
|
+
stamp (instrumented_twin's injected `__lv_view__(x, name='x')`) has NO
|
|
1163
|
+
call in the SOURCE at its line, so the site carries no arg_label — there
|
|
1164
|
+
the injected `name` IS the assignment target by construction, and only
|
|
1165
|
+
then is it trusted as the local's name. Module/class-body stores are
|
|
1166
|
+
skipped too — module-level names already complete via the live namespace
|
|
1167
|
+
walk. Cheap on hot publish paths: record_value no-ops on an unchanged
|
|
1168
|
+
type. Best-effort; a hiccup must never break a publish."""
|
|
1169
|
+
store_obj = site.store_obj
|
|
1170
|
+
if not isinstance(store_obj, types.FunctionType):
|
|
1171
|
+
return
|
|
1172
|
+
local_name = site.var_name if bare else site.arg_label
|
|
1173
|
+
if not (isinstance(local_name, str) and local_name.isidentifier()):
|
|
1174
|
+
if not (site.arg_label is None and not bare and isinstance(name, str)):
|
|
1175
|
+
return
|
|
1176
|
+
local_name = name
|
|
1177
|
+
if not local_name.isidentifier():
|
|
1178
|
+
return
|
|
1179
|
+
try:
|
|
1180
|
+
from meltygui.core.rendering.func_metadata import FuncsMetadata
|
|
1181
|
+
FuncsMetadata.record_value(store_obj, local_name, value)
|
|
1182
|
+
except Exception:
|
|
1183
|
+
pass
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
# The single owner of the run_capture currently open on THIS thread (a small
|
|
1187
|
+
# stack: nested captures are possible in principle). twin_snap publishes to
|
|
1188
|
+
# it instead of re-resolving the store by (file, line): the resolver walk can
|
|
1189
|
+
# find a DIFFERENT function object than the one being run (the module's real
|
|
1190
|
+
# function vs the def-run path's exec'd twin parked beside it - same
|
|
1191
|
+
# co_firstlineno, first-in-dict wins), and the run_capture prunes one store
|
|
1192
|
+
# while the run fills another: stale keys (every renamed/removed site,
|
|
1193
|
+
# each with its accumulated stack) were never swept, and the accumulator
|
|
1194
|
+
# never saw a fresh run - the "accumulators still growing" symptom.
|
|
1195
|
+
_run_owner = threading.local()
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def current_run_owner():
|
|
1199
|
+
stack = getattr(_run_owner, "stack", None)
|
|
1200
|
+
return stack[-1] if stack else None
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _register_store_owner(store_obj):
|
|
1204
|
+
try:
|
|
1205
|
+
_store_owners.add(store_obj)
|
|
1206
|
+
except TypeError:
|
|
1207
|
+
pass # not weak-referenceable - won't be swept, fine
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _discover_store_owners():
|
|
1211
|
+
"""Every function object carrying a live store, found by a gc walk —
|
|
1212
|
+
the fallback for a registry that missed owners. A module scan is NOT
|
|
1213
|
+
enough: the store rides the function OBJECT, and a recompile/hotswap
|
|
1214
|
+
replaces the module's attribute while the superseded function lives on
|
|
1215
|
+
(the `_sites` cache holds it via Site.store_obj) with its whole store —
|
|
1216
|
+
14 GB of stacks in one session, reachable from nothing nameable.
|
|
1217
|
+
Unfreezes first: gc_manager freezes the boot generation and
|
|
1218
|
+
gc.get_objects() skips frozen objects. OOM-time only (opt-in via
|
|
1219
|
+
release_all_live_stores(discover=True)): the walk is O(the process
|
|
1220
|
+
heap) — ~0.5 s for one session, but the model_server process keeps
|
|
1221
|
+
prior sessions' graphs, so it grows with every launch. Never run it
|
|
1222
|
+
on the session-teardown path."""
|
|
1223
|
+
import gc
|
|
1224
|
+
try:
|
|
1225
|
+
gc.unfreeze()
|
|
1226
|
+
except Exception:
|
|
1227
|
+
pass
|
|
1228
|
+
found = []
|
|
1229
|
+
for o in gc.get_objects():
|
|
1230
|
+
if not isinstance(o, types.FunctionType):
|
|
1231
|
+
continue
|
|
1232
|
+
try:
|
|
1233
|
+
d = vars(o)
|
|
1234
|
+
except TypeError:
|
|
1235
|
+
continue
|
|
1236
|
+
if "__live_values__" in d or "__live_accum__" in d:
|
|
1237
|
+
found.append(o)
|
|
1238
|
+
return found
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def release_all_live_stores(discover=False):
|
|
1242
|
+
"""Drop EVERY live-view store: values, loop accumulators (the per-run
|
|
1243
|
+
stacks — gigabytes), labels, watchers; close/release every marker and
|
|
1244
|
+
value window that watched them (GL textures included, via
|
|
1245
|
+
release_live_value). The CUDA-OOM response: after an out-of-memory the
|
|
1246
|
+
live set IS the VRAM, and a partial run's stacks + every window's pinned
|
|
1247
|
+
generation must go before anything can run again. Markers re-publish
|
|
1248
|
+
and windows re-fill on the next run; nothing is lost that a run doesn't
|
|
1249
|
+
recreate. Returns the number of keys dropped. Safe from any thread.
|
|
1250
|
+
|
|
1251
|
+
Owners come from the `_store_owners` registry, which is complete for
|
|
1252
|
+
this module instance: `_publish` registers on EVERY publish, and
|
|
1253
|
+
`run_capture` / `adopt_live_store` register too. `discover=True` adds
|
|
1254
|
+
the `_discover_store_owners` gc walk as a belt-and-braces fallback —
|
|
1255
|
+
O(the whole process heap), and the model_server process carries prior
|
|
1256
|
+
sessions' graphs (see gc_manager._boot_collect_and_freeze), so at
|
|
1257
|
+
session teardown that walk grew with every launch: the multi-second
|
|
1258
|
+
hang after "Cleaning CUDA context". Melty.cleanup therefore uses the
|
|
1259
|
+
registry alone; the OOM responder, a rare recovery where a missed
|
|
1260
|
+
14 GB store costs more than the walk, still discovers."""
|
|
1261
|
+
dropped = 0
|
|
1262
|
+
owners = list(_store_owners)
|
|
1263
|
+
if discover:
|
|
1264
|
+
known = {id(o) for o in owners}
|
|
1265
|
+
for o in _discover_store_owners():
|
|
1266
|
+
if id(o) not in known:
|
|
1267
|
+
owners.append(o)
|
|
1268
|
+
_register_store_owner(o)
|
|
1269
|
+
for owner in owners:
|
|
1270
|
+
try:
|
|
1271
|
+
store = vars(owner).get("__live_values__")
|
|
1272
|
+
except TypeError:
|
|
1273
|
+
continue
|
|
1274
|
+
keys = list(store) if store else []
|
|
1275
|
+
if keys:
|
|
1276
|
+
_prune_keys(owner, keys)
|
|
1277
|
+
dropped += len(keys)
|
|
1278
|
+
# Anything _prune_keys leaves behind (a key that never published a
|
|
1279
|
+
# value but has an accumulator entry, a fresh-run scope, ...).
|
|
1280
|
+
for attr in ("__live_accum__", "__live_dim_names__", "__live_touched__"):
|
|
1281
|
+
try:
|
|
1282
|
+
d = vars(owner).get(attr)
|
|
1283
|
+
if hasattr(d, "clear"):
|
|
1284
|
+
d.clear()
|
|
1285
|
+
except TypeError:
|
|
1286
|
+
pass
|
|
1287
|
+
return dropped
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
@contextmanager
|
|
1291
|
+
def run_capture(store_obj):
|
|
1292
|
+
"""Scope one instrumented run over `store_obj` (pass the UNWRAPPED
|
|
1293
|
+
function — the object capture attaches to). Keys published inside the
|
|
1294
|
+
with-block are recorded, and a SUCCESSFUL exit prunes every other key:
|
|
1295
|
+
an instrumented run republishes every assignment it still contains, so
|
|
1296
|
+
anything not touched is a REMOVED line — without this, stale keys linger
|
|
1297
|
+
forever as ghost markers, orphaned value windows, and line:N entries
|
|
1298
|
+
that jumble future resolution. An exception skips the prune: a partial
|
|
1299
|
+
run proves nothing about which sites still exist."""
|
|
1300
|
+
try:
|
|
1301
|
+
vars(store_obj)["__live_touched__"] = set()
|
|
1302
|
+
except (AttributeError, TypeError):
|
|
1303
|
+
yield
|
|
1304
|
+
return
|
|
1305
|
+
_register_store_owner(store_obj)
|
|
1306
|
+
# The previous generation goes BEFORE the run allocates the next generation -
|
|
1307
|
+
# every big value in the store (and the watchers' pins to it), not just
|
|
1308
|
+
# the keys this run fails to republish: an edit that removes a line
|
|
1309
|
+
# re-keys every `line:N#...` site below it, so the per-key release in
|
|
1310
|
+
# _publish never fires for them: the old stacks would live until the
|
|
1311
|
+
# end-of-run prune (or past it, if the run OOMs - a failed run prunes
|
|
1312
|
+
# nothing). Windows hold their last frame anyway; small values
|
|
1313
|
+
# (scalars, strings) stay so text markers don't blink.
|
|
1314
|
+
_release_store_generation(store_obj)
|
|
1315
|
+
stack = getattr(_run_owner, "stack", None)
|
|
1316
|
+
if stack is None:
|
|
1317
|
+
stack = _run_owner.stack = []
|
|
1318
|
+
stack.append(store_obj)
|
|
1319
|
+
try:
|
|
1320
|
+
yield
|
|
1321
|
+
except BaseException:
|
|
1322
|
+
touched = vars(store_obj).pop("__live_touched__", None)
|
|
1323
|
+
# A failed run (OOM mid-loop) still re-paints what it did publish
|
|
1324
|
+
# - the windows must show the store's truth, not a stale frame.
|
|
1325
|
+
settle_store(store_obj, touched)
|
|
1326
|
+
raise
|
|
1327
|
+
finally:
|
|
1328
|
+
if stack and stack[-1] is store_obj:
|
|
1329
|
+
stack.pop()
|
|
1330
|
+
touched = vars(store_obj).pop("__live_touched__", set())
|
|
1331
|
+
_prune_untouched(store_obj, touched)
|
|
1332
|
+
settle_store(store_obj, touched)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
_RELEASE_MIN_BYTES = 1 << 20
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _big_tensorish(v, min_bytes=_RELEASE_MIN_BYTES):
|
|
1339
|
+
kind = type(v).__name__
|
|
1340
|
+
try:
|
|
1341
|
+
if kind == "Tensor":
|
|
1342
|
+
return v.numel() * v.element_size() >= min_bytes
|
|
1343
|
+
if kind == "ndarray":
|
|
1344
|
+
return v.nbytes >= min_bytes
|
|
1345
|
+
except Exception:
|
|
1346
|
+
return False
|
|
1347
|
+
return False
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _release_store_generation(store_obj, min_bytes=_RELEASE_MIN_BYTES):
|
|
1351
|
+
"""Drop every big tensor/ndarray value in `store_obj`'s store (the entry
|
|
1352
|
+
stays, value None — the marker still reads as captured and its window
|
|
1353
|
+
holds its last frame), clear the matching accumulator buffers (keeping
|
|
1354
|
+
the row count as the next run's pre-size hint), and release the
|
|
1355
|
+
watchers' pins. Returns the number of values released. Any thread."""
|
|
1356
|
+
try:
|
|
1357
|
+
store = vars(store_obj).get("__live_values__")
|
|
1358
|
+
accums = vars(store_obj).get("__live_accum__") or {}
|
|
1359
|
+
except TypeError:
|
|
1360
|
+
return 0
|
|
1361
|
+
if not store:
|
|
1362
|
+
return 0
|
|
1363
|
+
n = 0
|
|
1364
|
+
for key in list(store):
|
|
1365
|
+
v = store.get(key)
|
|
1366
|
+
if not _big_tensorish(v, min_bytes):
|
|
1367
|
+
continue
|
|
1368
|
+
store[key] = None
|
|
1369
|
+
ent = accums.get(key)
|
|
1370
|
+
if ent is not None:
|
|
1371
|
+
ent["hint"] = int(ent.get("n") or ent.get("hint", 0))
|
|
1372
|
+
ent["buf"] = None
|
|
1373
|
+
ent["seq"] = None
|
|
1374
|
+
ent["slots"] = {}
|
|
1375
|
+
ent["n"] = 0
|
|
1376
|
+
_release_key_watchers(store_obj, key)
|
|
1377
|
+
n += 1
|
|
1378
|
+
return n
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def _release_key_watchers(store_obj, key_path):
|
|
1382
|
+
"""Drop the captured-value refs (+ GL resources) held by every marker and
|
|
1383
|
+
value window watching `key_path` — WITHOUT closing them or touching the
|
|
1384
|
+
store. The per-run peak-VRAM guard (see _publish); same release as a
|
|
1385
|
+
prune, minus the prune. Any thread."""
|
|
1386
|
+
try:
|
|
1387
|
+
from meltygui.editor.live_view_views import release_live_value
|
|
1388
|
+
except Exception:
|
|
1389
|
+
return
|
|
1390
|
+
for attr in ("__live_watchers__", "__live_first_watchers__"):
|
|
1391
|
+
watchers = getattr(store_obj, attr, None)
|
|
1392
|
+
if not watchers:
|
|
1393
|
+
continue
|
|
1394
|
+
try:
|
|
1395
|
+
targets = tuple(watchers.get(key_path) or ())
|
|
1396
|
+
except RuntimeError:
|
|
1397
|
+
targets = ()
|
|
1398
|
+
for ds in targets:
|
|
1399
|
+
try:
|
|
1400
|
+
release_live_value(ds, gl=False)
|
|
1401
|
+
win = getattr(ds, "_lv_window_ds", None)
|
|
1402
|
+
if win is not None:
|
|
1403
|
+
# keep the FBO's last image: the window shows its last
|
|
1404
|
+
# frame until the fresh value lands (no error flash)
|
|
1405
|
+
release_live_value(win, keep_image=True)
|
|
1406
|
+
except Exception:
|
|
1407
|
+
pass
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def _retained_markers(store_obj, store, removed):
|
|
1411
|
+
"""ids of watcher draw_states (markers) of `removed` keys that must keep
|
|
1412
|
+
their window: the marker still serves a SURVIVING key. Two tests, either
|
|
1413
|
+
suffices:
|
|
1414
|
+
- identity: the same ds is registered under a surviving key (the
|
|
1415
|
+
marker rendered since the re-key and re-watched);
|
|
1416
|
+
- stable name: the removed key's line-free name (label + ordinal
|
|
1417
|
+
among same-label keys, the convention marker/window names already
|
|
1418
|
+
use — live_view_views._stable_key_names) is also the name of a
|
|
1419
|
+
surviving key. Old-generation names rank over removed ∪ survivors
|
|
1420
|
+
that already have watchers (keys published before this run);
|
|
1421
|
+
new-generation names over the survivors — so an in-def shift of
|
|
1422
|
+
every `hidden` site keeps every `hidden` window.
|
|
1423
|
+
Markers are name-keyed (`lvs::fn::hidden#1`), so a site that moved
|
|
1424
|
+
lines comes back as the SAME draw_state — nothing to hand over."""
|
|
1425
|
+
keep = set()
|
|
1426
|
+
removed_set = set(removed)
|
|
1427
|
+
survivors = [k for k in store if k not in removed_set]
|
|
1428
|
+
if not survivors:
|
|
1429
|
+
return keep
|
|
1430
|
+
watcher_maps = []
|
|
1431
|
+
for attr in ("__live_watchers__", "__live_first_watchers__"):
|
|
1432
|
+
w = getattr(store_obj, attr, None)
|
|
1433
|
+
if w:
|
|
1434
|
+
watcher_maps.append(w)
|
|
1435
|
+
|
|
1436
|
+
def _targets(k):
|
|
1437
|
+
out = []
|
|
1438
|
+
for w in watcher_maps:
|
|
1439
|
+
try:
|
|
1440
|
+
out.extend(tuple(w.get(k) or ()))
|
|
1441
|
+
except RuntimeError:
|
|
1442
|
+
pass
|
|
1443
|
+
return out
|
|
1444
|
+
|
|
1445
|
+
surviving_ds = set()
|
|
1446
|
+
old_gen = list(removed_set)
|
|
1447
|
+
for k in survivors:
|
|
1448
|
+
t = _targets(k)
|
|
1449
|
+
if t:
|
|
1450
|
+
surviving_ds.update(id(d) for d in t)
|
|
1451
|
+
old_gen.append(k)
|
|
1452
|
+
try:
|
|
1453
|
+
from meltygui.editor.live_view_views import _stable_key_names
|
|
1454
|
+
old_names = _stable_key_names(old_gen)
|
|
1455
|
+
new_names = set(_stable_key_names(survivors).values())
|
|
1456
|
+
except Exception:
|
|
1457
|
+
old_names, new_names = {}, set()
|
|
1458
|
+
for k in removed:
|
|
1459
|
+
for d in _targets(k):
|
|
1460
|
+
if id(d) in surviving_ds or old_names.get(k) in new_names:
|
|
1461
|
+
keep.add(id(d))
|
|
1462
|
+
return keep
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def _prune_untouched(store_obj, touched):
|
|
1466
|
+
"""Drop every store key not in `touched` — run_capture's whole-store sweep.
|
|
1467
|
+
The per-key removal mechanics live in _prune_keys (shared with the frame-
|
|
1468
|
+
snapshot publisher, which prunes only ITS OWN stale keys)."""
|
|
1469
|
+
store = getattr(store_obj, "__live_values__", None)
|
|
1470
|
+
if not store:
|
|
1471
|
+
return
|
|
1472
|
+
_prune_keys(store_obj, [k for k in tuple(store) if k not in touched])
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _prune_keys(store_obj, removed):
|
|
1476
|
+
"""Remove the given store keys: value, label, per-key watcher sets, and
|
|
1477
|
+
any open value window (win.closed = True — the next root_draw_states
|
|
1478
|
+
dispatch discards it; if the key ever republishes, the marker re-registers
|
|
1479
|
+
its window with closed= driven fresh). Store-level watchers (the snapshot
|
|
1480
|
+
editors) are invalidated so the overlay re-runs without the removed
|
|
1481
|
+
markers. Safe from any thread — dict pops/copies are GIL-atomic and
|
|
1482
|
+
ds.invalidate() is the established cross-thread completion pattern."""
|
|
1483
|
+
store = getattr(store_obj, "__live_values__", None)
|
|
1484
|
+
if not store or not removed:
|
|
1485
|
+
return
|
|
1486
|
+
labels = getattr(store_obj, "__live_labels__", None)
|
|
1487
|
+
# Line shifts: keys are line-anchored (`line:N#name`), so an edit that
|
|
1488
|
+
# inserts/removes a line above a site re-keys it - the old key lands
|
|
1489
|
+
# here as "removed" while the SAME assignment republished under
|
|
1490
|
+
# `line:M#name`, served by the SAME name-keyed marker. Its window must
|
|
1491
|
+
# survive (see _retained_markers); only markers whose site is really
|
|
1492
|
+
# gone are closed.
|
|
1493
|
+
keep = _retained_markers(store_obj, store, removed)
|
|
1494
|
+
# One-time cleanup of the (temporary) parking lot: windows parked
|
|
1495
|
+
# under __live_window_handoff__ were never adopted and sat orphaned.
|
|
1496
|
+
try:
|
|
1497
|
+
for _w in (vars(store_obj).pop("__live_window_handoff__", None)
|
|
1498
|
+
or {}).values():
|
|
1499
|
+
_w.closed = True
|
|
1500
|
+
except Exception:
|
|
1501
|
+
pass
|
|
1502
|
+
for key in removed:
|
|
1503
|
+
store.pop(key, None)
|
|
1504
|
+
if labels:
|
|
1505
|
+
labels.pop(key, None)
|
|
1506
|
+
for _attr in ("__live_accum__", "__live_dim_names__"):
|
|
1507
|
+
_d = getattr(store_obj, _attr, None)
|
|
1508
|
+
if _d:
|
|
1509
|
+
_d.pop(key, None)
|
|
1510
|
+
for attr in ("__live_watchers__", "__live_first_watchers__"):
|
|
1511
|
+
watchers = getattr(store_obj, attr, None)
|
|
1512
|
+
if not watchers:
|
|
1513
|
+
continue
|
|
1514
|
+
try:
|
|
1515
|
+
targets = tuple(watchers.pop(key, None) or ())
|
|
1516
|
+
except RuntimeError:
|
|
1517
|
+
targets = ()
|
|
1518
|
+
for ds in targets:
|
|
1519
|
+
if id(ds) in keep:
|
|
1520
|
+
# The same marker (name-stable: line stripped, ordinal
|
|
1521
|
+
# among same-label keys) still serves a surviving key -
|
|
1522
|
+
# the site moved lines, it didn't go away. Its window
|
|
1523
|
+
# stays open and pinned; the marker re-watches the new
|
|
1524
|
+
# key on its next render (an open window keeps it out
|
|
1525
|
+
# of _marker_idle_skip).
|
|
1526
|
+
continue
|
|
1527
|
+
win = getattr(ds, "_lv_window_ds", None)
|
|
1528
|
+
if win is not None:
|
|
1529
|
+
try:
|
|
1530
|
+
win.closed = True
|
|
1531
|
+
except Exception:
|
|
1532
|
+
pass
|
|
1533
|
+
try:
|
|
1534
|
+
ds._lv_open = False
|
|
1535
|
+
ds.invalidate()
|
|
1536
|
+
except Exception:
|
|
1537
|
+
pass
|
|
1538
|
+
# The marker and its window stop rendering for good (the key
|
|
1539
|
+
# is gone) but their draw_states persist: drop the captured
|
|
1540
|
+
# value they hold (and the window's GPU texture), or every
|
|
1541
|
+
# pruned key leaves a generation of tensors for the session.
|
|
1542
|
+
try:
|
|
1543
|
+
from meltygui.editor.live_view_views import release_live_value
|
|
1544
|
+
release_live_value(ds, gl=False)
|
|
1545
|
+
if win is not None:
|
|
1546
|
+
release_live_value(win)
|
|
1547
|
+
except Exception:
|
|
1548
|
+
pass
|
|
1549
|
+
bump_keys_gen(store_obj) # key set shrank - view names re-rank
|
|
1550
|
+
try:
|
|
1551
|
+
store_targets = tuple(
|
|
1552
|
+
getattr(store_obj, "__live_store_watchers__", None) or ())
|
|
1553
|
+
except RuntimeError:
|
|
1554
|
+
store_targets = ()
|
|
1555
|
+
for ds in store_targets:
|
|
1556
|
+
try:
|
|
1557
|
+
ds.invalidate()
|
|
1558
|
+
except Exception:
|
|
1559
|
+
pass
|
|
1560
|
+
try:
|
|
1561
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
1562
|
+
request_render()
|
|
1563
|
+
except Exception:
|
|
1564
|
+
pass # headless (test)
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
_STORE_ATTRS = ("__live_values__", "__live_accum__", "__live_labels__",
|
|
1568
|
+
"__live_dim_names__", "__live_watchers__",
|
|
1569
|
+
"__live_first_watchers__", "__live_store_watchers__",
|
|
1570
|
+
"__frame_snapshot_keys__",
|
|
1571
|
+
"__live_return_line__", "__live_error_line__")
|
|
1572
|
+
# (__live_touched__ stays put: it is an in-flight run_capture's marker on the
|
|
1573
|
+
# OLD function - moving it would just make that run prune an empty store.)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def adopt_live_store(old, new):
|
|
1577
|
+
"""Move the live-view store from one function object to its SUCCESSOR —
|
|
1578
|
+
the def-run path (text_editor._fnrun_resolve) exec-compiles a FRESH
|
|
1579
|
+
function per body edit, and the old one — unreachable from any module
|
|
1580
|
+
var once re-parked — still owned its whole `__live_values__` /
|
|
1581
|
+
`__live_accum__` (the per-layer stacks: gigabytes per run). One owner
|
|
1582
|
+
per def: `new` takes over the dicts BY IDENTITY (watchers, markers and
|
|
1583
|
+
open value windows keep working unchanged — publishes to `new` land in
|
|
1584
|
+
the same dicts), and `old` drops them, so a superseded function pins
|
|
1585
|
+
nothing. If `new` already has a store (the real module function that ran
|
|
1586
|
+
before its exec twin did) its own wins and the old one is just dropped —
|
|
1587
|
+
the memory, not the continuity, is what matters."""
|
|
1588
|
+
if old is None or new is None or old is new:
|
|
1589
|
+
return
|
|
1590
|
+
try:
|
|
1591
|
+
od, nd = vars(old), vars(new)
|
|
1592
|
+
except TypeError:
|
|
1593
|
+
return
|
|
1594
|
+
for attr in _STORE_ATTRS:
|
|
1595
|
+
if attr not in od:
|
|
1596
|
+
continue
|
|
1597
|
+
moved = od.pop(attr)
|
|
1598
|
+
if attr not in nd:
|
|
1599
|
+
nd[attr] = moved
|
|
1600
|
+
_register_store_owner(new)
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def clear_file_stores(filename):
|
|
1604
|
+
"""Drop EVERY captured live_view value riding this file's stores: the
|
|
1605
|
+
module object plus each function/method whose code lives in the file
|
|
1606
|
+
(unwrapped — capture attaches to the inner function). Goes through
|
|
1607
|
+
_prune_keys, so open value windows close, marker dots flip back to gray
|
|
1608
|
+
and store-level watchers repaint; per-run markers (return/error line
|
|
1609
|
+
washes) are dropped too. The editor's live-badge × calls this. Returns
|
|
1610
|
+
the number of values dropped."""
|
|
1611
|
+
from meltygui.code.chain_converters import _module_for_file
|
|
1612
|
+
try:
|
|
1613
|
+
path = Path(filename).resolve()
|
|
1614
|
+
except Exception:
|
|
1615
|
+
return 0
|
|
1616
|
+
mod = _module_for_file(path)
|
|
1617
|
+
if mod is None:
|
|
1618
|
+
return 0
|
|
1619
|
+
fname = str(path)
|
|
1620
|
+
objs, seen = [mod], {id(mod)}
|
|
1621
|
+
|
|
1622
|
+
def _collect(ns):
|
|
1623
|
+
for v in list(ns.values()):
|
|
1624
|
+
try:
|
|
1625
|
+
v = inspect.unwrap(v)
|
|
1626
|
+
except Exception:
|
|
1627
|
+
pass
|
|
1628
|
+
if id(v) in seen:
|
|
1629
|
+
continue
|
|
1630
|
+
code = getattr(v, "__code__", None)
|
|
1631
|
+
if code is not None and getattr(code, "co_filename", None) == fname:
|
|
1632
|
+
seen.add(id(v))
|
|
1633
|
+
objs.append(v)
|
|
1634
|
+
elif (isinstance(v, type)
|
|
1635
|
+
and getattr(v, "__module__", None) == getattr(mod, "__name__", None)):
|
|
1636
|
+
seen.add(id(v))
|
|
1637
|
+
_collect(dict(vars(v)))
|
|
1638
|
+
|
|
1639
|
+
_collect(dict(vars(mod)))
|
|
1640
|
+
dropped = 0
|
|
1641
|
+
for obj in objs:
|
|
1642
|
+
store = getattr(obj, "__live_values__", None)
|
|
1643
|
+
if store:
|
|
1644
|
+
keys = tuple(store)
|
|
1645
|
+
dropped += len(keys)
|
|
1646
|
+
_prune_keys(obj, keys)
|
|
1647
|
+
d = getattr(obj, "__dict__", None)
|
|
1648
|
+
if d is not None:
|
|
1649
|
+
for attr in ("__live_return_line__", "__live_error_line__",
|
|
1650
|
+
"__live_touched__", "__live_accum__",
|
|
1651
|
+
"__live_dim_names__"):
|
|
1652
|
+
d.pop(attr, None)
|
|
1653
|
+
return dropped
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
# ── frame snapshots (context-menu capture -> live-value stores) ──────────────
|
|
1657
|
+
# The context menu's stack entry holds every called frame's fscope for one
|
|
1658
|
+
# snapshot. Publishing them through the SAME site/key pipeline the instrumented
|
|
1659
|
+
# twin uses makes them first-class live values: markers, live value windows,
|
|
1660
|
+
# watchers, voxel, and FuncsMetadata typing all come along for free, in
|
|
1661
|
+
# ANY editor that shows the function - no new rendering machinery.
|
|
1662
|
+
|
|
1663
|
+
def publish_frame_snapshot(fn, scope, upto_lineno=None):
|
|
1664
|
+
"""Publish a ``{name: value}`` scope snapshot into ``fn``'s live-value
|
|
1665
|
+
store, anchored at EVERY occurrence of each name in the def's own scope:
|
|
1666
|
+
parameters at their signature lines, and every Name reference — Load and
|
|
1667
|
+
Store alike (assignments in all forms, loop/with targets, walrus, and
|
|
1668
|
+
plain reads) — so any mention of a local in the source is a live view,
|
|
1669
|
+
not just its binding. One key per (name, line); all of a name's markers
|
|
1670
|
+
show the same captured value. ``upto_lineno`` is accepted for API
|
|
1671
|
+
stability but no longer gates anchors: the snapshot IS the frame's state
|
|
1672
|
+
at capture, and every reference line is an equally valid place to
|
|
1673
|
+
inspect it.
|
|
1674
|
+
|
|
1675
|
+
Deliberately NO site resolution: keys are synthesized ``line:N#name``
|
|
1676
|
+
tails, which the overlay anchors by line and boxes by label. The
|
|
1677
|
+
structural key `_resolve_site` derives costs a libcst parse of the whole
|
|
1678
|
+
enclosing span (~240ms for draw_collection, the whole CLASS for a
|
|
1679
|
+
method) and produces a prefix the overlay ignores for line-keyed
|
|
1680
|
+
entries. Total cost here: one cached whole-file ast + one walk of the
|
|
1681
|
+
def + N dict writes.
|
|
1682
|
+
|
|
1683
|
+
Keys this publisher created in a PREVIOUS snapshot that this one didn't
|
|
1684
|
+
re-touch are pruned (``__frame_snapshot_keys__`` on ``fn``) so edits
|
|
1685
|
+
between menu-opens can't leave ghost markers; keys owned by other
|
|
1686
|
+
writers (manual live_view calls, twin runs) are never touched.
|
|
1687
|
+
|
|
1688
|
+
Accepts a render_func WRAPPER too — unwrapped here, since anchors and
|
|
1689
|
+
the store must live on the real body function (the wrapper's __code__
|
|
1690
|
+
points at core_render)."""
|
|
1691
|
+
fn, items, _def_line = _frame_snapshot_items(fn, scope)
|
|
1692
|
+
if fn is None:
|
|
1693
|
+
return
|
|
1694
|
+
new_keys = set()
|
|
1695
|
+
for disk, n, val in items:
|
|
1696
|
+
site = _Site((f"line:{disk}#{n}",), None, None, fn, disk)
|
|
1697
|
+
_publish(site, val, n, bare=False)
|
|
1698
|
+
new_keys.add(site.key_path)
|
|
1699
|
+
try:
|
|
1700
|
+
prev = vars(fn).get("__frame_snapshot_keys__") or set()
|
|
1701
|
+
_prune_keys(fn, [k for k in prev - new_keys])
|
|
1702
|
+
vars(fn)["__frame_snapshot_keys__"] = new_keys
|
|
1703
|
+
except (AttributeError, TypeError):
|
|
1704
|
+
pass
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _frame_snapshot_items(fn, scope, tree=None):
|
|
1708
|
+
"""(unwrapped_fn, [(disk_lineno, name, value), ...], def_line) anchoring
|
|
1709
|
+
a ``{name: value}`` scope onto every occurrence of each captured name in
|
|
1710
|
+
``fn``'s def (see _occurrence_lines) — the pure computation shared by
|
|
1711
|
+
publish_frame_snapshot (global store) and frame_value_store (local
|
|
1712
|
+
store). `def_line` is the DISK line of the `def` STATEMENT itself (the
|
|
1713
|
+
ast node's lineno — NOT co_firstlineno, which points at the first
|
|
1714
|
+
decorator for a decorated function). (None, [], None) when the
|
|
1715
|
+
function/scope can't be resolved.
|
|
1716
|
+
|
|
1717
|
+
A caller that already parsed the file passes `tree` — the stack view
|
|
1718
|
+
hands its project_code ast: no second whole-file parse (the tab-open
|
|
1719
|
+
cost), and the anchor lines come out in THAT text's own coordinates
|
|
1720
|
+
(no pending delta — the caller renders the very same text)."""
|
|
1721
|
+
try:
|
|
1722
|
+
fn = inspect.unwrap(fn)
|
|
1723
|
+
except Exception:
|
|
1724
|
+
pass
|
|
1725
|
+
code = getattr(fn, "__code__", None)
|
|
1726
|
+
if code is None or not isinstance(fn, types.FunctionType) or not scope:
|
|
1727
|
+
return None, [], None
|
|
1728
|
+
if tree is not None:
|
|
1729
|
+
delta = 0
|
|
1730
|
+
else:
|
|
1731
|
+
path = Path(code.co_filename).resolve()
|
|
1732
|
+
try:
|
|
1733
|
+
tree, _text, _sig = _ast_for(path, path.stat().st_mtime)
|
|
1734
|
+
except (OSError, SyntaxError, ValueError):
|
|
1735
|
+
return None, [], None
|
|
1736
|
+
delta = _stamp_delta(path, code.co_firstlineno) # pending = disk + Δ
|
|
1737
|
+
fdef = _def_node_for(tree, fn, code.co_firstlineno + delta)
|
|
1738
|
+
if fdef is None:
|
|
1739
|
+
return None, [], None
|
|
1740
|
+
anchors = _occurrence_lines(fdef)
|
|
1741
|
+
items = []
|
|
1742
|
+
for n, lns in anchors.items():
|
|
1743
|
+
if "." in n:
|
|
1744
|
+
# Attribute chain (`draw_state.some_val`, each segment of
|
|
1745
|
+
# `a.b.c` anchors separately): resolve the value through the
|
|
1746
|
+
# captured base object - instance __dict__ / plain class attrs
|
|
1747
|
+
# only, never through properties or descriptors (a DrawState
|
|
1748
|
+
# geometry getter must be run on the main worker).
|
|
1749
|
+
ok, val = _resolve_dotted(scope, n)
|
|
1750
|
+
if not ok:
|
|
1751
|
+
continue
|
|
1752
|
+
elif n in scope:
|
|
1753
|
+
val = scope[n]
|
|
1754
|
+
else:
|
|
1755
|
+
continue
|
|
1756
|
+
for ln in lns:
|
|
1757
|
+
items.append((ln - delta, n, val))
|
|
1758
|
+
return fn, items, fdef.lineno - delta
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
class LocalValueStore:
|
|
1762
|
+
"""A live-value store computed locally and PASSED IN (draw_text's
|
|
1763
|
+
``live_store=``) instead of published onto the live function. Same
|
|
1764
|
+
attach-to-object shape the whole live-view stack reads — values, labels
|
|
1765
|
+
and watchers all ride this object (live_values_for, label_for, watch) —
|
|
1766
|
+
so the overlay/marker/window machinery works unchanged, but nothing
|
|
1767
|
+
global ever sees it: two views of two different captures of the same
|
|
1768
|
+
function coexist, and dropping the store releases everything it pinned.
|
|
1769
|
+
`__def_line__` (disk line of the owning def) is how the snapshot overlay
|
|
1770
|
+
matches the store to ITS def node and no other."""
|
|
1771
|
+
|
|
1772
|
+
def __init__(self, qualname="?", def_line=None):
|
|
1773
|
+
self.__qualname__ = qualname
|
|
1774
|
+
self.__name__ = qualname.rsplit(".", 1)[-1]
|
|
1775
|
+
self.__def_line__ = def_line
|
|
1776
|
+
self.__live_values__ = {}
|
|
1777
|
+
self.__live_labels__ = {}
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def frame_value_store(fn, scope, tree=None):
|
|
1781
|
+
"""A LocalValueStore of ``scope`` anchored on ``fn``'s def — the same
|
|
1782
|
+
anchor computation publish_frame_snapshot uses, with NOTHING written to
|
|
1783
|
+
``fn`` and nothing registered globally. For callers that own their
|
|
1784
|
+
capture (the stack trace view): build once, hand to draw_text as
|
|
1785
|
+
``live_store=``, drop when done. Pass `tree` (an already-parsed ast of
|
|
1786
|
+
the text being rendered) to skip the whole-file parse and anchor in
|
|
1787
|
+
that text's coordinates. Returns None when unresolvable."""
|
|
1788
|
+
fn, items, def_line = _frame_snapshot_items(fn, scope, tree=tree)
|
|
1789
|
+
if fn is None or not items:
|
|
1790
|
+
return None
|
|
1791
|
+
# def_line is the ast def statement's line - co_firstlineno points at
|
|
1792
|
+
# the first DECORATOR for a decorated function (every _render_func
|
|
1793
|
+
# view), and the overlay's def-match gate compares against the span
|
|
1794
|
+
# parse tree which starts at the def itself: stamping the decorator line
|
|
1795
|
+
# made the gate reject the store and no marker ever drew (08-31).
|
|
1796
|
+
store = LocalValueStore(qualname=getattr(fn, "__qualname__", fn.__name__),
|
|
1797
|
+
def_line=def_line)
|
|
1798
|
+
for disk, n, val in items:
|
|
1799
|
+
key_path = (f"line:{disk}#{n}",)
|
|
1800
|
+
store.__live_values__[key_path] = val
|
|
1801
|
+
store.__live_labels__[key_path] = n
|
|
1802
|
+
return store
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def _def_node_for(tree, fn, target_lineno):
|
|
1806
|
+
"""``fn``'s FunctionDef in the (pending-text) ast: name match, def line
|
|
1807
|
+
nearest ``target_lineno`` — the co_firstlineno proximity trick
|
|
1808
|
+
live_instrument uses, tolerant of the decorator-line offset. Scans
|
|
1809
|
+
module and class bodies only (a full ast.walk is O(file nodes) per
|
|
1810
|
+
call, and _enclosing_function can't resolve deeper functions anyway)."""
|
|
1811
|
+
best, best_d = None, None
|
|
1812
|
+
want = getattr(fn, "__name__", None)
|
|
1813
|
+
|
|
1814
|
+
def consider(node):
|
|
1815
|
+
nonlocal best, best_d
|
|
1816
|
+
d = abs(node.lineno - target_lineno)
|
|
1817
|
+
if best_d is None or d < best_d:
|
|
1818
|
+
best, best_d = node, d
|
|
1819
|
+
|
|
1820
|
+
for stmt in tree.body:
|
|
1821
|
+
if (isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
1822
|
+
and stmt.name == want):
|
|
1823
|
+
consider(stmt)
|
|
1824
|
+
elif isinstance(stmt, ast.ClassDef):
|
|
1825
|
+
for sub in stmt.body:
|
|
1826
|
+
if (isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
1827
|
+
and sub.name == want):
|
|
1828
|
+
consider(sub)
|
|
1829
|
+
return best
|
|
1830
|
+
|
|
1831
|
+
|
|
1832
|
+
# Scopes a binding walk must NOT descend into: their Store names bind in a
|
|
1833
|
+
# DIFFERENT frame (nested defs/classes, lambdas, comprehensions).
|
|
1834
|
+
_FOREIGN_SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef,
|
|
1835
|
+
ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp,
|
|
1836
|
+
ast.GeneratorExp)
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
def _occurrence_lines(fdef):
|
|
1840
|
+
"""``{name: set of pending linenos}`` of every occurrence of a name in
|
|
1841
|
+
``fdef``'s own scope: parameters at their signature lines, then EVERY
|
|
1842
|
+
Name node — Store and Load alike — so references anchor live views, not
|
|
1843
|
+
just bindings. (The publisher filters to names actually captured in the
|
|
1844
|
+
frame's scope, which is also what keeps module globals like `imgui` out:
|
|
1845
|
+
they're Load names here but never frame locals.) Pure ATTRIBUTE chains
|
|
1846
|
+
off a Name (`draw_state.some_val`, and each inner segment of `a.b.c`)
|
|
1847
|
+
anchor too, under their dotted spelling — single-line chains only (the
|
|
1848
|
+
overlay boxes the final segment by regex on its line) and never through
|
|
1849
|
+
calls/subscripts (`foo().x` has no frame-resolvable base). Nested
|
|
1850
|
+
defs/classes/lambdas/comprehensions are not descended — their names
|
|
1851
|
+
live in other frames."""
|
|
1852
|
+
anchors = {}
|
|
1853
|
+
a = fdef.args
|
|
1854
|
+
params = list(a.posonlyargs) + list(a.args) + list(a.kwonlyargs)
|
|
1855
|
+
for extra in (a.vararg, a.kwarg):
|
|
1856
|
+
if extra is not None:
|
|
1857
|
+
params.append(extra)
|
|
1858
|
+
for arg in params:
|
|
1859
|
+
anchors.setdefault(arg.arg, set()).add(arg.lineno)
|
|
1860
|
+
|
|
1861
|
+
def walk(node):
|
|
1862
|
+
for child in ast.iter_child_nodes(node):
|
|
1863
|
+
if isinstance(child, _FOREIGN_SCOPES):
|
|
1864
|
+
continue
|
|
1865
|
+
if isinstance(child, ast.Name):
|
|
1866
|
+
anchors.setdefault(child.id, set()).add(child.lineno)
|
|
1867
|
+
elif (isinstance(child, ast.Attribute)
|
|
1868
|
+
and getattr(child, "end_lineno", child.lineno) == child.lineno):
|
|
1869
|
+
dotted = _dotted_name(child)
|
|
1870
|
+
if dotted is not None:
|
|
1871
|
+
anchors.setdefault(dotted, set()).add(child.lineno)
|
|
1872
|
+
walk(child)
|
|
1873
|
+
|
|
1874
|
+
walk(ast.Module(body=fdef.body, type_ignores=[]))
|
|
1875
|
+
return anchors
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
def _dotted_name(node):
|
|
1879
|
+
"""`a.b.c` for a pure Name-rooted attribute chain, else None (a call,
|
|
1880
|
+
subscript or literal anywhere in the chain has no frame-local base)."""
|
|
1881
|
+
parts = []
|
|
1882
|
+
while isinstance(node, ast.Attribute):
|
|
1883
|
+
parts.append(node.attr)
|
|
1884
|
+
node = node.value
|
|
1885
|
+
if isinstance(node, ast.Name):
|
|
1886
|
+
parts.append(node.id)
|
|
1887
|
+
return ".".join(reversed(parts))
|
|
1888
|
+
return None
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _safe_attr_value(base, attr):
|
|
1892
|
+
"""(ok, value) of ``base.attr`` WITHOUT running any code: instance
|
|
1893
|
+
__dict__ first, then a plain class attribute via getattr_static —
|
|
1894
|
+
properties, descriptors, and class-level functions (methods) are
|
|
1895
|
+
refused rather than fired or published as noise."""
|
|
1896
|
+
d = getattr(base, "__dict__", None)
|
|
1897
|
+
if isinstance(d, dict) and attr in d:
|
|
1898
|
+
return True, d[attr]
|
|
1899
|
+
try:
|
|
1900
|
+
static = inspect.getattr_static(base, attr)
|
|
1901
|
+
except Exception:
|
|
1902
|
+
return False, None
|
|
1903
|
+
if isinstance(static, (staticmethod, classmethod, property)):
|
|
1904
|
+
return False, None
|
|
1905
|
+
if (inspect.isfunction(static) or inspect.ismethoddescriptor(static)
|
|
1906
|
+
or inspect.isdatadescriptor(static) or inspect.isbuiltin(static)):
|
|
1907
|
+
return False, None
|
|
1908
|
+
return True, static
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def _resolve_dotted(scope, dotted):
|
|
1912
|
+
"""(ok, value) of a dotted occurrence resolved from the captured frame
|
|
1913
|
+
scope — the base must be a captured local, every hop must pass
|
|
1914
|
+
_safe_attr_value."""
|
|
1915
|
+
parts = dotted.split(".")
|
|
1916
|
+
if parts[0] not in scope:
|
|
1917
|
+
return False, None
|
|
1918
|
+
obj = scope[parts[0]]
|
|
1919
|
+
for seg in parts[1:]:
|
|
1920
|
+
ok, obj = _safe_attr_value(obj, seg)
|
|
1921
|
+
if not ok:
|
|
1922
|
+
return False, None
|
|
1923
|
+
return True, obj
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
# Serializes snapshot workers: rapid menu-opens must not interleave two
|
|
1927
|
+
# publishes' shared bookkeeping on the same function.
|
|
1928
|
+
_snapshot_lock = threading.Lock()
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def publish_stack_locals(frames, extra_snapshots=None):
|
|
1932
|
+
"""The context-menu capture hook: publish every real caller frame's
|
|
1933
|
+
locals into that function's live-value store (see publish_frame_snapshot).
|
|
1934
|
+
``frames`` is the raw get_live_frames output — entry[4] is the frame's
|
|
1935
|
+
f_locals copy. ``extra_snapshots`` is a list of extra (fn, scope,
|
|
1936
|
+
upto_lineno) publishes to run in the same batch (the capture site adds
|
|
1937
|
+
the TARGET view function's entry scope).
|
|
1938
|
+
|
|
1939
|
+
Runs on a short-lived daemon worker: a first publish's site resolution
|
|
1940
|
+
parses the enclosing span through libcst (one linemap per function,
|
|
1941
|
+
cached per file-gen; a METHOD's span is its whole class) — far too heavy
|
|
1942
|
+
for the render thread at menu-open. The store/watcher machinery is
|
|
1943
|
+
worker-safe by design (the instrumented twin publishes from workers);
|
|
1944
|
+
markers appear a beat after the menu via the normal watcher wake."""
|
|
1945
|
+
threading.Thread(target=_publish_stack_locals_sync,
|
|
1946
|
+
args=(frames, extra_snapshots),
|
|
1947
|
+
name="lv-frame-snapshot", daemon=True).start()
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def _publish_stack_locals_sync(frames, extra_snapshots=None):
|
|
1951
|
+
"""Worker body of publish_stack_locals. Dispatch machinery and
|
|
1952
|
+
non-project source are skipped, and a frame whose resolved function's
|
|
1953
|
+
name doesn't match (lambdas, comprehensions) is dropped rather than
|
|
1954
|
+
mis-published. Each item ALSO records its full scope's runtime types
|
|
1955
|
+
into FuncsMetadata here (aliases like ds/value included — the anchored
|
|
1956
|
+
publishes only cover source-bound names), so the capture site pays for
|
|
1957
|
+
nothing but the stack grab itself. Best-effort per item — a resolution
|
|
1958
|
+
hiccup must never break the batch."""
|
|
1959
|
+
from meltygui.code.chain_converters import _is_dispatch_frame
|
|
1960
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
1961
|
+
from meltygui.code.fileref import is_editable_source
|
|
1962
|
+
from meltygui.core.rendering.func_metadata import FuncsMetadata
|
|
1963
|
+
with _snapshot_lock:
|
|
1964
|
+
for entry in frames or ():
|
|
1965
|
+
if len(entry) < 5 or not entry[4]:
|
|
1966
|
+
continue
|
|
1967
|
+
filename, lineno, func_name = entry[0], entry[1], entry[2]
|
|
1968
|
+
try:
|
|
1969
|
+
if (_is_dispatch_frame(filename, func_name)
|
|
1970
|
+
or not is_editable_source(filename)):
|
|
1971
|
+
continue
|
|
1972
|
+
fn = _enclosing_function(filename, lineno)
|
|
1973
|
+
if fn is None or getattr(fn, "__name__", None) != func_name:
|
|
1974
|
+
continue
|
|
1975
|
+
FuncsMetadata.record(fn, entry[4])
|
|
1976
|
+
publish_frame_snapshot(fn, entry[4], upto_lineno=lineno)
|
|
1977
|
+
except Exception:
|
|
1978
|
+
continue
|
|
1979
|
+
for fn, scope, upto in extra_snapshots or ():
|
|
1980
|
+
try:
|
|
1981
|
+
FuncsMetadata.record(fn, scope)
|
|
1982
|
+
publish_frame_snapshot(fn, scope, upto_lineno=upto)
|
|
1983
|
+
except Exception:
|
|
1984
|
+
continue
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def _stamp_delta(path, anchor_line):
|
|
1988
|
+
"""Stamp→pending line bridge. Line stamps (twin snapshot calls, editor
|
|
1989
|
+
overlay lookups) are DISK-anchored — enclosing span's disk start +
|
|
1990
|
+
pending-relative offset, the co_firstlineno invariant — while _ast_for's
|
|
1991
|
+
tree is the PENDING text. The difference is the net shift of queued
|
|
1992
|
+
edits fully above the enclosing def, so callers anchor at the DEF start
|
|
1993
|
+
(co_firstlineno / the resolved function), never at the stamp itself: a
|
|
1994
|
+
grown function's stamps can sit past its own span's disk end, which
|
|
1995
|
+
would wrongly count the span's own edit into the delta."""
|
|
1996
|
+
from meltygui.code.live_instrument import _delta_above
|
|
1997
|
+
from meltygui.code.live_instrument import _pending_gen
|
|
1998
|
+
if not _pending_gen(str(path)):
|
|
1999
|
+
return 0
|
|
2000
|
+
return _delta_above(str(path), anchor_line)
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
def _resolve_site(code, lineno):
|
|
2004
|
+
"""The slow once-per-(code, line) path: locate the enclosing top-level
|
|
2005
|
+
statement via ast, run the dict conversion on just that span, and derive
|
|
2006
|
+
this call's key, the preceding assignment's name, and the store object.
|
|
2007
|
+
Tree/LineMap lookups use pending coords (line_p); the store resolution
|
|
2008
|
+
and the published line:N keys keep the raw DISK-anchored stamp — live
|
|
2009
|
+
co_firstlineno values and the editor overlay both speak that
|
|
2010
|
+
convention."""
|
|
2011
|
+
from meltygui.code.chain_converters import _enclosing_function
|
|
2012
|
+
from meltygui.code.chain_converters import _module_for_file
|
|
2013
|
+
|
|
2014
|
+
path = Path(code.co_filename).resolve()
|
|
2015
|
+
mtime = path.stat().st_mtime
|
|
2016
|
+
tree, text, sig = _ast_for(path, mtime)
|
|
2017
|
+
line_p = lineno + _stamp_delta(
|
|
2018
|
+
path, code.co_firstlineno
|
|
2019
|
+
if code.co_flags & inspect.CO_OPTIMIZED else lineno)
|
|
2020
|
+
var_name = _previous_assign_name(tree, line_p)
|
|
2021
|
+
|
|
2022
|
+
# A class body or method frame is not CO_OPTIMIZED; _enclosing_function
|
|
2023
|
+
# would wrongly pick the nearest def ABOVE such a line (it has the end
|
|
2024
|
+
# check), so only function frames attach to a function.
|
|
2025
|
+
store_obj = None
|
|
2026
|
+
store_is_module = True
|
|
2027
|
+
if code.co_flags & inspect.CO_OPTIMIZED:
|
|
2028
|
+
store_obj = _enclosing_function(code.co_filename, lineno)
|
|
2029
|
+
store_is_module = store_obj is None
|
|
2030
|
+
if store_obj is None:
|
|
2031
|
+
store_obj = _module_for_file(path)
|
|
2032
|
+
|
|
2033
|
+
try:
|
|
2034
|
+
loop_dims = _loop_dims_at(tree, line_p)
|
|
2035
|
+
except Exception:
|
|
2036
|
+
loop_dims = () # never let loop context kill site resolution
|
|
2037
|
+
|
|
2038
|
+
span = _top_level_span(tree, line_p)
|
|
2039
|
+
lm = _linemap_for(path, sig, span, text)
|
|
2040
|
+
arg_label = None
|
|
2041
|
+
|
|
2042
|
+
ref = _live_view_ref(lm, line_p)
|
|
2043
|
+
if ref is not None:
|
|
2044
|
+
key_path = _store_relative(ref.path, store_obj, store_is_module)
|
|
2045
|
+
arg_label = _arg_source(ref.value)
|
|
2046
|
+
else:
|
|
2047
|
+
# The line isn't surfaced by a bare call: either it's an
|
|
2048
|
+
# assignment form (y = live_view(x)`, keyed by its target - a real
|
|
2049
|
+
# statement key, keep it) or it sits in a body the dict conversion
|
|
2050
|
+
# doesn't extract (while/try/catch, nested def). The latter resolves
|
|
2051
|
+
# to a CONTAINER, which would conflict across sites - qualify by line.
|
|
2052
|
+
fallback = lm.node_at_line(line_p, absolute=True)
|
|
2053
|
+
if fallback is not None:
|
|
2054
|
+
full_path, call_node = _truncate_into_call(lm.root, fallback.path)
|
|
2055
|
+
key_path = _store_relative(full_path, store_obj, store_is_module)
|
|
2056
|
+
# arg_label means "the explicit live_view(expr) argument" - only a
|
|
2057
|
+
# live_view call may supply it. The truncation cuts at ANY CallParse
|
|
2058
|
+
# the path hits, so a snapshot stamp on `x = obj.method("lit")`
|
|
2059
|
+
# lands here on the RHS call - reading ITS first arg produced a
|
|
2060
|
+
# bogus label ('"lit"'), which also blocked _record_scope_type's
|
|
2061
|
+
# trust-the-injected-name rule (arg_label must be None for stamps).
|
|
2062
|
+
arg_label = (_arg_source(call_node)
|
|
2063
|
+
if _is_live_view_callparse(call_node) else None)
|
|
2064
|
+
if (call_node is None and isinstance(fallback.value, dict)
|
|
2065
|
+
and fallback.span.start_line != line_p - lm.line_offset):
|
|
2066
|
+
# The line landed in an enclosing CONTAINER (a while/with
|
|
2067
|
+
# body the dict conversion doesn't surface) - that key would
|
|
2068
|
+
# collide across every site in the block, so qualify by
|
|
2069
|
+
# line. A hit whose statement STARTS at this line is the
|
|
2070
|
+
# statement's bare entry - including a dict-VALUED assignment
|
|
2071
|
+
# (`x = {...}`: value is a dict instance, but it's a call) -
|
|
2072
|
+
# and keeps its clean statement key.
|
|
2073
|
+
key_path = key_path + (f"line:{lineno}",)
|
|
2074
|
+
else:
|
|
2075
|
+
key_path = (f"line:{lineno}",)
|
|
2076
|
+
if not key_path:
|
|
2077
|
+
key_path = (f"line:{lineno}",)
|
|
2078
|
+
if arg_label is None:
|
|
2079
|
+
# No CallParse to read the arg from (un-surfaced body) - use the ast.
|
|
2080
|
+
arg_label = _arg_source_ast(tree, line_p)
|
|
2081
|
+
return _Site(key_path, var_name, arg_label, store_obj, lineno, loop_dims)
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def _loop_dims_at(tree, lineno):
|
|
2085
|
+
"""Auto dim names of the loops enclosing `lineno`, outermost first,
|
|
2086
|
+
within the line's innermost frame scope — a def/class boundary resets
|
|
2087
|
+
the chain (its body publishes from a different frame, so outer loops
|
|
2088
|
+
don't repeat ITS sites). Only a loop's BODY iterates: a line in its
|
|
2089
|
+
`else:` runs once and is not counted, and the header line itself
|
|
2090
|
+
(`for x in live_view(seq):`) contributes nothing."""
|
|
2091
|
+
dims, body = [], tree.body
|
|
2092
|
+
while True:
|
|
2093
|
+
stmt = next(
|
|
2094
|
+
(s for s in body
|
|
2095
|
+
if s.lineno <= lineno <= getattr(s, "end_lineno", s.lineno)),
|
|
2096
|
+
None)
|
|
2097
|
+
if stmt is None:
|
|
2098
|
+
return tuple(dims)
|
|
2099
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef,
|
|
2100
|
+
ast.ClassDef)):
|
|
2101
|
+
dims = []
|
|
2102
|
+
next_body = None
|
|
2103
|
+
for block, iterates in _stmt_blocks(stmt):
|
|
2104
|
+
if any(s.lineno <= lineno <= getattr(s, "end_lineno", s.lineno)
|
|
2105
|
+
for s in block):
|
|
2106
|
+
if iterates:
|
|
2107
|
+
dims.append(_loop_name(stmt))
|
|
2108
|
+
next_body = block
|
|
2109
|
+
break
|
|
2110
|
+
if next_body is None:
|
|
2111
|
+
return tuple(dims)
|
|
2112
|
+
body = next_body
|
|
2113
|
+
|
|
2114
|
+
|
|
2115
|
+
def _stmt_blocks(stmt):
|
|
2116
|
+
"""(statement block, iterates) pairs for every block of `stmt` — only a
|
|
2117
|
+
For/While `body` iterates."""
|
|
2118
|
+
is_loop = isinstance(stmt, (ast.For, ast.AsyncFor, ast.While))
|
|
2119
|
+
for field in _BODY_FIELDS:
|
|
2120
|
+
sub = getattr(stmt, field, None)
|
|
2121
|
+
if isinstance(sub, list) and sub:
|
|
2122
|
+
yield sub, is_loop and field == "body"
|
|
2123
|
+
for handler in getattr(stmt, "handlers", None) or []:
|
|
2124
|
+
yield handler.body, False
|
|
2125
|
+
for case in getattr(stmt, "cases", None) or []:
|
|
2126
|
+
yield case.body, False
|
|
2127
|
+
|
|
2128
|
+
|
|
2129
|
+
def _loop_name(stmt):
|
|
2130
|
+
"""The auto dim name for one loop: the first plain Name in a for-target
|
|
2131
|
+
('i' for `for i, layer in enumerate(…)` — the index var by position),
|
|
2132
|
+
'iter' for a while (no target to name it by)."""
|
|
2133
|
+
target = getattr(stmt, "target", None)
|
|
2134
|
+
if target is not None:
|
|
2135
|
+
for node in ast.walk(target):
|
|
2136
|
+
if isinstance(node, ast.Name):
|
|
2137
|
+
return node.id
|
|
2138
|
+
return "iter"
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _ast_for(path, mtime):
|
|
2142
|
+
"""(tree, text, sig) of the file's IN-MEMORY source — disk with every
|
|
2143
|
+
queued (unsaved) span edit spliced in (PendingSave.current_file_text:
|
|
2144
|
+
the same text the twin and Ctrl+Enter's hotswap compile). live_view must
|
|
2145
|
+
never parse raw disk text: deferred saves leave disk stale mid-session,
|
|
2146
|
+
and resolving keys against the pre-edit layout is exactly the
|
|
2147
|
+
adjacent-line key jumbling / bare line:N fallback bug. Cache signature =
|
|
2148
|
+
(mtime, pending gen) — both cheap; the O(file) splice runs on miss
|
|
2149
|
+
only."""
|
|
2150
|
+
from meltygui.code.live_instrument import _pending_gen
|
|
2151
|
+
key = str(path)
|
|
2152
|
+
gen = _pending_gen(key)
|
|
2153
|
+
sig = (mtime, gen)
|
|
2154
|
+
cached = _asts.get(key)
|
|
2155
|
+
if cached is not None and cached[0] == sig:
|
|
2156
|
+
return cached[1], cached[2], sig
|
|
2157
|
+
text = None
|
|
2158
|
+
if gen:
|
|
2159
|
+
from meltygui.editor.pending_save import PendingSave
|
|
2160
|
+
text = PendingSave.current_file_text(path)
|
|
2161
|
+
if text is None:
|
|
2162
|
+
text = path.read_text()
|
|
2163
|
+
_t0 = time.perf_counter()
|
|
2164
|
+
tree = ast.parse(text)
|
|
2165
|
+
try: # TEMP perf: how often the full-file reparse actually fires
|
|
2166
|
+
from meltygui.core.diagnostics.perf_trace import trace as _pt
|
|
2167
|
+
_pt("live_view ast reparse", path=path.name, gen=gen,
|
|
2168
|
+
ms=round((time.perf_counter() - _t0) * 1000.0, 1))
|
|
2169
|
+
except Exception:
|
|
2170
|
+
pass
|
|
2171
|
+
_asts[key] = (sig, tree, text)
|
|
2172
|
+
return tree, text, sig
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
def _top_level_span(tree, lineno):
|
|
2176
|
+
"""(start, end) 1-indexed file lines of the top-level statement containing
|
|
2177
|
+
`lineno` — the def/class (decorators included) whose span the dict
|
|
2178
|
+
conversion runs on. None when no statement contains the line."""
|
|
2179
|
+
for stmt in tree.body:
|
|
2180
|
+
end = getattr(stmt, "end_lineno", stmt.lineno)
|
|
2181
|
+
start = min([stmt.lineno] + [d.lineno for d in
|
|
2182
|
+
getattr(stmt, "decorator_list", [])])
|
|
2183
|
+
if start <= lineno <= end:
|
|
2184
|
+
return start, end
|
|
2185
|
+
return None
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
def _linemap_for(path, sig, span, text):
|
|
2189
|
+
"""The LineMap of one top-level statement's span (whole file when span is
|
|
2190
|
+
None), rebuilt when `sig` — _ast_for's (mtime, pending gen) — changes,
|
|
2191
|
+
so queued edits that never touch disk still invalidate. Span-bounded so
|
|
2192
|
+
a save re-parses one function, not the file — the whole-file position
|
|
2193
|
+
pass holds the GIL for ~1s on big modules (see chain_converters'
|
|
2194
|
+
measurement) and this runs on the calling thread."""
|
|
2195
|
+
import libcst as cst
|
|
2196
|
+
from meltygui.code.libcst_conversion import LineMap
|
|
2197
|
+
from meltygui.code.libcst_conversion import cst_module_to_dict
|
|
2198
|
+
|
|
2199
|
+
start = span[0] if span else 1
|
|
2200
|
+
key = (str(path), start)
|
|
2201
|
+
cached = _linemaps.get(key)
|
|
2202
|
+
if cached is not None and cached[0] == sig:
|
|
2203
|
+
return cached[1]
|
|
2204
|
+
if span is None:
|
|
2205
|
+
snippet = text
|
|
2206
|
+
else:
|
|
2207
|
+
lines = text.splitlines(keepends=True)
|
|
2208
|
+
snippet = "".join(lines[span[0] - 1:span[1]])
|
|
2209
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
2210
|
+
if Toggles.TextEditor.melty_syntax:
|
|
2211
|
+
parse = cst_module_to_dict(snippet) # core_syntax (raw input)
|
|
2212
|
+
else:
|
|
2213
|
+
parse = cst_module_to_dict(cst.parse_module(snippet))
|
|
2214
|
+
lm = LineMap(parse, line_offset=start - 1)
|
|
2215
|
+
_linemaps[key] = (sig, lm)
|
|
2216
|
+
return lm
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
def _live_view_ref(lm, lineno):
|
|
2220
|
+
"""The NodeRef of the live_view CALL whose span contains `lineno` — matched
|
|
2221
|
+
by key, not node_at_line, because the deepest node at the line is usually
|
|
2222
|
+
an ARG inside the CallParse. Deepest match wins. (Reads LineMap's _entries
|
|
2223
|
+
index directly — in-package machinery, same data node_at_line scans.)"""
|
|
2224
|
+
rel_line = lineno - lm.line_offset
|
|
2225
|
+
best = None
|
|
2226
|
+
for span, depth, ref in lm._entries:
|
|
2227
|
+
if not (span.start_line <= rel_line <= span.end_line):
|
|
2228
|
+
continue
|
|
2229
|
+
if not isinstance(ref.key, str):
|
|
2230
|
+
continue
|
|
2231
|
+
if ref.key.split("#", 1)[0] != "live_view()":
|
|
2232
|
+
continue
|
|
2233
|
+
if best is None or depth > best[0]:
|
|
2234
|
+
best = (depth, ref)
|
|
2235
|
+
return best[1] if best else None
|
|
2236
|
+
|
|
2237
|
+
|
|
2238
|
+
def _truncate_into_call(root, path):
|
|
2239
|
+
"""Cut `path` segments that descend INSIDE a call's argument dict — the
|
|
2240
|
+
statement key is the site's address; which argument the line landed on is
|
|
2241
|
+
not. Returns (path, call_parse) where call_parse is the CallParse the path
|
|
2242
|
+
was cut at (None if the path never enters one)."""
|
|
2243
|
+
from meltygui.code.libcst_conversion import CallParse
|
|
2244
|
+
node = root
|
|
2245
|
+
for i, seg in enumerate(path):
|
|
2246
|
+
node = node.get(seg) if isinstance(node, dict) else None
|
|
2247
|
+
if isinstance(node, CallParse):
|
|
2248
|
+
return tuple(path[:i + 1]), node
|
|
2249
|
+
if node is None:
|
|
2250
|
+
break
|
|
2251
|
+
return tuple(path), None
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
def _owning_def_name(path):
|
|
2255
|
+
"""The name of the innermost def a key path crosses (`..., <name>,
|
|
2256
|
+
"locals", ...`), or None for module/class-level paths."""
|
|
2257
|
+
for i in range(len(path) - 1, 0, -1):
|
|
2258
|
+
if path[i] == "locals":
|
|
2259
|
+
return path[i - 1]
|
|
2260
|
+
return None
|
|
2261
|
+
|
|
2262
|
+
|
|
2263
|
+
def _store_relative(path, store_obj, store_is_module):
|
|
2264
|
+
"""Key path relative to the STORE OBJECT's scope. For a function store,
|
|
2265
|
+
cut at the first `<func name>, "locals"` pair — nested-def segments stay,
|
|
2266
|
+
so two closures' sites on the same outer function can't collide, and the
|
|
2267
|
+
editor parsing that function's span sees the same paths. Module stores
|
|
2268
|
+
keep the full path."""
|
|
2269
|
+
if store_is_module:
|
|
2270
|
+
return tuple(path)
|
|
2271
|
+
fn_name = getattr(store_obj, "__name__", None)
|
|
2272
|
+
for i in range(len(path) - 1):
|
|
2273
|
+
if path[i] == fn_name and path[i + 1] == "locals":
|
|
2274
|
+
return tuple(path[i + 2:])
|
|
2275
|
+
# Renamed wrapper / span mismatch: fall back to the innermost scope cut.
|
|
2276
|
+
for i in range(len(path) - 1, -1, -1):
|
|
2277
|
+
if path[i] == "locals":
|
|
2278
|
+
return tuple(path[i + 1:])
|
|
2279
|
+
return tuple(path)
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def _is_live_view_callparse(call_parse):
|
|
2283
|
+
"""True when a CallParse is a `live_view(...)` call. Guards arg_label
|
|
2284
|
+
resolution: the statement-path truncation cuts at whatever CallParse it
|
|
2285
|
+
enters first, which for a snapshot-stamped assignment is the RHS's own
|
|
2286
|
+
call — whose arguments have nothing to do with live_view's."""
|
|
2287
|
+
if not isinstance(call_parse, dict):
|
|
2288
|
+
return False
|
|
2289
|
+
func_name = getattr(call_parse, "func_name", None) # CallParse, either form
|
|
2290
|
+
if isinstance(func_name, str):
|
|
2291
|
+
return func_name.rsplit(".", 1)[-1] == "live_view"
|
|
2292
|
+
try:
|
|
2293
|
+
func = call_parse.get("__cst__").func
|
|
2294
|
+
except Exception:
|
|
2295
|
+
return False
|
|
2296
|
+
name = getattr(func, "value", None) # cst.Name
|
|
2297
|
+
if not isinstance(name, str):
|
|
2298
|
+
name = getattr(getattr(func, "attr", None), "value", None) # cst.Attribute
|
|
2299
|
+
return name == "live_view"
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
def _arg_source(call_parse):
|
|
2303
|
+
"""The source text of an explicit `live_view(expr)` argument, for display.
|
|
2304
|
+
The positional arg binds to live_view's `value` parameter when the
|
|
2305
|
+
signature resolves, else surfaces under a synthetic `arg0` key — take the
|
|
2306
|
+
first real entry either way."""
|
|
2307
|
+
if not isinstance(call_parse, dict):
|
|
2308
|
+
return None
|
|
2309
|
+
arg = call_parse.get("value")
|
|
2310
|
+
if arg is None:
|
|
2311
|
+
for k, v in call_parse.items():
|
|
2312
|
+
if isinstance(k, str) and not k.startswith("__"):
|
|
2313
|
+
arg = v
|
|
2314
|
+
break
|
|
2315
|
+
return str(arg) if arg is not None else None
|
|
2316
|
+
|
|
2317
|
+
|
|
2318
|
+
# ── previous-assignment resolution (ast) ──────────────────────────────────────
|
|
2319
|
+
# The display dict drops statements it can't surface (aug-assigns, tuple
|
|
2320
|
+
# unpacks, while/with/match bodies), so walking its keys silently skips them
|
|
2321
|
+
# and captures an OLDER variable. The ast sees every statement; resolve the
|
|
2322
|
+
# bare form against it instead.
|
|
2323
|
+
|
|
2324
|
+
def _previous_assign_name(tree, lineno):
|
|
2325
|
+
"""The variable assigned by the statement preceding the live_view call at
|
|
2326
|
+
`lineno`: locate the deepest body list holding the call's statement, walk
|
|
2327
|
+
its preceding siblings backwards, and take the first assignment target —
|
|
2328
|
+
descending into a preceding block's last assignment, skipping defs."""
|
|
2329
|
+
found = _stmt_in_body(tree.body, lineno)
|
|
2330
|
+
if found is None:
|
|
2331
|
+
return None
|
|
2332
|
+
body, idx = found
|
|
2333
|
+
for stmt in reversed(body[:idx]):
|
|
2334
|
+
name = _direct_target(stmt)
|
|
2335
|
+
if name is _STOP:
|
|
2336
|
+
# The preceding statement DOES bind something we can't name (tuple
|
|
2337
|
+
# unpack, obj.attr / a[i] target) - publishing an older variable
|
|
2338
|
+
# instead would be silently losing data. Capture nothing.
|
|
2339
|
+
return None
|
|
2340
|
+
if name is not None:
|
|
2341
|
+
return name
|
|
2342
|
+
name = _last_target(stmt) # blocks capture their last assignment
|
|
2343
|
+
if name is not None:
|
|
2344
|
+
return name
|
|
2345
|
+
return None
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
_BODY_FIELDS = ("body", "orelse", "finalbody")
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
def _stmt_in_body(body, lineno):
|
|
2352
|
+
"""(body list, index) of the statement containing a live_view call at
|
|
2353
|
+
`lineno`, in the DEEPEST body list that holds it."""
|
|
2354
|
+
for i, stmt in enumerate(body):
|
|
2355
|
+
end = getattr(stmt, "end_lineno", stmt.lineno)
|
|
2356
|
+
if not (stmt.lineno <= lineno <= end):
|
|
2357
|
+
continue
|
|
2358
|
+
for field in _BODY_FIELDS:
|
|
2359
|
+
sub = getattr(stmt, field, None)
|
|
2360
|
+
if isinstance(sub, list) and sub:
|
|
2361
|
+
found = _stmt_in_body(sub, lineno)
|
|
2362
|
+
if found is not None:
|
|
2363
|
+
return found
|
|
2364
|
+
for handler in getattr(stmt, "handlers", None) or []:
|
|
2365
|
+
found = _stmt_in_body(handler.body, lineno)
|
|
2366
|
+
if found is not None:
|
|
2367
|
+
return found
|
|
2368
|
+
for case in getattr(stmt, "cases", None) or []:
|
|
2369
|
+
found = _stmt_in_body(case.body, lineno)
|
|
2370
|
+
if found is not None:
|
|
2371
|
+
return found
|
|
2372
|
+
if _holds_live_view_call(stmt):
|
|
2373
|
+
return body, i
|
|
2374
|
+
return None
|
|
2375
|
+
return None
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def _arg_source_ast(tree, lineno):
|
|
2379
|
+
"""The explicit argument's source for a live_view call at `lineno`, read
|
|
2380
|
+
from the ast — the label fallback for calls in bodies the dict conversion
|
|
2381
|
+
doesn't surface (while/with/match, nested defs)."""
|
|
2382
|
+
for node in ast.walk(tree):
|
|
2383
|
+
if not isinstance(node, ast.Call) or not node.args:
|
|
2384
|
+
continue
|
|
2385
|
+
end = getattr(node, "end_lineno", node.lineno)
|
|
2386
|
+
if not (node.lineno <= lineno <= end):
|
|
2387
|
+
continue
|
|
2388
|
+
func = node.func
|
|
2389
|
+
name = (func.id if isinstance(func, ast.Name)
|
|
2390
|
+
else func.attr if isinstance(func, ast.Attribute) else None)
|
|
2391
|
+
if name == "live_view":
|
|
2392
|
+
return ast.unparse(node.args[0])
|
|
2393
|
+
return None
|
|
2394
|
+
|
|
2395
|
+
|
|
2396
|
+
def _holds_live_view_call(stmt):
|
|
2397
|
+
for node in ast.walk(stmt):
|
|
2398
|
+
if isinstance(node, ast.Call):
|
|
2399
|
+
func = node.func
|
|
2400
|
+
name = (func.id if isinstance(func, ast.Name)
|
|
2401
|
+
else func.attr if isinstance(func, ast.Attribute) else None)
|
|
2402
|
+
if name == "live_view":
|
|
2403
|
+
return True
|
|
2404
|
+
return False
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
_STOP = object()
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
def _direct_target(stmt):
|
|
2411
|
+
"""A direct assignment's target name; _STOP for an assignment whose target
|
|
2412
|
+
can't be named as a frame local (tuple unpack, obj.attr, a[i]); None for a
|
|
2413
|
+
non-binding statement (defs and classes bind names whose body locals are
|
|
2414
|
+
not in the caller's frame — non-binding here)."""
|
|
2415
|
+
if isinstance(stmt, ast.Assign):
|
|
2416
|
+
for target in stmt.targets: # p = q = v → the first plain-name target
|
|
2417
|
+
if isinstance(target, ast.Name):
|
|
2418
|
+
return target.id
|
|
2419
|
+
return _STOP
|
|
2420
|
+
if isinstance(stmt, (ast.AugAssign, ast.AnnAssign)):
|
|
2421
|
+
return stmt.target.id if isinstance(stmt.target, ast.Name) else _STOP
|
|
2422
|
+
return None
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
def _last_target(stmt):
|
|
2426
|
+
"""The LAST direct assignment name anywhere inside a block statement, in
|
|
2427
|
+
source order — what a bare live_view right after an if/for/try captures."""
|
|
2428
|
+
last = None
|
|
2429
|
+
direct = _direct_target(stmt)
|
|
2430
|
+
if direct is not None:
|
|
2431
|
+
return direct if direct is not _STOP else None
|
|
2432
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
2433
|
+
return None
|
|
2434
|
+
for field in _BODY_FIELDS:
|
|
2435
|
+
for sub in getattr(stmt, field, None) or []:
|
|
2436
|
+
name = _last_target(sub)
|
|
2437
|
+
if name is not None:
|
|
2438
|
+
last = name
|
|
2439
|
+
for handler in getattr(stmt, "handlers", None) or []:
|
|
2440
|
+
for sub in handler.body:
|
|
2441
|
+
name = _last_target(sub)
|
|
2442
|
+
if name is not None:
|
|
2443
|
+
last = name
|
|
2444
|
+
return last
|
|
2445
|
+
|
|
2446
|
+
|
|
2447
|
+
def publish_external_values(filename, source, name, values):
|
|
2448
|
+
"""Attach project-process snapshots to the same owner the inline UI reads.
|
|
2449
|
+
|
|
2450
|
+
Only a no-op function is compiled locally: project imports, decorators,
|
|
2451
|
+
defaults and user code belong exclusively to the project interpreter.
|
|
2452
|
+
Values are (source line, variable name, decoded value) triples.
|
|
2453
|
+
"""
|
|
2454
|
+
import hashlib
|
|
2455
|
+
from meltygui.code import chain_converters as cc
|
|
2456
|
+
from meltygui.code.live_instrument import _delta_above
|
|
2457
|
+
|
|
2458
|
+
path = Path(filename).resolve()
|
|
2459
|
+
node = next(n for n in ast.parse(source).body
|
|
2460
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == name)
|
|
2461
|
+
start = min([node.lineno] + [d.lineno for d in node.decorator_list])
|
|
2462
|
+
delta = _delta_above(str(path), start)
|
|
2463
|
+
delta = _delta_above(str(path), max(1, start - delta))
|
|
2464
|
+
disk_start = max(1, node.lineno - delta)
|
|
2465
|
+
modules = cc._modules_for_file(path)
|
|
2466
|
+
if modules:
|
|
2467
|
+
module = modules[0]
|
|
2468
|
+
else:
|
|
2469
|
+
module_name = '_melty_fnrun_' + hashlib.sha256(str(path).encode()).hexdigest()
|
|
2470
|
+
module = types.ModuleType(module_name)
|
|
2471
|
+
module.__file__ = str(path)
|
|
2472
|
+
sys.modules[module_name] = module
|
|
2473
|
+
slot = f'_fnrun_live_{name}'
|
|
2474
|
+
previous = vars(module).get(slot)
|
|
2475
|
+
namespace = {'__name__': module.__name__}
|
|
2476
|
+
exec(compile('\n' * (disk_start - 1) + f'def {name}():\n pass\n',
|
|
2477
|
+
str(path), 'exec'), namespace)
|
|
2478
|
+
owner = namespace[name]
|
|
2479
|
+
owner.__fnrun_exec__ = True
|
|
2480
|
+
adopt_live_store(previous, owner)
|
|
2481
|
+
vars(module)[slot] = owner
|
|
2482
|
+
for key in list(cc._ENCLOSING_FN_CACHE):
|
|
2483
|
+
if key[0] == str(path):
|
|
2484
|
+
del cc._ENCLOSING_FN_CACHE[key]
|
|
2485
|
+
with run_capture(owner):
|
|
2486
|
+
for line, label, value in values:
|
|
2487
|
+
disk_line = line - delta
|
|
2488
|
+
site = _Site((f'line:{disk_line}#{label}',), None, None, owner, disk_line)
|
|
2489
|
+
_publish(site, value, label, bare=False)
|
|
2490
|
+
return owner
|