taut.tk 0.1.0__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.
taut_tk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.3
2
+ Name: taut.tk
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Sam I
6
+ Author-email: Sam I <aweptimum@gmail.com>
7
+ Requires-Dist: reaktiv>=0.22.0
8
+ Requires-Dist: pillow>=12.2.0 ; extra == 'dev'
9
+ Requires-Dist: watchdog>=6.0.0 ; extra == 'dev'
10
+ Requires-Python: >=3.13
11
+ Provides-Extra: dev
12
+ Description-Content-Type: text/markdown
13
+
14
+ # taut.tk
15
+
16
+ A small SolidJS-inspired runtime for Tkinter using
17
+ [reaktiv](https://pypi.org/project/reaktiv/) for fine-grained signals.
18
+
19
+ This is currently a prototype that I hope makes Tkinter more fun
20
+
21
+ ```python
22
+ from typing import Protocol
23
+
24
+ from taut import component
25
+ from taut import layout
26
+ from taut import tk
27
+ from taut.reactive import Accessor
28
+ from taut.reactive import Mutator
29
+ from taut.reactive import create_signal
30
+ from taut.runtime import create_root
31
+ from taut.control import For
32
+ from taut.control import Show
33
+
34
+
35
+ class CounterProps(Protocol):
36
+ label: Accessor[str]
37
+ count: Accessor[int]
38
+ set_count: Mutator[int]
39
+
40
+
41
+ @component
42
+ def counter(props: CounterProps):
43
+ todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])
44
+
45
+ return layout.VStack(
46
+ tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
47
+ tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
48
+ Show(
49
+ lambda: props.count() % 2 == 0,
50
+ lambda: tk.Label(text="Even"),
51
+ fallback=lambda: tk.Label(text="Odd"),
52
+ ),
53
+ For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
54
+ layout.HStack(
55
+ tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
56
+ gap=6,
57
+ ),
58
+ padding=12,
59
+ gap=6,
60
+ )
61
+
62
+ count, set_count = create_signal(0)
63
+ mount = create_root(
64
+ lambda: counter(label="Solid TK", count=count, set_count=set_count),
65
+ title="Solid TK",
66
+ )
67
+ mount.widget.mainloop()
68
+ ```
69
+
70
+ ## Why Tk?
71
+ I keep reaching for it every time I want a small UI at work and I keep getting bogged down in abstractions I think are loose but turn out to be more tightly-coupled than I realize. Imperatively updating state is also a chore and makes for widgets with lots of clunky helpers, and I find widget vars unwieldy. My most recent attempt I threw `reaktiv` at the problem and was surprised at how streamlined it made my widgets. I felt it could be more. So here we are.
72
+
73
+ ## What's Here So Far
74
+
75
+ - Functional components using `@component` decorator
76
+ - Class `Component` with `__init__()/setup()` and `render()`
77
+ - `Props`, where every attribute is an accessor
78
+ - first-class component children through `props.children`
79
+ - transparent `Fragment(...)` nodes for returning multiple children
80
+ - widget namespaces: `tk` for classic Tk widgets and `ttk` for themed widgets
81
+ - layout namespace: `layout.VStack`, `layout.HStack`, `layout.Grid`, `layout.Item`, `layout.GridItem`
82
+ - StyleX-ish style objects agnostic to tk/ttk widgets with `style.define()`, `style.merge()`, and `style.component()`.
83
+ - some control flow: `Show`, `For`, `Switch` / `Match`, `Index`, `Dynamic`
84
+ - context: `create_context()`, `Provider`, `use_context()`
85
+ - stores: `create_store()` for immutable updates and `create_mutable()` for
86
+ Solid-style mutable object state
87
+ - resources: `create_resource()` with `loading`, `error`, `state`, `mutate`, `refetch`
88
+ - lifecycle helpers: `create_effect()`, `on_mount()`, `on_cleanup()`
89
+ - `create_root()` and explicit disposal through the returned `Mount`
90
+
91
+ ## Doc Topics
92
+
93
+ - [Context](docs/context.md): providers, typed context helpers, and forwarded children.
94
+ - [Control](docs/control.md): conditional rendering.
95
+ - [Reactive primitives](docs/reactive.md): signals, memos, effects, and owner cleanup.
96
+ - [Resources](docs/resources.md): worker-thread loading, refreshes, mutation, and status.
97
+ - [Scheduling](docs/scheduling.md): queueing work
98
+ - [Stores](docs/stores.md): state management
99
+ - [Style](docs/style.md): StyleX-ish style objects, merging, and component styling.
100
+ - [Widgets](docs/widgets.md): Tkinter primitives
101
+
102
+ ## Examples
103
+
104
+ The runnable examples live in [examples](../examples/). Start with the
105
+ [examples README](../examples/README.md) for a short path through them.
106
+
107
+ ## Strong Typing
108
+ To follow Solid's model of named args collected into reactive props, there is some brittle trickery.
109
+
110
+ In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.<br>What it *does* have are .pyi files. We can define parameter transformations that type checkers can't inspect and then give them the publc facing API with a wink and a smile. The problem is generating those .pyi files.
111
+
112
+ The beginning of solving that problem is the `@component` decorator. In addition to transforming functions into rendered nodes, is also a syntax marker that can be inspected.
113
+
114
+ The next bit is stub-genning; basically a build step. Files in the working directory are scanned for component declarations and a corresponding `.pyi` file is generated, unrolling the typed prop object into a function signature.
115
+
116
+ This process can be manual with `uv run taut.tk stubs .`, or you can run a watcher to do it live with `uv run taut.tk watch` (it might be brittle though).
117
+
118
+ There are some caveats though, as this bit is honestly harder and more involved than the actual framework to get right. It imposes some limitations on import patterns.
119
+
120
+ When a component module imports sibling modules, prefer `from . import <module>` or a direct submodule import like `import examples.todo.module`. Relying on imports that use `__init__.py`, like `from examples.todo import module`, can route through generated package stubs instead of the source module. This is a limitation imposed by the current state of type checkers, and the only real work around that preserves usage of `__init__.py` is stubbing *everything*. Not really a feasible thing to develop or maintain right now.
121
+
122
+ ## Design Notes
123
+
124
+ `create_signal()` returns an accessor and a mutator:
125
+
126
+ ```python
127
+ from taut.reactive import create_signal
128
+ count, set_count = create_signal(0)
129
+ count()
130
+ set_count(lambda value: value + 1)
131
+ ```
132
+
133
+ The accessor is still compatible with `reaktiv` signals, so widgets can bind to
134
+ it directly. Writable widgets such as `tk.Entry(value=..., on_input=...)` receive
135
+ the accessor and mutator separately.
136
+
137
+ `Component.__new__` returns a renderable node to keep the class API simple
138
+
139
+ ```python
140
+ Counter(title="Solid TK")
141
+ ```
142
+
143
+ Inside a component, `self.props.title()` reads a reaktiv signal. This is
144
+ intentionally accessor-oriented.
145
+
146
+ Existing signals are preserved, while plain values and callbacks are wrapped as
147
+ signal values. Tk widget props use one additional convention: callable non-event
148
+ props are treated as reactive bindings, and event props such as `on_click` /
149
+ `command` are passed through as callbacks.
150
+
151
+ ```python
152
+ tk.Label(text=f"{count()}") # snapshot now
153
+ tk.Label(text=count) # reactive signal value
154
+ tk.Label(text=lambda: f"{count()}") # reactive derived expression
155
+ ```
156
+
157
+ That means a component prop can be read inside a derived expression:
158
+
159
+ ```python
160
+ tk.Label(text=lambda: f"Hello {self.props.name()}")
161
+ ```
162
+
163
+ or forwarded directly to a widget prop:
164
+
165
+ ```python
166
+ tk.Label(text=self.props.name)
167
+ ```
168
+
169
+ Component children are also collected into `props.children`, so container-like
170
+ components can feel like Solid components instead of manually passing a
171
+ `children=` prop:
172
+
173
+ ```python
174
+ @component
175
+ def panel(props):
176
+ return layout.VStack(
177
+ tk.Label(text=props.title),
178
+ props.children(),
179
+ padding=8,
180
+ )
181
+
182
+ panel(tk.Label(text="Nested"), title="Details")
183
+ ```
184
+
185
+ Control-flow nodes and `Fragment(...)` are transparent to layout. They produce
186
+ children; the parent widget or layout helper decides where those children go:
187
+
188
+ ```python
189
+ @component
190
+ def rows(props):
191
+ return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)
192
+
193
+ layout.VStack(
194
+ tk.Label(text="Before"),
195
+ rows(items=todos),
196
+ tk.Label(text="After"),
197
+ )
198
+ ```
199
+
200
+ In that example, the repeated labels are laid out by `VStack` between
201
+ `Before` and `After`; `For` and the component do not create wrapper frames.
202
+
203
+ ## Benchmarks
204
+
205
+ To estimate framework overhead without needing a display server, run the fake-Tk
206
+ benchmark:
207
+
208
+ ```sh
209
+ uv run python benchmarks/bench_overhead.py
210
+ ```
211
+
212
+ It compares raw Tk-style widget creation with taut.tk mounting for static
213
+ labels, reactive label props, and a component wrapper. The most useful line is
214
+ the reported extra microseconds per widget, because native Tk/Tcl startup and
215
+ platform display behavior are intentionally excluded.
@@ -0,0 +1,202 @@
1
+ # taut.tk
2
+
3
+ A small SolidJS-inspired runtime for Tkinter using
4
+ [reaktiv](https://pypi.org/project/reaktiv/) for fine-grained signals.
5
+
6
+ This is currently a prototype that I hope makes Tkinter more fun
7
+
8
+ ```python
9
+ from typing import Protocol
10
+
11
+ from taut import component
12
+ from taut import layout
13
+ from taut import tk
14
+ from taut.reactive import Accessor
15
+ from taut.reactive import Mutator
16
+ from taut.reactive import create_signal
17
+ from taut.runtime import create_root
18
+ from taut.control import For
19
+ from taut.control import Show
20
+
21
+
22
+ class CounterProps(Protocol):
23
+ label: Accessor[str]
24
+ count: Accessor[int]
25
+ set_count: Mutator[int]
26
+
27
+
28
+ @component
29
+ def counter(props: CounterProps):
30
+ todos, set_todos = create_signal(["wire props", "own effects", "dispose cleanly"])
31
+
32
+ return layout.VStack(
33
+ tk.Label(text=lambda: f"{props.label()}: {props.count()}"),
34
+ tk.Button(text="Increment", on_click=lambda: props.set_count(lambda n: n + 1)),
35
+ Show(
36
+ lambda: props.count() % 2 == 0,
37
+ lambda: tk.Label(text="Even"),
38
+ fallback=lambda: tk.Label(text="Odd"),
39
+ ),
40
+ For(todos, lambda item: tk.Label(text=item), key=lambda item: item),
41
+ layout.HStack(
42
+ tk.Button(text="-", on_click=lambda: set_todos(lambda items: items[:-1])),
43
+ gap=6,
44
+ ),
45
+ padding=12,
46
+ gap=6,
47
+ )
48
+
49
+ count, set_count = create_signal(0)
50
+ mount = create_root(
51
+ lambda: counter(label="Solid TK", count=count, set_count=set_count),
52
+ title="Solid TK",
53
+ )
54
+ mount.widget.mainloop()
55
+ ```
56
+
57
+ ## Why Tk?
58
+ I keep reaching for it every time I want a small UI at work and I keep getting bogged down in abstractions I think are loose but turn out to be more tightly-coupled than I realize. Imperatively updating state is also a chore and makes for widgets with lots of clunky helpers, and I find widget vars unwieldy. My most recent attempt I threw `reaktiv` at the problem and was surprised at how streamlined it made my widgets. I felt it could be more. So here we are.
59
+
60
+ ## What's Here So Far
61
+
62
+ - Functional components using `@component` decorator
63
+ - Class `Component` with `__init__()/setup()` and `render()`
64
+ - `Props`, where every attribute is an accessor
65
+ - first-class component children through `props.children`
66
+ - transparent `Fragment(...)` nodes for returning multiple children
67
+ - widget namespaces: `tk` for classic Tk widgets and `ttk` for themed widgets
68
+ - layout namespace: `layout.VStack`, `layout.HStack`, `layout.Grid`, `layout.Item`, `layout.GridItem`
69
+ - StyleX-ish style objects agnostic to tk/ttk widgets with `style.define()`, `style.merge()`, and `style.component()`.
70
+ - some control flow: `Show`, `For`, `Switch` / `Match`, `Index`, `Dynamic`
71
+ - context: `create_context()`, `Provider`, `use_context()`
72
+ - stores: `create_store()` for immutable updates and `create_mutable()` for
73
+ Solid-style mutable object state
74
+ - resources: `create_resource()` with `loading`, `error`, `state`, `mutate`, `refetch`
75
+ - lifecycle helpers: `create_effect()`, `on_mount()`, `on_cleanup()`
76
+ - `create_root()` and explicit disposal through the returned `Mount`
77
+
78
+ ## Doc Topics
79
+
80
+ - [Context](docs/context.md): providers, typed context helpers, and forwarded children.
81
+ - [Control](docs/control.md): conditional rendering.
82
+ - [Reactive primitives](docs/reactive.md): signals, memos, effects, and owner cleanup.
83
+ - [Resources](docs/resources.md): worker-thread loading, refreshes, mutation, and status.
84
+ - [Scheduling](docs/scheduling.md): queueing work
85
+ - [Stores](docs/stores.md): state management
86
+ - [Style](docs/style.md): StyleX-ish style objects, merging, and component styling.
87
+ - [Widgets](docs/widgets.md): Tkinter primitives
88
+
89
+ ## Examples
90
+
91
+ The runnable examples live in [examples](../examples/). Start with the
92
+ [examples README](../examples/README.md) for a short path through them.
93
+
94
+ ## Strong Typing
95
+ To follow Solid's model of named args collected into reactive props, there is some brittle trickery.
96
+
97
+ In JS/TS land, there's tons of tooling around jsx/tsx. Python doesn't have that.<br>What it *does* have are .pyi files. We can define parameter transformations that type checkers can't inspect and then give them the publc facing API with a wink and a smile. The problem is generating those .pyi files.
98
+
99
+ The beginning of solving that problem is the `@component` decorator. In addition to transforming functions into rendered nodes, is also a syntax marker that can be inspected.
100
+
101
+ The next bit is stub-genning; basically a build step. Files in the working directory are scanned for component declarations and a corresponding `.pyi` file is generated, unrolling the typed prop object into a function signature.
102
+
103
+ This process can be manual with `uv run taut.tk stubs .`, or you can run a watcher to do it live with `uv run taut.tk watch` (it might be brittle though).
104
+
105
+ There are some caveats though, as this bit is honestly harder and more involved than the actual framework to get right. It imposes some limitations on import patterns.
106
+
107
+ When a component module imports sibling modules, prefer `from . import <module>` or a direct submodule import like `import examples.todo.module`. Relying on imports that use `__init__.py`, like `from examples.todo import module`, can route through generated package stubs instead of the source module. This is a limitation imposed by the current state of type checkers, and the only real work around that preserves usage of `__init__.py` is stubbing *everything*. Not really a feasible thing to develop or maintain right now.
108
+
109
+ ## Design Notes
110
+
111
+ `create_signal()` returns an accessor and a mutator:
112
+
113
+ ```python
114
+ from taut.reactive import create_signal
115
+ count, set_count = create_signal(0)
116
+ count()
117
+ set_count(lambda value: value + 1)
118
+ ```
119
+
120
+ The accessor is still compatible with `reaktiv` signals, so widgets can bind to
121
+ it directly. Writable widgets such as `tk.Entry(value=..., on_input=...)` receive
122
+ the accessor and mutator separately.
123
+
124
+ `Component.__new__` returns a renderable node to keep the class API simple
125
+
126
+ ```python
127
+ Counter(title="Solid TK")
128
+ ```
129
+
130
+ Inside a component, `self.props.title()` reads a reaktiv signal. This is
131
+ intentionally accessor-oriented.
132
+
133
+ Existing signals are preserved, while plain values and callbacks are wrapped as
134
+ signal values. Tk widget props use one additional convention: callable non-event
135
+ props are treated as reactive bindings, and event props such as `on_click` /
136
+ `command` are passed through as callbacks.
137
+
138
+ ```python
139
+ tk.Label(text=f"{count()}") # snapshot now
140
+ tk.Label(text=count) # reactive signal value
141
+ tk.Label(text=lambda: f"{count()}") # reactive derived expression
142
+ ```
143
+
144
+ That means a component prop can be read inside a derived expression:
145
+
146
+ ```python
147
+ tk.Label(text=lambda: f"Hello {self.props.name()}")
148
+ ```
149
+
150
+ or forwarded directly to a widget prop:
151
+
152
+ ```python
153
+ tk.Label(text=self.props.name)
154
+ ```
155
+
156
+ Component children are also collected into `props.children`, so container-like
157
+ components can feel like Solid components instead of manually passing a
158
+ `children=` prop:
159
+
160
+ ```python
161
+ @component
162
+ def panel(props):
163
+ return layout.VStack(
164
+ tk.Label(text=props.title),
165
+ props.children(),
166
+ padding=8,
167
+ )
168
+
169
+ panel(tk.Label(text="Nested"), title="Details")
170
+ ```
171
+
172
+ Control-flow nodes and `Fragment(...)` are transparent to layout. They produce
173
+ children; the parent widget or layout helper decides where those children go:
174
+
175
+ ```python
176
+ @component
177
+ def rows(props):
178
+ return For(props.items, lambda item: tk.Label(text=item), key=lambda item: item)
179
+
180
+ layout.VStack(
181
+ tk.Label(text="Before"),
182
+ rows(items=todos),
183
+ tk.Label(text="After"),
184
+ )
185
+ ```
186
+
187
+ In that example, the repeated labels are laid out by `VStack` between
188
+ `Before` and `After`; `For` and the component do not create wrapper frames.
189
+
190
+ ## Benchmarks
191
+
192
+ To estimate framework overhead without needing a display server, run the fake-Tk
193
+ benchmark:
194
+
195
+ ```sh
196
+ uv run python benchmarks/bench_overhead.py
197
+ ```
198
+
199
+ It compares raw Tk-style widget creation with taut.tk mounting for static
200
+ labels, reactive label props, and a component wrapper. The most useful line is
201
+ the reported extra microseconds per widget, because native Tk/Tcl startup and
202
+ platform display behavior are intentionally excluded.
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "taut.tk"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Sam I", email = "aweptimum@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "reaktiv>=0.22.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ taut = "taut.cli:main"
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pillow>=12.2.0",
20
+ "watchdog>=6.0.0",
21
+ ]
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.11.21,<0.12.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [tool.uv.build-backend]
28
+ module-name = "taut"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pyright>=1.1.410",
33
+ "pytest>=9.1.1",
34
+ "ruff>=0.15.18",
35
+ ]
@@ -0,0 +1,35 @@
1
+ """A Solid-style layer for Tkinter powered by reaktiv."""
2
+
3
+ from . import context
4
+ from . import control
5
+ from . import layout
6
+ from . import props
7
+ from . import reactive
8
+ from . import resource
9
+ from . import runtime
10
+ from . import stores
11
+ from . import style
12
+ from . import tk
13
+ from . import tk_props
14
+ from . import ttk
15
+ from .component import Component
16
+ from .component import component
17
+ from .runtime import Fragment
18
+
19
+ __all__ = [
20
+ "Component",
21
+ "Fragment",
22
+ "component",
23
+ "context",
24
+ "control",
25
+ "layout",
26
+ "props",
27
+ "reactive",
28
+ "resource",
29
+ "runtime",
30
+ "style",
31
+ "stores",
32
+ "tk",
33
+ "tk_props",
34
+ "ttk",
35
+ ]
@@ -0,0 +1,36 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from taut.cli.stubs import main_cli as stubby
5
+ from taut.cli.watch import main as watcher
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(description="taut.tk development tools.")
10
+ subparsers = parser.add_subparsers(dest="command", required=True)
11
+
12
+ parser_stubs = subparsers.add_parser("stubs", help="Generate taut.tk .pyi stubs.")
13
+ parser_stubs.add_argument("paths", nargs="+", type=Path)
14
+ parser_stubs.add_argument(
15
+ "-o",
16
+ "--out-dir",
17
+ default="typings",
18
+ type=Path,
19
+ help="Directory for generated stubs. Defaults to typings.",
20
+ )
21
+ parser_stubs.add_argument(
22
+ "--in-place",
23
+ action="store_true",
24
+ help="Write sibling .pyi files next to source files.",
25
+ )
26
+ parser_stubs.set_defaults(func=stubby)
27
+
28
+ parser_watch = subparsers.add_parser("watch", help="Regenerate .pyi stubs on edits.")
29
+ parser_watch.set_defaults(func=watcher)
30
+
31
+ args = parser.parse_args()
32
+ args.func(args)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class PropField:
9
+ name: str
10
+ internal_type: str
11
+ external_type: str
12
+
13
+
14
+ @dataclass
15
+ class ComponentStub:
16
+ name: str
17
+ fields: list[PropField]
18
+
19
+
20
+ @dataclass
21
+ class ImportStub:
22
+ module: str
23
+ name: str
24
+ export_name: str
25
+
26
+
27
+ def collect_protocols(module: ast.Module) -> dict[str, list[PropField]]:
28
+ protocols: dict[str, list[PropField]] = {}
29
+ for node in module.body:
30
+ if not isinstance(node, ast.ClassDef):
31
+ continue
32
+ if not any(base_name(base) == "Protocol" for base in node.bases):
33
+ continue
34
+
35
+ protocols[node.name] = [
36
+ PropField(
37
+ statement.target.id,
38
+ ast.unparse(statement.annotation),
39
+ external_prop_type(ast.unparse(statement.annotation)),
40
+ )
41
+ for statement in node.body
42
+ if isinstance(statement, ast.AnnAssign)
43
+ and isinstance(statement.target, ast.Name)
44
+ ]
45
+ return protocols
46
+
47
+
48
+ def collect_components(
49
+ module: ast.Module,
50
+ protocols: dict[str, list[PropField]],
51
+ ) -> list[ComponentStub]:
52
+ components: list[ComponentStub] = []
53
+ for node in module.body:
54
+ if isinstance(node, ast.FunctionDef) and is_component_function(node):
55
+ protocol_name = first_arg_annotation(node)
56
+ elif isinstance(node, ast.ClassDef) and is_component_class(node):
57
+ protocol_name = init_props_annotation(node)
58
+ else:
59
+ continue
60
+
61
+ if protocol_name is None:
62
+ components.append(ComponentStub(node.name, []))
63
+ elif protocol_name in protocols:
64
+ components.append(ComponentStub(node.name, protocols[protocol_name]))
65
+ return components
66
+
67
+
68
+ def collect_public_imports(module: ast.Module) -> list[ImportStub]:
69
+ imports: list[ImportStub] = []
70
+ for node in module.body:
71
+ if not isinstance(node, ast.ImportFrom) or node.module is None:
72
+ continue
73
+ if node.module in {"__future__", "typing"} or node.module.startswith("solid_tk"):
74
+ continue
75
+ module_name = "." * node.level + node.module
76
+ for alias in node.names:
77
+ export_name = alias.asname or alias.name
78
+ if export_name.startswith("_"):
79
+ continue
80
+ imports.append(ImportStub(module_name, alias.name, export_name))
81
+ return imports
82
+
83
+
84
+ def render_component(component: ComponentStub) -> list[str]:
85
+ fields = [field for field in component.fields if field.name != "children"]
86
+ children_field = next(
87
+ (field for field in component.fields if field.name == "children"),
88
+ None,
89
+ )
90
+
91
+ if not component.fields:
92
+ return [
93
+ f"def {component.name}("
94
+ "*child_nodes: Any, children: Any = ..."
95
+ ") -> runtime.Node: ..."
96
+ ]
97
+
98
+ lines = [f"def {component.name}(", " *child_nodes: Any,"]
99
+ lines.extend(f" {field.name}: {field.external_type}," for field in fields)
100
+ if children_field is not None:
101
+ lines.append(f" children: {children_field.external_type},")
102
+ else:
103
+ lines.append(" children: Any = ...,")
104
+ lines.append(") -> runtime.Node: ...")
105
+ return lines
106
+
107
+
108
+ def is_component_function(node: ast.FunctionDef) -> bool:
109
+ return any(base_name(decorator) == "component" for decorator in node.decorator_list)
110
+
111
+
112
+ def is_component_class(node: ast.ClassDef) -> bool:
113
+ return any(base_name(base) == "Component" for base in node.bases)
114
+
115
+
116
+ def first_arg_annotation(node: ast.FunctionDef) -> str | None:
117
+ if not node.args.args:
118
+ return None
119
+ annotation = node.args.args[0].annotation
120
+ return ast.unparse(annotation) if annotation is not None else None
121
+
122
+
123
+ def init_props_annotation(node: ast.ClassDef) -> str | None:
124
+ for statement in node.body:
125
+ if not isinstance(statement, ast.FunctionDef) or statement.name != "__init__":
126
+ continue
127
+ if len(statement.args.args) < 2:
128
+ return None
129
+ annotation = statement.args.args[1].annotation
130
+ return ast.unparse(annotation) if annotation is not None else None
131
+ return None
132
+
133
+
134
+ def external_prop_type(internal_type: str) -> str:
135
+ if internal_type.startswith("Accessor[") and internal_type.endswith("]"):
136
+ inner = internal_type.removeprefix("Accessor[").removesuffix("]")
137
+ return f"{inner} | reactive.Accessor[{inner}]"
138
+ if internal_type.startswith("reactive.Accessor[") and internal_type.endswith("]"):
139
+ inner = internal_type.removeprefix("reactive.Accessor[").removesuffix("]")
140
+ return f"{inner} | reactive.Accessor[{inner}]"
141
+ if internal_type.startswith("Mutator[") and internal_type.endswith("]"):
142
+ inner = internal_type.removeprefix("Mutator[").removesuffix("]")
143
+ return f"reactive.Mutator[{inner}]"
144
+ if internal_type.startswith("reactive.Mutator[") and internal_type.endswith("]"):
145
+ return internal_type
146
+ return "Any"
147
+
148
+
149
+ def base_name(node: ast.AST) -> str:
150
+ match node:
151
+ case ast.Name(id=name):
152
+ return name
153
+ case ast.Attribute(attr=name):
154
+ return name
155
+ case ast.Call(func=func):
156
+ return base_name(func)
157
+ case ast.Subscript(value=value):
158
+ return base_name(value)
159
+ case _:
160
+ return ""
161
+