collagraph 0.8.6__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 (148) hide show
  1. collagraph-0.8.6/.coveragerc +3 -0
  2. collagraph-0.8.6/.github/workflows/ci.yml +119 -0
  3. collagraph-0.8.6/.gitignore +8 -0
  4. collagraph-0.8.6/.pre-commit-config.yaml +32 -0
  5. collagraph-0.8.6/LICENSE +21 -0
  6. collagraph-0.8.6/PKG-INFO +131 -0
  7. collagraph-0.8.6/README.md +116 -0
  8. collagraph-0.8.6/collagraph/__init__.py +9 -0
  9. collagraph-0.8.6/collagraph/__main__.py +133 -0
  10. collagraph-0.8.6/collagraph/__pyinstaller/__init__.py +7 -0
  11. collagraph-0.8.6/collagraph/__pyinstaller/hook-collagraph.py +58 -0
  12. collagraph-0.8.6/collagraph/collagraph.py +48 -0
  13. collagraph-0.8.6/collagraph/component.py +172 -0
  14. collagraph-0.8.6/collagraph/constants.py +6 -0
  15. collagraph-0.8.6/collagraph/fragment.py +619 -0
  16. collagraph-0.8.6/collagraph/renderers/__init__.py +88 -0
  17. collagraph-0.8.6/collagraph/renderers/dict_renderer.py +74 -0
  18. collagraph-0.8.6/collagraph/renderers/pygfx_renderer.py +117 -0
  19. collagraph-0.8.6/collagraph/renderers/pyside/__init__.py +30 -0
  20. collagraph-0.8.6/collagraph/renderers/pyside/objects/__init__.py +21 -0
  21. collagraph-0.8.6/collagraph/renderers/pyside/objects/combobox.py +12 -0
  22. collagraph-0.8.6/collagraph/renderers/pyside/objects/dialogbuttonbox.py +32 -0
  23. collagraph-0.8.6/collagraph/renderers/pyside/objects/dockwidget.py +17 -0
  24. collagraph-0.8.6/collagraph/renderers/pyside/objects/itemmodel.py +26 -0
  25. collagraph-0.8.6/collagraph/renderers/pyside/objects/layouts.py +142 -0
  26. collagraph-0.8.6/collagraph/renderers/pyside/objects/listview.py +46 -0
  27. collagraph-0.8.6/collagraph/renderers/pyside/objects/menu.py +32 -0
  28. collagraph-0.8.6/collagraph/renderers/pyside/objects/menubar.py +27 -0
  29. collagraph-0.8.6/collagraph/renderers/pyside/objects/qobject.py +21 -0
  30. collagraph-0.8.6/collagraph/renderers/pyside/objects/scrollarea.py +15 -0
  31. collagraph-0.8.6/collagraph/renderers/pyside/objects/splitter.py +19 -0
  32. collagraph-0.8.6/collagraph/renderers/pyside/objects/standarditem.py +67 -0
  33. collagraph-0.8.6/collagraph/renderers/pyside/objects/statusbar.py +39 -0
  34. collagraph-0.8.6/collagraph/renderers/pyside/objects/tab.py +21 -0
  35. collagraph-0.8.6/collagraph/renderers/pyside/objects/toolbar.py +29 -0
  36. collagraph-0.8.6/collagraph/renderers/pyside/objects/treewidget.py +20 -0
  37. collagraph-0.8.6/collagraph/renderers/pyside/objects/treewidgetitem.py +63 -0
  38. collagraph-0.8.6/collagraph/renderers/pyside/objects/widget.py +46 -0
  39. collagraph-0.8.6/collagraph/renderers/pyside/objects/window.py +32 -0
  40. collagraph-0.8.6/collagraph/renderers/pyside_renderer.py +511 -0
  41. collagraph-0.8.6/collagraph/sfc/__init__.py +62 -0
  42. collagraph-0.8.6/collagraph/sfc/compiler.py +902 -0
  43. collagraph-0.8.6/collagraph/sfc/importer.py +54 -0
  44. collagraph-0.8.6/collagraph/sfc/parser.py +115 -0
  45. collagraph-0.8.6/collagraph/weak.py +70 -0
  46. collagraph-0.8.6/examples/pygfx/README.md +35 -0
  47. collagraph-0.8.6/examples/pygfx/app_point_cloud.cgx +30 -0
  48. collagraph-0.8.6/examples/pygfx/button.cgx +61 -0
  49. collagraph-0.8.6/examples/pygfx/combined-example.py +24 -0
  50. collagraph-0.8.6/examples/pygfx/component-example.py +32 -0
  51. collagraph-0.8.6/examples/pygfx/numberpad.cgx +25 -0
  52. collagraph-0.8.6/examples/pygfx/point.cgx +28 -0
  53. collagraph-0.8.6/examples/pygfx/point_cloud.cgx +69 -0
  54. collagraph-0.8.6/examples/pygfx/pygfx-component.cgx +33 -0
  55. collagraph-0.8.6/examples/pygfx/render_widget.cgx +45 -0
  56. collagraph-0.8.6/examples/pyside/app.py +11 -0
  57. collagraph-0.8.6/examples/pyside/big_list.cgx +178 -0
  58. collagraph-0.8.6/examples/pyside/cgx_component_example.cgx +50 -0
  59. collagraph-0.8.6/examples/pyside/combobox_example.cgx +33 -0
  60. collagraph-0.8.6/examples/pyside/counter.cgx +25 -0
  61. collagraph-0.8.6/examples/pyside/debug_example.cgx +26 -0
  62. collagraph-0.8.6/examples/pyside/dialog_example.cgx +60 -0
  63. collagraph-0.8.6/examples/pyside/dialog_example_window.cgx +64 -0
  64. collagraph-0.8.6/examples/pyside/flow_layout.py +135 -0
  65. collagraph-0.8.6/examples/pyside/item.cgx +24 -0
  66. collagraph-0.8.6/examples/pyside/layout_example.cgx +153 -0
  67. collagraph-0.8.6/examples/pyside/list_and_form.cgx +46 -0
  68. collagraph-0.8.6/examples/pyside/lists_example.cgx +91 -0
  69. collagraph-0.8.6/examples/pyside/slider_example.cgx +56 -0
  70. collagraph-0.8.6/examples/pyside/tabs_example.cgx +39 -0
  71. collagraph-0.8.6/examples/pyside/todo_example.cgx +49 -0
  72. collagraph-0.8.6/examples/pyside/todo_list.cgx +26 -0
  73. collagraph-0.8.6/examples/pyside/tree_view_example.cgx +177 -0
  74. collagraph-0.8.6/examples/pyside/tree_view_item.cgx +28 -0
  75. collagraph-0.8.6/examples/pyside/tree_view_model.cgx +35 -0
  76. collagraph-0.8.6/examples/pyside/tree_widget_example.cgx +185 -0
  77. collagraph-0.8.6/examples/pyside/tree_widget_item.cgx +32 -0
  78. collagraph-0.8.6/pyproject.toml +78 -0
  79. collagraph-0.8.6/tests/__init__.py +0 -0
  80. collagraph-0.8.6/tests/conftest.py +53 -0
  81. collagraph-0.8.6/tests/data/component/events_child.cgx +17 -0
  82. collagraph-0.8.6/tests/data/component/events_parent.cgx +25 -0
  83. collagraph-0.8.6/tests/data/component/provide_child.cgx +16 -0
  84. collagraph-0.8.6/tests/data/component/provide_parent.cgx +13 -0
  85. collagraph-0.8.6/tests/data/component/provide_root.cgx +13 -0
  86. collagraph-0.8.6/tests/data/example.cgx +9 -0
  87. collagraph-0.8.6/tests/data/file_dunder.cgx +20 -0
  88. collagraph-0.8.6/tests/data/multiple_classes_right_order.cgx +20 -0
  89. collagraph-0.8.6/tests/data/multiple_classes_wrong_order.cgx +20 -0
  90. collagraph-0.8.6/tests/data/multiple_root_elements.cgx +12 -0
  91. collagraph-0.8.6/tests/data/no_component_class.cgx +8 -0
  92. collagraph-0.8.6/tests/data/pyside/app.cgx +144 -0
  93. collagraph-0.8.6/tests/data/pyside/test_pyside_update.cgx +63 -0
  94. collagraph-0.8.6/tests/data/shadow_imports.cgx +15 -0
  95. collagraph-0.8.6/tests/data/simple.cgx +11 -0
  96. collagraph-0.8.6/tests/data/slots/dynamic_for.cgx +15 -0
  97. collagraph-0.8.6/tests/data/slots/dynamic_for_template.cgx +17 -0
  98. collagraph-0.8.6/tests/data/slots/dynamic_if.cgx +14 -0
  99. collagraph-0.8.6/tests/data/slots/dynamic_if_template.cgx +14 -0
  100. collagraph-0.8.6/tests/data/slots/node.cgx +11 -0
  101. collagraph-0.8.6/tests/data/slots/simple_tree.cgx +12 -0
  102. collagraph-0.8.6/tests/data/slots/slots_implicit_default_slot.cgx +25 -0
  103. collagraph-0.8.6/tests/data/slots/template.cgx +23 -0
  104. collagraph-0.8.6/tests/data/slots/template_empty.cgx +10 -0
  105. collagraph-0.8.6/tests/data/slots/template_implicit_default_slot.cgx +22 -0
  106. collagraph-0.8.6/tests/data/slots/template_implicit_default_slot_name.cgx +24 -0
  107. collagraph-0.8.6/tests/data/slots/template_partial.cgx +18 -0
  108. collagraph-0.8.6/tests/data/slots/tree.cgx +15 -0
  109. collagraph-0.8.6/tests/data/slots/widget_with_slots.cgx +25 -0
  110. collagraph-0.8.6/tests/data/text/expressions.cgx +16 -0
  111. collagraph-0.8.6/tests/pyside/conftest.py +42 -0
  112. collagraph-0.8.6/tests/pyside/lists.cgx +74 -0
  113. collagraph-0.8.6/tests/pyside/test_pyside_ci.py +15 -0
  114. collagraph-0.8.6/tests/pyside/test_pyside_elements.py +596 -0
  115. collagraph-0.8.6/tests/pyside/test_pyside_renderer.py +315 -0
  116. collagraph-0.8.6/tests/pyside/test_pyside_sfc.py +61 -0
  117. collagraph-0.8.6/tests/pyside/test_pyside_trigger_signals.py +205 -0
  118. collagraph-0.8.6/tests/pyside/test_update.py +48 -0
  119. collagraph-0.8.6/tests/pyside/test_widget.py +114 -0
  120. collagraph-0.8.6/tests/test_attributes_dynamic.py +374 -0
  121. collagraph-0.8.6/tests/test_attributes_static.py +83 -0
  122. collagraph-0.8.6/tests/test_collagraph.py +176 -0
  123. collagraph-0.8.6/tests/test_component.py +171 -0
  124. collagraph-0.8.6/tests/test_component_events.py +31 -0
  125. collagraph-0.8.6/tests/test_component_life_cycle.py +251 -0
  126. collagraph-0.8.6/tests/test_component_provide_inject.py +30 -0
  127. collagraph-0.8.6/tests/test_directive_else.py +86 -0
  128. collagraph-0.8.6/tests/test_directive_else_if.py +172 -0
  129. collagraph-0.8.6/tests/test_directive_for.py +700 -0
  130. collagraph-0.8.6/tests/test_directive_if.py +290 -0
  131. collagraph-0.8.6/tests/test_events.py +146 -0
  132. collagraph-0.8.6/tests/test_load_string.py +51 -0
  133. collagraph-0.8.6/tests/test_parser.py +79 -0
  134. collagraph-0.8.6/tests/test_pygfx_renderer.py +166 -0
  135. collagraph-0.8.6/tests/test_reconciliation.py +149 -0
  136. collagraph-0.8.6/tests/test_resolve_names.py +85 -0
  137. collagraph-0.8.6/tests/test_sfc_comments.py +29 -0
  138. collagraph-0.8.6/tests/test_sfc_compilation.py +67 -0
  139. collagraph-0.8.6/tests/test_sfc_import.py +118 -0
  140. collagraph-0.8.6/tests/test_sfc_linting_support.py +16 -0
  141. collagraph-0.8.6/tests/test_sfc_shadowing.py +63 -0
  142. collagraph-0.8.6/tests/test_slots.py +157 -0
  143. collagraph-0.8.6/tests/test_slots_dynamic.py +110 -0
  144. collagraph-0.8.6/tests/test_tag_component.py +130 -0
  145. collagraph-0.8.6/tests/test_tag_dynamic.py +73 -0
  146. collagraph-0.8.6/tests/test_tag_static.py +113 -0
  147. collagraph-0.8.6/tests/test_tag_template.py +83 -0
  148. collagraph-0.8.6/tests/test_text_expressions.py +59 -0
