collagraph 0.9.2__tar.gz → 0.9.3__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.
Files changed (167) hide show
  1. {collagraph-0.9.2 → collagraph-0.9.3}/.github/workflows/ci.yml +17 -1
  2. {collagraph-0.9.2 → collagraph-0.9.3}/.gitignore +1 -0
  3. {collagraph-0.9.2 → collagraph-0.9.3}/PKG-INFO +1 -1
  4. collagraph-0.9.3/collagraph/__pyinstaller/hook-collagraph.py +126 -0
  5. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/sfc/importer.py +5 -2
  6. {collagraph-0.9.2 → collagraph-0.9.3}/pyproject.toml +2 -1
  7. collagraph-0.9.3/tests/pyinstaller/expected_output.json +28 -0
  8. collagraph-0.9.3/tests/pyinstaller/test_pyinstaller.sh +38 -0
  9. collagraph-0.9.3/tests/pyinstaller/todo_app/app.py +14 -0
  10. collagraph-0.9.3/tests/pyinstaller/todo_app/helpers.py +3 -0
  11. collagraph-0.9.3/tests/pyinstaller/todo_app/todo_item.cgx +11 -0
  12. collagraph-0.9.3/tests/pyinstaller/todo_app/todo_list.cgx +23 -0
  13. collagraph-0.9.2/collagraph/__pyinstaller/hook-collagraph.py +0 -58
  14. {collagraph-0.9.2 → collagraph-0.9.3}/.coveragerc +0 -0
  15. {collagraph-0.9.2 → collagraph-0.9.3}/.pre-commit-config.yaml +0 -0
  16. {collagraph-0.9.2 → collagraph-0.9.3}/LICENSE +0 -0
  17. {collagraph-0.9.2 → collagraph-0.9.3}/README.md +0 -0
  18. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/__init__.py +0 -0
  19. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/__main__.py +0 -0
  20. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/__pyinstaller/__init__.py +0 -0
  21. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/collagraph.py +0 -0
  22. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/component.py +0 -0
  23. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/constants.py +0 -0
  24. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/fragment.py +0 -0
  25. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/hot_reload.py +0 -0
  26. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/__init__.py +0 -0
  27. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/dict_renderer.py +0 -0
  28. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pygfx_renderer.py +0 -0
  29. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/__init__.py +0 -0
  30. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/__init__.py +0 -0
  31. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/combobox.py +0 -0
  32. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/dialogbuttonbox.py +0 -0
  33. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/dockwidget.py +0 -0
  34. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/itemmodel.py +0 -0
  35. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/layouts.py +0 -0
  36. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/listview.py +0 -0
  37. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/menu.py +0 -0
  38. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/menubar.py +0 -0
  39. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/qobject.py +0 -0
  40. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/scrollarea.py +0 -0
  41. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/splitter.py +0 -0
  42. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/standarditem.py +0 -0
  43. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/statusbar.py +0 -0
  44. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/tab.py +0 -0
  45. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/toolbar.py +0 -0
  46. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/treewidget.py +0 -0
  47. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/treewidgetitem.py +0 -0
  48. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/widget.py +0 -0
  49. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside/objects/window.py +0 -0
  50. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/renderers/pyside_renderer.py +0 -0
  51. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/sfc/__init__.py +0 -0
  52. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/sfc/compiler.py +0 -0
  53. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/sfc/parser.py +0 -0
  54. {collagraph-0.9.2 → collagraph-0.9.3}/collagraph/weak.py +0 -0
  55. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/README.md +0 -0
  56. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/app_point_cloud.cgx +0 -0
  57. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/button.cgx +0 -0
  58. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/combined-example.py +0 -0
  59. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/component-example.py +0 -0
  60. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/numberpad.cgx +0 -0
  61. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/point.cgx +0 -0
  62. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/point_cloud.cgx +0 -0
  63. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/pygfx-component.cgx +0 -0
  64. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pygfx/render_widget.cgx +0 -0
  65. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/app.py +0 -0
  66. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/big_list.cgx +0 -0
  67. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/cgx_component_example.cgx +0 -0
  68. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/combobox_example.cgx +0 -0
  69. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/counter.cgx +0 -0
  70. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/debug_example.cgx +0 -0
  71. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/dialog_example.cgx +0 -0
  72. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/dialog_example_window.cgx +0 -0
  73. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/flow_layout.py +0 -0
  74. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/item.cgx +0 -0
  75. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/layout_example.cgx +0 -0
  76. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/list_and_form.cgx +0 -0
  77. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/lists_example.cgx +0 -0
  78. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/slider_example.cgx +0 -0
  79. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tabs_example.cgx +0 -0
  80. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/template_refs.cgx +0 -0
  81. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/todo_example.cgx +0 -0
  82. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/todo_list.cgx +0 -0
  83. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tree_view_example.cgx +0 -0
  84. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tree_view_item.cgx +0 -0
  85. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tree_view_model.cgx +0 -0
  86. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tree_widget_example.cgx +0 -0
  87. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/tree_widget_item.cgx +0 -0
  88. {collagraph-0.9.2 → collagraph-0.9.3}/examples/pyside/treewidget_selection_demo.cgx +0 -0
  89. {collagraph-0.9.2 → collagraph-0.9.3}/tests/__init__.py +0 -0
  90. {collagraph-0.9.2 → collagraph-0.9.3}/tests/conftest.py +0 -0
  91. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/component/events_child.cgx +0 -0
  92. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/component/events_parent.cgx +0 -0
  93. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/component/provide_child.cgx +0 -0
  94. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/component/provide_parent.cgx +0 -0
  95. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/component/provide_root.cgx +0 -0
  96. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/example.cgx +0 -0
  97. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/file_dunder.cgx +0 -0
  98. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/multiple_classes_right_order.cgx +0 -0
  99. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/multiple_classes_wrong_order.cgx +0 -0
  100. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/multiple_root_elements.cgx +0 -0
  101. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/no_component_class.cgx +0 -0
  102. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/pyside/app.cgx +0 -0
  103. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/pyside/test_pyside_update.cgx +0 -0
  104. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/shadow_imports.cgx +0 -0
  105. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/simple.cgx +0 -0
  106. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/dynamic_component.cgx +0 -0
  107. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/dynamic_for.cgx +0 -0
  108. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/dynamic_for_template.cgx +0 -0
  109. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/dynamic_if.cgx +0 -0
  110. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/dynamic_if_template.cgx +0 -0
  111. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/node.cgx +0 -0
  112. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/simple_tree.cgx +0 -0
  113. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/slots_implicit_default_slot.cgx +0 -0
  114. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/template.cgx +0 -0
  115. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/template_empty.cgx +0 -0
  116. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/template_implicit_default_slot.cgx +0 -0
  117. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/template_implicit_default_slot_name.cgx +0 -0
  118. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/template_partial.cgx +0 -0
  119. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/tree.cgx +0 -0
  120. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/slots/widget_with_slots.cgx +0 -0
  121. {collagraph-0.9.2 → collagraph-0.9.3}/tests/data/text/expressions.cgx +0 -0
  122. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/conftest.py +0 -0
  123. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/lists.cgx +0 -0
  124. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_ci.py +0 -0
  125. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_elements.py +0 -0
  126. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_layouts.py +0 -0
  127. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_renderer.py +0 -0
  128. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_resilience.py +0 -0
  129. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_sfc.py +0 -0
  130. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_pyside_trigger_signals.py +0 -0
  131. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_treewidget_selection.py +0 -0
  132. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_update.py +0 -0
  133. {collagraph-0.9.2 → collagraph-0.9.3}/tests/pyside/test_widget.py +0 -0
  134. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_anchors.py +0 -0
  135. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_attributes_dynamic.py +0 -0
  136. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_attributes_static.py +0 -0
  137. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_collagraph.py +0 -0
  138. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_component.py +0 -0
  139. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_component_events.py +0 -0
  140. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_component_life_cycle.py +0 -0
  141. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_component_provide_inject.py +0 -0
  142. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_else.py +0 -0
  143. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_else_if.py +0 -0
  144. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_for.py +0 -0
  145. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_for_keyed.py +0 -0
  146. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_if.py +0 -0
  147. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_directive_if_no_unnecessary_remount.py +0 -0
  148. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_events.py +0 -0
  149. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_hot_reload.py +0 -0
  150. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_hot_reload_preservation.py +0 -0
  151. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_load_string.py +0 -0
  152. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_parser.py +0 -0
  153. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_pygfx_renderer.py +0 -0
  154. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_resolve_names.py +0 -0
  155. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_sfc_comments.py +0 -0
  156. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_sfc_compilation.py +0 -0
  157. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_sfc_import.py +0 -0
  158. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_sfc_linting_support.py +0 -0
  159. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_sfc_shadowing.py +0 -0
  160. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_slots.py +0 -0
  161. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_slots_dynamic.py +0 -0
  162. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_tag_component.py +0 -0
  163. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_tag_dynamic.py +0 -0
  164. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_tag_static.py +0 -0
  165. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_tag_template.py +0 -0
  166. {collagraph-0.9.2 → collagraph-0.9.3}/tests/test_template_refs.py +0 -0
  167. {collagraph-0.9.2 → collagraph-0.9.3}/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
