pythonnative 0.32.0__py3-none-any.whl → 0.34.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.
Files changed (78) hide show
  1. pythonnative/__init__.py +27 -15
  2. pythonnative/animated.py +7 -13
  3. pythonnative/cli/pn.py +1 -2
  4. pythonnative/component.py +255 -0
  5. pythonnative/components/__init__.py +106 -0
  6. pythonnative/components/_base.py +132 -0
  7. pythonnative/components/controls.py +515 -0
  8. pythonnative/components/layout.py +459 -0
  9. pythonnative/components/lists.py +809 -0
  10. pythonnative/components/media.py +204 -0
  11. pythonnative/components/overlays.py +107 -0
  12. pythonnative/components/pressable.py +252 -0
  13. pythonnative/components/structural.py +167 -0
  14. pythonnative/components/text.py +291 -0
  15. pythonnative/diagnostics.py +1 -1
  16. pythonnative/element.py +108 -29
  17. pythonnative/gestures.py +1 -1
  18. pythonnative/hooks.py +366 -671
  19. pythonnative/hosts/__init__.py +85 -0
  20. pythonnative/hosts/android.py +269 -0
  21. pythonnative/hosts/base.py +665 -0
  22. pythonnative/hosts/desktop.py +107 -0
  23. pythonnative/hosts/ios.py +404 -0
  24. pythonnative/hot_reload.py +16 -24
  25. pythonnative/layout.py +372 -66
  26. pythonnative/native_modules/__init__.py +23 -0
  27. pythonnative/native_modules/net_info.py +5 -1
  28. pythonnative/native_views/__init__.py +1 -1
  29. pythonnative/native_views/android.py +50 -6
  30. pythonnative/native_views/base.py +64 -1
  31. pythonnative/native_views/desktop.py +11 -3
  32. pythonnative/native_views/ios.py +72 -12
  33. pythonnative/navigation/__init__.py +102 -0
  34. pythonnative/navigation/container.py +154 -0
  35. pythonnative/navigation/handle.py +573 -0
  36. pythonnative/navigation/hooks.py +94 -0
  37. pythonnative/navigation/host.py +58 -0
  38. pythonnative/navigation/linking.py +200 -0
  39. pythonnative/navigation/navigators.py +637 -0
  40. pythonnative/navigation/screen.py +149 -0
  41. pythonnative/navigation/state.py +248 -0
  42. pythonnative/net.py +4 -0
  43. pythonnative/platform_metrics.py +1 -1
  44. pythonnative/preview.py +21 -19
  45. pythonnative/project/android.py +7 -1
  46. pythonnative/project/doctor.py +1 -1
  47. pythonnative/project/ios.py +1 -0
  48. pythonnative/project/runtime_assets.py +1 -1
  49. pythonnative/reconciler/__init__.py +29 -0
  50. pythonnative/reconciler/boundaries.py +365 -0
  51. pythonnative/reconciler/children.py +88 -0
  52. pythonnative/reconciler/core.py +1153 -0
  53. pythonnative/reconciler/layout_pass.py +359 -0
  54. pythonnative/reconciler/vnode.py +266 -0
  55. pythonnative/scheduler.py +159 -0
  56. pythonnative/sdk/_components.py +2 -4
  57. pythonnative/style.py +95 -13
  58. pythonnative/suspense.py +8 -12
  59. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
  60. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
  61. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
  62. pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
  63. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
  64. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
  65. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
  66. pythonnative/testing/__init__.py +53 -0
  67. pythonnative/testing/backend.py +276 -0
  68. pythonnative/testing/harness.py +391 -0
  69. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
  70. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
  71. pythonnative/components.py +0 -2856
  72. pythonnative/navigation.py +0 -1031
  73. pythonnative/reconciler.py +0 -2298
  74. pythonnative/screen.py +0 -2068
  75. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
  76. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
  77. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
  78. {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,167 @@
1
+ """Structural factories: ``Fragment``, ``ErrorBoundary``, and ``Suspense``.
2
+
3
+ These produce elements whose ``type`` is one of the reconciler-owned
4
+ singletons from [`pythonnative.element`][pythonnative.element]
5
+ (``FRAGMENT``, ``ERROR_BOUNDARY``, ``SUSPENSE``) rather than a native
6
+ view name, so they never create a platform view of their own.
7
+ """
8
+
9
+ from typing import Any, Callable, Dict, Optional
10
+
11
+ from ..element import ERROR_BOUNDARY, FRAGMENT, SUSPENSE, Element
12
+
13
+
14
+ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
15
+ """Group children without adding a wrapping native view.
16
+
17
+ Like React's ``<></>``: groups elements without introducing an
18
+ extra container. Each child mounts as a direct sibling of the
19
+ Fragment's position in the parent's child list. Components may
20
+ also simply return a plain ``list`` of elements; ``Fragment``
21
+ exists for when the group needs a ``key`` (e.g. rendering a list
22
+ of pairs) or when a single expression reads better.
23
+
24
+ ```python
25
+ pn.Column(
26
+ pn.Text("Top"),
27
+ pn.Fragment(
28
+ pn.Text("Middle A"),
29
+ pn.Text("Middle B"),
30
+ ),
31
+ pn.Text("Bottom"),
32
+ )
33
+ ```
34
+
35
+ Args:
36
+ *children: Child elements to expose at the parent level.
37
+ ``None`` and ``False`` children are dropped, which makes
38
+ conditional rendering with ``cond and pn.Text(...)``
39
+ ergonomic.
40
+ key: Stable identity for keyed reconciliation. A keyed
41
+ Fragment moves all of its children as one unit and
42
+ preserves their state across reorders.
43
+
44
+ Returns:
45
+ An [`Element`][pythonnative.Element] whose type is
46
+ [`FRAGMENT`][pythonnative.element.FRAGMENT].
47
+ """
48
+ kept = [c for c in children if c is not None and c is not False]
49
+ return Element(FRAGMENT, {}, kept, key=key)
50
+
51
+
52
+ def ErrorBoundary(
53
+ *children: Element,
54
+ fallback: Optional[Any] = None,
55
+ on_error: Optional[Callable[[BaseException], Any]] = None,
56
+ key: Optional[str] = None,
57
+ ) -> Element:
58
+ """Catch render errors in the wrapped subtree and display ``fallback`` instead.
59
+
60
+ When any descendant raises during render (initial mount, a parent
61
+ re-render, or a local state-driven update), the failed subtree is
62
+ torn down and ``fallback`` is mounted in its place. Without a
63
+ boundary the error propagates to the screen host (which shows the
64
+ dev error overlay in dev mode).
65
+
66
+ ``fallback`` may be:
67
+
68
+ - An [`Element`][pythonnative.Element], shown as-is.
69
+ - ``fallback(error) -> Element``.
70
+ - ``fallback(error, reset) -> Element``, where ``reset`` is a
71
+ zero-arg callable that clears the error and remounts the
72
+ original children (fresh state), for retry buttons.
73
+
74
+ Args:
75
+ *children: Subtree to wrap.
76
+ fallback: Fallback content (see above). Required for the
77
+ boundary to actually catch; without it errors propagate
78
+ to the next boundary up.
79
+ on_error: Callback invoked with the exception when the
80
+ boundary catches, before the fallback mounts. Use it for
81
+ error reporting.
82
+ key: Stable identity for keyed reconciliation.
83
+
84
+ Returns:
85
+ An [`Element`][pythonnative.Element] whose type is
86
+ [`ERROR_BOUNDARY`][pythonnative.element.ERROR_BOUNDARY], with
87
+ ``fallback`` and ``on_error`` in its props (omitted when
88
+ ``None``).
89
+
90
+ Example:
91
+ ```python
92
+ import pythonnative as pn
93
+
94
+ pn.ErrorBoundary(
95
+ MyRiskyComponent(),
96
+ fallback=lambda err, reset: pn.Column(
97
+ pn.Text(f"Error: {err}"),
98
+ pn.Button("Retry", on_press=reset),
99
+ ),
100
+ on_error=lambda err: log.exception(err),
101
+ )
102
+ ```
103
+ """
104
+ props: Dict[str, Any] = {}
105
+ if fallback is not None:
106
+ props["fallback"] = fallback
107
+ if on_error is not None:
108
+ props["on_error"] = on_error
109
+ return Element(ERROR_BOUNDARY, props, list(children), key=key)
110
+
111
+
112
+ def Suspense(
113
+ *children: Element,
114
+ fallback: Optional[Any] = None,
115
+ key: Optional[str] = None,
116
+ ) -> Element:
117
+ """Show ``fallback`` while descendants wait on async work, then swap in the content.
118
+
119
+ A Suspense boundary catches **suspensions** from the subtree it
120
+ wraps: an ``async def`` component body blocking on a pending
121
+ await, or a regular component calling
122
+ [`Resource.read`][pythonnative.suspense.Resource.read] on data that hasn't
123
+ arrived (see [`use_resource`][pythonnative.use_resource] and
124
+ [`lazy`][pythonnative.lazy]). While anything is pending the
125
+ boundary renders ``fallback``; when the awaited work completes it
126
+ retries the content and swaps it in. Suspended components keep
127
+ their hook state across retries, so cached resources aren't
128
+ refetched.
129
+
130
+ Two timing behaviors, matching React:
131
+
132
+ - **Initial mount**: the fallback shows until the content is ready.
133
+ - **Updates**: a component that's already on screen and suspends
134
+ again (its dependencies changed) keeps its previous content
135
+ visible and re-renders when ready; there's no fallback flash.
136
+
137
+ Args:
138
+ *children: Subtree to wrap (the async content).
139
+ fallback: Content shown while suspended: an
140
+ [`Element`][pythonnative.Element] or a zero-arg callable
141
+ returning one. Without it, suspensions propagate to the
142
+ next Suspense boundary up.
143
+ key: Stable identity for keyed reconciliation.
144
+
145
+ Returns:
146
+ An [`Element`][pythonnative.Element] whose type is
147
+ [`SUSPENSE`][pythonnative.element.SUSPENSE], with ``fallback``
148
+ in its props.
149
+
150
+ Example:
151
+ ```python
152
+ import pythonnative as pn
153
+
154
+ @pn.component
155
+ async def Profile(user_id: str):
156
+ user = await api.fetch_user(user_id)
157
+ return pn.Text(user.name)
158
+
159
+ @pn.component
160
+ def Screen():
161
+ return pn.Suspense(
162
+ Profile(user_id="42"),
163
+ fallback=pn.ActivityIndicator(),
164
+ )
165
+ ```
166
+ """
167
+ return Element(SUSPENSE, {"fallback": fallback}, list(children), key=key)
@@ -0,0 +1,291 @@
1
+ """Text-centric leaf factories: ``Text``, ``Button``, and ``TextInput``."""
2
+
3
+ from typing import Any, Callable, Literal, Optional
4
+
5
+ from ..element import Element
6
+ from ..hooks import Ref
7
+ from ..style import (
8
+ AccessibilityState,
9
+ AutoCapitalize,
10
+ Color,
11
+ KeyboardType,
12
+ ReturnKeyType,
13
+ StyleProp,
14
+ )
15
+ from ._base import _flatten_text_spans, _make_element
16
+
17
+
18
+ def Text(
19
+ *parts: Any,
20
+ style: StyleProp = None,
21
+ accessibility_label: Optional[str] = None,
22
+ accessibility_hint: Optional[str] = None,
23
+ accessibility_role: Optional[str] = None,
24
+ accessible: Optional[bool] = None,
25
+ accessibility_state: Optional[AccessibilityState] = None,
26
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
27
+ test_id: Optional[str] = None,
28
+ ref: Optional[Ref] = None,
29
+ key: Optional[str] = None,
30
+ ) -> Element:
31
+ """Display a string of text, optionally with styled nested spans.
32
+
33
+ Style properties: ``font_size``, ``color``, ``bold``,
34
+ ``font_weight``, ``font_family``, ``italic``, ``text_align``,
35
+ ``background_color``, ``max_lines``, ``letter_spacing``,
36
+ ``line_height``, ``text_decoration`` (``"underline"`` /
37
+ ``"line_through"``), ``border_radius``, ``border_width``,
38
+ ``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
39
+ the common layout props.
40
+
41
+ **Rich text**: pass multiple parts, mixing plain strings and
42
+ nested ``Text`` elements, to render one paragraph with per-span
43
+ styling (a single ``TextView`` / ``UILabel`` natively, so line
44
+ wrapping flows across spans):
45
+
46
+ ```python
47
+ pn.Text(
48
+ "Hello, ",
49
+ pn.Text("world", style=pn.style(bold=True, color="#0A84FF")),
50
+ "!",
51
+ style=pn.style(font_size=18),
52
+ )
53
+ ```
54
+
55
+ Nested spans inherit the outer element's text styling and may
56
+ override ``color``, ``background_color``, ``font_size``,
57
+ ``font_family``, ``font_weight``, ``bold``, ``italic``,
58
+ ``text_decoration``, and ``letter_spacing``.
59
+
60
+ Args:
61
+ *parts: Text content: a single string, or any mix of strings
62
+ and nested ``Text`` elements for rich text.
63
+ style: Style dict (or list of dicts).
64
+ accessibility_label: Spoken description for screen readers.
65
+ accessibility_hint: Spoken extra detail (iOS only).
66
+ accessibility_role: Semantic role for assistive tech.
67
+ accessible: Override whether the element is exposed to AT.
68
+ accessibility_state: Current widget state for assistive tech,
69
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
70
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
71
+ ``expanded``.
72
+ accessibility_live_region: How AT announces dynamic changes to
73
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
74
+ (Android only).
75
+ test_id: Stable identifier for UI tests; exposed as
76
+ ``resource-id`` on Android and ``accessibilityIdentifier``
77
+ on iOS.
78
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
79
+ key: Stable identity for keyed reconciliation.
80
+
81
+ Returns:
82
+ An [`Element`][pythonnative.Element] of type ``"Text"``.
83
+ """
84
+ rich = any(isinstance(p, Element) for p in parts) or len(parts) > 1
85
+ if rich:
86
+ spans = _flatten_text_spans(parts, {})
87
+ text = "".join(s["text"] for s in spans)
88
+ else:
89
+ spans = None
90
+ text = str(parts[0]) if parts else ""
91
+ return _make_element(
92
+ "Text",
93
+ style=style,
94
+ ref=ref,
95
+ key=key,
96
+ text=text,
97
+ spans=spans,
98
+ accessibility_label=accessibility_label,
99
+ accessibility_hint=accessibility_hint,
100
+ accessibility_role=accessibility_role,
101
+ accessible=accessible,
102
+ accessibility_state=accessibility_state,
103
+ accessibility_live_region=accessibility_live_region,
104
+ test_id=test_id,
105
+ )
106
+
107
+
108
+ def Button(
109
+ title: str = "",
110
+ *,
111
+ on_press: Optional[Callable[[], Any]] = None,
112
+ enabled: bool = True,
113
+ style: StyleProp = None,
114
+ accessibility_label: Optional[str] = None,
115
+ accessibility_hint: Optional[str] = None,
116
+ accessibility_role: Optional[str] = None,
117
+ accessible: Optional[bool] = None,
118
+ accessibility_state: Optional[AccessibilityState] = None,
119
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
120
+ test_id: Optional[str] = None,
121
+ ref: Optional[Ref] = None,
122
+ key: Optional[str] = None,
123
+ ) -> Element:
124
+ """Display a tappable button.
125
+
126
+ Style properties: ``color``, ``background_color``, ``font_size``,
127
+ ``border_radius``, ``border_width``, ``border_color``, ``shadow_*``,
128
+ ``opacity``, ``transform``, plus the common layout props.
129
+
130
+ Buttons get ``accessibility_role="button"`` by default.
131
+
132
+ Args:
133
+ title: Button label.
134
+ on_press: Callback invoked when the user taps the button.
135
+ enabled: When ``False``, the button is disabled and cannot be
136
+ tapped.
137
+ style: Style dict (or list of dicts).
138
+ accessibility_label: Spoken description for screen readers.
139
+ accessibility_hint: Spoken extra detail (iOS only).
140
+ accessibility_role: Override the default ``"button"`` role.
141
+ accessible: Override whether the element is exposed to AT.
142
+ accessibility_state: Current widget state for assistive tech,
143
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
144
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
145
+ ``expanded``.
146
+ accessibility_live_region: How AT announces dynamic changes to
147
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
148
+ (Android only).
149
+ test_id: Stable identifier for UI tests; exposed as
150
+ ``resource-id`` on Android and ``accessibilityIdentifier``
151
+ on iOS.
152
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
153
+ key: Stable identity for keyed reconciliation.
154
+
155
+ Returns:
156
+ An [`Element`][pythonnative.Element] of type ``"Button"``.
157
+ """
158
+ return _make_element(
159
+ "Button",
160
+ style=style,
161
+ ref=ref,
162
+ key=key,
163
+ title=title,
164
+ on_press=on_press,
165
+ enabled=enabled,
166
+ accessibility_label=accessibility_label,
167
+ accessibility_hint=accessibility_hint,
168
+ accessibility_role=accessibility_role,
169
+ accessible=accessible,
170
+ accessibility_state=accessibility_state,
171
+ accessibility_live_region=accessibility_live_region,
172
+ test_id=test_id,
173
+ _defaults={"accessibility_role": "button"},
174
+ )
175
+
176
+
177
+ def TextInput(
178
+ *,
179
+ value: str = "",
180
+ placeholder: Optional[str] = None,
181
+ on_change: Optional[Callable[[str], Any]] = None,
182
+ on_submit: Optional[Callable[[str], Any]] = None,
183
+ secure: bool = False,
184
+ multiline: bool = False,
185
+ keyboard_type: Optional[KeyboardType] = None,
186
+ auto_capitalize: Optional[AutoCapitalize] = None,
187
+ auto_correct: Optional[bool] = None,
188
+ auto_focus: bool = False,
189
+ return_key_type: Optional[ReturnKeyType] = None,
190
+ max_length: Optional[int] = None,
191
+ placeholder_color: Optional[Color] = None,
192
+ editable: bool = True,
193
+ clear_button: bool = False,
194
+ on_focus: Optional[Callable[[], Any]] = None,
195
+ on_blur: Optional[Callable[[], Any]] = None,
196
+ selection_color: Optional[Color] = None,
197
+ text_content_type: Optional[str] = None,
198
+ style: StyleProp = None,
199
+ accessibility_label: Optional[str] = None,
200
+ accessibility_hint: Optional[str] = None,
201
+ accessible: Optional[bool] = None,
202
+ accessibility_state: Optional[AccessibilityState] = None,
203
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
204
+ test_id: Optional[str] = None,
205
+ ref: Optional[Ref] = None,
206
+ key: Optional[str] = None,
207
+ ) -> Element:
208
+ """Display a text-entry field (single-line by default, or ``multiline``).
209
+
210
+ Style properties: ``font_size``, ``color``, ``background_color``,
211
+ ``border_*``, plus the common layout props.
212
+
213
+ Args:
214
+ value: Current text content (controlled-input pattern).
215
+ placeholder: Hint shown when ``value`` is empty.
216
+ on_change: Callback invoked with the new string each keystroke.
217
+ on_submit: Callback invoked when the user submits (Return /
218
+ Done / etc.). Receives the final text.
219
+ secure: When ``True``, characters are masked (use for passwords).
220
+ multiline: When ``True``, allows multiple lines of input.
221
+ keyboard_type: One of ``"default"``, ``"email_address"``,
222
+ ``"number_pad"``, ``"decimal_pad"``, ``"phone_pad"``, ``"url"``.
223
+ auto_capitalize: One of ``"none"``, ``"sentences"``, ``"words"``,
224
+ ``"characters"``.
225
+ auto_correct: Enable/disable autocorrection.
226
+ auto_focus: Request focus on mount.
227
+ return_key_type: One of ``"default"``, ``"done"``, ``"go"``,
228
+ ``"next"``, ``"send"``, ``"search"``.
229
+ max_length: Maximum number of characters allowed.
230
+ placeholder_color: Color used for the placeholder string.
231
+ editable: When ``False``, the field is read-only (still
232
+ selectable).
233
+ clear_button: When ``True``, shows a clear ("x") button while
234
+ editing (iOS ``clearButtonMode``; an inline button on
235
+ Android).
236
+ on_focus: Callback invoked when the field gains focus.
237
+ on_blur: Callback invoked when the field loses focus.
238
+ selection_color: Cursor / selection highlight color.
239
+ text_content_type: Semantic content hint for autofill (e.g.
240
+ ``"username"``, ``"password"``, ``"one_time_code"``).
241
+ style: Style dict (or list of dicts).
242
+ accessibility_label: Spoken description for screen readers.
243
+ accessibility_hint: Spoken extra detail (iOS only).
244
+ accessible: Override whether the element is exposed to AT.
245
+ accessibility_state: Current widget state for assistive tech,
246
+ e.g. ``{"disabled": True, "selected": False}``. Recognized
247
+ keys: ``disabled``, ``selected``, ``checked``, ``busy``,
248
+ ``expanded``.
249
+ accessibility_live_region: How AT announces dynamic changes to
250
+ this view: ``"none"``, ``"polite"``, or ``"assertive"``
251
+ (Android only).
252
+ test_id: Stable identifier for UI tests; exposed as
253
+ ``resource-id`` on Android and ``accessibilityIdentifier``
254
+ on iOS.
255
+ ref: Optional [`Ref`][pythonnative.Ref] from ``use_ref()``.
256
+ key: Stable identity for keyed reconciliation.
257
+
258
+ Returns:
259
+ An [`Element`][pythonnative.Element] of type ``"TextInput"``.
260
+ """
261
+ return _make_element(
262
+ "TextInput",
263
+ style=style,
264
+ ref=ref,
265
+ key=key,
266
+ value=value,
267
+ placeholder=placeholder,
268
+ on_change=on_change,
269
+ on_submit=on_submit,
270
+ secure=secure or None,
271
+ multiline=multiline or None,
272
+ keyboard_type=keyboard_type,
273
+ auto_capitalize=auto_capitalize,
274
+ auto_correct=auto_correct,
275
+ auto_focus=auto_focus or None,
276
+ return_key_type=return_key_type,
277
+ max_length=max_length,
278
+ placeholder_color=placeholder_color,
279
+ editable=False if editable is False else None,
280
+ clear_button=clear_button or None,
281
+ on_focus=on_focus,
282
+ on_blur=on_blur,
283
+ selection_color=selection_color,
284
+ text_content_type=text_content_type,
285
+ accessibility_label=accessibility_label,
286
+ accessibility_hint=accessibility_hint,
287
+ accessible=accessible,
288
+ accessibility_state=accessibility_state,
289
+ accessibility_live_region=accessibility_live_region,
290
+ test_id=test_id,
291
+ )
@@ -14,7 +14,7 @@ Dev mode turns on:
14
14
  immediately instead of cross-wiring state.
15
15
  - **The RedBox**: uncaught errors from render, effects, and event
16
16
  handlers are routed to the screen host, which presents a full-screen
17
- error overlay (see `pythonnative.screen`) instead of crashing or
17
+ error overlay (see `pythonnative.hosts`) instead of crashing or
18
18
  swallowing the traceback.
19
19
 
20
20
  In production none of this runs: validation is skipped, hook-order
pythonnative/element.py CHANGED
@@ -1,16 +1,29 @@
1
1
  """Lightweight element descriptors for the virtual view tree.
2
2
 
3
3
  An [`Element`][pythonnative.Element] is an immutable description of a UI
4
- node, analogous to a React element. It captures a type (name string or
5
- component function), a props dict, and an ordered list of children
6
- without creating any native platform objects. The reconciler consumes
7
- these trees to determine what native views must be created, updated, or
8
- removed.
4
+ node, analogous to a React element. It captures a type, a props dict,
5
+ and an ordered list of children without creating any native platform
6
+ objects. The reconciler consumes these trees to determine what native
7
+ views must be created, updated, or removed.
8
+
9
+ An element's ``type`` is one of three things:
10
+
11
+ - a ``str`` naming a **native view** (``"Text"``, ``"View"``, ...),
12
+ - a [`Component`][pythonnative.Component] produced by
13
+ [`@component`][pythonnative.component.component], or
14
+ - a **structural type**: one of the singletons defined here
15
+ ([`FRAGMENT`][pythonnative.element.FRAGMENT],
16
+ [`ERROR_BOUNDARY`][pythonnative.element.ERROR_BOUNDARY],
17
+ [`SUSPENSE`][pythonnative.element.SUSPENSE]) or a
18
+ [`Context`][pythonnative.Context] (whose elements are providers).
19
+
20
+ Structural types are real objects rather than magic strings so the
21
+ reconciler can dispatch on them with identity checks and no user
22
+ element can collide with them.
9
23
 
10
24
  Elements are produced by built-in factories such as
11
25
  [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], and
12
- [`Column`][pythonnative.Column], or by calling functions decorated with
13
- [`component`][pythonnative.component].
26
+ [`Column`][pythonnative.Column], or by calling components.
14
27
 
15
28
  Example:
16
29
  ```python
@@ -20,48 +33,86 @@ Example:
20
33
  ```
21
34
  """
22
35
 
23
- from typing import Any, Dict, List, Optional, Union
36
+ from __future__ import annotations
37
+
38
+ from typing import Any, Dict, Iterable, List, Optional, Union
39
+
40
+ __all__ = [
41
+ "ERROR_BOUNDARY",
42
+ "FRAGMENT",
43
+ "SUSPENSE",
44
+ "Element",
45
+ "Node",
46
+ "StructuralType",
47
+ "type_label",
48
+ ]
49
+
50
+
51
+ class StructuralType:
52
+ """Identity object naming a reconciler-owned element kind.
53
+
54
+ Instances are singletons compared by identity; ``repr`` shows the
55
+ kind for debugging (``<Fragment>``).
56
+ """
57
+
58
+ __slots__ = ("name",)
59
+
60
+ def __init__(self, name: str) -> None:
61
+ self.name = name
62
+
63
+ def __repr__(self) -> str:
64
+ return f"<{self.name}>"
65
+
66
+
67
+ FRAGMENT = StructuralType("Fragment")
68
+ """Type of [`Fragment`][pythonnative.Fragment] elements: a transparent group."""
69
+
70
+ ERROR_BOUNDARY = StructuralType("ErrorBoundary")
71
+ """Type of [`ErrorBoundary`][pythonnative.ErrorBoundary] elements."""
72
+
73
+ SUSPENSE = StructuralType("Suspense")
74
+ """Type of [`Suspense`][pythonnative.Suspense] elements."""
24
75
 
25
76
 
26
77
  class Element:
27
78
  """Immutable description of a single UI node.
28
79
 
29
- Built-in elements use a string `type` (`"Text"`, `"Button"`,
30
- `"Column"`, etc.); function components use the function itself as
31
- `type`. The reconciler dispatches on this distinction when mounting
32
- the tree.
80
+ Built-in elements use a string ``type`` (``"Text"``, ``"Button"``,
81
+ ``"Column"``, etc.); components use the
82
+ [`Component`][pythonnative.Component] object itself as ``type``;
83
+ structural elements use a [`StructuralType`][pythonnative.element.StructuralType]
84
+ or a [`Context`][pythonnative.Context]. The reconciler dispatches on
85
+ this distinction when mounting the tree.
33
86
 
34
87
  Attributes:
35
- type: A string for built-in elements or a callable for function
36
- components decorated with [`component`][pythonnative.component].
37
- props: Dict of properties passed to the native handler or
88
+ type: The element kind (see the module docstring).
89
+ props: Dict of properties passed to the native handler or the
38
90
  component function.
39
- children: Ordered list of child `Element` instances. `None` and
40
- `False` entries are permitted and dropped during
41
- reconciliation, so conditional children (`cond and Text(...)`)
42
- need no special casing.
91
+ children: Ordered list of child nodes. ``None`` and ``False``
92
+ entries are permitted and dropped during reconciliation, so
93
+ conditional children (``cond and Text(...)``) need no special
94
+ casing. Components receive these as their ``*children``.
43
95
  key: Optional stable identity used by the reconciler when
44
- diffing keyed lists. Two elements with the same `type` and
45
- `key` are treated as the same logical node across renders.
96
+ diffing keyed lists. Two elements with the same ``type`` and
97
+ ``key`` are treated as the same logical node across renders.
46
98
  """
47
99
 
48
100
  __slots__ = ("type", "props", "children", "key")
49
101
 
50
102
  def __init__(
51
103
  self,
52
- type_name: Union[str, Any],
53
- props: Dict[str, Any],
54
- children: List[Any],
104
+ type_: Any,
105
+ props: Optional[Dict[str, Any]] = None,
106
+ children: Optional[Iterable[Any]] = None,
55
107
  key: Optional[str] = None,
56
108
  ) -> None:
57
- self.type = type_name
58
- self.props = props
59
- self.children = children
109
+ self.type = type_
110
+ self.props: Dict[str, Any] = props if props is not None else {}
111
+ self.children: List[Any] = list(children) if children is not None else []
60
112
  self.key = key
61
113
 
62
114
  def __repr__(self) -> str:
63
- t = self.type if isinstance(self.type, str) else getattr(self.type, "__name__", repr(self.type))
64
- return f"Element({t!r}, props={set(self.props)}, children={len(self.children)})"
115
+ return f"Element({type_label(self.type)!r}, props={sorted(self.props)}, children={len(self.children)})"
65
116
 
66
117
  def __eq__(self, other: object) -> bool:
67
118
  if not isinstance(other, Element):
@@ -78,3 +129,31 @@ class Element:
78
129
  if result is NotImplemented:
79
130
  return result
80
131
  return not result
132
+
133
+ __hash__ = None # type: ignore[assignment,unused-ignore]
134
+
135
+ def with_key(self, key: Optional[str]) -> "Element":
136
+ """Return a copy of this element carrying ``key``.
137
+
138
+ Handy when a factory result needs a key after the fact, for
139
+ example while building a list comprehension over elements
140
+ produced by a helper that doesn't take ``key``.
141
+ """
142
+ return Element(self.type, self.props, self.children, key=key)
143
+
144
+
145
+ Node = Union[Element, None, bool, Iterable[Any]]
146
+ """Anything a component may render: an element, ``None`` / ``False``
147
+ for "nothing", or a (possibly nested) iterable of nodes."""
148
+
149
+
150
+ def type_label(type_obj: Any) -> str:
151
+ """Return a human-readable name for an element type (for messages)."""
152
+ if isinstance(type_obj, str):
153
+ return type_obj
154
+ if isinstance(type_obj, StructuralType):
155
+ return type_obj.name
156
+ name = getattr(type_obj, "display_name", None) or getattr(type_obj, "__name__", None)
157
+ if name:
158
+ return str(name)
159
+ return repr(type_obj)
pythonnative/gestures.py CHANGED
@@ -107,7 +107,7 @@ class GestureState:
107
107
 
108
108
  GestureStateName = Literal["began", "changed", "ended", "cancelled"]
109
109
 
110
- GestureCallback = Callable[["GestureEvent"], None]
110
+ GestureCallback = Callable[["GestureEvent"], Any]
111
111
 
112
112
  SwipeDirection = Literal["any", "left", "right", "up", "down"]
113
113