streamtree 0.1.0__py3-none-any.whl

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.
streamtree/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Streamtree: declarative, typed composition for Streamlit."""
2
+
3
+ from streamtree import elements, state, testing
4
+ from streamtree.core import ComponentCall, Element, Fragment, component, fragment, render
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = [
9
+ "ComponentCall",
10
+ "Element",
11
+ "Fragment",
12
+ "__version__",
13
+ "component",
14
+ "elements",
15
+ "fragment",
16
+ "render",
17
+ "state",
18
+ "testing",
19
+ ]
@@ -0,0 +1,13 @@
1
+ """Core primitives: elements, components, render."""
2
+
3
+ from streamtree.core.component import component, render
4
+ from streamtree.core.element import ComponentCall, Element, Fragment, fragment
5
+
6
+ __all__ = [
7
+ "ComponentCall",
8
+ "Element",
9
+ "Fragment",
10
+ "component",
11
+ "fragment",
12
+ "render",
13
+ ]
@@ -0,0 +1,36 @@
1
+ """@component decorator and public render entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from typing import ParamSpec, TypeVar
8
+
9
+ from streamtree.core.context import render_context
10
+ from streamtree.core.element import ComponentCall, Element
11
+ from streamtree.renderers.streamlit import render_element as _render_streamlit
12
+
13
+ P = ParamSpec("P")
14
+ R = TypeVar("R", bound=Element)
15
+
16
+
17
+ def component(fn: Callable[P, R]) -> Callable[P, ComponentCall]:
18
+ """Mark a function as a Streamtree component.
19
+
20
+ The function body runs when the tree is rendered (each Streamlit rerun),
21
+ not when building the virtual tree from call sites like ``Page(Counter())``.
22
+ """
23
+
24
+ @wraps(fn)
25
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ComponentCall:
26
+ kw = dict(kwargs)
27
+ key = kw.pop("key", None) # type: ignore[assignment]
28
+ return ComponentCall(fn=fn, args=args, kwargs=kw, key=key) # type: ignore[arg-type]
29
+
30
+ return wrapper # type: ignore[return-value]
31
+
32
+
33
+ def render(root: Element, *, context_root: str = "app") -> None:
34
+ """Render a virtual element tree using the Streamlit backend."""
35
+ with render_context(context_root):
36
+ _render_streamlit(root)
@@ -0,0 +1,66 @@
1
+ """Render-time context for stable keys and nested component paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
8
+ from dataclasses import dataclass, field
9
+
10
+ _ctx: ContextVar[RenderContext | None] = ContextVar("streamtree_render_context", default=None)
11
+
12
+
13
+ @dataclass
14
+ class RenderContext:
15
+ """Stacked path segments and a per-scope counter for anonymous state slots."""
16
+
17
+ parent: RenderContext | None
18
+ segment: str
19
+ _counter: int = field(default=0, repr=False)
20
+
21
+ def child(self, segment: str) -> RenderContext:
22
+ return RenderContext(parent=self, segment=segment)
23
+
24
+ def next_anonymous_index(self) -> int:
25
+ i = self._counter
26
+ self._counter += 1
27
+ return i
28
+
29
+ def path(self) -> str:
30
+ if self.parent is None:
31
+ return self.segment
32
+ base = self.parent.path()
33
+ return f"{base}.{self.segment}" if base else self.segment
34
+
35
+
36
+ def current_context() -> RenderContext:
37
+ c = _ctx.get()
38
+ if c is None:
39
+ raise RuntimeError("streamtree: no active render context; call from inside render()")
40
+ return c
41
+
42
+
43
+ @contextmanager
44
+ def render_context(root_segment: str = "app") -> Iterator[RenderContext]:
45
+ parent = _ctx.get()
46
+ ctx = (
47
+ RenderContext(parent=parent, segment=root_segment)
48
+ if parent
49
+ else RenderContext(parent=None, segment=root_segment)
50
+ )
51
+ token = _ctx.set(ctx)
52
+ try:
53
+ yield ctx
54
+ finally:
55
+ _ctx.reset(token)
56
+
57
+
58
+ @contextmanager
59
+ def push_segment(segment: str) -> Iterator[RenderContext]:
60
+ parent = current_context()
61
+ ctx = parent.child(segment)
62
+ token = _ctx.set(ctx)
63
+ try:
64
+ yield ctx
65
+ finally:
66
+ _ctx.reset(token)
@@ -0,0 +1,51 @@
1
+ """Virtual element tree nodes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, TypeAlias
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Element:
12
+ """Base class for virtual tree nodes."""
13
+
14
+ key: str | None = None
15
+
16
+
17
+ ElementChild: TypeAlias = Element | Sequence[Element | None] | None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Fragment(Element):
22
+ """Group multiple children without a Streamlit container."""
23
+
24
+ children: tuple[Element, ...] = field(default_factory=tuple)
25
+
26
+
27
+ def normalize_children(children: tuple[ElementChild, ...]) -> tuple[Element, ...]:
28
+ out: list[Element] = []
29
+ for ch in children:
30
+ if ch is None:
31
+ continue
32
+ if isinstance(ch, Element):
33
+ out.append(ch)
34
+ elif isinstance(ch, Sequence) and not isinstance(ch, (str, bytes)):
35
+ out.extend(normalize_children(tuple(ch))) # type: ignore[arg-type]
36
+ else:
37
+ raise TypeError(f"Invalid child type: {type(ch)!r}")
38
+ return tuple(out)
39
+
40
+
41
+ def fragment(*children: ElementChild, key: str | None = None) -> Fragment:
42
+ return Fragment(key=key, children=normalize_children(children))
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class ComponentCall(Element):
47
+ """Deferred invocation of a @component function."""
48
+
49
+ fn: Callable[..., Element] = field(kw_only=True, repr=False)
50
+ args: tuple[Any, ...] = field(default_factory=tuple, kw_only=True)
51
+ kwargs: dict[str, Any] = field(default_factory=dict, kw_only=True)
@@ -0,0 +1,59 @@
1
+ """Declarative layout and widget primitives."""
2
+
3
+ from streamtree.core.element import Element, Fragment, fragment
4
+ from streamtree.elements.layout import (
5
+ Card,
6
+ Columns,
7
+ Expander,
8
+ Form,
9
+ Grid,
10
+ HStack,
11
+ Page,
12
+ Sidebar,
13
+ Spacer,
14
+ Tabs,
15
+ VStack,
16
+ )
17
+ from streamtree.elements.widgets import (
18
+ Button,
19
+ Checkbox,
20
+ DataFrame,
21
+ Divider,
22
+ Image,
23
+ Markdown,
24
+ NumberInput,
25
+ Selectbox,
26
+ Subheader,
27
+ Text,
28
+ TextInput,
29
+ Title,
30
+ )
31
+
32
+ __all__ = [
33
+ "Button",
34
+ "Card",
35
+ "Checkbox",
36
+ "Columns",
37
+ "DataFrame",
38
+ "Divider",
39
+ "Element",
40
+ "Expander",
41
+ "Form",
42
+ "Fragment",
43
+ "Grid",
44
+ "HStack",
45
+ "Image",
46
+ "Markdown",
47
+ "NumberInput",
48
+ "Page",
49
+ "Selectbox",
50
+ "Sidebar",
51
+ "Spacer",
52
+ "Subheader",
53
+ "Tabs",
54
+ "Text",
55
+ "TextInput",
56
+ "Title",
57
+ "VStack",
58
+ "fragment",
59
+ ]
@@ -0,0 +1,164 @@
1
+ """Layout primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from streamtree.core.element import Element, ElementChild, normalize_children
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class VStack(Element):
12
+ """Render children vertically in order."""
13
+
14
+ children: tuple[Element, ...] = field(default_factory=tuple)
15
+
16
+ def __init__(self, *children: ElementChild, key: str | None = None) -> None:
17
+ object.__setattr__(self, "key", key)
18
+ object.__setattr__(self, "children", normalize_children(children))
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class HStack(Element):
23
+ """Render children in a horizontal row of equal columns."""
24
+
25
+ children: tuple[Element, ...] = field(default_factory=tuple)
26
+ gap: str | None = None
27
+
28
+ def __init__(
29
+ self,
30
+ *children: ElementChild,
31
+ gap: str | None = None,
32
+ key: str | None = None,
33
+ ) -> None:
34
+ object.__setattr__(self, "key", key)
35
+ object.__setattr__(self, "gap", gap)
36
+ object.__setattr__(self, "children", normalize_children(children))
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Columns(Element):
41
+ """Render children in weighted columns (``weights`` length must match children)."""
42
+
43
+ children: tuple[Element, ...] = field(default_factory=tuple)
44
+ weights: tuple[float, ...] = field(default_factory=tuple)
45
+
46
+ def __init__(
47
+ self,
48
+ *children: ElementChild,
49
+ weights: tuple[float, ...] | None = None,
50
+ key: str | None = None,
51
+ ) -> None:
52
+ ch = normalize_children(children)
53
+ object.__setattr__(self, "key", key)
54
+ object.__setattr__(self, "children", ch)
55
+ if weights is None:
56
+ object.__setattr__(self, "weights", tuple(1.0 for _ in ch))
57
+ else:
58
+ object.__setattr__(self, "weights", weights)
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Grid(Element):
63
+ """Render children left-to-right in a fixed column count."""
64
+
65
+ children: tuple[Element, ...] = field(default_factory=tuple)
66
+ columns: int = 2
67
+
68
+ def __init__(self, *children: ElementChild, columns: int = 2, key: str | None = None) -> None:
69
+ object.__setattr__(self, "key", key)
70
+ object.__setattr__(self, "columns", max(1, columns))
71
+ object.__setattr__(self, "children", normalize_children(children))
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class Card(Element):
76
+ """Group content in a bordered container when supported by Streamlit."""
77
+
78
+ children: tuple[Element, ...] = field(default_factory=tuple)
79
+
80
+ def __init__(self, *children: ElementChild, key: str | None = None) -> None:
81
+ object.__setattr__(self, "key", key)
82
+ object.__setattr__(self, "children", normalize_children(children))
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class Page(Element):
87
+ """Top-level page container (column layout)."""
88
+
89
+ children: tuple[Element, ...] = field(default_factory=tuple)
90
+
91
+ def __init__(self, *children: ElementChild, key: str | None = None) -> None:
92
+ object.__setattr__(self, "key", key)
93
+ object.__setattr__(self, "children", normalize_children(children))
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class Tabs(Element):
98
+ """Named tabs; each entry is (title, child_element)."""
99
+
100
+ tabs: tuple[tuple[str, Element], ...] = ()
101
+
102
+ def __init__(self, *tabs: tuple[str, Element], key: str | None = None) -> None:
103
+ object.__setattr__(self, "key", key)
104
+ object.__setattr__(self, "tabs", tabs)
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class Sidebar(Element):
109
+ """Render children in ``st.sidebar``."""
110
+
111
+ children: tuple[Element, ...] = field(default_factory=tuple)
112
+
113
+ def __init__(self, *children: ElementChild, key: str | None = None) -> None:
114
+ object.__setattr__(self, "key", key)
115
+ object.__setattr__(self, "children", normalize_children(children))
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class Form(Element):
120
+ """Streamlit form boundary."""
121
+
122
+ form_key: str = "form"
123
+ clear_on_submit: bool = False
124
+ children: tuple[Element, ...] = field(default_factory=tuple)
125
+
126
+ def __init__(
127
+ self,
128
+ *children: ElementChild,
129
+ form_key: str = "form",
130
+ clear_on_submit: bool = False,
131
+ key: str | None = None,
132
+ ) -> None:
133
+ object.__setattr__(self, "key", key)
134
+ object.__setattr__(self, "form_key", form_key)
135
+ object.__setattr__(self, "clear_on_submit", clear_on_submit)
136
+ object.__setattr__(self, "children", normalize_children(children))
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class Expander(Element):
141
+ """Collapsible section."""
142
+
143
+ label: str = ""
144
+ expanded: bool = False
145
+ children: tuple[Element, ...] = field(default_factory=tuple)
146
+
147
+ def __init__(
148
+ self,
149
+ label: str,
150
+ *children: ElementChild,
151
+ expanded: bool = False,
152
+ key: str | None = None,
153
+ ) -> None:
154
+ object.__setattr__(self, "key", key)
155
+ object.__setattr__(self, "label", label)
156
+ object.__setattr__(self, "expanded", expanded)
157
+ object.__setattr__(self, "children", normalize_children(children))
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class Spacer(Element):
162
+ """Vertical breathing room."""
163
+
164
+ height: int | None = None
@@ -0,0 +1,235 @@
1
+ """Leaf widgets and controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal
8
+
9
+ from streamtree.core.element import Element
10
+ from streamtree.state import FormState, StateVar, ToggleState
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Text(Element):
15
+ body: str = ""
16
+
17
+ def __init__(self, body: str, *, key: str | None = None) -> None:
18
+ object.__setattr__(self, "key", key)
19
+ object.__setattr__(self, "body", body)
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Markdown(Element):
24
+ body: str = ""
25
+ unsafe_allow_html: bool = False
26
+
27
+ def __init__(
28
+ self,
29
+ body: str,
30
+ *,
31
+ unsafe_allow_html: bool = False,
32
+ key: str | None = None,
33
+ ) -> None:
34
+ object.__setattr__(self, "key", key)
35
+ object.__setattr__(self, "body", body)
36
+ object.__setattr__(self, "unsafe_allow_html", unsafe_allow_html)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Button(Element):
41
+ label: str = ""
42
+ on_click: Callable[[], None] | None = None
43
+ disabled: bool = False
44
+ submit: bool = False
45
+ help: str | None = None
46
+
47
+ def __init__(
48
+ self,
49
+ label: str,
50
+ *,
51
+ on_click: Callable[[], None] | None = None,
52
+ disabled: bool = False,
53
+ submit: bool = False,
54
+ help: str | None = None,
55
+ key: str | None = None,
56
+ ) -> None:
57
+ object.__setattr__(self, "key", key)
58
+ object.__setattr__(self, "label", label)
59
+ object.__setattr__(self, "on_click", on_click)
60
+ object.__setattr__(self, "disabled", disabled)
61
+ object.__setattr__(self, "submit", submit)
62
+ object.__setattr__(self, "help", help)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class TextInput(Element):
67
+ label: str = ""
68
+ value: str | StateVar[str] | FormState[str] | None = None
69
+ placeholder: str | None = None
70
+ disabled: bool = False
71
+ type: Literal["default", "password"] = "default"
72
+
73
+ def __init__(
74
+ self,
75
+ label: str,
76
+ *,
77
+ value: str | StateVar[str] | FormState[str] | None = None,
78
+ placeholder: str | None = None,
79
+ disabled: bool = False,
80
+ type: Literal["default", "password"] = "default",
81
+ key: str | None = None,
82
+ ) -> None:
83
+ object.__setattr__(self, "key", key)
84
+ object.__setattr__(self, "label", label)
85
+ object.__setattr__(self, "value", value)
86
+ object.__setattr__(self, "placeholder", placeholder)
87
+ object.__setattr__(self, "disabled", disabled)
88
+ object.__setattr__(self, "type", type)
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class NumberInput(Element):
93
+ label: str = ""
94
+ value: int | float | StateVar[int] | StateVar[float] | None = None
95
+ min_value: int | float | None = None
96
+ max_value: int | float | None = None
97
+ step: int | float | None = None
98
+ format: str | None = None
99
+ disabled: bool = False
100
+
101
+ def __init__(
102
+ self,
103
+ label: str,
104
+ *,
105
+ value: int | float | StateVar[int] | StateVar[float] | None = None,
106
+ min_value: int | float | None = None,
107
+ max_value: int | float | None = None,
108
+ step: int | float | None = None,
109
+ format: str | None = None,
110
+ disabled: bool = False,
111
+ key: str | None = None,
112
+ ) -> None:
113
+ object.__setattr__(self, "key", key)
114
+ object.__setattr__(self, "label", label)
115
+ object.__setattr__(self, "value", value)
116
+ object.__setattr__(self, "min_value", min_value)
117
+ object.__setattr__(self, "max_value", max_value)
118
+ object.__setattr__(self, "step", step)
119
+ object.__setattr__(self, "format", format)
120
+ object.__setattr__(self, "disabled", disabled)
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class Selectbox(Element):
125
+ label: str = ""
126
+ options: Sequence[Any] = ()
127
+ index: int | StateVar[int] | None = None
128
+ format_func: Callable[[Any], str] | None = None
129
+ disabled: bool = False
130
+
131
+ def __init__(
132
+ self,
133
+ label: str,
134
+ *,
135
+ options: Sequence[Any],
136
+ index: int | StateVar[int] | None = 0,
137
+ format_func: Callable[[Any], str] | None = None,
138
+ disabled: bool = False,
139
+ key: str | None = None,
140
+ ) -> None:
141
+ object.__setattr__(self, "key", key)
142
+ object.__setattr__(self, "label", label)
143
+ object.__setattr__(self, "options", tuple(options))
144
+ object.__setattr__(self, "index", index)
145
+ object.__setattr__(self, "format_func", format_func)
146
+ object.__setattr__(self, "disabled", disabled)
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class Checkbox(Element):
151
+ label: str = ""
152
+ value: bool | StateVar[bool] | ToggleState | None = None
153
+ disabled: bool = False
154
+
155
+ def __init__(
156
+ self,
157
+ label: str,
158
+ *,
159
+ value: bool | StateVar[bool] | ToggleState | None = None,
160
+ disabled: bool = False,
161
+ key: str | None = None,
162
+ ) -> None:
163
+ object.__setattr__(self, "key", key)
164
+ object.__setattr__(self, "label", label)
165
+ object.__setattr__(self, "value", value)
166
+ object.__setattr__(self, "disabled", disabled)
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class DataFrame(Element):
171
+ data: Any = None
172
+ width: int | None = None
173
+ height: int | None = None
174
+
175
+ def __init__(
176
+ self,
177
+ data: Any,
178
+ *,
179
+ width: int | None = None,
180
+ height: int | None = None,
181
+ key: str | None = None,
182
+ ) -> None:
183
+ object.__setattr__(self, "key", key)
184
+ object.__setattr__(self, "data", data)
185
+ object.__setattr__(self, "width", width)
186
+ object.__setattr__(self, "height", height)
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class Image(Element):
191
+ image: Any = None
192
+ caption: str | None = None
193
+ width: int | None = None
194
+ use_column_width: bool | Literal["auto", "always", "never"] | None = None
195
+
196
+ def __init__(
197
+ self,
198
+ image: Any,
199
+ *,
200
+ caption: str | None = None,
201
+ width: int | None = None,
202
+ use_column_width: bool | Literal["auto", "always", "never"] | None = None,
203
+ key: str | None = None,
204
+ ) -> None:
205
+ object.__setattr__(self, "key", key)
206
+ object.__setattr__(self, "image", image)
207
+ object.__setattr__(self, "caption", caption)
208
+ object.__setattr__(self, "width", width)
209
+ object.__setattr__(self, "use_column_width", use_column_width)
210
+
211
+
212
+ @dataclass(frozen=True)
213
+ class Divider(Element):
214
+ """Horizontal rule."""
215
+
216
+ def __init__(self, *, key: str | None = None) -> None:
217
+ object.__setattr__(self, "key", key)
218
+
219
+
220
+ @dataclass(frozen=True)
221
+ class Title(Element):
222
+ text: str = ""
223
+
224
+ def __init__(self, text: str, *, key: str | None = None) -> None:
225
+ object.__setattr__(self, "key", key)
226
+ object.__setattr__(self, "text", text)
227
+
228
+
229
+ @dataclass(frozen=True)
230
+ class Subheader(Element):
231
+ text: str = ""
232
+
233
+ def __init__(self, text: str, *, key: str | None = None) -> None:
234
+ object.__setattr__(self, "key", key)
235
+ object.__setattr__(self, "text", text)
streamtree/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """Rendering backends."""
2
+
3
+ from streamtree.renderers.streamlit import render_element
4
+
5
+ __all__ = ["render_element"]