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.
- pythonnative/__init__.py +14 -3
- pythonnative/animated.py +420 -135
- pythonnative/cli/pn.py +450 -956
- pythonnative/components.py +519 -235
- pythonnative/events.py +210 -0
- pythonnative/gestures.py +875 -0
- pythonnative/layout.py +463 -149
- pythonnative/mutations.py +130 -0
- pythonnative/native_views/__init__.py +161 -97
- pythonnative/native_views/android.py +1050 -1124
- pythonnative/native_views/base.py +108 -18
- pythonnative/native_views/desktop.py +460 -417
- pythonnative/native_views/ios.py +1918 -1916
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +504 -0
- pythonnative/project/builder.py +555 -0
- pythonnative/project/config.py +642 -0
- pythonnative/project/doctor.py +233 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +344 -0
- pythonnative/project/permissions.py +343 -0
- pythonnative/project/runtime_assets.py +272 -0
- pythonnative/reconciler.py +540 -470
- pythonnative/screen.py +5 -2
- pythonnative/sdk/_components.py +2 -2
- pythonnative/templates/android_template/app/build.gradle +2 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/METADATA +10 -2
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/RECORD +32 -21
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -129
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.20.0.dist-info → pythonnative-0.22.0.dist-info}/top_level.txt +0 -0
pythonnative/events.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Tag-based event routing between native views and Python callbacks.
|
|
2
|
+
|
|
3
|
+
Before the batched-commit overhaul, every event prop (``on_click``,
|
|
4
|
+
``on_change``, …) was wired by storing the Python callable on (or next
|
|
5
|
+
to) the native view, and every re-render re-pushed fresh closures across
|
|
6
|
+
the bridge. This module replaces that with a single dispatch channel:
|
|
7
|
+
|
|
8
|
+
- The reconciler strips callable props out of the payload sent to
|
|
9
|
+
native handlers and registers them here, keyed by ``(tag, name)``.
|
|
10
|
+
- Handlers wire their platform listener **once** at view creation; the
|
|
11
|
+
listener calls [`dispatch_event`][pythonnative.events.dispatch_event]
|
|
12
|
+
with the view's tag and the event name.
|
|
13
|
+
- Re-renders only mutate this Python-side registry — no native call is
|
|
14
|
+
made when just a callback identity changes.
|
|
15
|
+
|
|
16
|
+
The set of event names present on an element is forwarded to handlers
|
|
17
|
+
under the [`EVENTS_PROP`][pythonnative.events.EVENTS_PROP] key (a
|
|
18
|
+
``frozenset``), so handlers that wire expensive listeners (scroll
|
|
19
|
+
delegates, gesture recognizers) can do so conditionally. Dispatching an
|
|
20
|
+
event nobody listens to is a cheap dict miss.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
from typing import Any, Callable, Dict, FrozenSet, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
EVENTS_PROP = "_pn_events"
|
|
27
|
+
"""Prop key carrying the ``frozenset`` of event names wired on an element."""
|
|
28
|
+
|
|
29
|
+
GESTURES_PROP = "gestures"
|
|
30
|
+
"""Prop key carrying gesture descriptors (see ``pythonnative.gestures``)."""
|
|
31
|
+
|
|
32
|
+
# Prop dicts that may carry nested callables, mapped to the event name
|
|
33
|
+
# each nested key is hoisted to.
|
|
34
|
+
_NESTED_EVENT_PROPS: Dict[str, Dict[str, str]] = {
|
|
35
|
+
"refresh_control": {"on_refresh": "on_refresh"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EventRegistry:
|
|
40
|
+
"""Process-wide map of ``(tag, event name) -> Python callback``.
|
|
41
|
+
|
|
42
|
+
Thread-safe: native backends may dispatch from the platform UI
|
|
43
|
+
thread while the reconciler updates registrations from the render
|
|
44
|
+
thread.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._lock = threading.Lock()
|
|
49
|
+
self._callbacks: Dict[int, Dict[str, Callable[..., Any]]] = {}
|
|
50
|
+
|
|
51
|
+
def set_events(self, tag: int, events: Dict[str, Callable[..., Any]]) -> None:
|
|
52
|
+
"""Replace every registration for ``tag`` with ``events``."""
|
|
53
|
+
with self._lock:
|
|
54
|
+
if events:
|
|
55
|
+
self._callbacks[tag] = dict(events)
|
|
56
|
+
else:
|
|
57
|
+
self._callbacks.pop(tag, None)
|
|
58
|
+
|
|
59
|
+
def clear(self, tag: int) -> None:
|
|
60
|
+
"""Drop every registration for ``tag`` (called on view destroy)."""
|
|
61
|
+
with self._lock:
|
|
62
|
+
self._callbacks.pop(tag, None)
|
|
63
|
+
|
|
64
|
+
def get(self, tag: int, name: str) -> Optional[Callable[..., Any]]:
|
|
65
|
+
"""Return the callback for ``(tag, name)``, or ``None``."""
|
|
66
|
+
with self._lock:
|
|
67
|
+
bucket = self._callbacks.get(tag)
|
|
68
|
+
if bucket is None:
|
|
69
|
+
return None
|
|
70
|
+
return bucket.get(name)
|
|
71
|
+
|
|
72
|
+
def has(self, tag: int, name: str) -> bool:
|
|
73
|
+
"""Return whether a callback is registered for ``(tag, name)``."""
|
|
74
|
+
return self.get(tag, name) is not None
|
|
75
|
+
|
|
76
|
+
def dispatch(self, tag: int, name: str, *args: Any) -> bool:
|
|
77
|
+
"""Invoke the callback for ``(tag, name)`` with ``args``.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
``True`` when a callback existed and was invoked (even if
|
|
81
|
+
it raised — exceptions are swallowed so a buggy app
|
|
82
|
+
callback can't crash the platform's UI thread), ``False``
|
|
83
|
+
when nothing is registered.
|
|
84
|
+
"""
|
|
85
|
+
callback = self.get(tag, name)
|
|
86
|
+
if callback is None:
|
|
87
|
+
return False
|
|
88
|
+
try:
|
|
89
|
+
callback(*args)
|
|
90
|
+
except Exception:
|
|
91
|
+
import traceback
|
|
92
|
+
|
|
93
|
+
traceback.print_exc()
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def reset(self) -> None:
|
|
97
|
+
"""Drop every registration (test helper)."""
|
|
98
|
+
with self._lock:
|
|
99
|
+
self._callbacks.clear()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_registry = EventRegistry()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_event_registry() -> EventRegistry:
|
|
106
|
+
"""Return the process-wide [`EventRegistry`][pythonnative.events.EventRegistry]."""
|
|
107
|
+
return _registry
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def dispatch_event(tag: int, name: str, *args: Any) -> bool:
|
|
111
|
+
"""Dispatch an event from a native view into Python.
|
|
112
|
+
|
|
113
|
+
This is the single entry point platform handlers call when a
|
|
114
|
+
native listener fires.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
tag: The view's reconciler-assigned tag.
|
|
118
|
+
name: Event name — the original prop name (``"on_click"``,
|
|
119
|
+
``"on_change"``, …) or a gesture channel (``"gesture:0"``).
|
|
120
|
+
*args: Positional arguments forwarded to the user callback,
|
|
121
|
+
preserving each prop's documented signature.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Whether a callback was registered for ``(tag, name)``.
|
|
125
|
+
"""
|
|
126
|
+
return _registry.dispatch(tag, name, *args)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ======================================================================
|
|
130
|
+
# Prop splitting
|
|
131
|
+
# ======================================================================
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_events(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Callable[..., Any]]]:
|
|
135
|
+
"""Split ``props`` into native-safe props and Python event callbacks.
|
|
136
|
+
|
|
137
|
+
Rules:
|
|
138
|
+
|
|
139
|
+
- Top-level callables named ``on_*`` become events under their prop
|
|
140
|
+
name and are removed from the native payload.
|
|
141
|
+
- ``refresh_control`` dicts have their nested ``on_refresh``
|
|
142
|
+
hoisted to the ``"on_refresh"`` event; the remaining keys
|
|
143
|
+
(``refreshing``, ``tint_color``) stay in the payload.
|
|
144
|
+
- ``gestures`` lists of gesture descriptors are serialized to plain
|
|
145
|
+
dicts (handlers wire recognizers from them) while their callbacks
|
|
146
|
+
are folded into per-gesture ``"gesture:<i>"`` routers.
|
|
147
|
+
- The resulting payload carries ``_pn_events`` — a frozenset of the
|
|
148
|
+
event names present — so handlers can wire listeners
|
|
149
|
+
conditionally and the prop differ can detect listener
|
|
150
|
+
addition/removal without comparing closures.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
props: Raw element props (already stripped of reconciler-owned
|
|
154
|
+
keys).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
``(clean_props, events)`` where ``clean_props`` contains no
|
|
158
|
+
callables and ``events`` maps event names to callbacks.
|
|
159
|
+
"""
|
|
160
|
+
clean: Dict[str, Any] = {}
|
|
161
|
+
events: Dict[str, Callable[..., Any]] = {}
|
|
162
|
+
|
|
163
|
+
for key, value in props.items():
|
|
164
|
+
if key.startswith("on_") and callable(value):
|
|
165
|
+
events[key] = value
|
|
166
|
+
continue
|
|
167
|
+
nested_spec = _NESTED_EVENT_PROPS.get(key)
|
|
168
|
+
if nested_spec is not None and isinstance(value, dict):
|
|
169
|
+
remainder: Dict[str, Any] = {}
|
|
170
|
+
for nested_key, nested_value in value.items():
|
|
171
|
+
event_name = nested_spec.get(nested_key)
|
|
172
|
+
if event_name is not None and callable(nested_value):
|
|
173
|
+
events[event_name] = nested_value
|
|
174
|
+
else:
|
|
175
|
+
remainder[nested_key] = nested_value
|
|
176
|
+
clean[key] = remainder
|
|
177
|
+
continue
|
|
178
|
+
if key == GESTURES_PROP and value:
|
|
179
|
+
from .gestures import serialize_gestures
|
|
180
|
+
|
|
181
|
+
specs, gesture_events = serialize_gestures(value)
|
|
182
|
+
clean[key] = specs
|
|
183
|
+
events.update(gesture_events)
|
|
184
|
+
continue
|
|
185
|
+
clean[key] = value
|
|
186
|
+
|
|
187
|
+
if events:
|
|
188
|
+
clean[EVENTS_PROP] = frozenset(events)
|
|
189
|
+
return clean, events
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def event_names(props: Dict[str, Any]) -> FrozenSet[str]:
|
|
193
|
+
"""Return the event-name set a handler should consult for ``props``."""
|
|
194
|
+
names = props.get(EVENTS_PROP)
|
|
195
|
+
if isinstance(names, frozenset):
|
|
196
|
+
return names
|
|
197
|
+
if isinstance(names, (set, list, tuple)):
|
|
198
|
+
return frozenset(names)
|
|
199
|
+
return frozenset()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"EVENTS_PROP",
|
|
204
|
+
"GESTURES_PROP",
|
|
205
|
+
"EventRegistry",
|
|
206
|
+
"get_event_registry",
|
|
207
|
+
"dispatch_event",
|
|
208
|
+
"extract_events",
|
|
209
|
+
"event_names",
|
|
210
|
+
]
|