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,876 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import collections.abc
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
import libcst as cst
|
|
6
|
+
|
|
7
|
+
from meltygui.core.rendering.core_decoration import exclude
|
|
8
|
+
from meltygui.core.rendering.core_decoration import defaults
|
|
9
|
+
|
|
10
|
+
# ==============================
|
|
11
|
+
# Formatting & whitespace constants
|
|
12
|
+
# ==============================
|
|
13
|
+
|
|
14
|
+
# no space '=' for keyword args
|
|
15
|
+
TIGHT_EQ = cst.AssignEqual(
|
|
16
|
+
whitespace_before=cst.SimpleWhitespace(""),
|
|
17
|
+
whitespace_after=cst.SimpleWhitespace("")
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# ==============================
|
|
21
|
+
# Trivia-insensitive subtree hashing
|
|
22
|
+
# ==============================
|
|
23
|
+
|
|
24
|
+
_TRIVIA_FIELD_NAMES = {
|
|
25
|
+
"whitespace", "leading_lines", "trailing_whitespace",
|
|
26
|
+
"lpar", "rpar", "star", "comma", "semicolon",
|
|
27
|
+
"header", "footer", "default_newline", "default_indent",
|
|
28
|
+
"encoding", "has_trailing_newline", "trailing_comma",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_TRIVIA_NODE_TYPES = (
|
|
32
|
+
cst.EmptyLine,
|
|
33
|
+
cst.TrailingWhitespace,
|
|
34
|
+
cst.SimpleWhitespace,
|
|
35
|
+
cst.Newline,
|
|
36
|
+
cst.ParenthesizedWhitespace,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_trivia_node(n: cst.CSTNode) -> bool:
|
|
41
|
+
return isinstance(n, _TRIVIA_NODE_TYPES)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _fp_primitive(x):
|
|
45
|
+
if x is None: return ("none",)
|
|
46
|
+
if x is True: return ("bool", 1)
|
|
47
|
+
if x is False: return ("bool", 0)
|
|
48
|
+
if isinstance(x, (int, float)): return ("num", repr(x))
|
|
49
|
+
if isinstance(x, str): return ("str", x)
|
|
50
|
+
return ("repr", repr(x))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _last_stmt(mapping) -> cst.BaseStatement | None:
|
|
54
|
+
last = None
|
|
55
|
+
for v in mapping.values():
|
|
56
|
+
n = v.node if hasattr(v, "node") else v
|
|
57
|
+
if isinstance(n, cst.BaseStatement):
|
|
58
|
+
last = n
|
|
59
|
+
return last
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _expr_placeholder_like_last_stmt(stmt: cst.BaseStatement | None) -> cst.BaseExpression:
|
|
64
|
+
"""
|
|
65
|
+
If the last statement is a simple constant expression, mirror its *type*.
|
|
66
|
+
Otherwise fall back to NAME('PLACEHOLDER').
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(stmt, cst.SimpleStatementLine) and stmt.body:
|
|
69
|
+
first = stmt.body[0]
|
|
70
|
+
if isinstance(first, cst.Expr):
|
|
71
|
+
v = first.value
|
|
72
|
+
if isinstance(v, (cst.Integer, cst.Float, cst.Imaginary, cst.SimpleString,
|
|
73
|
+
cst.Name, cst.List, cst.Tuple, cst.Set, cst.Dict,
|
|
74
|
+
cst.ConcatenatedString, cst.FormattedString)):
|
|
75
|
+
# reuse your existing shape logic
|
|
76
|
+
like = _placeholder_like(v)
|
|
77
|
+
# _placeholder_like returns a CSTNode; check BaseExpression
|
|
78
|
+
if isinstance(like, cst.BaseExpression):
|
|
79
|
+
return like
|
|
80
|
+
# default
|
|
81
|
+
return cst.Name("PLACEHOLDER")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _make_const_line_like(last_stmt: cst.BaseStatement | None) -> cst.SimpleStatementLine:
|
|
85
|
+
"""
|
|
86
|
+
Produce a one-line statement with a constant-like expression, inferred from last_stmt.
|
|
87
|
+
This renders with its own newline automatically.
|
|
88
|
+
"""
|
|
89
|
+
expr = _expr_placeholder_like_last_stmt(last_stmt)
|
|
90
|
+
return cst.SimpleStatementLine([cst.Expr(value=expr)])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _make_pass_line() -> cst.SimpleStatementLine:
|
|
94
|
+
return cst.SimpleStatementLine([cst.Pass()])
|
|
95
|
+
|
|
96
|
+
def _fingerprint_struct(obj):
|
|
97
|
+
# unwrap proxy
|
|
98
|
+
if isinstance(obj, CSTProxy):
|
|
99
|
+
obj = obj.node
|
|
100
|
+
|
|
101
|
+
if isinstance(obj, cst.CSTNode) and dataclasses.is_dataclass(obj):
|
|
102
|
+
typ = type(obj).__name__
|
|
103
|
+
# obj_hash = hash(obj)
|
|
104
|
+
parts = []
|
|
105
|
+
for f in dataclasses.fields(obj):
|
|
106
|
+
name = f.name
|
|
107
|
+
if name in _TRIVIA_FIELD_NAMES:
|
|
108
|
+
continue
|
|
109
|
+
val = getattr(obj, name)
|
|
110
|
+
|
|
111
|
+
if isinstance(val, cst.CSTNode) and _is_trivia_node(val):
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if isinstance(val, collections.abc.Sequence) and not isinstance(val, str):
|
|
115
|
+
seq_elems = []
|
|
116
|
+
for e in val:
|
|
117
|
+
if isinstance(e, cst.CSTNode) and _is_trivia_node(e):
|
|
118
|
+
continue
|
|
119
|
+
seq_elems.append(_fingerprint_struct(e))
|
|
120
|
+
parts.append((name, ("seq", tuple(seq_elems))))
|
|
121
|
+
else:
|
|
122
|
+
parts.append((name, _fingerprint_struct(val)))
|
|
123
|
+
return ("cst", typ, tuple(parts))
|
|
124
|
+
|
|
125
|
+
if isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str):
|
|
126
|
+
return ("seq", tuple(_fingerprint_struct(e) for e in obj))
|
|
127
|
+
|
|
128
|
+
return _fp_primitive(obj)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _hash_key_for_elem(elem, digest_size=3) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Stable short hex id for an element based on its trivia-insensitive structure.
|
|
134
|
+
digest_size=12 -> 24 hex chars.
|
|
135
|
+
"""
|
|
136
|
+
struct = _fingerprint_struct(elem)
|
|
137
|
+
payload = repr(struct).encode("utf-8")
|
|
138
|
+
h = hashlib.blake2b(payload, digest_size=digest_size)
|
|
139
|
+
return h.hexdigest()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _dedupe_key(base: str, mapping: collections.abc.Mapping) -> str:
|
|
143
|
+
key = base
|
|
144
|
+
i = 1
|
|
145
|
+
while key in mapping:
|
|
146
|
+
i += 1
|
|
147
|
+
key = f"{base}~{i}"
|
|
148
|
+
return key
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_CONST_NAME_RE = re.compile(r"^CONSTANT_(\d+)$")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _next_constant_index_in_body(mapping) -> int:
|
|
155
|
+
"""
|
|
156
|
+
Scan a CSTDictProxy representing a .body field and find the next CONSTANT_N index.
|
|
157
|
+
"""
|
|
158
|
+
max_idx = -1
|
|
159
|
+
for v in mapping.values():
|
|
160
|
+
n = v.node if hasattr(v, "node") else v
|
|
161
|
+
if isinstance(n, cst.SimpleStatementLine):
|
|
162
|
+
for small in n.body:
|
|
163
|
+
if isinstance(small, cst.Assign):
|
|
164
|
+
# consider only the `Name = ...` targets
|
|
165
|
+
for tgt in small.targets:
|
|
166
|
+
t = tgt.target
|
|
167
|
+
if isinstance(t, cst.Name):
|
|
168
|
+
m = _CONST_NAME_RE.match(t.value)
|
|
169
|
+
if m:
|
|
170
|
+
try:
|
|
171
|
+
idx = int(m.group(1))
|
|
172
|
+
if idx > max_idx:
|
|
173
|
+
max_idx = idx
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
return max_idx + 1
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _make_constant_assignment_line(name: str,
|
|
180
|
+
value_expr: cst.BaseExpression | None = None,
|
|
181
|
+
tight_equals: bool = True) -> cst.SimpleStatementLine:
|
|
182
|
+
if value_expr is None:
|
|
183
|
+
# default requested by you
|
|
184
|
+
value_expr = cst.SimpleString('"default_val"')
|
|
185
|
+
assign = cst.Assign(
|
|
186
|
+
targets=[cst.AssignTarget(target=cst.Name(name))],
|
|
187
|
+
value=value_expr
|
|
188
|
+
)
|
|
189
|
+
return cst.SimpleStatementLine([assign])
|
|
190
|
+
|
|
191
|
+
# ==============================
|
|
192
|
+
# Converters (primitive <-> LibCST)
|
|
193
|
+
# ==============================
|
|
194
|
+
|
|
195
|
+
def _lexeme(py):
|
|
196
|
+
if isinstance(py, bool): return "True" if py else "False"
|
|
197
|
+
if py is None: return "None"
|
|
198
|
+
if isinstance(py, float): return repr(py)
|
|
199
|
+
if isinstance(py, int): return str(py)
|
|
200
|
+
if isinstance(py, str): return py
|
|
201
|
+
return str(py)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _autobox_expr(py, hint: cst.CSTNode | None):
|
|
205
|
+
if isinstance(py, cst.CSTNode): return py
|
|
206
|
+
if isinstance(py, CSTProxy): return py.node
|
|
207
|
+
if isinstance(py, bool): return cst.Name("True" if py else "False")
|
|
208
|
+
if py is None: return cst.Name("None")
|
|
209
|
+
if isinstance(py, int): return cst.Integer(_lexeme(py))
|
|
210
|
+
if isinstance(py, float): return cst.Float(_lexeme(py))
|
|
211
|
+
if isinstance(py, str):
|
|
212
|
+
if isinstance(hint, cst.Name): return cst.Name(py)
|
|
213
|
+
if isinstance(hint, cst.SimpleString): return cst.SimpleString(repr(py))
|
|
214
|
+
return cst.SimpleString(repr(py))
|
|
215
|
+
return py
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _to_libcst(value, orig_field):
|
|
219
|
+
if isinstance(value, CSTProxy): return value.node
|
|
220
|
+
if isinstance(value, cst.CSTNode): return value
|
|
221
|
+
|
|
222
|
+
if isinstance(orig_field, collections.abc.Sequence) and not isinstance(orig_field, str):
|
|
223
|
+
elems = value.values() if isinstance(value, collections.abc.Mapping) else value
|
|
224
|
+
hint_elem = next((e for e in orig_field if isinstance(e, cst.CSTNode)), None)
|
|
225
|
+
out = []
|
|
226
|
+
for x in elems:
|
|
227
|
+
if isinstance(x, CSTProxy):
|
|
228
|
+
out.append(x.node)
|
|
229
|
+
elif isinstance(x, cst.CSTNode):
|
|
230
|
+
out.append(x)
|
|
231
|
+
else:
|
|
232
|
+
out.append(_autobox_expr(x, hint_elem))
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
if isinstance(orig_field, str):
|
|
236
|
+
return _lexeme(value)
|
|
237
|
+
|
|
238
|
+
if isinstance(orig_field, cst.CSTNode):
|
|
239
|
+
return _autobox_expr(value, orig_field)
|
|
240
|
+
|
|
241
|
+
return value
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ==============================
|
|
245
|
+
# Naming helpers & placeholders
|
|
246
|
+
# ==============================
|
|
247
|
+
|
|
248
|
+
def _first_hint_elem(seq):
|
|
249
|
+
for e in seq:
|
|
250
|
+
if isinstance(e, cst.CSTNode):
|
|
251
|
+
return e
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _default_placeholder_for_hint(hint: cst.CSTNode | None) -> cst.CSTNode:
|
|
256
|
+
if isinstance(hint, cst.Arg) or hint is None:
|
|
257
|
+
return cst.Arg(value=cst.Name("PLACEHOLDER"))
|
|
258
|
+
if isinstance(hint, cst.BaseExpression):
|
|
259
|
+
return cst.Name("PLACEHOLDER")
|
|
260
|
+
if isinstance(hint, cst.Param):
|
|
261
|
+
return cst.Param(name=cst.Name("placeholder"))
|
|
262
|
+
if isinstance(hint, cst.Element):
|
|
263
|
+
return cst.Element(value=cst.Name("PLACEHOLDER"))
|
|
264
|
+
if isinstance(hint, cst.DictElement):
|
|
265
|
+
return cst.DictElement(key=cst.SimpleString("'key'"), value=cst.Name("PLACEHOLDER"))
|
|
266
|
+
if isinstance(hint, cst.BaseStatement):
|
|
267
|
+
return cst.Pass()
|
|
268
|
+
return cst.Name("PLACEHOLDER")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _placeholder_like(expr: cst.CSTNode) -> cst.CSTNode:
|
|
272
|
+
if isinstance(expr, cst.Integer): return cst.Integer("0")
|
|
273
|
+
if isinstance(expr, cst.Float): return cst.Float("0.0")
|
|
274
|
+
if isinstance(expr, cst.Imaginary): return cst.Imaginary("0j")
|
|
275
|
+
if isinstance(expr, cst.SimpleString): return cst.SimpleString("''")
|
|
276
|
+
if isinstance(expr, cst.Name):
|
|
277
|
+
if expr.value in ("True", "False"): return cst.Name("False")
|
|
278
|
+
if expr.value == "None": return cst.Name("None")
|
|
279
|
+
return cst.Name("PLACEHOLDER")
|
|
280
|
+
if isinstance(expr, cst.List): return cst.List([])
|
|
281
|
+
if isinstance(expr, cst.Tuple): return cst.Tuple([])
|
|
282
|
+
if isinstance(expr, cst.Set): return cst.Set([])
|
|
283
|
+
if isinstance(expr, cst.Dict): return cst.Dict([])
|
|
284
|
+
if isinstance(expr, (cst.ConcatenatedString, cst.FormattedString)):
|
|
285
|
+
return cst.SimpleString("''")
|
|
286
|
+
return cst.Name("PLACEHOLDER")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _next_indexed_name(prefix: str, taken: set[int]) -> str:
|
|
290
|
+
n = 0
|
|
291
|
+
while n in taken:
|
|
292
|
+
n += 1
|
|
293
|
+
return f"{prefix}_{n}"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _collect_taken_indices_from_names(names: list[str], prefix: str) -> set[int]:
|
|
297
|
+
pat = re.compile(rf"^{re.escape(prefix)}_(\d+)$")
|
|
298
|
+
out = set()
|
|
299
|
+
for s in names:
|
|
300
|
+
m = pat.match(s)
|
|
301
|
+
if m:
|
|
302
|
+
out.add(int(m.group(1)))
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _last_arg_value(args_mapping) -> cst.CSTNode | None:
|
|
307
|
+
last_val = None
|
|
308
|
+
for v in args_mapping.values():
|
|
309
|
+
n = v.node if hasattr(v, "node") else v
|
|
310
|
+
if isinstance(n, cst.Arg):
|
|
311
|
+
last_val = n.value
|
|
312
|
+
return last_val
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ==============================
|
|
316
|
+
# Error state
|
|
317
|
+
# ==============================
|
|
318
|
+
|
|
319
|
+
@dataclasses.dataclass
|
|
320
|
+
class ErrorState:
|
|
321
|
+
message: str
|
|
322
|
+
field: str | None = None
|
|
323
|
+
detail: str | None = None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ==============================
|
|
327
|
+
# DictProxy for sequence fields
|
|
328
|
+
# ==============================
|
|
329
|
+
|
|
330
|
+
class CSTDictProxy(dict):
|
|
331
|
+
"""
|
|
332
|
+
Ordered, dict-like proxy for sequence fields. Keys are stable subtree-hash IDs.
|
|
333
|
+
Any mutation bubbles dirty to the parent CSTProxy.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def reorder(self, keys: list[str]) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Reorder this mapping without changing values.
|
|
339
|
+
Unknown keys are ignored; missing existing keys are appended in original order.
|
|
340
|
+
"""
|
|
341
|
+
# Snapshot of in in current order
|
|
342
|
+
current = dict(self) # plain dict: preserves order since 3.7+
|
|
343
|
+
new = {}
|
|
344
|
+
|
|
345
|
+
# First, take keys in the requested order
|
|
346
|
+
for k in keys:
|
|
347
|
+
if k in current:
|
|
348
|
+
new[k] = current.pop(k)
|
|
349
|
+
|
|
350
|
+
# Then append any leftovers in their original relative order
|
|
351
|
+
for k, v in current.items():
|
|
352
|
+
new[k] = v
|
|
353
|
+
|
|
354
|
+
# Apply atomically via base methods (avoid double _touch)
|
|
355
|
+
super().clear()
|
|
356
|
+
super().update(new)
|
|
357
|
+
self._touch()
|
|
358
|
+
|
|
359
|
+
def __init__(self, items_kv, parent: "CSTProxy", field_name: str):
|
|
360
|
+
super().__init__()
|
|
361
|
+
self._parent = parent
|
|
362
|
+
self._field = field_name
|
|
363
|
+
for k, v in items_kv:
|
|
364
|
+
w = cst_wrap(v)
|
|
365
|
+
if isinstance(w, CSTProxy):
|
|
366
|
+
w._set_parent(parent, field_name)
|
|
367
|
+
super().__setitem__(k, w)
|
|
368
|
+
|
|
369
|
+
# @property
|
|
370
|
+
# def unique_id(self):
|
|
371
|
+
# return str(self.id)
|
|
372
|
+
|
|
373
|
+
# --- helpers ---
|
|
374
|
+
def _touch(self):
|
|
375
|
+
self._parent._mark_dirty_up()
|
|
376
|
+
|
|
377
|
+
# --- mutators ---
|
|
378
|
+
def __setitem__(self, k, v):
|
|
379
|
+
w = cst_wrap(v)
|
|
380
|
+
if isinstance(w, CSTProxy):
|
|
381
|
+
w._set_parent(self._parent, self._field)
|
|
382
|
+
super().__setitem__(k, w)
|
|
383
|
+
self._touch()
|
|
384
|
+
|
|
385
|
+
def __delitem__(self, k):
|
|
386
|
+
super().__delitem__(k)
|
|
387
|
+
self._touch()
|
|
388
|
+
|
|
389
|
+
def clear(self):
|
|
390
|
+
super().clear()
|
|
391
|
+
self._touch()
|
|
392
|
+
|
|
393
|
+
def pop(self, k, *default):
|
|
394
|
+
val = super().pop(k, *default) if default else super().pop(k)
|
|
395
|
+
self._touch()
|
|
396
|
+
return val
|
|
397
|
+
|
|
398
|
+
def popitem(self):
|
|
399
|
+
kv = super().popitem()
|
|
400
|
+
self._touch()
|
|
401
|
+
return kv
|
|
402
|
+
|
|
403
|
+
def setdefault(self, k, default=None):
|
|
404
|
+
if k in self:
|
|
405
|
+
return super().get(k)
|
|
406
|
+
w = cst_wrap(default)
|
|
407
|
+
if isinstance(w, CSTProxy):
|
|
408
|
+
w._set_parent(self._parent, self._field)
|
|
409
|
+
super().__setitem__(k, w)
|
|
410
|
+
self._touch()
|
|
411
|
+
return w
|
|
412
|
+
|
|
413
|
+
def update(self, other=None, /, **kw):
|
|
414
|
+
if other is None:
|
|
415
|
+
other = {}
|
|
416
|
+
it = other.items() if hasattr(other, "items") else other
|
|
417
|
+
changed = False
|
|
418
|
+
for k, v in it:
|
|
419
|
+
w = cst_wrap(v)
|
|
420
|
+
if isinstance(w, CSTProxy):
|
|
421
|
+
w._set_parent(self._parent, self._field)
|
|
422
|
+
super().__setitem__(k, w);
|
|
423
|
+
changed = True
|
|
424
|
+
for k, v in kw.items():
|
|
425
|
+
w = cst_wrap(v)
|
|
426
|
+
if isinstance(w, CSTProxy):
|
|
427
|
+
w._set_parent(self._parent, self._field)
|
|
428
|
+
super().__setitem__(k, w);
|
|
429
|
+
changed = True
|
|
430
|
+
if changed:
|
|
431
|
+
self._touch()
|
|
432
|
+
|
|
433
|
+
def __ior__(self, other):
|
|
434
|
+
self.update(other)
|
|
435
|
+
return self
|
|
436
|
+
|
|
437
|
+
# --- ordering helpers ---
|
|
438
|
+
def insert_before(self, key_to_insert, before_key):
|
|
439
|
+
if key_to_insert not in self or before_key not in self:
|
|
440
|
+
return
|
|
441
|
+
val = super().pop(key_to_insert)
|
|
442
|
+
new = {}
|
|
443
|
+
for k, v in self.items():
|
|
444
|
+
if k == before_key:
|
|
445
|
+
new[key_to_insert] = val
|
|
446
|
+
new[k] = v
|
|
447
|
+
super().clear();
|
|
448
|
+
super().update(new);
|
|
449
|
+
self._touch()
|
|
450
|
+
|
|
451
|
+
def insert_after(self, key_to_insert, after_key):
|
|
452
|
+
if key_to_insert not in self or after_key not in self:
|
|
453
|
+
return
|
|
454
|
+
val = super().pop(key_to_insert)
|
|
455
|
+
new = {}
|
|
456
|
+
for k, v in self.items():
|
|
457
|
+
new[k] = v
|
|
458
|
+
if k == after_key:
|
|
459
|
+
new[key_to_insert] = val
|
|
460
|
+
super().clear();
|
|
461
|
+
super().update(new);
|
|
462
|
+
self._touch()
|
|
463
|
+
|
|
464
|
+
# --- auto append that infers kind/value from context ---
|
|
465
|
+
def append_to(self, value=None, *, factory=None, key: str | None = None) -> str:
|
|
466
|
+
"""
|
|
467
|
+
Append an element to the end of this dict-backed sequence.
|
|
468
|
+
|
|
469
|
+
Context-aware defaults:
|
|
470
|
+
- Call.args:
|
|
471
|
+
* infer positional/keyword legality from existing args
|
|
472
|
+
* infer default value "shape" from last arg's value (e.g., int->0, str->'')
|
|
473
|
+
* keyword names: arg_0, arg_1, ...
|
|
474
|
+
* keyword '=' tight formatting
|
|
475
|
+
- Parameters.{params,posonly_params,kwonly_params}: param_0, param_1, ...
|
|
476
|
+
- Dict.elements: 'key_0', 'key_1', ...
|
|
477
|
+
- List/Tuple.elements: Element(Name('arg_N'))
|
|
478
|
+
- Module/IndentedBlock.body: Pass()
|
|
479
|
+
- Fallback: type-shaped placeholder from hint
|
|
480
|
+
"""
|
|
481
|
+
parent_px = self._parent
|
|
482
|
+
parent_node = object.__getattribute__(parent_px, "_node")
|
|
483
|
+
orig_seq = getattr(parent_node, self._field)
|
|
484
|
+
hint_elem = _first_hint_elem(orig_seq)
|
|
485
|
+
|
|
486
|
+
if value is None and not callable(factory):
|
|
487
|
+
# ----------- Call.args: ----------
|
|
488
|
+
if isinstance(parent_node, cst.Call) and self._field == "args":
|
|
489
|
+
any_keyword = False
|
|
490
|
+
keyword_names = []
|
|
491
|
+
last_arg = None
|
|
492
|
+
last_is_positional = False
|
|
493
|
+
for v in self.values():
|
|
494
|
+
n = v.node if hasattr(v, "node") else v
|
|
495
|
+
if isinstance(n, cst.Arg):
|
|
496
|
+
last_arg = n
|
|
497
|
+
if n.keyword:
|
|
498
|
+
any_keyword = True
|
|
499
|
+
if isinstance(n.keyword, cst.Name):
|
|
500
|
+
keyword_names.append(n.keyword.value)
|
|
501
|
+
if isinstance(last_arg, cst.Arg):
|
|
502
|
+
last_is_positional = (last_arg.keyword is None)
|
|
503
|
+
|
|
504
|
+
exemplar_val = _last_arg_value(self) # last existing arg value
|
|
505
|
+
inferred_default = (
|
|
506
|
+
_placeholder_like(exemplar_val)
|
|
507
|
+
if exemplar_val is not None
|
|
508
|
+
else cst.Name("PLACEHOLDER")
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if any_keyword or not last_is_positional:
|
|
512
|
+
taken = _collect_taken_indices_from_names(keyword_names, "arg")
|
|
513
|
+
name = _next_indexed_name("arg", taken)
|
|
514
|
+
value = cst.Arg(keyword=cst.Name(name), equal=TIGHT_EQ, value=inferred_default)
|
|
515
|
+
else:
|
|
516
|
+
value = cst.Arg(value=inferred_default)
|
|
517
|
+
|
|
518
|
+
# ----------- Parameters -----------
|
|
519
|
+
elif isinstance(parent_node, cst.Parameters) and self._field in {"params", "posonly_params",
|
|
520
|
+
"kwonly_params"}:
|
|
521
|
+
existing = []
|
|
522
|
+
for v in self.values():
|
|
523
|
+
n = v.node if hasattr(v, "node") else v
|
|
524
|
+
if isinstance(n, cst.Param) and isinstance(n.name, cst.Name):
|
|
525
|
+
existing.append(n.name.value)
|
|
526
|
+
prefix = "param" # you can split: po/kw/param if desired
|
|
527
|
+
taken = _collect_taken_indices_from_names(existing, prefix)
|
|
528
|
+
name = _next_indexed_name(prefix, taken)
|
|
529
|
+
value = cst.Param(name=cst.Name(name))
|
|
530
|
+
|
|
531
|
+
# ----------- Dict elements -----------
|
|
532
|
+
elif isinstance(parent_node, cst.Dict) and self._field == "elements":
|
|
533
|
+
existing = []
|
|
534
|
+
for v in self.values():
|
|
535
|
+
n = v.node if hasattr(v, "node") else v
|
|
536
|
+
if isinstance(n, cst.DictElement) and isinstance(n.key, cst.SimpleString):
|
|
537
|
+
try:
|
|
538
|
+
existing.append(eval(n.key.value))
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
taken = _collect_taken_indices_from_names(existing, "key")
|
|
542
|
+
key_name = _next_indexed_name("key", taken)
|
|
543
|
+
value = cst.DictElement(key=cst.SimpleString(repr(key_name)),
|
|
544
|
+
value=cst.Name("PLACEHOLDER"))
|
|
545
|
+
|
|
546
|
+
# ----------- List/Tuple elements -----------
|
|
547
|
+
elif isinstance(parent_node, (cst.List, cst.Tuple)) and self._field == "elements":
|
|
548
|
+
existing = []
|
|
549
|
+
for v in self.values():
|
|
550
|
+
n = v.node if hasattr(v, "node") else v
|
|
551
|
+
if isinstance(n, cst.Element) and isinstance(n.value, cst.Name):
|
|
552
|
+
existing.append(n.value.value)
|
|
553
|
+
taken = _collect_taken_indices_from_names(existing, "arg")
|
|
554
|
+
name = _next_indexed_name("arg", taken)
|
|
555
|
+
value = cst.Element(value=cst.Name(name))
|
|
556
|
+
|
|
557
|
+
# ----------- Bodies & fallback -----------
|
|
558
|
+
elif isinstance(parent_node, (cst.Module, cst.IndentedBlock)) and self._field == "body":
|
|
559
|
+
# Build CONSTANT_N="default_val" as a full statement line (renders with newline)
|
|
560
|
+
next_idx = _next_constant_index_in_body(self)
|
|
561
|
+
const_name = f"CONSTANT_{next_idx}"
|
|
562
|
+
value = _make_constant_assignment_line(const_name)
|
|
563
|
+
|
|
564
|
+
else:
|
|
565
|
+
value = _default_placeholder_for_hint(hint_elem)
|
|
566
|
+
|
|
567
|
+
if value is None and callable(factory):
|
|
568
|
+
value = factory(hint_elem)
|
|
569
|
+
|
|
570
|
+
# wrap & wire
|
|
571
|
+
w = cst_wrap(value)
|
|
572
|
+
if isinstance(w, CSTProxy):
|
|
573
|
+
w._set_parent(parent_px, self._field)
|
|
574
|
+
|
|
575
|
+
# key: use hash (dedupe)
|
|
576
|
+
if key is None:
|
|
577
|
+
base = _hash_key_for_elem(w)
|
|
578
|
+
key = _dedupe_key(base, self)
|
|
579
|
+
|
|
580
|
+
self[key] = w # go through override so _touch() runs # __setitem__ bubbles dirty
|
|
581
|
+
return key
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# ==============================
|
|
585
|
+
# CSTProxy with error handling
|
|
586
|
+
# ==============================
|
|
587
|
+
|
|
588
|
+
@exclude(["star", "header", "lpar", "rpar", "footer", "comma", "lbracket", "rbracket", "default_newline", "default_indent", "encoding", "has_trailing_newline"])
|
|
589
|
+
class CSTProxy:
|
|
590
|
+
|
|
591
|
+
def __init__(self, node: cst.CSTNode):
|
|
592
|
+
object.__setattr__(self, "_node", node)
|
|
593
|
+
object.__setattr__(self, "_dirty", False)
|
|
594
|
+
object.__setattr__(self, "_parent", None)
|
|
595
|
+
object.__setattr__(self, "_parent_field", None)
|
|
596
|
+
object.__setattr__(self, "_error", None) # ErrorState | None
|
|
597
|
+
self._refresh_fields_from(node)
|
|
598
|
+
|
|
599
|
+
# class spoofing so type-based routing works
|
|
600
|
+
@property
|
|
601
|
+
def __class__(self):
|
|
602
|
+
return type(self._node)
|
|
603
|
+
|
|
604
|
+
# make hashable
|
|
605
|
+
def __eq__(self, other):
|
|
606
|
+
if not isinstance(other, CSTProxy):
|
|
607
|
+
return False
|
|
608
|
+
return self.node == other.node
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# ---- error handling ----
|
|
612
|
+
@property
|
|
613
|
+
def has_error(self) -> bool:
|
|
614
|
+
return object.__getattribute__(self, "_error") is not None
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def error(self) -> ErrorState | None:
|
|
618
|
+
return object.__getattribute__(self, "_error")
|
|
619
|
+
|
|
620
|
+
def clear_error(self):
|
|
621
|
+
object.__setattr__(self, "_error", None)
|
|
622
|
+
|
|
623
|
+
# materialize node safely
|
|
624
|
+
@property
|
|
625
|
+
def node(self) -> cst.CSTNode:
|
|
626
|
+
if self._dirty:
|
|
627
|
+
self._rebuild()
|
|
628
|
+
return object.__getattribute__(self, "_node")
|
|
629
|
+
|
|
630
|
+
# To string
|
|
631
|
+
# def __str__(self) -> str:
|
|
632
|
+
# # fingerprint
|
|
633
|
+
# finger_print = _hash_key_for_elem(self.node)
|
|
634
|
+
# return str(finger_print)
|
|
635
|
+
# def __hash__(self):
|
|
636
|
+
# return hash(_hash_node_struct(self.node))
|
|
637
|
+
|
|
638
|
+
def flush(self):
|
|
639
|
+
if self._dirty:
|
|
640
|
+
self._rebuild()
|
|
641
|
+
|
|
642
|
+
# --- parent plumbing & dirtiness bubbling ---
|
|
643
|
+
def _set_parent(self, parent: "CSTProxy|None", field_name: str | None):
|
|
644
|
+
object.__setattr__(self, "_parent", parent)
|
|
645
|
+
object.__setattr__(self, "_parent_field", field_name)
|
|
646
|
+
|
|
647
|
+
def _mark_dirty_up(self, depth=0):
|
|
648
|
+
object.__setattr__(self, "_dirty", True)
|
|
649
|
+
p = object.__getattribute__(self, "_parent")
|
|
650
|
+
if depth > 100:
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
if p is not None:
|
|
654
|
+
p._mark_dirty_up(depth + 1)
|
|
655
|
+
|
|
656
|
+
# --- field reflection & rebuild ---
|
|
657
|
+
def _refresh_fields_from(self, node):
|
|
658
|
+
if not dataclasses.is_dataclass(node):
|
|
659
|
+
object.__setattr__(self, "_field_names", ())
|
|
660
|
+
return
|
|
661
|
+
fns = tuple(f.name for f in dataclasses.fields(node))
|
|
662
|
+
object.__setattr__(self, "_field_names", fns)
|
|
663
|
+
|
|
664
|
+
keep = {k: v for k, v in self.__dict__.items() if k not in fns}
|
|
665
|
+
self.__dict__.clear()
|
|
666
|
+
self.__dict__.update(keep)
|
|
667
|
+
|
|
668
|
+
for name in fns:
|
|
669
|
+
val = getattr(node, name)
|
|
670
|
+
if isinstance(val, collections.abc.Sequence) and not isinstance(val, str):
|
|
671
|
+
self.__dict__[name] = _make_sequence_proxy(self, name, list(val))
|
|
672
|
+
else:
|
|
673
|
+
child = cst_wrap(val)
|
|
674
|
+
if isinstance(child, CSTProxy):
|
|
675
|
+
child._set_parent(self, name)
|
|
676
|
+
self.__dict__[name] = child
|
|
677
|
+
|
|
678
|
+
def _rebuild(self):
|
|
679
|
+
node = object.__getattribute__(self, "_node")
|
|
680
|
+
try:
|
|
681
|
+
kwargs = {}
|
|
682
|
+
for name in self._field_names:
|
|
683
|
+
current = self.__dict__[name]
|
|
684
|
+
original = getattr(node, name)
|
|
685
|
+
kwargs[name] = _to_libcst(current, original)
|
|
686
|
+
new_node = node.with_changes(**kwargs)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
object.__setattr__(self, "_error", ErrorState(
|
|
689
|
+
message=f"rebuild failed on {type(node).__name__}",
|
|
690
|
+
field=None,
|
|
691
|
+
detail=repr(e),
|
|
692
|
+
))
|
|
693
|
+
# keep old node; leave _ = True so caller/UI can decide to auto-resolve
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
object.__setattr__(self, "_node", new_node)
|
|
697
|
+
object.__setattr__(self, "_dirty", False)
|
|
698
|
+
object.__setattr__(self, "_error", None)
|
|
699
|
+
self._refresh_fields_from(new_node)
|
|
700
|
+
|
|
701
|
+
def auto_resolve_error(self) -> bool:
|
|
702
|
+
"""
|
|
703
|
+
Attempt to repair bad field shapes in-place.
|
|
704
|
+
Returns True if a subsequent rebuild succeeds.
|
|
705
|
+
"""
|
|
706
|
+
node = object.__getattribute__(self, "_node")
|
|
707
|
+
repaired_any = False
|
|
708
|
+
|
|
709
|
+
for name in self._field_names:
|
|
710
|
+
original = getattr(node, name)
|
|
711
|
+
current = self.__dict__[name]
|
|
712
|
+
|
|
713
|
+
# 1) Sequence field as mapping (CSTDictProxy)
|
|
714
|
+
if isinstance(original, collections.abc.Sequence) and not isinstance(original, str):
|
|
715
|
+
hint = _first_hint_elem(original)
|
|
716
|
+
|
|
717
|
+
# If not a mapping, coerce into mapping
|
|
718
|
+
if not isinstance(current, collections.abc.Mapping):
|
|
719
|
+
seq = current if isinstance(current, collections.abc.Sequence) else [current]
|
|
720
|
+
fixed = []
|
|
721
|
+
for elem in seq:
|
|
722
|
+
if isinstance(elem, CSTProxy):
|
|
723
|
+
fixed.append(elem.node)
|
|
724
|
+
elif isinstance(elem, cst.CSTNode):
|
|
725
|
+
fixed.append(elem)
|
|
726
|
+
else:
|
|
727
|
+
fixed.append(_autobox_expr(elem, hint) if hint else _default_placeholder_for_hint(hint))
|
|
728
|
+
self.__dict__[name] = _make_sequence_proxy(self, name, fixed)
|
|
729
|
+
repaired_any = True
|
|
730
|
+
else:
|
|
731
|
+
# validate each mapping value
|
|
732
|
+
mapping = current
|
|
733
|
+
new_values = []
|
|
734
|
+
for _, v in mapping.items():
|
|
735
|
+
n = v.node if isinstance(v, CSTProxy) else v
|
|
736
|
+
if isinstance(n, cst.CSTNode):
|
|
737
|
+
new_values.append(n)
|
|
738
|
+
else:
|
|
739
|
+
try:
|
|
740
|
+
new_values.append(
|
|
741
|
+
_autobox_expr(n, hint) if hint else _default_placeholder_for_hint(hint))
|
|
742
|
+
except Exception:
|
|
743
|
+
new_values.append(_default_placeholder_for_hint(hint))
|
|
744
|
+
repaired_any = True
|
|
745
|
+
|
|
746
|
+
# Special legality for Call.args: no positional after keyword
|
|
747
|
+
if isinstance(node, cst.Call) and name == "args":
|
|
748
|
+
vals = new_values
|
|
749
|
+
# normalize order legality
|
|
750
|
+
seen_kw = False
|
|
751
|
+
kw_names = [a.keyword.value for a in vals if
|
|
752
|
+
isinstance(a, cst.Arg) and isinstance(a.keyword, cst.Name)]
|
|
753
|
+
taken = _collect_taken_indices_from_names(kw_names, "arg")
|
|
754
|
+
|
|
755
|
+
coerced = []
|
|
756
|
+
for a in vals:
|
|
757
|
+
if isinstance(a, cst.Arg):
|
|
758
|
+
if a.keyword is not None:
|
|
759
|
+
seen_kw = True
|
|
760
|
+
coerced.append(a)
|
|
761
|
+
else:
|
|
762
|
+
if seen_kw:
|
|
763
|
+
idx_name = _next_indexed_name("arg", taken)
|
|
764
|
+
taken.add(int(idx_name.split("_")[-1]))
|
|
765
|
+
coerced.append(
|
|
766
|
+
cst.Arg(keyword=cst.Name(idx_name), equal=TIGHT_EQ, value=a.value))
|
|
767
|
+
else:
|
|
768
|
+
coerced.append(a)
|
|
769
|
+
else:
|
|
770
|
+
coerced.append(cst.Arg(value=cst.Name("PLACEHOLDER")))
|
|
771
|
+
new_values = coerced
|
|
772
|
+
|
|
773
|
+
self.__dict__[name] = _make_sequence_proxy(self, name, new_values)
|
|
774
|
+
repaired_any = True
|
|
775
|
+
|
|
776
|
+
# 2) Leaf string field
|
|
777
|
+
elif isinstance(original, str):
|
|
778
|
+
try:
|
|
779
|
+
self.__dict__[name] = _lexeme(current)
|
|
780
|
+
except Exception:
|
|
781
|
+
self.__dict__[name] = _lexeme("")
|
|
782
|
+
repaired_any = True
|
|
783
|
+
|
|
784
|
+
# 3) Node-typed field
|
|
785
|
+
elif isinstance(original, cst.CSTNode):
|
|
786
|
+
v = current
|
|
787
|
+
n = v.node if isinstance(v, CSTProxy) else v
|
|
788
|
+
if not isinstance(n, cst.CSTNode):
|
|
789
|
+
try:
|
|
790
|
+
self.__dict__[name] = cst_wrap(_autobox_expr(n, original))
|
|
791
|
+
except Exception:
|
|
792
|
+
self.__dict__[name] = cst_wrap(_placeholder_like(original))
|
|
793
|
+
repaired_any = True
|
|
794
|
+
|
|
795
|
+
# 4) Other scalars: rely on _to_libcst during rebuild
|
|
796
|
+
|
|
797
|
+
pre = self._error
|
|
798
|
+
self._rebuild()
|
|
799
|
+
return (not self.has_error) and (repaired_any or pre is not None)
|
|
800
|
+
|
|
801
|
+
# generic append API working on any sequence proxy this proxy owns
|
|
802
|
+
def append_to(self,
|
|
803
|
+
field_name: str,
|
|
804
|
+
value=None,
|
|
805
|
+
*,
|
|
806
|
+
factory=None,
|
|
807
|
+
key: str | None = None) -> str:
|
|
808
|
+
seq = self.__dict__.get(field_name, None)
|
|
809
|
+
if not isinstance(seq, CSTDictProxy):
|
|
810
|
+
raise TypeError(f"Field '{field_name}' is not a sequence (CSTDictProxy); got {type(seq).__name__}")
|
|
811
|
+
|
|
812
|
+
# Decide placeholder if value=None
|
|
813
|
+
orig_seq = getattr(object.__getattribute__(self, "_node"), field_name)
|
|
814
|
+
hint_elem = _first_hint_elem(orig_seq)
|
|
815
|
+
if value is None:
|
|
816
|
+
if callable(factory):
|
|
817
|
+
value = factory(hint_elem)
|
|
818
|
+
else:
|
|
819
|
+
# Defer to the dict proxy's context-aware append_to (smarts live there)
|
|
820
|
+
return seq.append_to(value=None, factory=None, key=key)
|
|
821
|
+
|
|
822
|
+
# Wrap & ded parent if needed, then append via seq
|
|
823
|
+
w = cst_wrap(value)
|
|
824
|
+
if isinstance(w, CSTProxy):
|
|
825
|
+
w._set_parent(self, field_name)
|
|
826
|
+
if key is None:
|
|
827
|
+
base = _hash_key_for_elem(w)
|
|
828
|
+
key = _dedupe_key(base, seq)
|
|
829
|
+
seq[key] = w # bubbles dirty
|
|
830
|
+
return key
|
|
831
|
+
|
|
832
|
+
# Hash & repr
|
|
833
|
+
def __hash__(self):
|
|
834
|
+
return hash(_fingerprint_struct(self.node))
|
|
835
|
+
|
|
836
|
+
def __setattr__(self, name, value):
|
|
837
|
+
if name in getattr(self, "_field_names", ()):
|
|
838
|
+
w = cst_wrap(value)
|
|
839
|
+
if isinstance(w, CSTProxy):
|
|
840
|
+
w._set_parent(self, name)
|
|
841
|
+
self.__dict__[name] = w
|
|
842
|
+
self._mark_dirty_up()
|
|
843
|
+
else:
|
|
844
|
+
self.__dict__[name] = value
|
|
845
|
+
|
|
846
|
+
def __repr__(self):
|
|
847
|
+
return f"<{type(self._node).__name__}Proxy>"
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# ==============================
|
|
851
|
+
# Sequence proxy factory
|
|
852
|
+
# ==============================
|
|
853
|
+
|
|
854
|
+
def _make_sequence_proxy(parent_px: CSTProxy, field_name: str, seq_values: list) -> CSTDictProxy:
|
|
855
|
+
used = {}
|
|
856
|
+
kv = []
|
|
857
|
+
for elem in seq_values:
|
|
858
|
+
base = _hash_key_for_elem(elem)
|
|
859
|
+
key = _dedupe_key(base, used)
|
|
860
|
+
used[key] = True
|
|
861
|
+
kv.append((key, elem))
|
|
862
|
+
return CSTDictProxy(kv, parent_px, field_name)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# ==============================
|
|
866
|
+
# Public wrapper
|
|
867
|
+
# ==============================
|
|
868
|
+
|
|
869
|
+
def cst_wrap(obj):
|
|
870
|
+
if isinstance(obj, CSTProxy): return obj
|
|
871
|
+
if isinstance(obj, cst.CSTNode) and dataclasses.is_dataclass(obj):
|
|
872
|
+
return CSTProxy(obj)
|
|
873
|
+
if isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str):
|
|
874
|
+
# bare sequences outside of fields become simple wrapped lists
|
|
875
|
+
return [cst_wrap(x) for x in obj]
|
|
876
|
+
return obj
|