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,39 @@
|
|
|
1
|
+
"""Keep saved objects readable and re-saveable after their class is deleted."""
|
|
2
|
+
import importlib
|
|
3
|
+
import logging
|
|
4
|
+
import types
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@lru_cache(maxsize=None)
|
|
11
|
+
def missing_saved_class(class_path):
|
|
12
|
+
logging.getLogger("load_save_v2").warning(
|
|
13
|
+
"Saved class %s is unavailable; preserving its state in a placeholder", class_path)
|
|
14
|
+
return type(class_path.rsplit(".", 1)[-1], (DictConversion,), {
|
|
15
|
+
"__missing_saved_path__": class_path,
|
|
16
|
+
"default_instance": None,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def restore_saved_class(class_path):
|
|
21
|
+
# The unpickler's find_class also resolves function globals. A missing
|
|
22
|
+
# renderer can therefore have been preserved as this placeholder too.
|
|
23
|
+
# Recover its original function when available, rather than keeping a
|
|
24
|
+
# class that crashes when the renderer is called with view arguments.
|
|
25
|
+
from meltygui.core.module_names import canonical_name
|
|
26
|
+
class_path = canonical_name(class_path)
|
|
27
|
+
parts = class_path.split(".")
|
|
28
|
+
for boundary in range(len(parts) - 1, 0, -1):
|
|
29
|
+
try:
|
|
30
|
+
value = importlib.import_module(".".join(parts[:boundary]))
|
|
31
|
+
for part in parts[boundary:]:
|
|
32
|
+
value = getattr(value, part)
|
|
33
|
+
except Exception:
|
|
34
|
+
# A syntax error or import-time exception in today's code says
|
|
35
|
+
# nothing about the validity of the saved object's type.
|
|
36
|
+
continue
|
|
37
|
+
if isinstance(value, (type, types.FunctionType, types.BuiltinFunctionType)):
|
|
38
|
+
return value
|
|
39
|
+
return missing_saved_class(class_path)
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Automatic converter chaining via shortest-path search.
|
|
3
|
+
|
|
4
|
+
Given a registry of direct A→B converters, this module finds multi-hop
|
|
5
|
+
paths (A→X→…→B) and composes them into a single callable. The shortest
|
|
6
|
+
path (fewest hops) is always chosen to minimise data loss.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
result = convert(my_obj, str, registry=Melty)
|
|
10
|
+
chain = find_chain(MyType, str, registry=Melty) # returns the composed fn
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import types
|
|
16
|
+
from collections import deque
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Callable, TypeVar
|
|
19
|
+
|
|
20
|
+
from meltygui.core.windowing.glfw_utils import print_stack_trace
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
# Sentinel cached for type pairs with no conversion path,
|
|
25
|
+
# so we don't re-run BFS every frame.
|
|
26
|
+
_NO_PATH = object()
|
|
27
|
+
NO_VALUE = object()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PendingState(Enum):
|
|
31
|
+
BACKGROUND = "background"
|
|
32
|
+
CONFIRM = "blocking"
|
|
33
|
+
BROKEN_PATH = "broken_path"
|
|
34
|
+
ERROR = "error"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Pending wrapper ──────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
class Pending:
|
|
40
|
+
"""Wraps a converter result that hasn't been fully applied yet.
|
|
41
|
+
|
|
42
|
+
Converters that support deferred execution (file I/O, network calls,
|
|
43
|
+
etc.) return Pending(partial_result) when apply=False. The UI can
|
|
44
|
+
inspect pending.wrapped for metadata, then call convert() again with
|
|
45
|
+
apply=True when ready.
|
|
46
|
+
|
|
47
|
+
When a Pending is encountered mid-chain, the chain stops immediately
|
|
48
|
+
and returns the Pending as-is. The caller can inspect the partial
|
|
49
|
+
result and re-run with apply=True to complete.
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
result = convert(path, dict, registry=R, apply=False)
|
|
53
|
+
if isinstance(result, Pending):
|
|
54
|
+
print(result.wrapped["name"]) # metadata available
|
|
55
|
+
# later:
|
|
56
|
+
full = convert(path, dict, registry=R, apply=True)
|
|
57
|
+
"""
|
|
58
|
+
__slots__ = ("wrapped", "status", "state", "originated")
|
|
59
|
+
|
|
60
|
+
def __init__(self, originated: types.FunctionType | type, wrapped=NO_VALUE, status="pending", state=PendingState.CONFIRM):
|
|
61
|
+
self.wrapped = wrapped
|
|
62
|
+
self.status = status
|
|
63
|
+
self.state = state
|
|
64
|
+
self.originated = originated
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
return f"Pending({self.wrapped!r})"
|
|
68
|
+
|
|
69
|
+
def __bool__(self):
|
|
70
|
+
return self.wrapped is not None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Apply introspection ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
_accepts_apply_cache: dict[Callable, bool] = {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _accepts_apply(fn: Callable) -> bool:
|
|
79
|
+
"""Check (cached) whether a converter function accepts an 'apply' kwarg."""
|
|
80
|
+
return True
|
|
81
|
+
# cached = _accepts_apply_cache.get(fn)
|
|
82
|
+
# if cached is not None:
|
|
83
|
+
# return cached
|
|
84
|
+
# import inspect
|
|
85
|
+
# try:
|
|
86
|
+
# sig = inspect.signature(fn)
|
|
87
|
+
# result = "apply" in sig.parameters
|
|
88
|
+
# except (ValueError, TypeError):
|
|
89
|
+
# result = False
|
|
90
|
+
# _accepts_apply_cache[fn] = result
|
|
91
|
+
# return result
|
|
92
|
+
#
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_path_type(item) -> bool:
|
|
98
|
+
"""True if item is a type (waypoint in a path), False if callable (edge)."""
|
|
99
|
+
return isinstance(item, type)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _find_converter(converters: dict, from_type: type, to_type: type) -> Callable | None:
|
|
103
|
+
"""Look up a converter with MRO fallback."""
|
|
104
|
+
fn = converters.get((from_type, to_type))
|
|
105
|
+
if fn is None and hasattr(from_type, "__mro__"):
|
|
106
|
+
for ancestor in from_type.__mro__[1:]:
|
|
107
|
+
fn = converters.get((ancestor, to_type))
|
|
108
|
+
if fn is not None:
|
|
109
|
+
break
|
|
110
|
+
return fn
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def convert(value: Any, target: type[T] = None, *, registry, path: list | None = None, apply: bool = False, cache_id=None) -> T:
|
|
114
|
+
"""Convert *value* to *target* type using the registry.
|
|
115
|
+
|
|
116
|
+
If *path* is provided, follows it exactly. Path items can be:
|
|
117
|
+
- types (waypoints): looked up in the registry
|
|
118
|
+
- callables (edges): called directly on the current value
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
# All types — registry lookup between each consecutive pair
|
|
122
|
+
convert(obj, dict, registry=R, path=[str, cst.Module, dict])
|
|
123
|
+
|
|
124
|
+
# All functions — called in sequence
|
|
125
|
+
convert(obj, registry=R, path=[my_parser, my_formatter])
|
|
126
|
+
|
|
127
|
+
# Mixed — types trigger registry lookup, functions called directly
|
|
128
|
+
convert(obj, dict, registry=R, path=[str, my_custom_parser, dict])
|
|
129
|
+
|
|
130
|
+
If *apply* is False (default), converters that support deferred
|
|
131
|
+
execution may return Pending(partial_result). Pass apply=True
|
|
132
|
+
to force full execution (e.g. file writes, network calls).
|
|
133
|
+
|
|
134
|
+
If any link in the chain returns Pending, the chain stops
|
|
135
|
+
immediately and returns that Pending. The caller can inspect
|
|
136
|
+
the partial result and re-run with apply=True to complete.
|
|
137
|
+
|
|
138
|
+
Otherwise, tries a direct converter first, then BFS for a multi-hop
|
|
139
|
+
path through intermediate types. Raises TypeError if no path exists.
|
|
140
|
+
"""
|
|
141
|
+
if target is not None:
|
|
142
|
+
if isinstance(value, target):
|
|
143
|
+
return value # type: ignore[return-value]
|
|
144
|
+
|
|
145
|
+
if cache_id is None:
|
|
146
|
+
from meltygui.core.melty import Melty
|
|
147
|
+
cache_id = Melty.unique_stack[-1] if Melty.unique_stack else None
|
|
148
|
+
|
|
149
|
+
if path is not None:
|
|
150
|
+
return _run_explicit_path(value, target, cache_id=cache_id, path=path, registry=registry, apply=apply)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Value-aware shortcut: dicts produced by object_to_dict carry a
|
|
155
|
+
# __meta__ key with the original class info. The graph can't know
|
|
156
|
+
# that dict→object will produce the right type at runtime, so we
|
|
157
|
+
# try it here before falling into BFS.
|
|
158
|
+
converters = getattr(registry, "_converters", {})
|
|
159
|
+
dict_to_obj = converters.get((dict, object))
|
|
160
|
+
|
|
161
|
+
if dict_to_obj is not None:
|
|
162
|
+
meta_dict = None
|
|
163
|
+
|
|
164
|
+
if isinstance(value, dict) and "__meta__" in value:
|
|
165
|
+
meta_dict = value
|
|
166
|
+
else:
|
|
167
|
+
# The value might be one hop away from a __meta__ dict
|
|
168
|
+
# (e.g. a list of pairs that returned from dict_to_list).
|
|
169
|
+
# Try converting to dict first and check.
|
|
170
|
+
try:
|
|
171
|
+
to_dict_fn = find_chain(type(value), dict, registry=registry)
|
|
172
|
+
candidate = to_dict_fn(value)
|
|
173
|
+
if isinstance(candidate, dict) and "__meta__" in candidate:
|
|
174
|
+
meta_dict = candidate
|
|
175
|
+
except (TypeError, Exception):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
if meta_dict is not None:
|
|
179
|
+
try:
|
|
180
|
+
result = dict_to_obj(meta_dict)
|
|
181
|
+
if isinstance(result, target):
|
|
182
|
+
return result # type: ignore[return-value]
|
|
183
|
+
# Result isn't the target yet, but it's more specific
|
|
184
|
+
# than `object` - try one more conversion from here.
|
|
185
|
+
if not isinstance(result, type(value)): # avoid loops
|
|
186
|
+
try:
|
|
187
|
+
final = convert(result, target, registry=registry)
|
|
188
|
+
return final
|
|
189
|
+
except (TypeError, ValueError):
|
|
190
|
+
pass
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
chain_fn = find_chain(type(value), target, registry=registry)
|
|
195
|
+
|
|
196
|
+
# If we need to pass apply, decompose the chain and walk step by step
|
|
197
|
+
steps = getattr(chain_fn, "__converter_chain__", None)
|
|
198
|
+
if steps is not None:
|
|
199
|
+
return _run_chain_steps(value, steps,cache_id=cache_id, apply=apply)
|
|
200
|
+
# Direct call (single hop)
|
|
201
|
+
if getattr(chain_fn, '_accepts_cache_id', False):
|
|
202
|
+
return chain_fn(value, cache_id=cache_id, apply=apply)
|
|
203
|
+
return chain_fn(value)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _run_explicit_path(value: Any, target: type, *, path: list, registry, apply: any, cache_id=None) -> Any:
|
|
207
|
+
"""Follow an explicit path of types and/or converter functions.
|
|
208
|
+
|
|
209
|
+
Each path item is either:
|
|
210
|
+
- a type: a waypoint — if the value isn't already this type,
|
|
211
|
+
look up a converter from type(value) → this type
|
|
212
|
+
- a callable: an explicit edge — called directly on the value
|
|
213
|
+
|
|
214
|
+
Examples:
|
|
215
|
+
path=[str, cst.Module, dict]
|
|
216
|
+
→ ensure value is str, convert str→Module, convert Module→dict
|
|
217
|
+
|
|
218
|
+
path=[my_parser, my_formatter]
|
|
219
|
+
→ call my_parser(value), then my_formatter(result)
|
|
220
|
+
|
|
221
|
+
path=[str, my_custom_parser, dict]
|
|
222
|
+
→ ensure value is str, call my_custom_parser, convert →dict
|
|
223
|
+
|
|
224
|
+
If any step returns Pending, the chain stops and returns it.
|
|
225
|
+
Raises TypeError if a type→type edge is missing from the registry.
|
|
226
|
+
"""
|
|
227
|
+
converters = getattr(registry, "_converters", {})
|
|
228
|
+
result = value
|
|
229
|
+
last_fn = None
|
|
230
|
+
fn_name = "N/A"
|
|
231
|
+
|
|
232
|
+
for i, item in enumerate(path):
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if _is_path_type(item):
|
|
236
|
+
# Type waypoint - skip if already there, otherwise look up
|
|
237
|
+
if isinstance(result, item):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
fn = _find_converter(converters, type(result), item)
|
|
241
|
+
if fn is None:
|
|
242
|
+
# print(last_fn.__name__ if last_fn else "N/A", item.__name__)
|
|
243
|
+
# raise TypeError(
|
|
244
|
+
# f"No converter registered for {type(result).__name__!r} → "
|
|
245
|
+
# f"{item.__name__!r} (step {i + 1} of explicit path)"
|
|
246
|
+
# )
|
|
247
|
+
print_stack_trace(watch=["path", "target", "result", "item", "apply", "value", "cache_id", "last_fn", "fn_name"])
|
|
248
|
+
print(f"Missing converter for {type(result).__name__!r} -> {item.__name__!r} at step {i + 1} of explicit path")
|
|
249
|
+
|
|
250
|
+
return Pending(originated=fn, state=PendingState.BROKEN_PATH, wrapped=item, status=f"Missing converter for {type(result).__name__!r} -> {item.__name__!r} at step {i + 1} of explicit path")
|
|
251
|
+
|
|
252
|
+
# if apply is not False and apply is not None:
|
|
253
|
+
# if fn.__name__ == apply.__name__:
|
|
254
|
+
# do_apply = True
|
|
255
|
+
# print(f"Applying at step {i + 1} of explicit path via {getattr(item, '__name__', repr(item))}")
|
|
256
|
+
# else:
|
|
257
|
+
# do_apply = False
|
|
258
|
+
# print(
|
|
259
|
+
# f"Not applying at step {i + 1} of explicit path: apply={apply}, item={fn} ")
|
|
260
|
+
# else:
|
|
261
|
+
# do_apply = False
|
|
262
|
+
last_fn = fn
|
|
263
|
+
if getattr(fn, '_accepts_cache_id', False):
|
|
264
|
+
result = fn(result, cache_id=cache_id, apply=apply)
|
|
265
|
+
else:
|
|
266
|
+
result = fn(result)
|
|
267
|
+
else:
|
|
268
|
+
# Callable edge - skip if already the target type
|
|
269
|
+
type_info = getattr(registry, "_converter_to_type", {}).get(item)
|
|
270
|
+
if type_info is not None:
|
|
271
|
+
_, to_type = type_info
|
|
272
|
+
if isinstance(result, to_type):
|
|
273
|
+
result = value
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if getattr(item, '_accepts_cache_id', False):
|
|
278
|
+
result = item(result, cache_id=cache_id, apply=apply)
|
|
279
|
+
else:
|
|
280
|
+
result = item(result)
|
|
281
|
+
|
|
282
|
+
if isinstance(result, Pending):
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _run_chain_steps(value: Any, steps: list[Callable], *, apply: bool, cache_id=None) -> Any:
|
|
289
|
+
"""Walk a list of converter functions, passing apply and handling Pending.
|
|
290
|
+
|
|
291
|
+
If any step returns Pending, stops immediately and returns it.
|
|
292
|
+
"""
|
|
293
|
+
result = value
|
|
294
|
+
|
|
295
|
+
for fn in steps:
|
|
296
|
+
if getattr(fn, '_accepts_cache_id', False):
|
|
297
|
+
result = fn(result, cache_id=cache_id, apply=apply)
|
|
298
|
+
else:
|
|
299
|
+
result = fn(result)
|
|
300
|
+
if isinstance(result, Pending):
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def find_chain(source: type, target: type, *, registry) -> Callable:
|
|
307
|
+
"""Return a single callable that converts *source* → *target*.
|
|
308
|
+
|
|
309
|
+
Builds a directed graph from every converter in the registry, runs
|
|
310
|
+
BFS to find the shortest path, composes the converters along that
|
|
311
|
+
path into a single function, and caches the result.
|
|
312
|
+
|
|
313
|
+
The graph and all discovered chains are cached on the registry itself.
|
|
314
|
+
Call invalidate_cache() after registering new converters at runtime.
|
|
315
|
+
|
|
316
|
+
Raises TypeError if no path can be found.
|
|
317
|
+
"""
|
|
318
|
+
# ── Cache lookup ─────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
if not hasattr(registry, "_chain_cache"):
|
|
321
|
+
registry._chain_cache = {}
|
|
322
|
+
key = (source, target)
|
|
323
|
+
if key in registry._chain_cache:
|
|
324
|
+
cached = registry._chain_cache[key]
|
|
325
|
+
if cached is _NO_PATH:
|
|
326
|
+
raise TypeError(
|
|
327
|
+
f"No conversion path from {source.__name__!r} to {target.__name__!r} (cached)"
|
|
328
|
+
)
|
|
329
|
+
return cached
|
|
330
|
+
|
|
331
|
+
converters = getattr(registry, "_converters", {})
|
|
332
|
+
|
|
333
|
+
# ── Direct lookup with MRO fallback ──────────────────────────────
|
|
334
|
+
#
|
|
335
|
+
# Check for an exact (source, target) converter first, then walk the
|
|
336
|
+
# source's MRO so that e.g. ValueError finds Exception→dict.
|
|
337
|
+
|
|
338
|
+
direct = converters.get((source, target))
|
|
339
|
+
if direct is None and hasattr(source, "__mro__"):
|
|
340
|
+
for ancestor in source.__mro__[1:]:
|
|
341
|
+
direct = converters.get((ancestor, target))
|
|
342
|
+
if direct is not None:
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
if direct is not None:
|
|
346
|
+
registry._chain_cache[key] = direct
|
|
347
|
+
return direct
|
|
348
|
+
|
|
349
|
+
# ── Build the type graph ─────────────────────────────────────────
|
|
350
|
+
#
|
|
351
|
+
# Adjacency list built from every registered (from_type, to_type)
|
|
352
|
+
# pair. Cached on the registry so it's only built once.
|
|
353
|
+
|
|
354
|
+
if not hasattr(registry, "_graph_cache") or registry._graph_cache is None:
|
|
355
|
+
graph: dict[type, list[type]] = {}
|
|
356
|
+
for (from_type, to_type) in converters.keys():
|
|
357
|
+
graph.setdefault(from_type, []).append(to_type)
|
|
358
|
+
registry._graph_cache = graph
|
|
359
|
+
graph = registry._graph_cache
|
|
360
|
+
|
|
361
|
+
# ── Find starting types via MRO ───────────────────────────────
|
|
362
|
+
#
|
|
363
|
+
# If source isn't directly in the graph, we can still start from any
|
|
364
|
+
# registered ancestor type. Sort by MRO distance so BFS explores
|
|
365
|
+
# the most specific converters first (ValueError before Exception
|
|
366
|
+
# before object).
|
|
367
|
+
|
|
368
|
+
all_from_types = set(graph.keys())
|
|
369
|
+
start_entries: list[tuple[int, type]] = []
|
|
370
|
+
for rt in all_from_types:
|
|
371
|
+
if rt == source:
|
|
372
|
+
start_entries.append((0, rt))
|
|
373
|
+
elif isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
|
|
374
|
+
try:
|
|
375
|
+
distance = source.__mro__.index(rt)
|
|
376
|
+
except ValueError:
|
|
377
|
+
distance = 999
|
|
378
|
+
start_entries.append((distance, rt))
|
|
379
|
+
start_entries.sort(key=lambda x: x[0])
|
|
380
|
+
start_types = [t for _, t in start_entries] if start_entries else [source]
|
|
381
|
+
|
|
382
|
+
# ── BFS ──────────────────────────────────────────────────────────
|
|
383
|
+
#
|
|
384
|
+
# Standard breadth-first search over the type graph. Each queue
|
|
385
|
+
# entry is a full path (list of types) so we can reconstruct the
|
|
386
|
+
# chain when we hit the target.
|
|
387
|
+
|
|
388
|
+
queue: deque[list[type]] = deque()
|
|
389
|
+
visited: set[type] = {source}
|
|
390
|
+
for st in start_types:
|
|
391
|
+
# When starting from an MRO ancestor, begin the path there
|
|
392
|
+
# directly - the value IS an instance of the ancestor, so no
|
|
393
|
+
# converter is needed for that step.
|
|
394
|
+
queue.append([st])
|
|
395
|
+
visited.add(st)
|
|
396
|
+
|
|
397
|
+
path: list[type] | None = None
|
|
398
|
+
while queue:
|
|
399
|
+
current_path = queue.popleft()
|
|
400
|
+
current = current_path[-1]
|
|
401
|
+
|
|
402
|
+
if current == target or (isinstance(current, type) and issubclass(current, target)):
|
|
403
|
+
path = current_path
|
|
404
|
+
break
|
|
405
|
+
|
|
406
|
+
for neighbour in graph.get(current, []):
|
|
407
|
+
if neighbour not in visited:
|
|
408
|
+
visited.add(neighbour)
|
|
409
|
+
new_path = current_path + [neighbour]
|
|
410
|
+
if neighbour == target:
|
|
411
|
+
path = new_path
|
|
412
|
+
break
|
|
413
|
+
queue.append(new_path)
|
|
414
|
+
if path is not None:
|
|
415
|
+
break
|
|
416
|
+
|
|
417
|
+
if path is None:
|
|
418
|
+
registry._chain_cache[key] = _NO_PATH
|
|
419
|
+
raise TypeError(
|
|
420
|
+
f"No conversion path from {source.__name__!r} to {target.__name__!r}"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# ── Collect edge converters ──────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
edge_fns: list[Callable] = []
|
|
426
|
+
for i in range(len(path) - 1):
|
|
427
|
+
step_from, step_to = path[i], path[i + 1]
|
|
428
|
+
fn = converters.get((step_from, step_to))
|
|
429
|
+
if fn is None and hasattr(step_from, "__mro__"):
|
|
430
|
+
for ancestor in step_from.__mro__[1:]:
|
|
431
|
+
fn = converters.get((ancestor, step_to))
|
|
432
|
+
if fn is not None:
|
|
433
|
+
break
|
|
434
|
+
assert fn is not None, f"BFS found edge {step_from} → {step_to} but lookup failed"
|
|
435
|
+
edge_fns.append(fn)
|
|
436
|
+
|
|
437
|
+
# ── Compose into a single callable ───────────────────────────────
|
|
438
|
+
|
|
439
|
+
if len(edge_fns) == 1:
|
|
440
|
+
registry._chain_cache[key] = edge_fns[0]
|
|
441
|
+
return edge_fns[0]
|
|
442
|
+
|
|
443
|
+
fns = list(edge_fns) # bind for closure
|
|
444
|
+
|
|
445
|
+
def composed(value: Any) -> Any:
|
|
446
|
+
result = value
|
|
447
|
+
for fn in fns:
|
|
448
|
+
result = fn(result)
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
type_names = [source.__name__]
|
|
452
|
+
# If the path starts from an MRO ancestor, show the actual start
|
|
453
|
+
if path[0] != source:
|
|
454
|
+
type_names.append(path[0].__name__)
|
|
455
|
+
for fn in edge_fns:
|
|
456
|
+
ret = getattr(fn, "__annotations__", {}).get("return")
|
|
457
|
+
if isinstance(ret, type):
|
|
458
|
+
type_names.append(ret.__name__)
|
|
459
|
+
elif isinstance(ret, str):
|
|
460
|
+
type_names.append(ret)
|
|
461
|
+
else:
|
|
462
|
+
type_names.append("?")
|
|
463
|
+
|
|
464
|
+
chain_str = " → ".join(type_names)
|
|
465
|
+
composed.__name__ = f"chain[{chain_str}]"
|
|
466
|
+
composed.__qualname__ = composed.__name__
|
|
467
|
+
composed.__doc__ = (
|
|
468
|
+
f"Auto-composed converter: {chain_str}\n"
|
|
469
|
+
f"Hops: {len(edge_fns)}\n"
|
|
470
|
+
f"Steps: {', '.join(getattr(fn, '__name__', '?') for fn in edge_fns)}"
|
|
471
|
+
)
|
|
472
|
+
composed.__converter_chain__ = edge_fns # type: ignore[attr-defined]
|
|
473
|
+
composed.__converter_path__ = chain_str # type: ignore[attr-defined]
|
|
474
|
+
|
|
475
|
+
registry._chain_cache[key] = composed
|
|
476
|
+
return composed
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def invalidate_cache(registry) -> None:
|
|
480
|
+
"""Call this after registering new converters at runtime to rebuild
|
|
481
|
+
the graph and clear cached chains.
|
|
482
|
+
"""
|
|
483
|
+
registry._chain_cache = {}
|
|
484
|
+
registry._graph_cache = None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def invert_path(path: list, *, registry) -> list:
|
|
488
|
+
"""Return the reverse of a mixed path, swapping each callable for its inverse.
|
|
489
|
+
|
|
490
|
+
Types are kept as-is (just reversed). Callables are replaced with
|
|
491
|
+
their inverse via registry.converter_flags[fn].
|
|
492
|
+
|
|
493
|
+
Example:
|
|
494
|
+
path = [str, cst.Module, my_custom_fn, dict]
|
|
495
|
+
invert_path(path, registry=Melty)
|
|
496
|
+
→ [dict, inverse_of_my_custom_fn, cst.Module, str]
|
|
497
|
+
|
|
498
|
+
Raises KeyError if a callable has no registered inverse.
|
|
499
|
+
"""
|
|
500
|
+
flags = getattr(registry, "converter_flags", {})
|
|
501
|
+
result = []
|
|
502
|
+
for item in reversed(path):
|
|
503
|
+
if _is_path_type(item):
|
|
504
|
+
result.append(item)
|
|
505
|
+
else:
|
|
506
|
+
inverse = flags.get(item, {}).get("inverse_of", None)
|
|
507
|
+
if inverse is None:
|
|
508
|
+
raise KeyError(
|
|
509
|
+
f"No inverse registered for {getattr(item, '__name__', repr(item))}. "
|
|
510
|
+
f"Register with @converter(registry=..., inverse_of=...)"
|
|
511
|
+
)
|
|
512
|
+
result.append(inverse)
|
|
513
|
+
return result
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def explain_chain(source: type, target: type, *, registry) -> str:
|
|
517
|
+
"""Return a human-readable explanation of how source → target would
|
|
518
|
+
be converted, without actually performing the conversion.
|
|
519
|
+
"""
|
|
520
|
+
if isinstance(source, type) and issubclass(source, target):
|
|
521
|
+
return f"{source.__name__} is already a subclass of {target.__name__}"
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
chain_fn = find_chain(source, target, registry=registry)
|
|
525
|
+
except TypeError as e:
|
|
526
|
+
return str(e)
|
|
527
|
+
|
|
528
|
+
path = getattr(chain_fn, "__converter_path__", None)
|
|
529
|
+
steps = getattr(chain_fn, "__converter_chain__", [])
|
|
530
|
+
|
|
531
|
+
lines = [f"Path: {path or '(direct)'}"]
|
|
532
|
+
for i, fn in enumerate(steps, 1):
|
|
533
|
+
lines.append(f" Step {i}: {getattr(fn, '__name__', '?')}")
|
|
534
|
+
return "\n".join(lines)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def all_reachable_from(source: type, *, registry) -> set[type]:
|
|
538
|
+
"""Return every type reachable from *source* through any chain of
|
|
539
|
+
converters. Walks the full graph via BFS from source (including
|
|
540
|
+
MRO-matched ancestors) and collects every type visited.
|
|
541
|
+
"""
|
|
542
|
+
converters = getattr(registry, "_converters", {})
|
|
543
|
+
|
|
544
|
+
graph: dict[type, list[type]] = {}
|
|
545
|
+
for (from_type, to_type) in converters.keys():
|
|
546
|
+
graph.setdefault(from_type, []).append(to_type)
|
|
547
|
+
|
|
548
|
+
start_types = [source]
|
|
549
|
+
for rt in graph.keys():
|
|
550
|
+
if rt != source and isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
|
|
551
|
+
start_types.append(rt)
|
|
552
|
+
|
|
553
|
+
visited: set[type] = set(start_types)
|
|
554
|
+
visited.add(source)
|
|
555
|
+
queue = deque(start_types)
|
|
556
|
+
|
|
557
|
+
while queue:
|
|
558
|
+
current = queue.popleft()
|
|
559
|
+
for neighbour in graph.get(current, []):
|
|
560
|
+
if neighbour not in visited:
|
|
561
|
+
visited.add(neighbour)
|
|
562
|
+
queue.append(neighbour)
|
|
563
|
+
|
|
564
|
+
visited.discard(source)
|
|
565
|
+
return visited
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def all_paths(source: type, target: type, *, registry, max_depth: int = 6) -> list[list[type]]:
|
|
569
|
+
"""Return ALL paths from source → target up to max_depth hops,
|
|
570
|
+
sorted shortest-first. Useful for understanding why a particular
|
|
571
|
+
chain was chosen and what alternatives exist.
|
|
572
|
+
"""
|
|
573
|
+
converters = getattr(registry, "_converters", {})
|
|
574
|
+
|
|
575
|
+
graph: dict[type, list[type]] = {}
|
|
576
|
+
for (from_type, to_type) in converters.keys():
|
|
577
|
+
graph.setdefault(from_type, []).append(to_type)
|
|
578
|
+
|
|
579
|
+
start_types = [source]
|
|
580
|
+
for rt in graph.keys():
|
|
581
|
+
if rt != source and isinstance(source, type) and isinstance(rt, type) and issubclass(source, rt):
|
|
582
|
+
start_types.append(rt)
|
|
583
|
+
|
|
584
|
+
results: list[list[type]] = []
|
|
585
|
+
|
|
586
|
+
def dfs(path: list[type], visited: set[type]) -> None:
|
|
587
|
+
if len(path) > max_depth + 1:
|
|
588
|
+
return
|
|
589
|
+
current = path[-1]
|
|
590
|
+
if current == target and len(path) > 1:
|
|
591
|
+
results.append(list(path))
|
|
592
|
+
return
|
|
593
|
+
for neighbour in graph.get(current, []):
|
|
594
|
+
if neighbour not in visited:
|
|
595
|
+
visited.add(neighbour)
|
|
596
|
+
path.append(neighbour)
|
|
597
|
+
dfs(path, visited)
|
|
598
|
+
path.pop()
|
|
599
|
+
visited.discard(neighbour)
|
|
600
|
+
|
|
601
|
+
for st in start_types:
|
|
602
|
+
initial = [source] if st == source else [source, st]
|
|
603
|
+
dfs(initial, set(initial))
|
|
604
|
+
|
|
605
|
+
results.sort(key=len)
|
|
606
|
+
return results
|