collagraph 0.9.2__tar.gz → 0.9.4__tar.gz
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.
- {collagraph-0.9.2 → collagraph-0.9.4}/.github/workflows/ci.yml +17 -1
- {collagraph-0.9.2 → collagraph-0.9.4}/.gitignore +1 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/PKG-INFO +1 -1
- collagraph-0.9.4/collagraph/__pyinstaller/hook-collagraph.py +126 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/fragment.py +121 -38
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/treewidgetitem.py +49 -3
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/sfc/importer.py +5 -2
- collagraph-0.9.4/examples/pyside/tree_dnd_example.cgx +290 -0
- collagraph-0.9.4/examples/pyside/tree_dnd_item.cgx +28 -0
- collagraph-0.9.4/examples/pyside/tree_dnd_model.py +232 -0
- collagraph-0.9.4/examples/pyside/tree_dnd_widget.py +77 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/pyproject.toml +2 -1
- collagraph-0.9.4/tests/conftest.py +169 -0
- collagraph-0.9.4/tests/pyinstaller/expected_output.json +28 -0
- collagraph-0.9.4/tests/pyinstaller/test_pyinstaller.sh +38 -0
- collagraph-0.9.4/tests/pyinstaller/todo_app/app.py +14 -0
- collagraph-0.9.4/tests/pyinstaller/todo_app/helpers.py +3 -0
- collagraph-0.9.4/tests/pyinstaller/todo_app/todo_item.cgx +11 -0
- collagraph-0.9.4/tests/pyinstaller/todo_app/todo_list.cgx +23 -0
- collagraph-0.9.4/tests/pyside/test_tree_dnd_example.py +621 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_for_keyed.py +19 -119
- collagraph-0.9.4/tests/test_directive_for_keyed_multi_root.py +249 -0
- collagraph-0.9.4/tests/test_directive_for_keyed_nested.py +197 -0
- collagraph-0.9.4/tests/test_directive_for_keyed_reverse.py +377 -0
- collagraph-0.9.4/tests/test_tree_dnd_model.py +367 -0
- collagraph-0.9.2/collagraph/__pyinstaller/hook-collagraph.py +0 -58
- collagraph-0.9.2/tests/conftest.py +0 -47
- {collagraph-0.9.2 → collagraph-0.9.4}/.coveragerc +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/.pre-commit-config.yaml +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/LICENSE +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/README.md +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/__main__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/__pyinstaller/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/collagraph.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/component.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/constants.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/hot_reload.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/dict_renderer.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pygfx_renderer.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/combobox.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/dialogbuttonbox.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/dockwidget.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/itemmodel.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/layouts.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/listview.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/menu.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/menubar.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/qobject.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/scrollarea.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/splitter.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/standarditem.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/statusbar.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/tab.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/toolbar.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/treewidget.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/widget.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside/objects/window.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/renderers/pyside_renderer.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/sfc/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/sfc/compiler.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/sfc/parser.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/collagraph/weak.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/README.md +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/app_point_cloud.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/button.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/combined-example.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/component-example.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/numberpad.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/point.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/point_cloud.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/pygfx-component.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pygfx/render_widget.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/app.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/big_list.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/cgx_component_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/combobox_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/counter.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/debug_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/dialog_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/dialog_example_window.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/flow_layout.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/item.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/layout_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/list_and_form.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/lists_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/slider_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tabs_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/template_refs.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/todo_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/todo_list.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tree_view_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tree_view_item.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tree_view_model.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tree_widget_example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/tree_widget_item.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/examples/pyside/treewidget_selection_demo.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/__init__.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/component/events_child.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/component/events_parent.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/component/provide_child.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/component/provide_parent.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/component/provide_root.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/example.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/file_dunder.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/multiple_classes_right_order.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/multiple_classes_wrong_order.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/multiple_root_elements.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/no_component_class.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/pyside/app.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/pyside/test_pyside_update.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/shadow_imports.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/simple.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/dynamic_component.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/dynamic_for.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/dynamic_for_template.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/dynamic_if.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/dynamic_if_template.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/node.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/simple_tree.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/slots_implicit_default_slot.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/template.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/template_empty.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/template_implicit_default_slot.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/template_implicit_default_slot_name.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/template_partial.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/tree.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/slots/widget_with_slots.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/data/text/expressions.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/conftest.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/lists.cgx +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_ci.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_elements.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_layouts.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_renderer.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_resilience.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_sfc.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_pyside_trigger_signals.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_treewidget_selection.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_update.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/pyside/test_widget.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_anchors.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_attributes_dynamic.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_attributes_static.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_collagraph.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_component.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_component_events.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_component_life_cycle.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_component_provide_inject.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_else.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_else_if.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_for.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_if.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_directive_if_no_unnecessary_remount.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_events.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_hot_reload.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_hot_reload_preservation.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_load_string.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_parser.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_pygfx_renderer.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_resolve_names.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_sfc_comments.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_sfc_compilation.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_sfc_import.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_sfc_linting_support.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_sfc_shadowing.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_slots.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_slots_dynamic.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_tag_component.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_tag_dynamic.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_tag_static.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_tag_template.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_template_refs.py +0 -0
- {collagraph-0.9.2 → collagraph-0.9.4}/tests/test_text_expressions.py +0 -0
|
@@ -65,6 +65,22 @@ jobs:
|
|
|
65
65
|
env:
|
|
66
66
|
QT_QPA_PLATFORM: offscreen
|
|
67
67
|
|
|
68
|
+
pyinstaller:
|
|
69
|
+
name: Test PyInstaller hooks
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v5
|
|
73
|
+
- name: Install uv
|
|
74
|
+
uses: astral-sh/setup-uv@v6
|
|
75
|
+
- name: Set up Python
|
|
76
|
+
uses: actions/setup-python@v5
|
|
77
|
+
with:
|
|
78
|
+
python-version-file: "pyproject.toml"
|
|
79
|
+
- name: Install dependencies
|
|
80
|
+
run: uv sync
|
|
81
|
+
- name: Test PyInstaller hooks
|
|
82
|
+
run: bash tests/pyinstaller/test_pyinstaller.sh
|
|
83
|
+
|
|
68
84
|
build:
|
|
69
85
|
name: Build and test wheel
|
|
70
86
|
runs-on: ubuntu-latest
|
|
@@ -91,7 +107,7 @@ jobs:
|
|
|
91
107
|
publish:
|
|
92
108
|
name: Publish to Github and Pypi
|
|
93
109
|
runs-on: ubuntu-latest
|
|
94
|
-
needs: [lint, test, build]
|
|
110
|
+
needs: [lint, test, build, pyinstaller]
|
|
95
111
|
if: success() && startsWith(github.ref, 'refs/tags/v')
|
|
96
112
|
environment:
|
|
97
113
|
name: pypi
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import atexit
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from PyInstaller.lib.modulegraph.modulegraph import DependencyInfo, MissingModule
|
|
6
|
+
|
|
7
|
+
from collagraph.sfc.compiler import construct_ast, get_script_ast
|
|
8
|
+
from collagraph.sfc.parser import CGXParser
|
|
9
|
+
|
|
10
|
+
# Keep track of generated files so they can be cleaned up
|
|
11
|
+
_generated_files: list[Path] = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cleanup_generated_files():
|
|
15
|
+
for path in _generated_files:
|
|
16
|
+
path.unlink(missing_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
atexit.register(_cleanup_generated_files)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def hook(hook_api):
|
|
23
|
+
collagraph_uses = hook_api.analysis.graph.get_code_using("collagraph")
|
|
24
|
+
|
|
25
|
+
hidden_imports = set()
|
|
26
|
+
cgx_modules = set()
|
|
27
|
+
|
|
28
|
+
for package, code in collagraph_uses.items():
|
|
29
|
+
source_dir = Path(code.co_filename).parent
|
|
30
|
+
|
|
31
|
+
# Collect CGX files and their imports from the source directory
|
|
32
|
+
cgx_files = list(source_dir.glob("**/*.cgx"))
|
|
33
|
+
hidden_imports |= collect_hidden_imports(cgx_files)
|
|
34
|
+
|
|
35
|
+
# Pre-compile each CGX file to Python source so that
|
|
36
|
+
# PyInstaller can bundle them as regular Python modules,
|
|
37
|
+
# removing the need to bundle .cgx files and compile them
|
|
38
|
+
# at runtime
|
|
39
|
+
for cgx_path in cgx_files:
|
|
40
|
+
module_name = cgx_path.stem
|
|
41
|
+
tree, _name = construct_ast(cgx_path)
|
|
42
|
+
python_source = ast.unparse(tree)
|
|
43
|
+
|
|
44
|
+
# Write compiled module next to the CGX file so
|
|
45
|
+
# PyInstaller can discover it on its existing search path
|
|
46
|
+
py_path = cgx_path.with_suffix(".py")
|
|
47
|
+
if not py_path.exists():
|
|
48
|
+
py_path.write_text(python_source, encoding="utf-8")
|
|
49
|
+
_generated_files.append(py_path)
|
|
50
|
+
cgx_modules.add(module_name)
|
|
51
|
+
|
|
52
|
+
# Filter out imports that correspond to CGX modules, since
|
|
53
|
+
# they are now available as pre-compiled Python modules
|
|
54
|
+
hidden_imports -= cgx_modules
|
|
55
|
+
|
|
56
|
+
graph = hook_api.analysis.graph
|
|
57
|
+
|
|
58
|
+
# Remove any MissingModule nodes for CGX modules that were
|
|
59
|
+
# already marked as missing during initial analysis (before
|
|
60
|
+
# the compiled .py files existed).
|
|
61
|
+
# removeNode() calls hide_node() which moves the node to
|
|
62
|
+
# hidden_nodes. Graph.add_node() silently ignores nodes in
|
|
63
|
+
# hidden_nodes, so we must also delete from hidden_nodes to
|
|
64
|
+
# allow import_hook to re-add the module as a SourceModule.
|
|
65
|
+
for name in cgx_modules:
|
|
66
|
+
node = graph.find_node(name)
|
|
67
|
+
if isinstance(node, MissingModule):
|
|
68
|
+
graph.removeNode(node)
|
|
69
|
+
if name in graph.graph.hidden_nodes:
|
|
70
|
+
del graph.graph.hidden_nodes[name]
|
|
71
|
+
|
|
72
|
+
# Import CGX modules via the module graph and create edges
|
|
73
|
+
# to the collagraph node so they are included in the bundle.
|
|
74
|
+
collagraph_node = graph.find_node("collagraph")
|
|
75
|
+
for name in cgx_modules:
|
|
76
|
+
try:
|
|
77
|
+
nodes = graph.import_hook(name, collagraph_node)
|
|
78
|
+
for node in nodes:
|
|
79
|
+
graph._updateReference(
|
|
80
|
+
collagraph_node,
|
|
81
|
+
node,
|
|
82
|
+
edge_data=DependencyInfo(
|
|
83
|
+
conditional=False,
|
|
84
|
+
fromlist=False,
|
|
85
|
+
function=False,
|
|
86
|
+
tryexcept=False,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
except ImportError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# Add remaining hidden imports (non-CGX Python modules)
|
|
93
|
+
hook_api.add_imports(*hidden_imports)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def collect_hidden_imports(cgx_files):
|
|
97
|
+
hidden_imports = set()
|
|
98
|
+
for path in cgx_files:
|
|
99
|
+
template = path.read_text(encoding="utf-8")
|
|
100
|
+
# Parse the file component into a tree of Node instances
|
|
101
|
+
parser = CGXParser()
|
|
102
|
+
parser.feed(template)
|
|
103
|
+
|
|
104
|
+
# Get the AST from the script tag
|
|
105
|
+
script_tree = get_script_ast(parser, path)
|
|
106
|
+
|
|
107
|
+
# Find a list of imported module names
|
|
108
|
+
imported_names = ImportsCollector()
|
|
109
|
+
imported_names.visit(script_tree)
|
|
110
|
+
|
|
111
|
+
hidden_imports |= imported_names.names
|
|
112
|
+
|
|
113
|
+
return hidden_imports
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ImportsCollector(ast.NodeVisitor):
|
|
117
|
+
def __init__(self):
|
|
118
|
+
self.names = set()
|
|
119
|
+
|
|
120
|
+
def visit_ImportFrom(self, node):
|
|
121
|
+
if node.module:
|
|
122
|
+
self.names.add(node.module)
|
|
123
|
+
|
|
124
|
+
def visit_Import(self, node):
|
|
125
|
+
for alias in node.names:
|
|
126
|
+
self.names.add(alias.name)
|
|
@@ -581,11 +581,20 @@ class ListFragment(Fragment):
|
|
|
581
581
|
|
|
582
582
|
# Keys that are no longer present - unmount them
|
|
583
583
|
removed_keys = old_key_set - new_key_set
|
|
584
|
+
removed_fragments = set()
|
|
584
585
|
for key in removed_keys:
|
|
585
586
|
fragment = self.key_to_fragment.pop(key)
|
|
586
|
-
# Remove from children list
|
|
587
|
-
self.children.remove(fragment)
|
|
588
587
|
fragment.unmount()
|
|
588
|
+
removed_fragments.add(fragment)
|
|
589
|
+
|
|
590
|
+
# Create a filtered snapshot of the old children order
|
|
591
|
+
# (excluding removed fragments) for position comparisons
|
|
592
|
+
old_children = [f for f in self.children if f not in removed_fragments]
|
|
593
|
+
|
|
594
|
+
# Build a map from fragment to its old position
|
|
595
|
+
old_fragment_positions = {
|
|
596
|
+
frag: i for i, frag in enumerate(old_children)
|
|
597
|
+
}
|
|
589
598
|
|
|
590
599
|
# Build new children array in the correct order
|
|
591
600
|
new_children = []
|
|
@@ -605,9 +614,33 @@ class ListFragment(Fragment):
|
|
|
605
614
|
self.key_to_fragment[key] = fragment
|
|
606
615
|
new_children.append(fragment)
|
|
607
616
|
|
|
608
|
-
#
|
|
609
|
-
|
|
610
|
-
|
|
617
|
+
# Map reused fragments to their old positions, skip new ones
|
|
618
|
+
old_positions = []
|
|
619
|
+
reused_indices = []
|
|
620
|
+
for i, fragment in enumerate(new_children):
|
|
621
|
+
if fragment in old_fragment_positions:
|
|
622
|
+
old_positions.append(old_fragment_positions[fragment])
|
|
623
|
+
reused_indices.append(i)
|
|
624
|
+
|
|
625
|
+
# Find LIS - these fragments are already in correct order
|
|
626
|
+
# Example: [A, B, C] -> [C, A, B]
|
|
627
|
+
# Old positions: {A:0, B:1, C:2}
|
|
628
|
+
# Map to old positions: [2, 0, 1]
|
|
629
|
+
# LIS: [0, 1] (A, B already in order)
|
|
630
|
+
# Only move C
|
|
631
|
+
lis_indices = longest_increasing_subsequence(old_positions)
|
|
632
|
+
# Convert LIS result to set of new_children indices
|
|
633
|
+
dont_move = {reused_indices[i] for i in lis_indices}
|
|
634
|
+
|
|
635
|
+
# Reconciliation strategy: Process new_children from end to
|
|
636
|
+
# start using anchors. Three different orderings in play:
|
|
637
|
+
# - old_children: The OLD order (filtered snapshot)
|
|
638
|
+
# - new_children: The desired NEW order
|
|
639
|
+
# - DOM state: Updated incrementally as we process each fragment
|
|
640
|
+
#
|
|
641
|
+
# We process backwards (end to beginning) so that anchors
|
|
642
|
+
# (elements to the right) are already in their final
|
|
643
|
+
# positions when we use them as insertion points.
|
|
611
644
|
for i in range(len(new_children) - 1, -1, -1):
|
|
612
645
|
fragment = new_children[i]
|
|
613
646
|
|
|
@@ -625,40 +658,14 @@ class ListFragment(Fragment):
|
|
|
625
658
|
if not fragment._mounted:
|
|
626
659
|
# Mount new fragment at the correct position
|
|
627
660
|
fragment.mount(target, anchor=anchor)
|
|
628
|
-
|
|
629
|
-
# Fragment
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# Get the actual next sibling in the current DOM
|
|
633
|
-
current_next = self._get_next_sibling(
|
|
634
|
-
fragment.element, target
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
# Only move if not already in correct position
|
|
638
|
-
if current_next != anchor:
|
|
639
|
-
# Remove from current position
|
|
640
|
-
# (but keep element reference)
|
|
641
|
-
# and insert at new position
|
|
642
|
-
self.renderer.remove(fragment.element, target)
|
|
643
|
-
self.renderer.insert(
|
|
644
|
-
fragment.element, parent=target, anchor=anchor
|
|
645
|
-
)
|
|
646
|
-
|
|
647
|
-
# Update children list
|
|
648
|
-
self.children = new_children
|
|
649
|
-
|
|
650
|
-
def _get_next_sibling(element, parent):
|
|
651
|
-
"""Get the next sibling element in the parent's children list"""
|
|
652
|
-
try:
|
|
653
|
-
idx = parent.children.index(element)
|
|
654
|
-
if idx + 1 < len(parent.children):
|
|
655
|
-
return parent.children[idx + 1]
|
|
656
|
-
return None
|
|
657
|
-
except (ValueError, AttributeError):
|
|
658
|
-
return None
|
|
661
|
+
elif i not in dont_move:
|
|
662
|
+
# Fragment needs to be moved (not in LIS)
|
|
663
|
+
# Move the fragment and all its DOM children
|
|
664
|
+
move_fragment_dom(fragment, self.renderer, target, anchor)
|
|
659
665
|
|
|
660
|
-
|
|
661
|
-
|
|
666
|
+
# Update children list to match new_children
|
|
667
|
+
# This removes any unmounted fragments and ensures correct order
|
|
668
|
+
self.children = new_children
|
|
662
669
|
|
|
663
670
|
# Watch for changes
|
|
664
671
|
self._watchers["list"] = watch_effect(update_children_keyed)
|
|
@@ -1098,3 +1105,79 @@ class DynamicFragment(Fragment):
|
|
|
1098
1105
|
def _remove(self):
|
|
1099
1106
|
if self._active_fragment:
|
|
1100
1107
|
self._active_fragment._remove()
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def move_fragment_dom(
|
|
1111
|
+
fragment: Fragment, renderer: Renderer, target: Any, anchor: Any | None
|
|
1112
|
+
) -> None:
|
|
1113
|
+
"""
|
|
1114
|
+
Recursively move all DOM elements in a fragment tree to a new position.
|
|
1115
|
+
|
|
1116
|
+
For fragments with a direct element, moves that element.
|
|
1117
|
+
For fragments without (e.g. ComponentFragment), recursively moves children.
|
|
1118
|
+
"""
|
|
1119
|
+
if fragment.element:
|
|
1120
|
+
# Fragment has a single element - move it
|
|
1121
|
+
renderer.remove(fragment.element, target)
|
|
1122
|
+
renderer.insert(fragment.element, parent=target, anchor=anchor)
|
|
1123
|
+
else:
|
|
1124
|
+
# Fragment without direct element - move all child DOM elements
|
|
1125
|
+
for child in fragment.children:
|
|
1126
|
+
if child._mounted:
|
|
1127
|
+
move_fragment_dom(child, renderer, target, anchor)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def longest_increasing_subsequence(nums: list[int]) -> list[int]:
|
|
1131
|
+
"""
|
|
1132
|
+
Find the longest increasing subsequence in a list of numbers.
|
|
1133
|
+
Returns the indices of elements in the LIS.
|
|
1134
|
+
|
|
1135
|
+
Used to optimize keyed list reconciliation by finding which elements
|
|
1136
|
+
are already in the correct relative order and don't need to move.
|
|
1137
|
+
|
|
1138
|
+
Example:
|
|
1139
|
+
nums = [2, 0, 1]
|
|
1140
|
+
returns [1, 2] # indices of values 0, 1 (the LIS)
|
|
1141
|
+
"""
|
|
1142
|
+
if not nums:
|
|
1143
|
+
return []
|
|
1144
|
+
|
|
1145
|
+
n = len(nums)
|
|
1146
|
+
# tails[i] = smallest tail element for LIS of length i+1
|
|
1147
|
+
tails = []
|
|
1148
|
+
# predecessor[i] = index of previous element in LIS ending at i
|
|
1149
|
+
predecessor = [-1] * n
|
|
1150
|
+
# tail_indices[i] = index in nums of tails[i]
|
|
1151
|
+
tail_indices = []
|
|
1152
|
+
|
|
1153
|
+
for i, num in enumerate(nums):
|
|
1154
|
+
# Binary search for the position to insert num
|
|
1155
|
+
left, right = 0, len(tails)
|
|
1156
|
+
while left < right:
|
|
1157
|
+
mid = (left + right) // 2
|
|
1158
|
+
if tails[mid] < num:
|
|
1159
|
+
left = mid + 1
|
|
1160
|
+
else:
|
|
1161
|
+
right = mid
|
|
1162
|
+
|
|
1163
|
+
# Update predecessor
|
|
1164
|
+
if left > 0:
|
|
1165
|
+
predecessor[i] = tail_indices[left - 1]
|
|
1166
|
+
|
|
1167
|
+
# Update or append to tails
|
|
1168
|
+
if left < len(tails):
|
|
1169
|
+
tails[left] = num
|
|
1170
|
+
tail_indices[left] = i
|
|
1171
|
+
else:
|
|
1172
|
+
tails.append(num)
|
|
1173
|
+
tail_indices.append(i)
|
|
1174
|
+
|
|
1175
|
+
# Reconstruct the LIS indices
|
|
1176
|
+
result = []
|
|
1177
|
+
current = tail_indices[-1]
|
|
1178
|
+
while current != -1:
|
|
1179
|
+
result.append(current)
|
|
1180
|
+
current = predecessor[current]
|
|
1181
|
+
|
|
1182
|
+
result.reverse()
|
|
1183
|
+
return result
|
|
@@ -4,6 +4,34 @@ from ... import PySideRenderer
|
|
|
4
4
|
from .qobject import set_attribute as qobject_set_attribute
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def _snapshot_item_state(
|
|
8
|
+
item: QTreeWidgetItem,
|
|
9
|
+
) -> list[tuple[QTreeWidgetItem, bool, bool]]:
|
|
10
|
+
"""Record ``(isExpanded, isSelected)`` for ``item`` and all descendants.
|
|
11
|
+
|
|
12
|
+
Qt resets both bits across a ``removeChild``, so callers snapshot
|
|
13
|
+
here before removing and restore after re-inserting (the path
|
|
14
|
+
keyed-v-for reordering takes).
|
|
15
|
+
"""
|
|
16
|
+
snapshot: list[tuple[QTreeWidgetItem, bool, bool]] = []
|
|
17
|
+
|
|
18
|
+
def walk(it: QTreeWidgetItem) -> None:
|
|
19
|
+
snapshot.append((it, it.isExpanded(), it.isSelected()))
|
|
20
|
+
for i in range(it.childCount()):
|
|
21
|
+
walk(it.child(i))
|
|
22
|
+
|
|
23
|
+
walk(item)
|
|
24
|
+
return snapshot
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _restore_item_state(
|
|
28
|
+
snapshot: list[tuple[QTreeWidgetItem, bool, bool]],
|
|
29
|
+
) -> None:
|
|
30
|
+
for it, expanded, selected in snapshot:
|
|
31
|
+
it.setExpanded(expanded)
|
|
32
|
+
it.setSelected(selected)
|
|
33
|
+
|
|
34
|
+
|
|
7
35
|
@PySideRenderer.register_insert(QTreeWidgetItem)
|
|
8
36
|
def insert(self, el: QTreeWidgetItem, anchor=None):
|
|
9
37
|
if not isinstance(el, QTreeWidgetItem):
|
|
@@ -17,13 +45,26 @@ def insert(self, el: QTreeWidgetItem, anchor=None):
|
|
|
17
45
|
if anchor is not None:
|
|
18
46
|
index = self.indexOfChild(anchor)
|
|
19
47
|
if self.treeWidget():
|
|
48
|
+
# ``el`` may already be in the tree (legacy code paths
|
|
49
|
+
# call insert without removing first); snapshot so we
|
|
50
|
+
# can preserve its expansion/selection across removeChild.
|
|
51
|
+
state = _snapshot_item_state(el)
|
|
20
52
|
self.removeChild(el)
|
|
21
|
-
|
|
53
|
+
self.insertChild(index, el)
|
|
54
|
+
_restore_item_state(state)
|
|
55
|
+
else:
|
|
56
|
+
self.insertChild(index, el)
|
|
22
57
|
else:
|
|
23
58
|
self.addChild(el)
|
|
24
59
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
60
|
+
# Restore state snapshotted at remove-time (keyed reorder via a
|
|
61
|
+
# separate remove/insert pair, see remove() below).
|
|
62
|
+
if hasattr(el, "_state_snapshot"):
|
|
63
|
+
_restore_item_state(el._state_snapshot)
|
|
64
|
+
del el._state_snapshot
|
|
65
|
+
|
|
66
|
+
# After mounting, process some attributes that can only be
|
|
67
|
+
# adjusted when the item is mounted in the tree structure.
|
|
27
68
|
if hasattr(el, "_expanded"):
|
|
28
69
|
el.setExpanded(el._expanded)
|
|
29
70
|
delattr(el, "_expanded")
|
|
@@ -43,6 +84,11 @@ def remove(self, el: QTreeWidgetItem):
|
|
|
43
84
|
tree_widget.blockSignals(True)
|
|
44
85
|
|
|
45
86
|
try:
|
|
87
|
+
# Stash UI state so a paired insert() (keyed reorder) can put
|
|
88
|
+
# the subtree back the way it was. The stash is consumed by
|
|
89
|
+
# the next insert of ``el``; if the item is being removed for
|
|
90
|
+
# good the attribute is garbage-collected with it.
|
|
91
|
+
el._state_snapshot = _snapshot_item_state(el)
|
|
46
92
|
self.removeChild(el)
|
|
47
93
|
finally:
|
|
48
94
|
if tree_widget:
|
|
@@ -66,5 +66,8 @@ class CGXPathFinder(MetaPathFinder):
|
|
|
66
66
|
return spec
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
# Add cgx path finder at the end of the list of finders
|
|
70
|
-
|
|
69
|
+
# Add cgx path finder at the end of the list of finders.
|
|
70
|
+
# Skip when running as a frozen PyInstaller bundle, since CGX files
|
|
71
|
+
# are pre-compiled to Python modules by the PyInstaller hook.
|
|
72
|
+
if not getattr(sys, "frozen", False):
|
|
73
|
+
sys.meta_path.append(CGXPathFinder())
|