Add pure-Python view API as alternative to cgx templates - #193
Merged
Conversation
Components can now describe their UI in a `view` method using the
element builder `h` and control flow helpers, instead of (and alongside)
a .cgx template:
class Counter(cg.Component):
def view(self):
with h.widget():
h.label(text=lambda: f"Count: {self.state['count']}")
h.button("bump", on_clicked=self.bump)
The view method runs once, like a compiled render function; fine-grained
reactivity is kept intact through one rule: a plain value is a static
attribute, a zero-arg callable is a reactive bind. The builder in
collagraph/dsl.py constructs the exact same Fragment tree that the cgx
compiler generates, so both frontends share the entire runtime:
* when/elif_/otherwise -> ControlFlowFragment (v-if/v-else-if/v-else)
* each decorator -> ListFragment, loop variables as getters (v-for/:key)
* slot/fill -> SlotFragment/slot content (v-slot)
* dynamic -> DynamicFragment (<component :is>)
* on_* kwargs -> events, bind= -> set_bind_dict, ref= -> template refs
* positional args -> text children ({{ interpolation }})
View code (including each() row builds) runs inside a temporary watcher:
any reactive read during the build is almost certainly a bug (the value
would be baked in statically), so a warning with the lambda fix is
emitted, and ambient watchers are shielded from picking up stray
dependencies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* examples/pyside/counter_view.py: counter.cgx written with view() * examples/pyside/todo_view.py: the todo_example.cgx/todo_list.cgx pair in a single file, additionally showing keyed each() and when()/otherwise() (an "All done!" message when the list is empty) * docs/guide/python-views.md: full guide with a template-to-Python cheat sheet, linked from the nav, template syntax page, index and the PySide examples page Both examples verified end-to-end against an offscreen QApplication (adding items through the line edit, completing items, conditional switch when the list empties). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalize the hot reloader beyond .cgx files: plain Python modules that define view components (Component subclasses overriding view()) used in the component tree are now watched and reloaded on change. - Watch .py modules of view components, including modules pulled in for dynamic (:is) components - Reload script modules (__main__) by re-executing them from file under a substitute module name, so their `if __name__ == "__main__"` block does not run again - Resolve the root component class by name when a reloaded module does not set __component_class (only cgx modules do) - Clear stale bytecode caches before reimporting, so rewrites within the same mtime second are picked up Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `collagraph` command now also accepts .py modules containing view
components, with the same --hot-reload and --state options as .cgx files:
collagraph -H examples/pyside/counter_view.py
collagraph examples/pyside/todo_view.py:TodoApp
The root component is resolved from the module by finding the single
view component it defines. When a module defines multiple, select one
by appending :ClassName, or assign `__component_class` in the module.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`title` is not a QMainWindow property, so `<window title=...>` was a silent no-op; the Qt property is `windowTitle`, set via `window_title`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Explain that templates accept dashes or underscores but Python views require underscores (keyword-argument identifiers), and that accepted attribute names depend on the renderer. Cross-link the PySide and Pygfx attribute sections, and add dash/underscore examples to the PySide docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trailing underscore on elif_ (needed to avoid the elif keyword) read awkwardly next to when and otherwise. or_when composes with them as plain English -- "when A, or when B, otherwise C" -- and needs no underscore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The internals architecture page was written entirely around the .cgx compiler as the sole way a Fragment tree is produced. Update it to present the pure-Python view API as a second front-end that builds the same Fragment tree directly, with no compile step: - Broaden the intro and add a paragraph on the two front-ends - Add the view() path to the overview and summary diagrams - Add a "same tree from Python" subsection to the compiler walkthrough - Reword the summary to be front-end agnostic Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merged
berendkleinhaneveld
added a commit
that referenced
this pull request
Jul 21, 2026
Features: - Add pure-Python view API as alternative to cgx templates (#193) - Support text elements for PySide widgets that display text (#191) Fixes & internals: - Fragment parenting overhaul (#162) - Fix PyInstaller hook for CGX files inside packages (#184) - Write compiled AST to temp file when CGX_DEBUG is set (#175) Performance: - Speed up mount path: cheap arity check, reuse first(), leaner emit (#186) - Cache Fragment._component_parent() lookups (#187) - Avoid redundant anchor lookups in Fragment.anchor() and unkeyed v-for (#188) Documentation: - Add MkDocs documentation with GitHub Pages deployment (#176) - Add internals architecture documentation page (#194) - Add docs badge and links to README (#192) Tooling & CI: - Add benchmark suite and per-PR benchmark CI workflow (#185) - Make benchmark CI guard robust against run-to-run noise (#196) - Update GitHub actions from Node 20 to Node 24 (#190) - Migrate from pre-commit to prek (#195) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a pure-Python way to describe component UIs — the
view()method — as a fully supported alternative to.cgxtemplates, for users who prefer to stay in plain Python (better tooling, type checkers, no separate file format):Templates remain fully supported; the two coexist (a component defines either a template or a
viewmethod).How it works
Like compiled cgx render functions,
view()runs once per component instance: it's a setup function that builds the sameFragmenttree the compiler produces, so all fine-grained reactivity lives in the existing Fragment layer — no new reactivity machinery. The one rule: a plain value is a static attribute, a zero-arg callable is a reactive bind.The full template feature set is covered: static/bound attributes and
bind=dicts, events (on_*), text content,when/elif_/otherwise, keyed and unkeyedeachloops (loop variables as Solid-style getters), component usage with props/events, slots andfill, refs, anddynamic(:is) components. View build code additionally runs inside a temporary watcher that detects the eager-read footgun (reading reactive state without wrapping it in a lambda) and warns with a pointer to the fix.Also in this PR
.pymodules defining view components are watched and reloaded, including scripts run directly (python my_app.py— the file is re-executed under a substitute module name so the__main__guard doesn't re-fire). Also fixes a stale-bytecode edge where a same-size rewrite within the same mtime second would reload old code.collagraph -H examples/pyside/counter_view.pyworks, withfile.py:ClassNameto select a class when a module defines several.counter_view.py,todo_view.py), a docs guide (Python Views, with a template→Python cheat sheet), and cross-links from the existing docs.<window title=...>in the todo example was a silent no-op (the Qt property iswindowTitle); now useswindow_title.Testing
QApplication, including keyed list reconciliation and conditional switching.collagraph -H.mkdocs build --strictpasses.🤖 Generated with Claude Code