@@ -0,0 +1,3 @@
1
+ [run]
2
+ omit =
3
+ collagraph/renderers/dom_renderer.py
@@ -0,0 +1,119 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ tags:
8
+ - "v*"
9
+ pull_request:
10
+ branches:
11
+ - master
12
+
13
+ jobs:
14
+ lint:
15
+ name: Lint
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v5
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version-file: "pyproject.toml"
25
+ - name: Install only dev dependencies
26
+ run: uv sync --only-group dev
27
+ - name: Lint
28
+ run: uv run --no-sync ruff check .
29
+ - name: Format
30
+ run: uv run --no-sync ruff format --check .
31
+
32
+ test:
33
+ name: Test on ${{ matrix.name }}
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ include:
39
+ - name: Linux py310
40
+ pyversion: "3.10"
41
+ - name: Linux py311
42
+ pyversion: "3.11"
43
+ - name: Linux py312
44
+ pyversion: "3.12"
45
+ - name: Linux py313
46
+ pyversion: "3.13"
47
+ - name: Linux py314
48
+ pyversion: "3.14"
49
+ steps:
50
+ - uses: actions/checkout@v5
51
+ - name: Install uv
52
+ uses: astral-sh/setup-uv@v6
53
+ - name: Set up Python ${{ matrix.pyversion }}
54
+ uses: actions/setup-python@v5
55
+ with:
56
+ python-version: ${{ matrix.pyversion }}
57
+ - name: Install system dependencies
58
+ run: |
59
+ sudo apt-get update -y -qq
60
+ sudo apt-get install -y libgles2-mesa-dev
61
+ - name: Install dependencies
62
+ run: uv sync --all-groups
63
+ - name: Test
64
+ run: uv run pytest -v --cov=collagraph --cov-report=term-missing
65
+ env:
66
+ QT_QPA_PLATFORM: offscreen
67
+
68
+ build:
69
+ name: Build and test wheel
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 3.10
76
+ uses: actions/setup-python@v2
77
+ with:
78
+ python-version: "3.10"
79
+ - name: Install dependencies
80
+ run: uv sync
81
+ - name: Build wheel
82
+ run: uv build
83
+ - name: Twine check
84
+ run: uv run twine check dist/*
85
+ - name: Upload wheel artifact
86
+ uses: actions/upload-artifact@v4
87
+ with:
88
+ path: dist
89
+ name: dist
90
+
91
+ publish:
92
+ name: Publish to Github and Pypi
93
+ runs-on: ubuntu-latest
94
+ needs: [lint, test, build]
95
+ if: success() && startsWith(github.ref, 'refs/tags/v')
96
+ environment:
97
+ name: pypi
98
+ url: https://pypi.org/p/collagraph
99
+ permissions:
100
+ id-token: write
101
+ contents: write
102
+ steps:
103
+ - uses: actions/checkout@v5
104
+ - name: Download wheel artifact
105
+ uses: actions/download-artifact@v4
106
+ with:
107
+ name: dist
108
+ path: dist
109
+ - name: Release to GitHub
110
+ uses: softprops/action-gh-release@v2
111
+ with:
112
+ token: ${{ secrets.GITHUB_TOKEN }}
113
+ files: |
114
+ dist/*.tar.gz
115
+ dist/*.whl
116
+ draft: true
117
+ prerelease: false
118
+ - name: Publish to PyPI
119
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ *.egg-info/
2
+ __pycache__
3
+ .vscode
4
+ .coverage
5
+ dist
6
+ build
7
+ uv.lock
8
+ .venv
@@ -0,0 +1,32 @@
1
+ # See https://pre-commit.com for more information
2
+ # See https://pre-commit.com/hooks.html for more hooks
3
+ repos:
4
+ # - repo: https://github.com/pre-commit/pre-commit-hooks
5
+ # rev: v3.2.0
6
+ # hooks:
7
+ # - id: trailing-whitespace
8
+ # - id: end-of-file-fixer
9
+ # - id: check-yaml
10
+ # - id: check-added-large-files
11
+ - repo: local
12
+ hooks:
13
+ - id: lint
14
+ name: lint
15
+ entry: uv run ruff check
16
+ language: system
17
+ types: [python]
18
+ require_serial: true
19
+ - id: format
20
+ name: format
21
+ entry: uv run ruff format --check
22
+ language: system
23
+ types: [python]
24
+ require_serial: true
25
+ - id: test
26
+ name: pytest
27
+ entry: uv run pytest tests
28
+ language: system
29
+ types: [text]
30
+ files: \.(py|cgx)$
31
+ require_serial: true
32
+ pass_filenames: false
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2022-2025 collagraph authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: collagraph
3
+ Version: 0.8.6
4
+ Summary: Reactive user interfaces
5
+ Author-email: Berend Klein Haneveld <berendkleinhaneveld@gmail.com>, Korijn van Golen <korijn@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: observ>=0.14.1
10
+ Provides-Extra: pygfx
11
+ Requires-Dist: pygfx>=0.9.0; extra == 'pygfx'
12
+ Provides-Extra: pyside
13
+ Requires-Dist: pyside6!=6.8.3,!=6.9.0,>=6.6.2; (python_version < '3.15') and extra == 'pyside'
14
+ Description-Content-Type: text/markdown
15
+
16
+ [![PyPI version](https://badge.fury.io/py/collagraph.svg)](https://badge.fury.io/py/collagraph)
17
+ [![CI status](https://github.com/fork-tongue/collagraph/workflows/CI/badge.svg)](https://github.com/fork-tongue/collagraph/actions)
18
+
19
+ # Collagraph 📓
20
+
21
+ Reactive user interfaces.
22
+
23
+ > The word [Collagraphy](https://en.wikipedia.org/wiki/Collagraphy) is derived from the Greek word _koll_ or _kolla_, meaning glue, and graph, meaning the activity of drawing.
24
+
25
+ Inspired by Vue and React.
26
+
27
+
28
+ ## Features
29
+
30
+ Write your Python interfaces in a declarative manner with plain render functions, component classes or even single-file components using Vue-like syntax, but with Python!
31
+
32
+ * Reactivity (made possible by leveraging [observ](https://github.com/fork-tongue/observ))
33
+ * Class components with local state and life-cycle methods/hooks
34
+ * Single-file components with Vue-like template syntax (`.cgx` files)
35
+ * Custom renderers
36
+
37
+ Here is an example that shows a counter, made with a component with Vue-like syntax:
38
+
39
+ Contents of `counter.cgx`:
40
+ ```html
41
+ <widget>
42
+ <label
43
+ :text="f'Count: {count}'"
44
+ />
45
+ <button
46
+ text="bump"
47
+ @clicked="bump"
48
+ />
49
+ </widget>
50
+
51
+ <script>
52
+ import collagraph as cg
53
+
54
+
55
+ class Counter(cg.Component):
56
+ def init(self):
57
+ self.state["count"] = 0
58
+
59
+ def bump(self):
60
+ self.state["count"] += 1
61
+ </script>
62
+ ```
63
+
64
+ Contents of `app.py`:
65
+ ```python
66
+ from PySide6 import QtWidgets
67
+ import collagraph as cg
68
+
69
+ # After importing collagraph, it's possible to import
70
+ # components directly from .cgx files
71
+ from counter import Counter
72
+
73
+ # Create a Collagraph instance with a PySide renderer
74
+ # and register with the Qt event loop
75
+ gui = cg.Collagraph(renderer=cg.PySideRenderer())
76
+ # Render the component into a container
77
+ # (in this case the app but can be another widget)
78
+ app = QtWidgets.QApplication()
79
+ gui.render(Counter, app)
80
+ app.exec()
81
+ ```
82
+
83
+ Which looks something like this:
84
+
85
+ ![collagraph example](https://github.com/fork-tongue/collagraph/assets/1000968/4ebae92e-d7be-48ea-b76a-c6eab8d62112)
86
+
87
+ Instead of using a python file as an entry point to run components, you can run them directly using the collagraph CLI:
88
+
89
+ ```sh
90
+ uv run collagraph examples/pyside/counter.cgx
91
+ ```
92
+
93
+ For more examples, please take a look at the [examples folder](examples).
94
+
95
+ Currently there are two renderers:
96
+
97
+ * [PysideRenderer](collagraph/renderers/pyside_renderer.py): for rendering PySide6 applications
98
+ * [PygfxRenderer](collagraph/renderers/pygfx_renderer.py): for rendering 3D graphic scenes with [Pygfx](https://github.com/pygfx/pygfx)
99
+
100
+ It is possible to create a custom Renderer using the [Renderer](collagraph/renderers/__init__.py) interface, to render to other UI frameworks, for instance wxPython.
101
+
102
+
103
+ ## Development
104
+
105
+ To try out Collagraph or start development, run:
106
+
107
+ ```sh
108
+ # Basic dev setup (no pygfx or pyside)
109
+ uv sync
110
+ # Full dev setup
111
+ uv sync --all-groups
112
+ # Run example:
113
+ uv run python examples/pyside/layout-example.py
114
+ # Run test suite:
115
+ uv run pytest
116
+ # Install git pre-commit hooks to make sure tests/linting passes before committing
117
+ uv run pre-commit install
118
+ ```
119
+
120
+
121
+ ### Syntax Highlighting
122
+
123
+ Syntax highlighting for single-file components (`.cgx`) is supported for VSCode and Sublime Text:
124
+
125
+ * [CGX syntax highlight for Sublime Text](https://github.com/fork-tongue/cgx-syntax-highlight-sublime)
126
+ * [CGX syntax highlight for VSCode](https://github.com/fork-tongue/cgx-syntax-highlight-vscode)
127
+
128
+
129
+ ### Formatting and linting
130
+
131
+ Linting and formatting cgx files is possible with: [ruff-cgx](https://github.com/fork-tongue/ruff-cgx).
@@ -0,0 +1,116 @@
1
+ [![PyPI version](https://badge.fury.io/py/collagraph.svg)](https://badge.fury.io/py/collagraph)
2
+ [![CI status](https://github.com/fork-tongue/collagraph/workflows/CI/badge.svg)](https://github.com/fork-tongue/collagraph/actions)
3
+
4
+ # Collagraph 📓
5
+
6
+ Reactive user interfaces.
7
+
8
+ > The word [Collagraphy](https://en.wikipedia.org/wiki/Collagraphy) is derived from the Greek word _koll_ or _kolla_, meaning glue, and graph, meaning the activity of drawing.
9
+
10
+ Inspired by Vue and React.
11
+
12
+
13
+ ## Features
14
+
15
+ Write your Python interfaces in a declarative manner with plain render functions, component classes or even single-file components using Vue-like syntax, but with Python!
16
+
17
+ * Reactivity (made possible by leveraging [observ](https://github.com/fork-tongue/observ))
18
+ * Class components with local state and life-cycle methods/hooks
19
+ * Single-file components with Vue-like template syntax (`.cgx` files)
20
+ * Custom renderers
21
+
22
+ Here is an example that shows a counter, made with a component with Vue-like syntax:
23
+
24
+ Contents of `counter.cgx`:
25
+ ```html
26
+ <widget>
27
+ <label
28
+ :text="f'Count: {count}'"
29
+ />
30
+ <button
31
+ text="bump"
32
+ @clicked="bump"
33
+ />
34
+ </widget>
35
+
36
+ <script>
37
+ import collagraph as cg
38
+
39
+
40
+ class Counter(cg.Component):
41
+ def init(self):
42
+ self.state["count"] = 0
43
+
44
+ def bump(self):
45
+ self.state["count"] += 1
46
+ </script>
47
+ ```
48
+
49
+ Contents of `app.py`:
50
+ ```python
51
+ from PySide6 import QtWidgets
52
+ import collagraph as cg
53
+
54
+ # After importing collagraph, it's possible to import
55
+ # components directly from .cgx files
56
+ from counter import Counter
57
+
58
+ # Create a Collagraph instance with a PySide renderer
59
+ # and register with the Qt event loop
60
+ gui = cg.Collagraph(renderer=cg.PySideRenderer())
61
+ # Render the component into a container
62
+ # (in this case the app but can be another widget)
63
+ app = QtWidgets.QApplication()
64
+ gui.render(Counter, app)
65
+ app.exec()
66
+ ```
67
+
68
+ Which looks something like this:
69
+
70
+ ![collagraph example](https://github.com/fork-tongue/collagraph/assets/1000968/4ebae92e-d7be-48ea-b76a-c6eab8d62112)
71
+
72
+ Instead of using a python file as an entry point to run components, you can run them directly using the collagraph CLI:
73
+
74
+ ```sh
75
+ uv run collagraph examples/pyside/counter.cgx
76
+ ```
77
+
78
+ For more examples, please take a look at the [examples folder](examples).
79
+
80
+ Currently there are two renderers:
81
+
82
+ * [PysideRenderer](collagraph/renderers/pyside_renderer.py): for rendering PySide6 applications
83
+ * [PygfxRenderer](collagraph/renderers/pygfx_renderer.py): for rendering 3D graphic scenes with [Pygfx](https://github.com/pygfx/pygfx)
84
+
85
+ It is possible to create a custom Renderer using the [Renderer](collagraph/renderers/__init__.py) interface, to render to other UI frameworks, for instance wxPython.
86
+
87
+
88
+ ## Development
89
+
90
+ To try out Collagraph or start development, run:
91
+
92
+ ```sh
93
+ # Basic dev setup (no pygfx or pyside)
94
+ uv sync
95
+ # Full dev setup
96
+ uv sync --all-groups
97
+ # Run example:
98
+ uv run python examples/pyside/layout-example.py
99
+ # Run test suite:
100
+ uv run pytest
101
+ # Install git pre-commit hooks to make sure tests/linting passes before committing
102
+ uv run pre-commit install
103
+ ```
104
+
105
+
106
+ ### Syntax Highlighting
107
+
108
+ Syntax highlighting for single-file components (`.cgx`) is supported for VSCode and Sublime Text:
109
+
110
+ * [CGX syntax highlight for Sublime Text](https://github.com/fork-tongue/cgx-syntax-highlight-sublime)
111
+ * [CGX syntax highlight for VSCode](https://github.com/fork-tongue/cgx-syntax-highlight-vscode)
112
+
113
+
114
+ ### Formatting and linting
115
+
116
+ Linting and formatting cgx files is possible with: [ruff-cgx](https://github.com/fork-tongue/ruff-cgx).
@@ -0,0 +1,9 @@
1
+ from importlib.metadata import version
2
+
3
+ from .collagraph import Collagraph # noqa: F401
4
+ from .component import Component # noqa: F401
5
+ from .constants import EventLoopType # noqa: F401
6
+ from .renderers import * # noqa: F403
7
+ from .sfc import importer # noqa: F401
8
+
9
+ __version__ = version("collagraph")
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from observ import reactive
9
+
10
+ import collagraph as cg
11
+
12
+
13
+ def available_renderers():
14
+ result = set()
15
+ for renderer_type, name in {
16
+ "PySideRenderer": "pyside",
17
+ "PygfxRenderer": "pygfx",
18
+ "DictRenderer": "dict",
19
+ }.items():
20
+ try:
21
+ importlib.import_module("collagraph", renderer_type)
22
+ result.add(name)
23
+ except ImportError:
24
+ continue
25
+ return result
26
+
27
+
28
+ def init_collagraph(
29
+ renderer_type: str, component_path: Path, state: dict | None = None
30
+ ):
31
+ file_as_module = ".".join([*component_path.parts[:-1], component_path.stem])
32
+ component_module = importlib.import_module(file_as_module)
33
+ component_class = component_module.__component_class
34
+ props = reactive(state or {})
35
+
36
+ if renderer_type == "pygfx":
37
+ import pygfx as gfx
38
+ from wgpu.gui.auto import WgpuCanvas, run
39
+
40
+ canvas = WgpuCanvas(size=(600, 400))
41
+ wgpu_renderer = gfx.renderers.WgpuRenderer(canvas)
42
+
43
+ camera = gfx.PerspectiveCamera(70)
44
+ camera.local.z = 15
45
+ camera.show_pos((0, 0, 0))
46
+
47
+ controls = gfx.OrbitController(camera)
48
+ controls.register_events(wgpu_renderer)
49
+
50
+ container = gfx.Scene()
51
+
52
+ def animate():
53
+ wgpu_renderer.render(container, camera)
54
+
55
+ renderer = cg.PygfxRenderer()
56
+ renderer.add_on_change_handler(lambda: canvas.request_draw(animate))
57
+ gui = cg.Collagraph(renderer=renderer)
58
+ gui.render(component_class, container, state=props)
59
+
60
+ run()
61
+ elif renderer_type == "pyside":
62
+ from PySide6 import QtWidgets
63
+
64
+ app = QtWidgets.QApplication()
65
+ gui = cg.Collagraph(renderer=cg.PySideRenderer())
66
+ gui.render(component_class, app, state=props)
67
+ app.exec()
68
+ elif renderer_type == "dict":
69
+ container = {"root": None}
70
+ gui = cg.Collagraph(
71
+ renderer=cg.DictRenderer(),
72
+ event_loop_type=cg.EventLoopType.SYNC,
73
+ )
74
+ gui.render(component_class, container, state=props)
75
+ # Start debugger to allow for inspection of container
76
+ # and manipulation of props
77
+ breakpoint() # noqa: T100
78
+
79
+
80
+ def existing_component_file(value):
81
+ path = Path(value)
82
+ if not path.exists():
83
+ raise argparse.ArgumentTypeError(f"{value} does not exist")
84
+ if not path.is_file():
85
+ raise argparse.ArgumentTypeError(f"{value} is not a file")
86
+ if path.suffix != ".cgx":
87
+ raise argparse.ArgumentTypeError(f"{value} is not a collagraph component")
88
+ return path
89
+
90
+
91
+ def json_contents(value):
92
+ path = Path(value)
93
+ if path.is_file():
94
+ with path.open(mode="r", encoding="utf-8") as fh:
95
+ try:
96
+ return json.load(fh)
97
+ except Exception:
98
+ raise argparse.ArgumentTypeError(f"{value} is not valid json")
99
+ else:
100
+ try:
101
+ return json.loads(value)
102
+ except Exception:
103
+ raise argparse.ArgumentTypeError(f"{value} is not valid json")
104
+
105
+
106
+ def run():
107
+ parser = argparse.ArgumentParser(
108
+ description="Run collagraph components directly",
109
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
110
+ )
111
+ parser.add_argument(
112
+ "--renderer",
113
+ default="pyside",
114
+ choices=available_renderers(),
115
+ help="The type of renderer to use",
116
+ )
117
+ parser.add_argument(
118
+ "--state",
119
+ type=json_contents,
120
+ help="Optional state/props to load (json file or string)",
121
+ )
122
+ parser.add_argument(
123
+ "component",
124
+ type=existing_component_file,
125
+ help="Path to component to render",
126
+ )
127
+ args = parser.parse_args()
128
+
129
+ init_collagraph(args.renderer, args.component, args.state)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ run()
@@ -0,0 +1,7 @@
1
+ from os.path import dirname
2
+
3
+ HERE = dirname(__file__)
4
+
5
+
6
+ def get_hook_dirs():
7
+ return [HERE]
@@ -0,0 +1,58 @@
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()
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)
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from observ import scheduler
7
+
8
+ from collagraph.component import Component
9
+ from collagraph.constants import EventLoopType
10
+ from collagraph.renderers import Renderer
11
+
12
+
13
+ class Collagraph:
14
+ def __init__(
15
+ self, renderer: Renderer, *, event_loop_type: EventLoopType | None = None
16
+ ):
17
+ if not isinstance(renderer, Renderer):
18
+ raise TypeError(f"Expected a Renderer but got a {type(renderer)}")
19
+ self.renderer = renderer
20
+ if not event_loop_type:
21
+ event_loop_type = (
22
+ renderer.preferred_event_loop_type() or EventLoopType.DEFAULT
23
+ )
24
+ self.event_loop_type = event_loop_type
25
+ if self.event_loop_type is EventLoopType.DEFAULT:
26
+ scheduler.register_asyncio()
27
+ renderer.register_asyncio()
28
+ else:
29
+ scheduler.register_request_flush(scheduler.flush)
30
+
31
+ def render(
32
+ self,
33
+ component_class: Callable[[dict], Component],
34
+ target: Any,
35
+ state: dict | None = None,
36
+ ):
37
+ """
38
+ target: DOM element/instance to render into.
39
+ state: state that gets passed into the component as top-level props.
40
+ """
41
+ # Here is the 'root' component which will carry the state
42
+ component = component_class(state or {})
43
+
44
+ # component.render() returns a fragment which is then mounted
45
+ # into the target (DOM) element
46
+ self.fragment = component.render(renderer=self.renderer)
47
+ self.fragment.component = component
48
+ self.fragment.mount(target)