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.
- collagraph-0.8.6/.coveragerc +3 -0
- collagraph-0.8.6/.github/workflows/ci.yml +119 -0
- collagraph-0.8.6/.gitignore +8 -0
- collagraph-0.8.6/.pre-commit-config.yaml +32 -0
- collagraph-0.8.6/LICENSE +21 -0
- collagraph-0.8.6/PKG-INFO +131 -0
- collagraph-0.8.6/README.md +116 -0
- collagraph-0.8.6/collagraph/__init__.py +9 -0
- collagraph-0.8.6/collagraph/__main__.py +133 -0
- collagraph-0.8.6/collagraph/__pyinstaller/__init__.py +7 -0
- collagraph-0.8.6/collagraph/__pyinstaller/hook-collagraph.py +58 -0
- collagraph-0.8.6/collagraph/collagraph.py +48 -0
- collagraph-0.8.6/collagraph/component.py +172 -0
- collagraph-0.8.6/collagraph/constants.py +6 -0
- collagraph-0.8.6/collagraph/fragment.py +619 -0
- collagraph-0.8.6/collagraph/renderers/__init__.py +88 -0
- collagraph-0.8.6/collagraph/renderers/dict_renderer.py +74 -0
- collagraph-0.8.6/collagraph/renderers/pygfx_renderer.py +117 -0
- collagraph-0.8.6/collagraph/renderers/pyside/__init__.py +30 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/__init__.py +21 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/combobox.py +12 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/dialogbuttonbox.py +32 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/dockwidget.py +17 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/itemmodel.py +26 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/layouts.py +142 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/listview.py +46 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/menu.py +32 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/menubar.py +27 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/qobject.py +21 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/scrollarea.py +15 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/splitter.py +19 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/standarditem.py +67 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/statusbar.py +39 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/tab.py +21 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/toolbar.py +29 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/treewidget.py +20 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/treewidgetitem.py +63 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/widget.py +46 -0
- collagraph-0.8.6/collagraph/renderers/pyside/objects/window.py +32 -0
- collagraph-0.8.6/collagraph/renderers/pyside_renderer.py +511 -0
- collagraph-0.8.6/collagraph/sfc/__init__.py +62 -0
- collagraph-0.8.6/collagraph/sfc/compiler.py +902 -0
- collagraph-0.8.6/collagraph/sfc/importer.py +54 -0
- collagraph-0.8.6/collagraph/sfc/parser.py +115 -0
- collagraph-0.8.6/collagraph/weak.py +70 -0
- collagraph-0.8.6/examples/pygfx/README.md +35 -0
- collagraph-0.8.6/examples/pygfx/app_point_cloud.cgx +30 -0
- collagraph-0.8.6/examples/pygfx/button.cgx +61 -0
- collagraph-0.8.6/examples/pygfx/combined-example.py +24 -0
- collagraph-0.8.6/examples/pygfx/component-example.py +32 -0
- collagraph-0.8.6/examples/pygfx/numberpad.cgx +25 -0
- collagraph-0.8.6/examples/pygfx/point.cgx +28 -0
- collagraph-0.8.6/examples/pygfx/point_cloud.cgx +69 -0
- collagraph-0.8.6/examples/pygfx/pygfx-component.cgx +33 -0
- collagraph-0.8.6/examples/pygfx/render_widget.cgx +45 -0
- collagraph-0.8.6/examples/pyside/app.py +11 -0
- collagraph-0.8.6/examples/pyside/big_list.cgx +178 -0
- collagraph-0.8.6/examples/pyside/cgx_component_example.cgx +50 -0
- collagraph-0.8.6/examples/pyside/combobox_example.cgx +33 -0
- collagraph-0.8.6/examples/pyside/counter.cgx +25 -0
- collagraph-0.8.6/examples/pyside/debug_example.cgx +26 -0
- collagraph-0.8.6/examples/pyside/dialog_example.cgx +60 -0
- collagraph-0.8.6/examples/pyside/dialog_example_window.cgx +64 -0
- collagraph-0.8.6/examples/pyside/flow_layout.py +135 -0
- collagraph-0.8.6/examples/pyside/item.cgx +24 -0
- collagraph-0.8.6/examples/pyside/layout_example.cgx +153 -0
- collagraph-0.8.6/examples/pyside/list_and_form.cgx +46 -0
- collagraph-0.8.6/examples/pyside/lists_example.cgx +91 -0
- collagraph-0.8.6/examples/pyside/slider_example.cgx +56 -0
- collagraph-0.8.6/examples/pyside/tabs_example.cgx +39 -0
- collagraph-0.8.6/examples/pyside/todo_example.cgx +49 -0
- collagraph-0.8.6/examples/pyside/todo_list.cgx +26 -0
- collagraph-0.8.6/examples/pyside/tree_view_example.cgx +177 -0
- collagraph-0.8.6/examples/pyside/tree_view_item.cgx +28 -0
- collagraph-0.8.6/examples/pyside/tree_view_model.cgx +35 -0
- collagraph-0.8.6/examples/pyside/tree_widget_example.cgx +185 -0
- collagraph-0.8.6/examples/pyside/tree_widget_item.cgx +32 -0
- collagraph-0.8.6/pyproject.toml +78 -0
- collagraph-0.8.6/tests/__init__.py +0 -0
- collagraph-0.8.6/tests/conftest.py +53 -0
- collagraph-0.8.6/tests/data/component/events_child.cgx +17 -0
- collagraph-0.8.6/tests/data/component/events_parent.cgx +25 -0
- collagraph-0.8.6/tests/data/component/provide_child.cgx +16 -0
- collagraph-0.8.6/tests/data/component/provide_parent.cgx +13 -0
- collagraph-0.8.6/tests/data/component/provide_root.cgx +13 -0
- collagraph-0.8.6/tests/data/example.cgx +9 -0
- collagraph-0.8.6/tests/data/file_dunder.cgx +20 -0
- collagraph-0.8.6/tests/data/multiple_classes_right_order.cgx +20 -0
- collagraph-0.8.6/tests/data/multiple_classes_wrong_order.cgx +20 -0
- collagraph-0.8.6/tests/data/multiple_root_elements.cgx +12 -0
- collagraph-0.8.6/tests/data/no_component_class.cgx +8 -0
- collagraph-0.8.6/tests/data/pyside/app.cgx +144 -0
- collagraph-0.8.6/tests/data/pyside/test_pyside_update.cgx +63 -0
- collagraph-0.8.6/tests/data/shadow_imports.cgx +15 -0
- collagraph-0.8.6/tests/data/simple.cgx +11 -0
- collagraph-0.8.6/tests/data/slots/dynamic_for.cgx +15 -0
- collagraph-0.8.6/tests/data/slots/dynamic_for_template.cgx +17 -0
- collagraph-0.8.6/tests/data/slots/dynamic_if.cgx +14 -0
- collagraph-0.8.6/tests/data/slots/dynamic_if_template.cgx +14 -0
- collagraph-0.8.6/tests/data/slots/node.cgx +11 -0
- collagraph-0.8.6/tests/data/slots/simple_tree.cgx +12 -0
- collagraph-0.8.6/tests/data/slots/slots_implicit_default_slot.cgx +25 -0
- collagraph-0.8.6/tests/data/slots/template.cgx +23 -0
- collagraph-0.8.6/tests/data/slots/template_empty.cgx +10 -0
- collagraph-0.8.6/tests/data/slots/template_implicit_default_slot.cgx +22 -0
- collagraph-0.8.6/tests/data/slots/template_implicit_default_slot_name.cgx +24 -0
- collagraph-0.8.6/tests/data/slots/template_partial.cgx +18 -0
- collagraph-0.8.6/tests/data/slots/tree.cgx +15 -0
- collagraph-0.8.6/tests/data/slots/widget_with_slots.cgx +25 -0
- collagraph-0.8.6/tests/data/text/expressions.cgx +16 -0
- collagraph-0.8.6/tests/pyside/conftest.py +42 -0
- collagraph-0.8.6/tests/pyside/lists.cgx +74 -0
- collagraph-0.8.6/tests/pyside/test_pyside_ci.py +15 -0
- collagraph-0.8.6/tests/pyside/test_pyside_elements.py +596 -0
- collagraph-0.8.6/tests/pyside/test_pyside_renderer.py +315 -0
- collagraph-0.8.6/tests/pyside/test_pyside_sfc.py +61 -0
- collagraph-0.8.6/tests/pyside/test_pyside_trigger_signals.py +205 -0
- collagraph-0.8.6/tests/pyside/test_update.py +48 -0
- collagraph-0.8.6/tests/pyside/test_widget.py +114 -0
- collagraph-0.8.6/tests/test_attributes_dynamic.py +374 -0
- collagraph-0.8.6/tests/test_attributes_static.py +83 -0
- collagraph-0.8.6/tests/test_collagraph.py +176 -0
- collagraph-0.8.6/tests/test_component.py +171 -0
- collagraph-0.8.6/tests/test_component_events.py +31 -0
- collagraph-0.8.6/tests/test_component_life_cycle.py +251 -0
- collagraph-0.8.6/tests/test_component_provide_inject.py +30 -0
- collagraph-0.8.6/tests/test_directive_else.py +86 -0
- collagraph-0.8.6/tests/test_directive_else_if.py +172 -0
- collagraph-0.8.6/tests/test_directive_for.py +700 -0
- collagraph-0.8.6/tests/test_directive_if.py +290 -0
- collagraph-0.8.6/tests/test_events.py +146 -0
- collagraph-0.8.6/tests/test_load_string.py +51 -0
- collagraph-0.8.6/tests/test_parser.py +79 -0
- collagraph-0.8.6/tests/test_pygfx_renderer.py +166 -0
- collagraph-0.8.6/tests/test_reconciliation.py +149 -0
- collagraph-0.8.6/tests/test_resolve_names.py +85 -0
- collagraph-0.8.6/tests/test_sfc_comments.py +29 -0
- collagraph-0.8.6/tests/test_sfc_compilation.py +67 -0
- collagraph-0.8.6/tests/test_sfc_import.py +118 -0
- collagraph-0.8.6/tests/test_sfc_linting_support.py +16 -0
- collagraph-0.8.6/tests/test_sfc_shadowing.py +63 -0
- collagraph-0.8.6/tests/test_slots.py +157 -0
- collagraph-0.8.6/tests/test_slots_dynamic.py +110 -0
- collagraph-0.8.6/tests/test_tag_component.py +130 -0
- collagraph-0.8.6/tests/test_tag_dynamic.py +73 -0
- collagraph-0.8.6/tests/test_tag_static.py +113 -0
- collagraph-0.8.6/tests/test_tag_template.py +83 -0
- collagraph-0.8.6/tests/test_text_expressions.py +59 -0
|
@@ -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,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
|
collagraph-0.8.6/LICENSE
ADDED
|
@@ -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
|
+
[](https://badge.fury.io/py/collagraph)
|
|
17
|
+
[](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
|
+

|
|
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
|
+
[](https://badge.fury.io/py/collagraph)
|
|
2
|
+
[](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
|
+

|
|
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,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)
|