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,231 @@
|
|
|
1
|
+
"""Keep live definitions canonical when their source moves between modules."""
|
|
2
|
+
import inspect
|
|
3
|
+
import gc
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
from meltygui.core.melty import Melty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _patch_nested_functions(previous, replacement, namespace):
|
|
11
|
+
"""Update existing factory-created callbacks without recreating closures."""
|
|
12
|
+
def nested_codes(code):
|
|
13
|
+
result = {}
|
|
14
|
+
for child in code.co_consts:
|
|
15
|
+
if isinstance(child, types.CodeType):
|
|
16
|
+
if not child.co_name.startswith("<"):
|
|
17
|
+
result[child.co_qualname] = child
|
|
18
|
+
result.update(nested_codes(child))
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
fresh = nested_codes(replacement)
|
|
22
|
+
if not fresh:
|
|
23
|
+
return lambda: None
|
|
24
|
+
edits = []
|
|
25
|
+
# Earlier factory-only swaps can leave callbacks more than one code
|
|
26
|
+
# generation behind. Match their defining namespace, source and qualified
|
|
27
|
+
# local name rather than only referrers of the immediately previous code.
|
|
28
|
+
for function in gc.get_objects():
|
|
29
|
+
if type(function) is not types.FunctionType or function.__globals__ is not namespace:
|
|
30
|
+
continue
|
|
31
|
+
old_code = function.__code__
|
|
32
|
+
new_code = fresh.get(old_code.co_qualname)
|
|
33
|
+
if (new_code is None or new_code is old_code
|
|
34
|
+
or old_code.co_filename != previous.co_filename):
|
|
35
|
+
continue
|
|
36
|
+
if old_code.co_freevars != new_code.co_freevars:
|
|
37
|
+
raise ValueError(f"cannot preserve live closure for {old_code.co_qualname}: free variables changed")
|
|
38
|
+
edits.append((function, old_code, new_code))
|
|
39
|
+
for function, _old_code, new_code in edits:
|
|
40
|
+
function.__code__ = new_code
|
|
41
|
+
|
|
42
|
+
def restore():
|
|
43
|
+
for function, old_code, _new_code in edits:
|
|
44
|
+
function.__code__ = old_code
|
|
45
|
+
return restore
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def patch_function(live, replacement, *, force=False):
|
|
49
|
+
"""Patch in place, retaining the defining namespace after a source move.
|
|
50
|
+
|
|
51
|
+
Python cannot reassign a function's __globals__. For a relocated function,
|
|
52
|
+
its original identity becomes a small forwarding entry point; __wrapped__
|
|
53
|
+
exposes the actual source implementation to inspection and later edits.
|
|
54
|
+
Normal edits still patch code directly and pay no forwarding cost.
|
|
55
|
+
"""
|
|
56
|
+
previous = (live.__code__, live.__defaults__, live.__kwdefaults__,
|
|
57
|
+
dict(live.__annotations__), live.__doc__, live.__module__,
|
|
58
|
+
live.__qualname__, dict(live.__dict__))
|
|
59
|
+
previous_target = Melty.relocated_functions.get(id(live))
|
|
60
|
+
target = inspect.unwrap(live) if live.__dict__.get('__melty_relocated__') else live
|
|
61
|
+
target_previous = (target.__code__, target.__defaults__, target.__kwdefaults__,
|
|
62
|
+
dict(target.__annotations__), target.__doc__, target.__module__,
|
|
63
|
+
target.__qualname__)
|
|
64
|
+
|
|
65
|
+
if force and replacement.__globals__ is live.__globals__:
|
|
66
|
+
target = live
|
|
67
|
+
live.__dict__.pop('__wrapped__', None)
|
|
68
|
+
live.__dict__.pop('__melty_relocated__', None)
|
|
69
|
+
Melty.relocated_functions.pop(id(live), None)
|
|
70
|
+
|
|
71
|
+
restore_nested = None
|
|
72
|
+
if target.__globals__ is replacement.__globals__:
|
|
73
|
+
restore_nested = _patch_nested_functions(target.__code__, replacement.__code__, target.__globals__)
|
|
74
|
+
target.__code__ = replacement.__code__
|
|
75
|
+
target.__defaults__ = replacement.__defaults__
|
|
76
|
+
target.__kwdefaults__ = replacement.__kwdefaults__
|
|
77
|
+
target.__annotations__ = replacement.__annotations__
|
|
78
|
+
target.__doc__ = replacement.__doc__
|
|
79
|
+
target.__module__ = replacement.__module__
|
|
80
|
+
target.__qualname__ = replacement.__qualname__
|
|
81
|
+
else:
|
|
82
|
+
# Keep existing lexical state when the free-variable contract survives.
|
|
83
|
+
implementation = replacement
|
|
84
|
+
if target.__code__.co_freevars and target.__code__.co_freevars == replacement.__code__.co_freevars:
|
|
85
|
+
implementation = types.FunctionType(
|
|
86
|
+
replacement.__code__, replacement.__globals__, replacement.__name__,
|
|
87
|
+
replacement.__defaults__, target.__closure__)
|
|
88
|
+
implementation.__kwdefaults__ = replacement.__kwdefaults__
|
|
89
|
+
implementation.__annotations__ = replacement.__annotations__
|
|
90
|
+
implementation.__doc__ = replacement.__doc__
|
|
91
|
+
implementation.__qualname__ = replacement.__qualname__
|
|
92
|
+
Melty.relocated_functions[id(live)] = implementation
|
|
93
|
+
captures = [f'capture_{index}' for index in range(len(live.__closure__ or ()))]
|
|
94
|
+
capture_line = f' ({", ".join(captures)},)\n' if captures else ''
|
|
95
|
+
source = (
|
|
96
|
+
f'def make_entry({", ".join(captures)}):\n'
|
|
97
|
+
' def entry(*args, **kwargs):\n'
|
|
98
|
+
+ capture_line
|
|
99
|
+
+ ' from meltygui.core.melty import Melty\n'
|
|
100
|
+
+ f' return Melty.relocated_functions[{id(live)}](*args, **kwargs)\n'
|
|
101
|
+
+ ' return entry\n')
|
|
102
|
+
namespace = {}
|
|
103
|
+
exec(compile(source, '<melty definition relocation>', 'exec'), namespace)
|
|
104
|
+
entry = namespace['make_entry'](*([None] * len(captures)))
|
|
105
|
+
live.__code__ = entry.__code__
|
|
106
|
+
live.__defaults__ = None
|
|
107
|
+
live.__kwdefaults__ = None
|
|
108
|
+
live.__wrapped__ = implementation
|
|
109
|
+
live.__melty_relocated__ = True
|
|
110
|
+
live.__annotations__ = replacement.__annotations__
|
|
111
|
+
live.__doc__ = replacement.__doc__
|
|
112
|
+
live.__module__ = replacement.__module__
|
|
113
|
+
live.__qualname__ = replacement.__qualname__
|
|
114
|
+
|
|
115
|
+
def restore():
|
|
116
|
+
if restore_nested is not None:
|
|
117
|
+
restore_nested()
|
|
118
|
+
(live.__code__, live.__defaults__, live.__kwdefaults__, annotations,
|
|
119
|
+
live.__doc__, live.__module__, live.__qualname__, attributes) = previous
|
|
120
|
+
live.__annotations__ = annotations
|
|
121
|
+
live.__dict__.clear()
|
|
122
|
+
live.__dict__.update(attributes)
|
|
123
|
+
if target is not live:
|
|
124
|
+
(target.__code__, target.__defaults__, target.__kwdefaults__, annotations,
|
|
125
|
+
target.__doc__, target.__module__, target.__qualname__) = target_previous
|
|
126
|
+
target.__annotations__ = annotations
|
|
127
|
+
if previous_target is None:
|
|
128
|
+
Melty.relocated_functions.pop(id(live), None)
|
|
129
|
+
else:
|
|
130
|
+
Melty.relocated_functions[id(live)] = previous_target
|
|
131
|
+
return restore
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def canonicalize_definitions(replacements):
|
|
135
|
+
"""Rebind exports and type-bearing function metadata to the live objects.
|
|
136
|
+
|
|
137
|
+
Only exact object identities are replaced, never names shared by unrelated
|
|
138
|
+
modules. Runtime instances and arbitrary application object graphs are not
|
|
139
|
+
traversed. Closure metadata matters for render_func's injection plans and
|
|
140
|
+
for zero-argument super() in relocated methods.
|
|
141
|
+
"""
|
|
142
|
+
# Keep the original alive too: replaced tuples/signatures can otherwise
|
|
143
|
+
# be freed mid-walk and their ids reused by later function metadata.
|
|
144
|
+
seen = {}
|
|
145
|
+
|
|
146
|
+
def replace(value):
|
|
147
|
+
pair = replacements.get(id(value))
|
|
148
|
+
if pair is not None and pair[0] is value:
|
|
149
|
+
return pair[1]
|
|
150
|
+
identity = id(value)
|
|
151
|
+
if identity in seen:
|
|
152
|
+
return seen[identity][1]
|
|
153
|
+
seen[identity] = (value, value)
|
|
154
|
+
if isinstance(value, dict):
|
|
155
|
+
for key, item in list(value.items()):
|
|
156
|
+
new_key, new_item = replace(key), replace(item)
|
|
157
|
+
if new_key is not key:
|
|
158
|
+
del value[key]
|
|
159
|
+
if new_key is not key or new_item is not item:
|
|
160
|
+
value[new_key] = new_item
|
|
161
|
+
elif isinstance(value, list):
|
|
162
|
+
for index, item in enumerate(value):
|
|
163
|
+
value[index] = replace(item)
|
|
164
|
+
elif isinstance(value, tuple):
|
|
165
|
+
result = tuple(replace(item) for item in value)
|
|
166
|
+
if any(a is not b for a, b in zip(result, value)):
|
|
167
|
+
seen[identity] = (value, result)
|
|
168
|
+
return result
|
|
169
|
+
elif isinstance(value, inspect.Parameter):
|
|
170
|
+
result = value.replace(annotation=replace(value.annotation), default=replace(value.default))
|
|
171
|
+
seen[identity] = (value, result)
|
|
172
|
+
return result
|
|
173
|
+
elif isinstance(value, inspect.Signature):
|
|
174
|
+
result = value.replace(parameters=[replace(p) for p in value.parameters.values()],
|
|
175
|
+
return_annotation=replace(value.return_annotation))
|
|
176
|
+
seen[identity] = (value, result)
|
|
177
|
+
return result
|
|
178
|
+
return value
|
|
179
|
+
|
|
180
|
+
visited_functions = set()
|
|
181
|
+
|
|
182
|
+
def function_metadata(function):
|
|
183
|
+
if id(function) in visited_functions:
|
|
184
|
+
return
|
|
185
|
+
visited_functions.add(id(function))
|
|
186
|
+
function.__annotations__ = replace(function.__annotations__)
|
|
187
|
+
function.__defaults__ = replace(function.__defaults__)
|
|
188
|
+
function.__kwdefaults__ = replace(function.__kwdefaults__)
|
|
189
|
+
for cell in function.__closure__ or ():
|
|
190
|
+
try:
|
|
191
|
+
value = cell.cell_contents
|
|
192
|
+
except ValueError:
|
|
193
|
+
continue
|
|
194
|
+
replacement = replace(value)
|
|
195
|
+
if replacement is not value:
|
|
196
|
+
cell.cell_contents = replacement
|
|
197
|
+
if isinstance(replacement, types.FunctionType):
|
|
198
|
+
function_metadata(replacement)
|
|
199
|
+
wrapped = function.__dict__.get('__wrapped__')
|
|
200
|
+
if isinstance(wrapped, types.FunctionType):
|
|
201
|
+
function_metadata(wrapped)
|
|
202
|
+
|
|
203
|
+
def definition_metadata(value):
|
|
204
|
+
if isinstance(value, types.FunctionType):
|
|
205
|
+
function_metadata(value)
|
|
206
|
+
elif isinstance(value, type):
|
|
207
|
+
for member in vars(value).values():
|
|
208
|
+
if isinstance(member, types.FunctionType):
|
|
209
|
+
function_metadata(member)
|
|
210
|
+
elif isinstance(member, (staticmethod, classmethod)):
|
|
211
|
+
function_metadata(member.__func__)
|
|
212
|
+
elif isinstance(member, property):
|
|
213
|
+
for function in (member.fget, member.fset, member.fdel):
|
|
214
|
+
if function is not None:
|
|
215
|
+
function_metadata(function)
|
|
216
|
+
|
|
217
|
+
for fresh, live in replacements.values():
|
|
218
|
+
definition_metadata(fresh)
|
|
219
|
+
definition_metadata(live)
|
|
220
|
+
for module in tuple(sys.modules.values()):
|
|
221
|
+
if not isinstance(module, types.ModuleType):
|
|
222
|
+
continue
|
|
223
|
+
touched = False
|
|
224
|
+
for name, value in tuple(vars(module).items()):
|
|
225
|
+
pair = replacements.get(id(value))
|
|
226
|
+
if pair is not None and pair[0] is value:
|
|
227
|
+
vars(module)[name] = pair[1]
|
|
228
|
+
touched = True
|
|
229
|
+
if touched:
|
|
230
|
+
for value in tuple(vars(module).values()):
|
|
231
|
+
definition_metadata(value)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared framework diagnostics machinery."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Dict, Set, Tuple
|
|
2
|
+
|
|
3
|
+
from meltygui.core.rendering.window_decoration import window
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@window
|
|
7
|
+
class AttributeChurnMonitor:
|
|
8
|
+
"""Per-frame counter of @live attribute writes.
|
|
9
|
+
|
|
10
|
+
Reset at frame end; pair with Toggles.attrib_churn_log to dump
|
|
11
|
+
a top-N summary each frame the counter is non-empty.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
attributes_changed: Set[Tuple[str, str]] = set()
|
|
16
|
+
attribute_change_count: Dict[Tuple[str, str], int] = {}
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def record(cls, cls_name: str, attr_name: str) -> None:
|
|
20
|
+
key = (cls_name, attr_name)
|
|
21
|
+
cls.attributes_changed.add(key)
|
|
22
|
+
cls.attribute_change_count[key] = cls.attribute_change_count.get(key, 0) + 1
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def on_frame_end(cls) -> None:
|
|
26
|
+
if not cls.attribute_change_count:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
30
|
+
if Toggles.attrib_churn_log:
|
|
31
|
+
from meltygui.core.melty import Melty
|
|
32
|
+
top = sorted(cls.attribute_change_count.items(), key=lambda kv: -kv[1])[:10]
|
|
33
|
+
line = ", ".join(f"{c}.{a}={n}" for (c, a), n in top)
|
|
34
|
+
print(f"[churn f{Melty.frame_count}] {line}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
cls.attributes_changed.clear()
|
|
38
|
+
cls.attribute_change_count.clear()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Per-OS-window frame rate for the Toggles.show_fps titlebar readout.
|
|
2
|
+
|
|
3
|
+
Frames render on request, so an idle window presents nothing: the rate is
|
|
4
|
+
measured over the recent burst of consecutive frames only. A gap longer than
|
|
5
|
+
IDLE_GAP_S starts a new burst instead of averaging the idle time in; the
|
|
6
|
+
previous burst's rate stays on display until the new one has two frames.
|
|
7
|
+
"""
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
# Frames the rate is averaged over. Raise for a steadier, slower readout.
|
|
11
|
+
WINDOW_FRAMES = 60
|
|
12
|
+
# A pause this long between frames counts as idle, not as a slow frame.
|
|
13
|
+
IDLE_GAP_S = 0.25
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FpsCounter:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.frame_starts = []
|
|
19
|
+
self.rate = None
|
|
20
|
+
self.frame_ms = 0.0
|
|
21
|
+
|
|
22
|
+
def frame_started(self):
|
|
23
|
+
now = time.perf_counter()
|
|
24
|
+
if self.frame_starts and now - self.frame_starts[-1] > IDLE_GAP_S:
|
|
25
|
+
self.frame_starts.clear()
|
|
26
|
+
self.frame_starts.append(now)
|
|
27
|
+
del self.frame_starts[:-WINDOW_FRAMES]
|
|
28
|
+
if len(self.frame_starts) >= 2:
|
|
29
|
+
self.rate = (len(self.frame_starts) - 1) / (now - self.frame_starts[0])
|
|
30
|
+
return now
|
|
31
|
+
|
|
32
|
+
def frame_finished(self, started):
|
|
33
|
+
self.frame_ms = (time.perf_counter() - started) * 1000.0
|
|
34
|
+
|
|
35
|
+
def label(self):
|
|
36
|
+
"""'118 fps 7.9 ms': the latest burst's rate and the previous frame's
|
|
37
|
+
render-thread time. A lone frame after idle keeps the last burst's rate."""
|
|
38
|
+
rate = "--" if self.rate is None else f"{self.rate:.0f}"
|
|
39
|
+
return f"{rate} fps {self.frame_ms:.1f} ms"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""GPU frame-segment timing via GL_TIMESTAMP query objects.
|
|
2
|
+
|
|
3
|
+
CPU timers around post_frame's segments show where the RENDER THREAD blocks,
|
|
4
|
+
but a present stall is GPU backpressure: the CPU submits everything quickly
|
|
5
|
+
and swap_buffers waits for the queue to drain — the CPU split blames "swap"
|
|
6
|
+
while the actual cost sits in whichever segment's draw calls flooded the GPU
|
|
7
|
+
(UI draw data, the tile-capture pass, the filter/shadow passes, ...).
|
|
8
|
+
Timestamp queries mark when the GPU *executes* each boundary, so a completed
|
|
9
|
+
frame yields per-segment GPU milliseconds.
|
|
10
|
+
|
|
11
|
+
Non-blocking by design: results are polled a few frames late (ring of DEPTH
|
|
12
|
+
slots) and a slot whose result isn't ready yet is simply dropped when its
|
|
13
|
+
turn to be reused comes. Inert unless the perf-trace toggle is on, and it
|
|
14
|
+
permanently disables itself on the first GL error (headless contexts, old
|
|
15
|
+
drivers, missing GL 3.3 timer support)."""
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GpuFrameTimer:
|
|
21
|
+
DEPTH = 4 # frames in flight before a result is read
|
|
22
|
+
MAX_STAMPS = 8 # boundaries per frame
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._gl = None
|
|
26
|
+
self._rings = None # DEPTH x MAX_STAMPS query IDs
|
|
27
|
+
self._labels = None # DEPTH x MAX_STAMPS stamp labels
|
|
28
|
+
self._counts = [0] * self.DEPTH
|
|
29
|
+
self._frames = [None] * self.DEPTH # app frame number per slot
|
|
30
|
+
self._slot = 0
|
|
31
|
+
self._dead = False
|
|
32
|
+
self._active = False # stamping enabled for the current frame
|
|
33
|
+
|
|
34
|
+
def begin(self, enabled, frame_no):
|
|
35
|
+
"""Arm the current slot for this frame. GL context must be current."""
|
|
36
|
+
if self._dead or not enabled:
|
|
37
|
+
self._active = False
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
if self._rings is None:
|
|
41
|
+
from OpenGL import GL as gl
|
|
42
|
+
self._gl = gl
|
|
43
|
+
self._rings = [[int(q) for q in gl.glGenQueries(self.MAX_STAMPS)]
|
|
44
|
+
for _ in range(self.DEPTH)]
|
|
45
|
+
self._labels = [[None] * self.MAX_STAMPS
|
|
46
|
+
for _ in range(self.DEPTH)]
|
|
47
|
+
self._counts[self._slot] = 0
|
|
48
|
+
self._frames[self._slot] = frame_no
|
|
49
|
+
self._active = True
|
|
50
|
+
except Exception:
|
|
51
|
+
self._dead = True
|
|
52
|
+
self._active = False
|
|
53
|
+
|
|
54
|
+
def stamp(self, label):
|
|
55
|
+
if not self._active:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
i = self._counts[self._slot]
|
|
59
|
+
if i >= self.MAX_STAMPS:
|
|
60
|
+
return
|
|
61
|
+
self._gl.glQueryCounter(self._rings[self._slot][i],
|
|
62
|
+
self._gl.GL_TIMESTAMP)
|
|
63
|
+
self._labels[self._slot][i] = label
|
|
64
|
+
self._counts[self._slot] = i + 1
|
|
65
|
+
except Exception:
|
|
66
|
+
self._dead = True
|
|
67
|
+
self._active = False
|
|
68
|
+
|
|
69
|
+
def end(self):
|
|
70
|
+
"""Advance the ring; poll the OLDEST slot. Returns (frame_no,
|
|
71
|
+
{label: gpu_ms}) for a completed frame, else None. Never blocks: an
|
|
72
|
+
unavailable result stays queued until its slot is about to be reused,
|
|
73
|
+
then drops."""
|
|
74
|
+
if not self._active:
|
|
75
|
+
return None
|
|
76
|
+
out = None
|
|
77
|
+
try:
|
|
78
|
+
gl = self._gl
|
|
79
|
+
nxt = (self._slot + 1) % self.DEPTH
|
|
80
|
+
n = self._counts[nxt]
|
|
81
|
+
if n >= 2 and self._frames[nxt] is not None:
|
|
82
|
+
avail = gl.glGetQueryObjectiv(self._rings[nxt][n - 1],
|
|
83
|
+
gl.GL_QUERY_RESULT_AVAILABLE)
|
|
84
|
+
if avail:
|
|
85
|
+
ts = [int(gl.glGetQueryObjectui64v(self._rings[nxt][i],
|
|
86
|
+
gl.GL_QUERY_RESULT))
|
|
87
|
+
for i in range(n)]
|
|
88
|
+
segs = {}
|
|
89
|
+
for i in range(1, n):
|
|
90
|
+
segs[self._labels[nxt][i]] = (ts[i] - ts[i - 1]) / 1e6
|
|
91
|
+
out = (self._frames[nxt], segs)
|
|
92
|
+
self._frames[nxt] = None
|
|
93
|
+
self._counts[nxt] = 0
|
|
94
|
+
self._slot = nxt
|
|
95
|
+
except Exception:
|
|
96
|
+
self._dead = True
|
|
97
|
+
self._active = False
|
|
98
|
+
return None
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Module singleton; survives hotswap (re-exec reuses existing context).
|
|
103
|
+
GPU_TIMER = globals().get("GPU_TIMER") or GpuFrameTimer()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# fastinspect.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import inspect
|
|
4
|
+
import weakref
|
|
5
|
+
from types import FunctionType, MethodType
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
# Mapping of function -> (signature, code_object_id)
|
|
9
|
+
# WeakKeyDictionary avoids memory leaks when functions go out of scope.
|
|
10
|
+
_SIG_CACHE: "weakref.WeakKeyDictionary[object, tuple[inspect.Signature, int | None]]" = weakref.WeakKeyDictionary()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _unwrap_callable(obj):
|
|
14
|
+
"""Normalize bound methods, functools.partial, and wrappers to a base callable."""
|
|
15
|
+
# Bound method -> underlying function (keep method object as key if you prefer)
|
|
16
|
+
if isinstance(obj, MethodType):
|
|
17
|
+
return obj.__func__
|
|
18
|
+
# functools.partial exposes .func
|
|
19
|
+
func = getattr(obj, "func", None)
|
|
20
|
+
if func is not None and callable(func):
|
|
21
|
+
return func
|
|
22
|
+
# Many wrappers set __wrapped__
|
|
23
|
+
return inspect.unwrap(obj)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _code_id(func) -> int | None:
|
|
27
|
+
"""Stable identifier for the current implementation of a Python function."""
|
|
28
|
+
code = getattr(func, "__code__", None)
|
|
29
|
+
return id(code) if code is not None else None # For builtins/extension funcs returns None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_signature(callable_obj) -> inspect.Signature:
|
|
33
|
+
"""
|
|
34
|
+
Fast, safe signature fetch with caching and auto-invalidating when the function's
|
|
35
|
+
code object changes (i.e., after hot-reload).
|
|
36
|
+
"""
|
|
37
|
+
base = _unwrap_callable(callable_obj)
|
|
38
|
+
current_code_id = _code_id(base)
|
|
39
|
+
|
|
40
|
+
cached = _SIG_CACHE.get(base)
|
|
41
|
+
if cached is not None:
|
|
42
|
+
sig, cached_code_id = cached
|
|
43
|
+
if cached_code_id == current_code_id:
|
|
44
|
+
return sig # up-to-date
|
|
45
|
+
|
|
46
|
+
# Compute once; this is the slow part we want to avoid doing repeatedly
|
|
47
|
+
sig = inspect.signature(base)
|
|
48
|
+
|
|
49
|
+
# Store in WeakKeyDictionary
|
|
50
|
+
_SIG_CACHE[base] = (sig, current_code_id)
|
|
51
|
+
|
|
52
|
+
# Bonus: stamp __signature__ so *future* inspect.signature(base) is O(1)
|
|
53
|
+
try:
|
|
54
|
+
setattr(base, "__signature__", sig)
|
|
55
|
+
except Exception:
|
|
56
|
+
# Not all callables allow setting attributes (e.g., many builtins)
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return sig
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
import inspect
|
|
63
|
+
from functools import partial
|
|
64
|
+
from types import FunctionType, MethodType
|
|
65
|
+
from typing import Mapping, Any
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_runtime_defaults_in_place(fn: Any, new_defaults: Mapping[str, Any]) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Change the *runtime* defaults of a callable so that calling it uses the new values.
|
|
71
|
+
Supports:
|
|
72
|
+
- plain Python functions
|
|
73
|
+
- bound/unbound methods (updates the underlying function)
|
|
74
|
+
- classmethod/staticmethod (via bound method's __func__)
|
|
75
|
+
- functools.partial (you normally want to rebuild/patch the partial; see note)
|
|
76
|
+
|
|
77
|
+
Returns True if runtime defaults were updated; False if unsupported (e.g., builtins).
|
|
78
|
+
"""
|
|
79
|
+
# 1) Find the underlying Python function object we can mutate.
|
|
80
|
+
base = fn
|
|
81
|
+
# bound method -> underlying function
|
|
82
|
+
if isinstance(base, MethodType):
|
|
83
|
+
base = base.__func__
|
|
84
|
+
# functools.partial (defaults live on the wrapped func; partial itself has args/keywords)
|
|
85
|
+
if isinstance(base, partial):
|
|
86
|
+
base = base.func
|
|
87
|
+
# unwrap decorator wrappers
|
|
88
|
+
base = inspect.unwrap(base)
|
|
89
|
+
|
|
90
|
+
# Only pure Python functions have writable __defaults__/__kwdefaults__
|
|
91
|
+
if not isinstance(base, FunctionType):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
sig = inspect.signature(base)
|
|
95
|
+
|
|
96
|
+
# Build a new parameter list with updated defaults by name
|
|
97
|
+
params = []
|
|
98
|
+
for p in sig.parameters.values():
|
|
99
|
+
if p.name in new_defaults and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD) and not isinstance(p.default, inspect.Parameter):
|
|
100
|
+
p = p.replace(default=new_defaults[p.name])
|
|
101
|
+
params.append(p)
|
|
102
|
+
|
|
103
|
+
# Positional defaults must be a trailing contiguous block
|
|
104
|
+
pos = [p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
|
|
105
|
+
flags = [p.default is not inspect._empty for p in pos]
|
|
106
|
+
try:
|
|
107
|
+
first = next(i for i, f in enumerate(flags) if f)
|
|
108
|
+
if not all(flags[i] for i in range(first, len(flags))):
|
|
109
|
+
# Invalid layout for runtime defaults; keep function usable, but don't set.
|
|
110
|
+
return False
|
|
111
|
+
pos_defaults = tuple(p.default if not isinstance(p.default, inspect.Parameter) else p.default for p in pos[first:]) if pos else None
|
|
112
|
+
except StopIteration:
|
|
113
|
+
pos_defaults = None
|
|
114
|
+
|
|
115
|
+
# Keyword-only defaults: free-form dict
|
|
116
|
+
kwonly = [p for p in params if p.kind is p.KEYWORD_ONLY]
|
|
117
|
+
kwdefaults = {p.name: p.default for p in kwonly if p.default is not inspect._empty} or None
|
|
118
|
+
|
|
119
|
+
# 2) Mutate the function object in place
|
|
120
|
+
try:
|
|
121
|
+
base.__defaults__ = pos_defaults
|
|
122
|
+
except Exception:
|
|
123
|
+
return False
|
|
124
|
+
try:
|
|
125
|
+
base.__kwdefaults__ = kwdefaults
|
|
126
|
+
except Exception:
|
|
127
|
+
# Some functions do not allow this; that's okay for functions without kw-only defaults
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def set_fn_defaults(fn, new_defaults: Mapping[str, Any]) -> bool:
|
|
133
|
+
ok = set_runtime_defaults_in_place(fn, new_defaults)
|
|
134
|
+
# Update the inspect signature to match (optional but nice)
|
|
135
|
+
# try:
|
|
136
|
+
# sig = inspect.signature(fn)
|
|
137
|
+
# new_params = [(p.replace(default=new_defaults[p.name])
|
|
138
|
+
# if p.name in new_defaults and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
|
|
139
|
+
# else p)
|
|
140
|
+
# for p in sig.parameters.values()]
|
|
141
|
+
# new_sig = inspect.Signature(new_params, return_annotation=sig.return_annotation)
|
|
142
|
+
# # set on the exact object and the unwrapped base for robustness
|
|
143
|
+
# setattr(fn, "__signature__", new_sig)
|
|
144
|
+
# setattr(inspect.unwrap(fn), "__signature__", new_sig)
|
|
145
|
+
#
|
|
146
|
+
# except Exception:
|
|
147
|
+
# pass
|
|
148
|
+
return ok
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_params(callable_obj) -> Mapping[str, inspect.Parameter]:
|
|
152
|
+
"""Convenience: directly get the .parameters mapping (ordered)."""
|
|
153
|
+
return get_signature(callable_obj).parameters
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def invalidate(callable_obj=None):
|
|
157
|
+
"""
|
|
158
|
+
Invalidate the cache for one callable (or all, if None).
|
|
159
|
+
Useful if you mutate __code__ in-place (rare); otherwise hot-reload creates
|
|
160
|
+
new function objects and the cache auto-updates by key.
|
|
161
|
+
"""
|
|
162
|
+
if callable_obj is None:
|
|
163
|
+
_SIG_CACHE.clear()
|
|
164
|
+
return
|
|
165
|
+
base = _unwrap_callable(callable_obj)
|
|
166
|
+
try:
|
|
167
|
+
del _SIG_CACHE[base]
|
|
168
|
+
except KeyError:
|
|
169
|
+
pass
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from meltygui.core.automation.mcp_server import _LOG_RESULT_CAP
|
|
6
|
+
from meltygui.core.rendering.render_funcs import RenderFuncs
|
|
7
|
+
from meltygui.core.rendering.window_decoration import window
|
|
8
|
+
|
|
9
|
+
_SENTINEL = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MonitorMeta(type):
|
|
13
|
+
_brightness = None
|
|
14
|
+
_pending = None
|
|
15
|
+
_event = threading.Event()
|
|
16
|
+
_thread = None
|
|
17
|
+
_lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
def __init__(cls, *args):
|
|
20
|
+
super().__init__(*args)
|
|
21
|
+
|
|
22
|
+
def _worker():
|
|
23
|
+
while True:
|
|
24
|
+
cls._event.wait()
|
|
25
|
+
cls._event.clear()
|
|
26
|
+
with cls._lock:
|
|
27
|
+
value = cls._pending
|
|
28
|
+
if value is _SENTINEL:
|
|
29
|
+
break
|
|
30
|
+
try:
|
|
31
|
+
value = min(max(1, value), 100)
|
|
32
|
+
subprocess.run(["ddcutil", "setvcp", "10", str(value)])
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Monitor error: {e}")
|
|
35
|
+
|
|
36
|
+
cls._thread = threading.Thread(target=_worker, daemon=True)
|
|
37
|
+
cls._thread.start()
|
|
38
|
+
|
|
39
|
+
def _fetch():
|
|
40
|
+
# result = subprocess.run(["ddcutil", "getvcp", "10"], capture_output=True, text=True)
|
|
41
|
+
# for part in result.stdout.split(","):
|
|
42
|
+
# if "current value" in part:
|
|
43
|
+
# cls._brightness = int(part.split("=")[1].strip())
|
|
44
|
+
# return
|
|
45
|
+
cls._brightness = 30
|
|
46
|
+
|
|
47
|
+
threading.Thread(target=_fetch, daemon=True).start()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def brightness(cls):
|
|
51
|
+
return cls._brightness
|
|
52
|
+
|
|
53
|
+
@brightness.setter
|
|
54
|
+
def brightness(cls, value):
|
|
55
|
+
cls._brightness = value
|
|
56
|
+
with cls._lock:
|
|
57
|
+
cls._pending = value
|
|
58
|
+
cls._event.set()
|
|
59
|
+
|
|
60
|
+
def shutdown(cls):
|
|
61
|
+
with cls._lock:
|
|
62
|
+
cls._pending = _SENTINEL
|
|
63
|
+
cls._event.set()
|
|
64
|
+
cls._thread.join()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Monitor(metaclass=_MonitorMeta):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@window(live=True, view_func=RenderFuncs.draw_type)
|
|
72
|
+
class MCPServerLog:
|
|
73
|
+
"""Append-only, in-memory record of MCP tool interactions for this launcher.
|
|
74
|
+
|
|
75
|
+
Every tool call (and its outcome) is appended to the ``logs`` class
|
|
76
|
+
attribute by the logging wrapper in ``start_launcher_mcp``. It's a class
|
|
77
|
+
attribute rather than an instance field so it's reachable from anywhere —
|
|
78
|
+
including ``eval_python`` — as just ``MCPServerLog.logs`` without threading
|
|
79
|
+
an instance around.
|
|
80
|
+
|
|
81
|
+
Each entry is a dict::
|
|
82
|
+
|
|
83
|
+
{"tool": str, "args": dict, "result": str|None,
|
|
84
|
+
"error": str|None, "timestamp": float}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
logs = []
|
|
88
|
+
def __init__(self):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def record(cls, tool, args, result=None, error=None):
|
|
93
|
+
def _cap(v):
|
|
94
|
+
if v is None:
|
|
95
|
+
return None
|
|
96
|
+
s = v if isinstance(v, str) else repr(v)
|
|
97
|
+
return s if len(s) <= _LOG_RESULT_CAP else s[:_LOG_RESULT_CAP] + "…"
|
|
98
|
+
|
|
99
|
+
cls.logs.append({
|
|
100
|
+
"tool": tool,
|
|
101
|
+
"args": dict(args),
|
|
102
|
+
"result": _cap(result),
|
|
103
|
+
"error": _cap(error),
|
|
104
|
+
"timestamp": time.time(),
|
|
105
|
+
})
|