pythonnative 0.20.0__py3-none-any.whl → 0.22.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 (33) hide show
  1. pythonnative/__init__.py +14 -3
  2. pythonnative/animated.py +420 -135
  3. pythonnative/cli/pn.py +450 -956
  4. pythonnative/components.py +519 -235
  5. pythonnative/events.py +210 -0
  6. pythonnative/gestures.py +875 -0
  7. pythonnative/layout.py +463 -149
  8. pythonnative/mutations.py +130 -0
  9. pythonnative/native_views/__init__.py +161 -97
  10. pythonnative/native_views/android.py +1050 -1124
  11. pythonnative/native_views/base.py +108 -18
  12. pythonnative/native_views/desktop.py +460 -417
  13. pythonnative/native_views/ios.py +1918 -1916
  14. pythonnative/project/__init__.py +68 -0
  15. pythonnative/project/android.py +504 -0
  16. pythonnative/project/builder.py +555 -0
  17. pythonnative/project/config.py +642 -0
  18. pythonnative/project/doctor.py +233 -0
  19. pythonnative/project/icons.py +247 -0
  20. pythonnative/project/ios.py +344 -0
  21. pythonnative/project/permissions.py +343 -0
  22. pythonnative/project/runtime_assets.py +272 -0
  23. pythonnative/reconciler.py +540 -470
  24. pythonnative/screen.py +5 -2
  25. pythonnative/sdk/_components.py +2 -2
  26. pythonnative/templates/android_template/app/build.gradle +2 -0
  27. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
  28. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
  29. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
  30. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
  31. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
  32. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
  33. {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
1
+ """Batched mutation protocol between the reconciler and native backends.
2
+
3
+ The reconciler no longer talks to the native layer one call at a time.
4
+ Instead, every commit pass produces an ordered list of small mutation
5
+ ops referencing integer **tags** (stable per-view identifiers), and the
6
+ whole list is applied in a single
7
+ [`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]
8
+ call. This mirrors React Native's Fabric mounting layer: the diff phase
9
+ is pure, and the native side sees one coherent transaction per commit.
10
+
11
+ Why tags instead of view objects?
12
+
13
+ - The diff phase runs *before* any native view exists, so ops cannot
14
+ reference views directly.
15
+ - Tags give the native side a stable identity to key its own view
16
+ registry, event routing, and animation bookkeeping on.
17
+ - A flat list of `(op, tag, payload)` tuples is trivially serializable,
18
+ which keeps the door open for applying mutations from a background
19
+ thread or through a single JNI/ObjC crossing in the future.
20
+
21
+ Op ordering rules (the reconciler guarantees these):
22
+
23
+ 1. A `CreateOp` for a tag precedes any other op referencing that tag.
24
+ 2. `InsertOp` ops appear after both the parent and child exist.
25
+ 3. `DestroyOp` ops appear after the corresponding `RemoveOp` (if the
26
+ node was attached) and are emitted children-first.
27
+ 4. `SetFrameOp` ops are only emitted for frames that actually changed
28
+ since the last layout pass (frame diffing).
29
+ """
30
+
31
+ from dataclasses import dataclass, field
32
+ from typing import Any, Dict, Tuple, Union
33
+
34
+ __all__ = [
35
+ "CreateOp",
36
+ "UpdateOp",
37
+ "InsertOp",
38
+ "RemoveOp",
39
+ "DestroyOp",
40
+ "SetFrameOp",
41
+ "Mutation",
42
+ ]
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class CreateOp:
47
+ """Create a native view for ``tag`` of element type ``type_name``.
48
+
49
+ Attributes:
50
+ tag: Unique integer identity assigned by the reconciler.
51
+ type_name: Element type name (e.g. ``"Text"``).
52
+ props: Initial *clean* props — callables have already been
53
+ routed to the [`EventRegistry`][pythonnative.events.EventRegistry]
54
+ and replaced by the ``_pn_events`` name set.
55
+ """
56
+
57
+ tag: int
58
+ type_name: str
59
+ props: Dict[str, Any] = field(default_factory=dict)
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class UpdateOp:
64
+ """Apply ``changed_props`` to the view registered under ``tag``.
65
+
66
+ Removed props are signaled with a value of ``None``, matching the
67
+ pre-existing handler contract.
68
+ """
69
+
70
+ tag: int
71
+ changed_props: Dict[str, Any] = field(default_factory=dict)
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class InsertOp:
76
+ """Ensure the child view sits at ``index`` inside the parent view.
77
+
78
+ Handlers must treat this as *move-aware*: if the child is already
79
+ attached to the parent at a different position, it is moved rather
80
+ than duplicated. ``index`` is clamped by handlers to the current
81
+ child count.
82
+ """
83
+
84
+ parent_tag: int
85
+ child_tag: int
86
+ index: int
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class RemoveOp:
91
+ """Detach the child view from the parent view (without destroying it)."""
92
+
93
+ parent_tag: int
94
+ child_tag: int
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class DestroyOp:
99
+ """Release the native view registered under ``tag``.
100
+
101
+ The registry drops its tag record and calls the handler's
102
+ ``destroy`` hook so platform resources (listeners, timers, image
103
+ loads) can be released eagerly instead of waiting for GC.
104
+ """
105
+
106
+ tag: int
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class SetFrameOp:
111
+ """Position and size the view registered under ``tag``.
112
+
113
+ Coordinates are points relative to the parent's content origin,
114
+ exactly as computed by the layout engine.
115
+ """
116
+
117
+ tag: int
118
+ x: float
119
+ y: float
120
+ width: float
121
+ height: float
122
+
123
+ @property
124
+ def frame(self) -> Tuple[float, float, float, float]:
125
+ """Return ``(x, y, width, height)`` as a tuple."""
126
+ return (self.x, self.y, self.width, self.height)
127
+
128
+
129
+ Mutation = Union[CreateOp, UpdateOp, InsertOp, RemoveOp, DestroyOp, SetFrameOp]
130
+ """Union of every op type carried by a commit transaction."""
@@ -5,8 +5,15 @@ This package provides the
5
5
  that maps element type names (e.g., `"Text"`, `"Button"`) to
6
6
  platform-specific
7
7
  [`ViewHandler`][pythonnative.native_views.base.ViewHandler]
8
- implementations. The reconciler calls the registry to create, update,
9
- and re-parent native views.
8
+ implementations, and owns the **tag table** mapping each
9
+ reconciler-assigned integer tag to its live native view.
10
+
11
+ The reconciler communicates exclusively through
12
+ [`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]:
13
+ one ordered batch of create/update/insert/remove/destroy/frame ops per
14
+ commit (see `pythonnative.mutations`). Imperative escape hatches
15
+ (commands, animation control, intrinsic measurement) resolve views
16
+ through the same tag table.
10
17
 
11
18
  Platform handlers live in dedicated submodules:
12
19
 
@@ -15,6 +22,7 @@ Platform handlers live in dedicated submodules:
15
22
  - `pythonnative.native_views.android`: Android handlers
16
23
  (Chaquopy / Java bridge).
17
24
  - `pythonnative.native_views.ios`: iOS handlers (rubicon-objc).
25
+ - `pythonnative.native_views.desktop`: Tkinter preview handlers.
18
26
 
19
27
  All platform-branching is handled at registration time via lazy
20
28
  imports, so this package can be imported on any platform for testing.
@@ -27,8 +35,9 @@ import math
27
35
  import sys
28
36
  import threading
29
37
  import time
30
- from typing import Any, Dict, Optional, Tuple
38
+ from typing import Any, Dict, Optional, Sequence, Tuple
31
39
 
40
+ from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, RemoveOp, SetFrameOp, UpdateOp
32
41
  from .base import ViewHandler
33
42
 
34
43
  # ======================================================================
@@ -85,17 +94,30 @@ def _tripwire_log(label: str, message: str) -> None:
85
94
  pass
86
95
 
87
96
 
97
+ class ViewRecord:
98
+ """One live native view tracked by the tag table."""
99
+
100
+ __slots__ = ("tag", "type_name", "view", "handler")
101
+
102
+ def __init__(self, tag: int, type_name: str, view: Any, handler: ViewHandler) -> None:
103
+ self.tag = tag
104
+ self.type_name = type_name
105
+ self.view = view
106
+ self.handler = handler
107
+
108
+
88
109
  class NativeViewRegistry:
89
- """Map element type names to platform-specific view handlers.
110
+ """Map element type names to handlers and tags to live native views.
90
111
 
91
- The reconciler depends only on this protocol:
92
- `create_view`, `update_view`, `add_child`, `remove_child`,
93
- `insert_child`, `set_frame`, `measure_intrinsic`. Implementations
94
- may be real (Android/iOS) or mocked for tests.
112
+ The reconciler depends only on this protocol: ``apply_mutations``,
113
+ ``resolve_view``, ``measure_intrinsic``, and ``command``.
114
+ Implementations may host real platform handlers (Android/iOS/
115
+ desktop) or mocks for tests.
95
116
  """
96
117
 
97
118
  def __init__(self) -> None:
98
119
  self._handlers: Dict[str, ViewHandler] = {}
120
+ self._records: Dict[int, ViewRecord] = {}
99
121
 
100
122
  def register(self, type_name: str, handler: ViewHandler) -> None:
101
123
  """Register `handler` to service elements of type `type_name`.
@@ -106,116 +128,115 @@ class NativeViewRegistry:
106
128
  """
107
129
  self._handlers[type_name] = handler
108
130
 
109
- def create_view(self, type_name: str, props: Dict[str, Any]) -> Any:
110
- """Create a native view for `type_name` and apply initial props.
131
+ def handler_for(self, type_name: str) -> Optional[ViewHandler]:
132
+ """Return the handler registered for ``type_name``, if any."""
133
+ return self._handlers.get(type_name)
111
134
 
112
- Args:
113
- type_name: The element type name.
114
- props: Initial props dict.
135
+ # ------------------------------------------------------------------
136
+ # Tag table
137
+ # ------------------------------------------------------------------
115
138
 
116
- Returns:
117
- The platform-native view object.
139
+ def resolve_view(self, tag: int) -> Any:
140
+ """Return the native view registered under ``tag``, or ``None``."""
141
+ record = self._records.get(tag)
142
+ return record.view if record is not None else None
118
143
 
119
- Raises:
120
- ValueError: If no handler is registered for `type_name`.
121
- """
122
- handler = self._handlers.get(type_name)
123
- if handler is None:
124
- raise ValueError(f"Unknown element type: {type_name!r}")
125
- return handler.create(props)
144
+ def record_for(self, tag: int) -> Optional[ViewRecord]:
145
+ """Return the full [`ViewRecord`][pythonnative.native_views.ViewRecord] for ``tag``."""
146
+ return self._records.get(tag)
126
147
 
127
- def update_view(self, native_view: Any, type_name: str, changed_props: Dict[str, Any]) -> None:
128
- """Apply `changed_props` to an existing native view.
148
+ def live_view_count(self) -> int:
149
+ """Number of views currently tracked (test/diagnostic helper)."""
150
+ return len(self._records)
129
151
 
130
- Silently ignored if no handler is registered for `type_name`.
152
+ # ------------------------------------------------------------------
153
+ # The commit channel
154
+ # ------------------------------------------------------------------
131
155
 
132
- Args:
133
- native_view: The platform-native view.
134
- type_name: The element type name.
135
- changed_props: A dict containing only props whose values
136
- changed since the previous render. Removed props are
137
- signaled with a value of `None`.
138
- """
139
- handler = self._handlers.get(type_name)
140
- if handler is not None:
141
- handler.update(native_view, changed_props)
142
-
143
- def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
144
- """Append `child` to `parent`.
145
-
146
- Args:
147
- parent: Parent native view.
148
- child: Native view to append.
149
- parent_type: Element type of the parent (for handler lookup).
150
- """
151
- handler = self._handlers.get(parent_type)
152
- if handler is not None:
153
- handler.add_child(parent, child)
156
+ def apply_mutations(self, ops: Sequence[Mutation]) -> None:
157
+ """Apply one commit transaction.
154
158
 
155
- def remove_child(self, parent: Any, child: Any, parent_type: str) -> None:
156
- """Remove `child` from `parent`.
159
+ Ops are applied strictly in order. Failures are isolated per
160
+ op: a handler exception is logged (rate-limited) and the
161
+ remaining ops still apply, so one bad prop can't desync the
162
+ whole native tree.
157
163
 
158
164
  Args:
159
- parent: Parent native view.
160
- child: Child native view to remove.
161
- parent_type: Element type of the parent.
162
- """
163
- handler = self._handlers.get(parent_type)
164
- if handler is not None:
165
- handler.remove_child(parent, child)
166
-
167
- def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
168
- """Insert `child` into `parent` at `index`.
169
-
170
- Args:
171
- parent: Parent native view.
172
- child: Child native view to insert.
173
- parent_type: Element type of the parent.
174
- index: Zero-based insertion position among `parent`'s
175
- existing children.
176
- """
177
- handler = self._handlers.get(parent_type)
178
- if handler is not None:
179
- handler.insert_child(parent, child, index)
180
-
181
- def set_frame(
182
- self,
183
- native_view: Any,
184
- type_name: str,
185
- x: float,
186
- y: float,
187
- width: float,
188
- height: float,
189
- ) -> None:
190
- """Position and size a native view via the appropriate handler.
191
-
192
- Called by the reconciler's layout pass after every commit, with
193
- coordinates computed by ``pythonnative.layout`` in points
194
- relative to the parent's content origin.
165
+ ops: Ordered mutations emitted by the reconciler.
195
166
  """
167
+ for op in ops:
168
+ try:
169
+ self._apply_one(op)
170
+ except Exception as exc:
171
+ _tripwire_log(
172
+ f"apply:{type(op).__name__}",
173
+ f"[PN] apply_mutations: {type(op).__name__} failed: {type(exc).__name__}: {exc!r}",
174
+ )
175
+
176
+ def _apply_one(self, op: Mutation) -> None:
177
+ if isinstance(op, CreateOp):
178
+ handler = self._handlers.get(op.type_name)
179
+ if handler is None:
180
+ raise ValueError(f"Unknown element type: {op.type_name!r}")
181
+ view = handler.create(op.tag, op.props)
182
+ self._records[op.tag] = ViewRecord(op.tag, op.type_name, view, handler)
183
+ return
184
+ if isinstance(op, UpdateOp):
185
+ record = self._records.get(op.tag)
186
+ if record is not None:
187
+ record.handler.update(record.view, op.changed_props)
188
+ return
189
+ if isinstance(op, InsertOp):
190
+ parent = self._records.get(op.parent_tag)
191
+ child = self._records.get(op.child_tag)
192
+ if parent is not None and child is not None:
193
+ parent.handler.insert_child(parent.view, child.view, op.index)
194
+ return
195
+ if isinstance(op, RemoveOp):
196
+ parent = self._records.get(op.parent_tag)
197
+ child = self._records.get(op.child_tag)
198
+ if parent is not None and child is not None:
199
+ parent.handler.remove_child(parent.view, child.view)
200
+ return
201
+ if isinstance(op, DestroyOp):
202
+ record = self._records.pop(op.tag, None)
203
+ if record is not None:
204
+ record.handler.destroy(record.view)
205
+ return
206
+ if isinstance(op, SetFrameOp):
207
+ self._apply_frame(op)
208
+ return
209
+ raise TypeError(f"Unknown mutation op: {op!r}")
210
+
211
+ def _apply_frame(self, op: SetFrameOp) -> None:
212
+ record = self._records.get(op.tag)
213
+ if record is None:
214
+ return
196
215
  # Tripwire: log non-finite layout values so we can diagnose
197
216
  # crashes like iOS `CALayerInvalidGeometry` without losing the
198
217
  # repro. Handlers are responsible for clamping before applying.
199
- # Rate-limited via ``_tripwire_log`` to avoid 60 Hz floods when
200
- # an animated value is stuck at NaN.
218
+ # Rate-limited via ``_tripwire_log`` to avoid floods when an
219
+ # animated value is stuck at NaN.
201
220
  try:
202
- finite = math.isfinite(x) and math.isfinite(y) and math.isfinite(width) and math.isfinite(height)
221
+ finite = (
222
+ math.isfinite(op.x) and math.isfinite(op.y) and math.isfinite(op.width) and math.isfinite(op.height)
223
+ )
203
224
  except (TypeError, ValueError):
204
225
  finite = False
205
226
  if not finite:
206
227
  _tripwire_log(
207
228
  "set_frame:nan",
208
- f"[set_frame:nan] type={type_name!r} " f"x={x!r} y={y!r} w={width!r} h={height!r}",
229
+ f"[set_frame:nan] type={record.type_name!r} " f"x={op.x!r} y={op.y!r} w={op.width!r} h={op.height!r}",
209
230
  )
231
+ record.handler.set_frame(record.view, op.x, op.y, op.width, op.height)
210
232
 
211
- handler = self._handlers.get(type_name)
212
- if handler is not None:
213
- handler.set_frame(native_view, x, y, width, height)
233
+ # ------------------------------------------------------------------
234
+ # Imperative escape hatches (resolved through the tag table)
235
+ # ------------------------------------------------------------------
214
236
 
215
237
  def measure_intrinsic(
216
238
  self,
217
- native_view: Any,
218
- type_name: str,
239
+ tag: int,
219
240
  max_width: float,
220
241
  max_height: float,
221
242
  ) -> Tuple[float, float]:
@@ -224,10 +245,53 @@ class NativeViewRegistry:
224
245
  Used by the layout engine for leaves whose intrinsic size
225
246
  depends on their content (text, buttons, images).
226
247
  """
227
- handler = self._handlers.get(type_name)
228
- if handler is None:
248
+ record = self._records.get(tag)
249
+ if record is None:
229
250
  return (0.0, 0.0)
230
- return handler.measure_intrinsic(native_view, max_width, max_height)
251
+ return record.handler.measure_intrinsic(record.view, max_width, max_height)
252
+
253
+ def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
254
+ """Execute an imperative command against the view for ``tag``.
255
+
256
+ Args:
257
+ tag: Target view tag.
258
+ name: Command name (handler-specific, e.g.
259
+ ``"scroll_to_offset"``).
260
+ args: Optional command arguments.
261
+
262
+ Returns:
263
+ The handler's command result, or ``None`` when the tag is
264
+ unknown.
265
+ """
266
+ record = self._records.get(tag)
267
+ if record is None:
268
+ return None
269
+ return record.handler.command(record.view, name, args or {})
270
+
271
+ def set_animated_property(self, tag: int, prop_name: str, value: Any) -> None:
272
+ """Apply one Python-driven animation frame to the view for ``tag``."""
273
+ record = self._records.get(tag)
274
+ if record is not None:
275
+ record.handler.set_animated_property(record.view, prop_name, value)
276
+
277
+ def start_animation(self, tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool:
278
+ """Start a natively-driven animation on the view for ``tag``.
279
+
280
+ Returns:
281
+ Whether the platform accepted the animation (``False``
282
+ means the caller should drive it from the Python ticker).
283
+ """
284
+ record = self._records.get(tag)
285
+ if record is None:
286
+ return False
287
+ return bool(record.handler.start_animation(record.view, anim_id, prop_name, spec))
288
+
289
+ def cancel_animation(self, tag: int, anim_id: int) -> Any:
290
+ """Cancel a natively-driven animation; returns the presentation value if known."""
291
+ record = self._records.get(tag)
292
+ if record is None:
293
+ return None
294
+ return record.handler.cancel_animation(record.view, anim_id)
231
295
 
232
296
 
233
297
  # ======================================================================