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,468 @@
|
|
|
1
|
+
"""File import graph — which files import which files.
|
|
2
|
+
|
|
3
|
+
Built from the meltygui syntax scanner (`melty_scan.scan` → `iter_imports`),
|
|
4
|
+
never from `ast`: the same tokenize-based front end the meltygui syntax trees
|
|
5
|
+
use, so the graph reads exactly the statements the trees do. Every `.py`
|
|
6
|
+
under the root is scanned once (`build_import_graph`), each import resolved
|
|
7
|
+
to a FILE under the same root:
|
|
8
|
+
|
|
9
|
+
import a.b.c → root/a/b/c.py | root/a/b/c/__init__.py
|
|
10
|
+
from a.b import x, y → the module a.b, PLUS a/b/x.py | a/b/x/__init__.py
|
|
11
|
+
when the imported name is itself a submodule
|
|
12
|
+
from ..m import x → `level` dots up from the importing file's folder
|
|
13
|
+
|
|
14
|
+
Absolute imports resolve against the tree root and its ANCESTORS (the
|
|
15
|
+
repo keeps `src/…` on sys.path from the repo root, so a tree rooted at
|
|
16
|
+
`src/` still reads `from src.lsd… import …`), like sys.path; a target is
|
|
17
|
+
kept only when it lies inside the tree root. Anything that doesn't land on
|
|
18
|
+
a project file (stdlib, site-packages, a name that is a symbol rather than
|
|
19
|
+
a module) is dropped. The result is an
|
|
20
|
+
`ImportGraph` with both directions. Building runs on a background thread
|
|
21
|
+
(`start_build`), the file tree polls `ImportGraphBuild.result`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
import os
|
|
28
|
+
import pickle
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
from meltygui.text_index import _SKIP_DIRS
|
|
35
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
36
|
+
from meltygui.code.melty_scan import Import
|
|
37
|
+
from meltygui.code.melty_scan import ImportFrom
|
|
38
|
+
from meltygui.code.melty_scan import ScanError
|
|
39
|
+
from meltygui.code.melty_scan import alias
|
|
40
|
+
from meltygui.code.melty_scan import scan_imports
|
|
41
|
+
from meltygui.code.melty_scan import _k
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ImportGraph:
|
|
45
|
+
"""`imports[file]` = files it imports, `imported_by[file]` = files that
|
|
46
|
+
import it (both `set[Path]`, keys = resolved absolute paths).
|
|
47
|
+
`errors` = files the scanner refused (path → message)."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, root):
|
|
50
|
+
self.root = Path(root).resolve()
|
|
51
|
+
self.imports: dict[Path, set[Path]] = {}
|
|
52
|
+
self.imported_by: dict[Path, set[Path]] = {}
|
|
53
|
+
self.errors: dict[Path, str] = {}
|
|
54
|
+
self.built_at = 0.0
|
|
55
|
+
self.seconds = 0.0
|
|
56
|
+
self.layout: dict[Path, tuple[float, float]] = {} # node → (x, y) in [-1, 1]
|
|
57
|
+
self.columns = 0 # layered layout depth (layout_graph)
|
|
58
|
+
self.layers: list[list[Path]] = [] # column → nodes in vertical order
|
|
59
|
+
|
|
60
|
+
def add(self, importer, imported):
|
|
61
|
+
if importer == imported:
|
|
62
|
+
return
|
|
63
|
+
self.imports.setdefault(importer, set()).add(imported)
|
|
64
|
+
self.imported_by.setdefault(imported, set()).add(importer)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def file_count(self):
|
|
68
|
+
return len(self.imports)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def edge_count(self):
|
|
72
|
+
return sum(len(v) for v in self.imports.values())
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def max_importers(self):
|
|
76
|
+
"""The largest importer count of any file (0 for an empty graph) —
|
|
77
|
+
the normalizer for usage-driven visuals."""
|
|
78
|
+
if not self.imported_by:
|
|
79
|
+
return 0
|
|
80
|
+
return max(len(v) for v in self.imported_by.values())
|
|
81
|
+
|
|
82
|
+
def usage(self, path):
|
|
83
|
+
"""Importer count of `path` on a 0..1 log scale against the graph's
|
|
84
|
+
most-used file — a few importers already lift a file noticeably,
|
|
85
|
+
the long tail of heavily used ones spreads over the top half."""
|
|
86
|
+
top = self.max_importers
|
|
87
|
+
if top <= 0:
|
|
88
|
+
return 0.0
|
|
89
|
+
n = len(self.importers_of(path))
|
|
90
|
+
return math.log1p(n) / math.log1p(top) if n else 0.0
|
|
91
|
+
|
|
92
|
+
def imports_of(self, path):
|
|
93
|
+
return self.imports.get(Path(path), set())
|
|
94
|
+
|
|
95
|
+
def importers_of(self, path):
|
|
96
|
+
return self.imported_by.get(Path(path), set())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def project_python_files(root):
|
|
100
|
+
"""Every `.py` under `root`, skipping the trigram index's skip set, venvs,
|
|
101
|
+
dot-directories and __pycache__ — the same pruning the studio's other
|
|
102
|
+
project walks use."""
|
|
103
|
+
root = Path(root)
|
|
104
|
+
out = []
|
|
105
|
+
stack = [root]
|
|
106
|
+
while stack:
|
|
107
|
+
folder = stack.pop()
|
|
108
|
+
try:
|
|
109
|
+
entries = sorted(folder.iterdir(), key=lambda p: p.name)
|
|
110
|
+
except OSError:
|
|
111
|
+
continue
|
|
112
|
+
for p in entries:
|
|
113
|
+
name = p.name
|
|
114
|
+
if p.is_dir():
|
|
115
|
+
if name in _SKIP_DIRS or name.startswith(".") or "venv" in name:
|
|
116
|
+
continue
|
|
117
|
+
stack.append(p)
|
|
118
|
+
elif p.suffix == ".py":
|
|
119
|
+
out.append(p)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# How many ancestors of the tree root absolute imports may resolve against
|
|
124
|
+
# (root first, then its parent, ...) - the repo root sits two above `src/`.
|
|
125
|
+
IMPORT_ROOT_DEPTH = 3
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def import_roots(root):
|
|
129
|
+
root = Path(root).resolve()
|
|
130
|
+
roots = [root]
|
|
131
|
+
for _ in range(IMPORT_ROOT_DEPTH - 1):
|
|
132
|
+
if roots[-1].parent == roots[-1]:
|
|
133
|
+
break
|
|
134
|
+
roots.append(roots[-1].parent)
|
|
135
|
+
return roots
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _module_file_under(base, parts):
|
|
139
|
+
"""The file a dotted module `parts` names under `base`, or None."""
|
|
140
|
+
if not parts:
|
|
141
|
+
return None
|
|
142
|
+
target = base.joinpath(*parts)
|
|
143
|
+
candidate = target.with_suffix(".py")
|
|
144
|
+
if candidate.is_file():
|
|
145
|
+
return candidate
|
|
146
|
+
candidate = target / "__init__.py"
|
|
147
|
+
if candidate.is_file():
|
|
148
|
+
return candidate
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _module_file(root, parts, roots=None, memo=None):
|
|
153
|
+
"""`parts` resolved against the import roots in order (first hit wins,
|
|
154
|
+
like sys.path); only a file inside the tree `root` counts. `memo`
|
|
155
|
+
(one dict per build) caches the answer per (search bases, parts): the
|
|
156
|
+
same module is imported from dozens of files, and every probe is a
|
|
157
|
+
stat — a warm build was 290 ms of stats without it."""
|
|
158
|
+
root = Path(root)
|
|
159
|
+
search = tuple(roots or import_roots(root))
|
|
160
|
+
key = (search, tuple(parts))
|
|
161
|
+
if memo is not None and key in memo:
|
|
162
|
+
return memo[key]
|
|
163
|
+
found = None
|
|
164
|
+
for base in search:
|
|
165
|
+
f = _module_file_under(base, parts)
|
|
166
|
+
if f is not None:
|
|
167
|
+
found = f if (f == root or root in f.parents) else None
|
|
168
|
+
break
|
|
169
|
+
if memo is not None:
|
|
170
|
+
memo[key] = found
|
|
171
|
+
return found
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def resolve_import(root, importer, node, roots=None, memo=None):
|
|
175
|
+
"""The project files one Import / ImportFrom node of `importer` refers
|
|
176
|
+
to (a list; empty when nothing under `root` matches)."""
|
|
177
|
+
root = Path(root).resolve()
|
|
178
|
+
roots = roots or import_roots(root)
|
|
179
|
+
found = []
|
|
180
|
+
if _k(node) == "Import":
|
|
181
|
+
for a in node.names:
|
|
182
|
+
f = _module_file(root, a.name.split("."), roots, memo)
|
|
183
|
+
if f is not None:
|
|
184
|
+
found.append(f)
|
|
185
|
+
return found
|
|
186
|
+
# ImportFrom: the base package is the importer's folder `level` dots up
|
|
187
|
+
# (level 1 is its own folder) for a relative import, the root otherwise.
|
|
188
|
+
if node.level:
|
|
189
|
+
# Relative: anchored on the importer's folder, no root search.
|
|
190
|
+
base = importer.parent
|
|
191
|
+
for _ in range(node.level - 1):
|
|
192
|
+
base = base.parent
|
|
193
|
+
module_parts = node.module.split(".") if node.module else []
|
|
194
|
+
search = [base]
|
|
195
|
+
else:
|
|
196
|
+
module_parts = node.module.split(".")
|
|
197
|
+
search = roots
|
|
198
|
+
module_file = _module_file(root, module_parts, search, memo) if module_parts else None
|
|
199
|
+
if module_file is not None:
|
|
200
|
+
found.append(module_file)
|
|
201
|
+
for a in node.names:
|
|
202
|
+
if a.name == "*":
|
|
203
|
+
continue
|
|
204
|
+
f = _module_file(root, module_parts + [a.name], search, memo)
|
|
205
|
+
if f is not None:
|
|
206
|
+
found.append(f)
|
|
207
|
+
return found
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── per-file import cache: (mtime, size) + the file's import records ─────────
|
|
211
|
+
# Invalidation is content-free (stat only - never a digest); a hit costs one
|
|
212
|
+
# stat, a miss one tokenize pass (scan_imports). Persisted in ~/.lsd so the
|
|
213
|
+
# first build of a session is as fast as a rebuild. Records are plain tuples:
|
|
214
|
+
# ("Import", None, 0, ((name, asname), ...)) / ("ImportFrom", module, level, names)
|
|
215
|
+
|
|
216
|
+
CACHE_VERSION = 1
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def cache_path():
|
|
220
|
+
return Path.home() / ".lsd" / "import_graph_cache.pkl"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load_cache(path=None):
|
|
224
|
+
path = path or cache_path()
|
|
225
|
+
try:
|
|
226
|
+
with open(path, "rb") as f:
|
|
227
|
+
data = pickle.load(f)
|
|
228
|
+
if isinstance(data, dict) and data.get("version") == CACHE_VERSION:
|
|
229
|
+
return data["files"]
|
|
230
|
+
except (OSError, pickle.PickleError, EOFError, KeyError, TypeError, ValueError):
|
|
231
|
+
pass
|
|
232
|
+
return {}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def save_cache(files, path=None):
|
|
236
|
+
path = path or cache_path()
|
|
237
|
+
try:
|
|
238
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
tmp = path.with_suffix(".tmp")
|
|
240
|
+
with open(tmp, "wb") as f:
|
|
241
|
+
pickle.dump({"version": CACHE_VERSION, "files": files}, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
242
|
+
os.replace(tmp, path)
|
|
243
|
+
except OSError:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _records(nodes):
|
|
248
|
+
return tuple((_k(n), getattr(n, "module", None), getattr(n, "level", 0),
|
|
249
|
+
tuple((a.name, a.asname) for a in n.names)) for n in nodes)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _nodes(records):
|
|
253
|
+
out = []
|
|
254
|
+
for kind, module, level, names in records:
|
|
255
|
+
aliases = [alias(name, asname) for name, asname in names]
|
|
256
|
+
out.append(Import(aliases) if kind == "Import" else ImportFrom(module, aliases, level))
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_import_graph(root, files=None, read_text=None, cache=None, persist=True):
|
|
261
|
+
"""Scan every project file (or `files`) and resolve its imports. Pure:
|
|
262
|
+
no Melty state, safe on a background thread. `read_text(path)` lets the
|
|
263
|
+
caller serve pending (unsaved) text (a file so served bypasses the
|
|
264
|
+
cache); `cache` = a files dict (load_cache) — None loads the persisted
|
|
265
|
+
one; `persist` writes it back when anything was rescanned."""
|
|
266
|
+
root = Path(root).resolve()
|
|
267
|
+
started = time.monotonic()
|
|
268
|
+
graph = ImportGraph(root)
|
|
269
|
+
roots = import_roots(root)
|
|
270
|
+
if files is None:
|
|
271
|
+
files = project_python_files(root)
|
|
272
|
+
if cache is None:
|
|
273
|
+
cache = load_cache() if persist else {}
|
|
274
|
+
dirty = False
|
|
275
|
+
memo = {}
|
|
276
|
+
for path in files:
|
|
277
|
+
path = Path(path)
|
|
278
|
+
graph.imports.setdefault(path, set())
|
|
279
|
+
key = str(path)
|
|
280
|
+
try:
|
|
281
|
+
if read_text is None:
|
|
282
|
+
stat = path.stat()
|
|
283
|
+
stamp = (stat.st_mtime_ns, stat.st_size)
|
|
284
|
+
hit = cache.get(key)
|
|
285
|
+
if hit is not None and hit[0] == stamp:
|
|
286
|
+
records = hit[1]
|
|
287
|
+
else:
|
|
288
|
+
# A scan failure is cached too (with its message), else an
|
|
289
|
+
# unparsable file would be rescanned on every build.
|
|
290
|
+
try:
|
|
291
|
+
records = _records(scan_imports(path.read_text(encoding="utf-8", errors="replace")))
|
|
292
|
+
except ScanError as e:
|
|
293
|
+
records = str(e)
|
|
294
|
+
cache[key] = (stamp, records)
|
|
295
|
+
dirty = True
|
|
296
|
+
else:
|
|
297
|
+
records = _records(scan_imports(read_text(path)))
|
|
298
|
+
except ScanError as e:
|
|
299
|
+
graph.errors[path] = str(e)
|
|
300
|
+
continue
|
|
301
|
+
except OSError as e:
|
|
302
|
+
graph.errors[path] = str(e)
|
|
303
|
+
continue
|
|
304
|
+
if isinstance(records, str):
|
|
305
|
+
graph.errors[path] = records
|
|
306
|
+
continue
|
|
307
|
+
for node in _nodes(records):
|
|
308
|
+
for target in resolve_import(root, path, node, roots, memo):
|
|
309
|
+
graph.add(path, target)
|
|
310
|
+
if dirty and persist:
|
|
311
|
+
save_cache(cache)
|
|
312
|
+
graph.seconds = time.monotonic() - started
|
|
313
|
+
graph.built_at = time.time()
|
|
314
|
+
return graph
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ── layout: layered, left to right ───────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
def layout_graph(graph, sweeps=8, seed=0):
|
|
320
|
+
"""Stamp `graph.layout` — one (x, y) in [-1, 1] per file — as a LAYERED
|
|
321
|
+
left-to-right drawing (the Sugiyama idea, kept simple):
|
|
322
|
+
|
|
323
|
+
column 0 = the roots: files nothing imports (isolated files too)
|
|
324
|
+
column c = one past the deepest column among the file's importers,
|
|
325
|
+
so `toggles.py` — imported from everywhere — lands at the
|
|
326
|
+
far right and every import line runs left → right.
|
|
327
|
+
|
|
328
|
+
Cycles (the repo has them) are broken by usage rank — an edge from a
|
|
329
|
+
more-used file to a less-used one is ignored for the column assignment
|
|
330
|
+
only — so a hub can never be pulled left by a cycle. Within a column, nodes are ordered
|
|
331
|
+
by `sweeps` barycenter passes over their neighbours' rows (down then up),
|
|
332
|
+
which keeps lines short and mostly uncrossed; rows share one pitch, so a
|
|
333
|
+
tall column fills the height and a short one sits centred. Deterministic."""
|
|
334
|
+
nodes = sorted(set(graph.imports) | set(graph.imported_by))
|
|
335
|
+
n = len(nodes)
|
|
336
|
+
if n == 0:
|
|
337
|
+
graph.layout = {}
|
|
338
|
+
return graph.layout
|
|
339
|
+
index = {p: i for i, p in enumerate(nodes)}
|
|
340
|
+
# edges importer → imported (order grows along an import)
|
|
341
|
+
out = [[] for _ in range(n)]
|
|
342
|
+
for a, targets in graph.imports.items():
|
|
343
|
+
for b in targets:
|
|
344
|
+
if a in index and b in index and a != b:
|
|
345
|
+
out[index[a]].append(index[b])
|
|
346
|
+
for lst in out:
|
|
347
|
+
lst.sort()
|
|
348
|
+
|
|
349
|
+
# ── cycle breaking: rank files by USAGE (importer count, ties → the file
|
|
350
|
+
# with more imports first) and keep, for the column assignment only,
|
|
351
|
+
# the edges that run from a less-used file to a more-used one. That is a
|
|
352
|
+
# DAG by construction, and it decides the cycles the way the picture
|
|
353
|
+
# should read: the hubs everything imports can only ever move RIGHT.
|
|
354
|
+
rank = sorted(range(n), key=lambda v: (len(graph.importers_of(nodes[v])),
|
|
355
|
+
-len(graph.imports_of(nodes[v])), nodes[v].name))
|
|
356
|
+
position = {v: i for i, v in enumerate(rank)}
|
|
357
|
+
dag_out = [[w for w in out[v] if position[w] > position[v]] for v in range(n)]
|
|
358
|
+
indeg = [0] * n
|
|
359
|
+
for v in range(n):
|
|
360
|
+
for w in dag_out[v]:
|
|
361
|
+
indeg[w] += 1
|
|
362
|
+
|
|
363
|
+
# ── columns: longest path from the roots (Kahn order) ──
|
|
364
|
+
column = [0] * n
|
|
365
|
+
ready = [v for v in range(n) if indeg[v] == 0]
|
|
366
|
+
order = []
|
|
367
|
+
while ready:
|
|
368
|
+
v = ready.pop(0)
|
|
369
|
+
order.append(v)
|
|
370
|
+
for w in dag_out[v]:
|
|
371
|
+
column[w] = max(column[w], column[v] + 1)
|
|
372
|
+
indeg[w] -= 1
|
|
373
|
+
if indeg[w] == 0:
|
|
374
|
+
ready.append(w)
|
|
375
|
+
columns = max(column) + 1
|
|
376
|
+
layers = [[] for _ in range(columns)]
|
|
377
|
+
for v in range(n):
|
|
378
|
+
layers[column[v]].append(v)
|
|
379
|
+
|
|
380
|
+
# ── rows: barycenter sweeps over ALL neighbours (both edge directions) ──
|
|
381
|
+
neighbours = [set() for _ in range(n)]
|
|
382
|
+
for v in range(n):
|
|
383
|
+
for w in out[v]:
|
|
384
|
+
neighbours[v].add(w)
|
|
385
|
+
neighbours[w].add(v)
|
|
386
|
+
row = [0.0] * n
|
|
387
|
+
for layer in layers:
|
|
388
|
+
for i, v in enumerate(layer):
|
|
389
|
+
row[v] = float(i)
|
|
390
|
+
|
|
391
|
+
def sweep(layer_indices):
|
|
392
|
+
for c in layer_indices:
|
|
393
|
+
layer = layers[c]
|
|
394
|
+
keyed = []
|
|
395
|
+
for v in layer:
|
|
396
|
+
adjacent = [row[w] for w in neighbours[v] if column[w] != c]
|
|
397
|
+
bary = sum(adjacent) / len(adjacent) if adjacent else row[v]
|
|
398
|
+
keyed.append((bary, nodes[v].name, v))
|
|
399
|
+
keyed.sort()
|
|
400
|
+
layers[c] = [v for _b, _name, v in keyed]
|
|
401
|
+
for i, v in enumerate(layers[c]):
|
|
402
|
+
row[v] = float(i)
|
|
403
|
+
|
|
404
|
+
for _ in range(sweeps):
|
|
405
|
+
sweep(range(1, columns))
|
|
406
|
+
sweep(range(columns - 2, -1, -1))
|
|
407
|
+
|
|
408
|
+
# ── normalize: x by column, y by row with one shared pitch ──
|
|
409
|
+
tallest = max(len(layer) for layer in layers)
|
|
410
|
+
pitch = 2.0 / max(1, tallest)
|
|
411
|
+
graph.layout = {}
|
|
412
|
+
for c, layer in enumerate(layers):
|
|
413
|
+
x = -1.0 + 2.0 * c / max(1, columns - 1) if columns > 1 else 0.0
|
|
414
|
+
offset = (len(layer) - 1) * 0.5
|
|
415
|
+
for i, v in enumerate(layer):
|
|
416
|
+
graph.layout[nodes[v]] = (float(x), float((i - offset) * pitch))
|
|
417
|
+
graph.columns = columns
|
|
418
|
+
# The column/row ORDER itself, for views that draw draw in pixels
|
|
419
|
+
# (import_graph_view lays labelled boxes out from it, rather of the
|
|
420
|
+
# normalized coordinates above).
|
|
421
|
+
graph.layers = [[nodes[v] for v in layer] for layer in layers]
|
|
422
|
+
return graph.layout
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ── the shared current graph: built by either window, read by both ─────────────
|
|
426
|
+
# Hotswap-guarded module state (the file re-execs on hotswap; the graph
|
|
427
|
+
# survives). Data, not UI state - each window's own injected state keeps
|
|
428
|
+
# its selection / camera.
|
|
429
|
+
_CURRENT = globals().get("_CURRENT", {"graph": None})
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def current():
|
|
433
|
+
"""The last graph built in this session (any window), or None."""
|
|
434
|
+
return _CURRENT["graph"]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def set_current(graph):
|
|
438
|
+
_CURRENT["graph"] = graph
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class ImportGraphBuild:
|
|
442
|
+
"""One background build: `running` while the thread works, then
|
|
443
|
+
`result` (an ImportGraph) or `error`."""
|
|
444
|
+
|
|
445
|
+
def __init__(self, root):
|
|
446
|
+
self.root = Path(root)
|
|
447
|
+
self.running = True
|
|
448
|
+
self.result = None
|
|
449
|
+
self.error = None
|
|
450
|
+
self._thread = threading.Thread(target=self._run, name="import-graph",
|
|
451
|
+
daemon=True)
|
|
452
|
+
self._thread.start()
|
|
453
|
+
|
|
454
|
+
def _run(self):
|
|
455
|
+
try:
|
|
456
|
+
graph = build_import_graph(self.root)
|
|
457
|
+
layout_graph(graph)
|
|
458
|
+
set_current(graph)
|
|
459
|
+
self.result = graph
|
|
460
|
+
except Exception as e: # surfaced on the UI's status line
|
|
461
|
+
self.error = f"{type(e).__name__}: {e}"
|
|
462
|
+
finally:
|
|
463
|
+
self.running = False
|
|
464
|
+
request_render()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def start_build(root):
|
|
468
|
+
return ImportGraphBuild(root)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Layout model functions and supporting definitions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Columns(dict):
|
|
6
|
+
"""Marker dict: values render side by side (draw_columns is its default
|
|
7
|
+
renderer), so column layouts nest by data:
|
|
8
|
+
Columns({"a": ..., "b": Columns({...})})."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Rows(dict):
|
|
12
|
+
"""Marker dict: values render stacked top to bottom (draw_rows is its
|
|
13
|
+
default renderer) with draggable edges between them — the row-axis twin
|
|
14
|
+
of Columns, and the two nest freely by data:
|
|
15
|
+
Rows({"top": Columns({...}), "bottom": ...})."""
|