@@ -4,5 +4,6 @@ __pycache__
4
4
  .coverage
5
5
  dist
6
6
  build
7
+ *.spec
7
8
  uv.lock
8
9
  .venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collagraph
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: Reactive user interfaces
5
5
  Author-email: Berend Klein Haneveld <berendkleinhaneveld@gmail.com>, Korijn van Golen <korijn@gmail.com>
6
6
  License-Expression: MIT
@@ -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)
@@ -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
- sys.meta_path.append(CGXPathFinder())
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())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "collagraph"
3
- version = "0.9.2"
3
+ version = "0.9.3"
4
4
  description = "Reactive user interfaces"
5
5
  authors = [
6
6
  { name = "Berend Klein Haneveld", email = "berendkleinhaneveld@gmail.com" },
@@ -59,6 +59,7 @@ pyside-dev = [
59
59
  "examples/pygfx/*.py" = ["I001"] # unsorted-imports
60
60
  "examples/pyside/flow_layout.py" = ["N802"] # invalid-function-name
61
61
  "tests/*" = ["N806", "N802"] # non-lowercase-variable-in-function, invalid-function-name
62
+ "tests/pyinstaller/*" = ["I001", "T20"] # unsorted-imports, flake8-print
62
63
  "tests/pyside/test_*.py" = ["E402"] # module-import-not-at-top-of-file
63
64
 
64
65
  [tool.ruff.lint]
@@ -0,0 +1,28 @@
1
+ {
2
+ "type": "root",
3
+ "children": [
4
+ {
5
+ "type": "list",
6
+ "children": [
7
+ {
8
+ "type": "item",
9
+ "attrs": {
10
+ "label": "[todo] Buy groceries"
11
+ }
12
+ },
13
+ {
14
+ "type": "item",
15
+ "attrs": {
16
+ "label": "[done] Write tests"
17
+ }
18
+ },
19
+ {
20
+ "type": "item",
21
+ "attrs": {
22
+ "label": "[todo] Deploy app"
23
+ }
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
6
+ APP_DIR="$SCRIPT_DIR/todo_app"
7
+ EXPECTED="$SCRIPT_DIR/expected_output.json"
8
+
9
+ echo "==> Installing pyinstaller..."
10
+ uv pip install pyinstaller
11
+
12
+ echo "==> Building todo_app with PyInstaller..."
13
+ cd "$APP_DIR"
14
+ uv run pyinstaller \
15
+ --noconfirm \
16
+ --log-level WARN \
17
+ --onefile \
18
+ --name todo_app \
19
+ app.py
20
+
21
+ echo "==> Running bundled application..."
22
+ OUTPUT=$(./dist/todo_app)
23
+
24
+ echo "==> Comparing output..."
25
+ EXPECTED_CONTENT=$(cat "$EXPECTED")
26
+
27
+ if [ "$OUTPUT" = "$EXPECTED_CONTENT" ]; then
28
+ echo "==> PyInstaller test PASSED"
29
+ else
30
+ echo "==> PyInstaller test FAILED"
31
+ echo ""
32
+ echo "Expected:"
33
+ echo "$EXPECTED_CONTENT"
34
+ echo ""
35
+ echo "Got:"
36
+ echo "$OUTPUT"
37
+ exit 1
38
+ fi
@@ -0,0 +1,14 @@
1
+ import json
2
+
3
+ import collagraph as cg
4
+
5
+ from todo_list import TodoList
6
+
7
+ container = {"type": "root"}
8
+ gui = cg.Collagraph(
9
+ renderer=cg.DictRenderer(),
10
+ event_loop_type=cg.EventLoopType.SYNC,
11
+ )
12
+ gui.render(TodoList, container)
13
+
14
+ print(json.dumps(container, indent=2))
@@ -0,0 +1,3 @@
1
+ def format_label(text, done):
2
+ status = "done" if done else "todo"
3
+ return f"[{status}] {text}"
@@ -0,0 +1,11 @@
1
+ <item :label="format_label(props['text'], props['done'])" />
2
+
3
+ <script>
4
+ import collagraph as cg
5
+ from helpers import format_label
6
+
7
+
8
+ class TodoItem(cg.Component):
9
+ pass
10
+ </script>
11
+
@@ -0,0 +1,23 @@
1
+ <list>
2
+ <TodoItem
3
+ v-for="todo in state['todos']"
4
+ :done="todo['done']"
5
+ :key="todo['id']"
6
+ :text="todo['text']"
7
+ />
8
+ </list>
9
+
10
+ <script>
11
+ import collagraph as cg
12
+ from todo_item import TodoItem
13
+
14
+
15
+ class TodoList(cg.Component):
16
+ def init(self):
17
+ self.state["todos"] = [
18
+ {"id": 1, "text": "Buy groceries", "done": False},
19
+ {"id": 2, "text": "Write tests", "done": True},
20
+ {"id": 3, "text": "Deploy app", "done": False},
21
+ ]
22
+ </script>
23
+
@@ -1,58 +0,0 @@
1
- import ast
2
- from pathlib import Path
3
-
4
- from PyInstaller.utils.hooks import collect_data_files
5
-
6
- from collagraph.sfc.compiler import get_script_ast
7
- from collagraph.sfc.parser import CGXParser
8
-
9
-
10
- def hook(hook_api):
11
- collagraph_uses = hook_api.analysis.graph.get_code_using("collagraph")
12
-
13
- hidden_imports = set()
14
- datas = []
15
- for package, code in collagraph_uses.items():
16
- filename = Path(code.co_filename)
17
- hidden_imports |= collect_hidden_imports(filename.parent)
18
- datas += collect_data_files(package, includes=["**/*.cgx"])
19
-
20
- hook_api.add_imports(*hidden_imports)
21
- hook_api.add_datas(datas)
22
-
23
-
24
- def collect_hidden_imports(folder):
25
- folder = Path(folder)
26
-
27
- hidden_imports = set()
28
- for path in folder.glob("**/*.cgx"):
29
- template = path.read_text(encoding="utf-8")
30
- # Parse the file component into a tree of Node instances
31
- parser = CGXParser()
32
- parser.feed(template)
33
-
34
- # Get the AST from the script tag
35
- script_tree = get_script_ast(parser, path)
36
-
37
- # Find a list of imported names (or aliases, if any)
38
- # Those names don't have to be wrapped by `_lookup`
39
- imported_names = ImportsCollector()
40
- imported_names.visit(script_tree)
41
-
42
- hidden_imports |= imported_names.names
43
-
44
- return hidden_imports
45
-
46
-
47
- class ImportsCollector(ast.NodeVisitor):
48
- def __init__(self):
49
- self.names = set()
50
-
51
- def visit_ImportFrom(self, node):
52
- for alias in node.names:
53
- self.names.add(".".join([node.module, alias.name]))
54
- self.names.add(node.module)
55
-
56
- def visit_Import(self, node):
57
- for alias in node.names:
58
- self.names.add(alias.name)
File without changes
File without changes
File without changes
File without changes
File without changes