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.
- pythonnative/__init__.py +27 -15
- pythonnative/animated.py +7 -13
- pythonnative/cli/pn.py +1 -2
- pythonnative/component.py +255 -0
- pythonnative/components/__init__.py +106 -0
- pythonnative/components/_base.py +132 -0
- pythonnative/components/controls.py +515 -0
- pythonnative/components/layout.py +459 -0
- pythonnative/components/lists.py +809 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/diagnostics.py +1 -1
- pythonnative/element.py +108 -29
- pythonnative/gestures.py +1 -1
- pythonnative/hooks.py +366 -671
- pythonnative/hosts/__init__.py +85 -0
- pythonnative/hosts/android.py +269 -0
- pythonnative/hosts/base.py +665 -0
- pythonnative/hosts/desktop.py +107 -0
- pythonnative/hosts/ios.py +404 -0
- pythonnative/hot_reload.py +16 -24
- pythonnative/layout.py +372 -66
- pythonnative/native_modules/__init__.py +23 -0
- pythonnative/native_modules/net_info.py +5 -1
- pythonnative/native_views/__init__.py +1 -1
- pythonnative/native_views/android.py +50 -6
- pythonnative/native_views/base.py +64 -1
- pythonnative/native_views/desktop.py +11 -3
- pythonnative/native_views/ios.py +72 -12
- pythonnative/navigation/__init__.py +102 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +573 -0
- pythonnative/navigation/hooks.py +94 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +637 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +248 -0
- pythonnative/net.py +4 -0
- pythonnative/platform_metrics.py +1 -1
- pythonnative/preview.py +21 -19
- pythonnative/project/android.py +7 -1
- pythonnative/project/doctor.py +1 -1
- pythonnative/project/ios.py +1 -0
- pythonnative/project/runtime_assets.py +1 -1
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +365 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1153 -0
- pythonnative/reconciler/layout_pass.py +359 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/_components.py +2 -4
- pythonnative/style.py +95 -13
- pythonnative/suspense.py +8 -12
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +85 -1
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +6 -7
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +1 -1
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +3 -1
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +33 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -10
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +391 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/METADATA +2 -21
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/RECORD +74 -43
- pythonnative/components.py +0 -2856
- pythonnative/navigation.py +0 -1031
- pythonnative/reconciler.py +0 -2298
- pythonnative/screen.py +0 -2068
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.32.0.dist-info → pythonnative-0.34.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Update scheduling primitives shared by hooks and the reconciler.
|
|
2
|
+
|
|
3
|
+
Two pieces live here:
|
|
4
|
+
|
|
5
|
+
- [`batch_updates`][pythonnative.scheduler.batch_updates], a context manager that
|
|
6
|
+
coalesces the render triggers fired by several state setters into
|
|
7
|
+
one, tracked per execution context (so it composes with ``async``
|
|
8
|
+
code).
|
|
9
|
+
- [`TransitionQueue`][pythonnative.scheduler.TransitionQueue], the
|
|
10
|
+
per-reconciler queue behind
|
|
11
|
+
[`use_transition`][pythonnative.use_transition]: renders marked as
|
|
12
|
+
transitions are deferred to a later turn of the framework loop so
|
|
13
|
+
urgent updates (typing, presses) stay responsive.
|
|
14
|
+
|
|
15
|
+
Nothing here is global mutable state beyond context variables, so
|
|
16
|
+
several reconcilers (screens, list rows, tests) never interfere.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from contextvars import ContextVar
|
|
23
|
+
from typing import Callable, Generator, List, Optional
|
|
24
|
+
|
|
25
|
+
from . import diagnostics
|
|
26
|
+
|
|
27
|
+
__all__ = ["TransitionQueue", "batch_updates", "in_transition", "run_in_transition", "schedule_trigger"]
|
|
28
|
+
|
|
29
|
+
# Depth of nested ``batch_updates`` blocks plus the triggers deferred by
|
|
30
|
+
# the outermost block, per execution context.
|
|
31
|
+
_batch_depth: ContextVar[int] = ContextVar("pn_batch_depth", default=0)
|
|
32
|
+
_batch_pending: ContextVar[Optional[List[Callable[[], None]]]] = ContextVar("pn_batch_pending", default=None)
|
|
33
|
+
|
|
34
|
+
# Whether state updates in the current execution context are marked as
|
|
35
|
+
# transitions (see ``use_transition``).
|
|
36
|
+
_transition_var: ContextVar[bool] = ContextVar("pn_transition", default=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def schedule_trigger(trigger: Callable[[], None]) -> None:
|
|
40
|
+
"""Run ``trigger`` now, or defer it to the end of the enclosing ``batch_updates`` block."""
|
|
41
|
+
pending = _batch_pending.get()
|
|
42
|
+
if _batch_depth.get() > 0 and pending is not None:
|
|
43
|
+
if trigger not in pending:
|
|
44
|
+
pending.append(trigger)
|
|
45
|
+
else:
|
|
46
|
+
trigger()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextmanager
|
|
50
|
+
def batch_updates() -> Generator[None, None, None]:
|
|
51
|
+
"""Coalesce multiple state updates into a single re-render.
|
|
52
|
+
|
|
53
|
+
State setters called inside the ``with`` block defer their
|
|
54
|
+
re-render trigger until the block exits, so any number of
|
|
55
|
+
``set_*`` calls produce at most one render pass.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
```python
|
|
59
|
+
import pythonnative as pn
|
|
60
|
+
|
|
61
|
+
with pn.batch_updates():
|
|
62
|
+
set_count(1)
|
|
63
|
+
set_name("hello")
|
|
64
|
+
```
|
|
65
|
+
"""
|
|
66
|
+
depth = _batch_depth.get()
|
|
67
|
+
depth_token = _batch_depth.set(depth + 1)
|
|
68
|
+
pending_token = None
|
|
69
|
+
if depth == 0:
|
|
70
|
+
pending_token = _batch_pending.set([])
|
|
71
|
+
try:
|
|
72
|
+
yield
|
|
73
|
+
finally:
|
|
74
|
+
_batch_depth.reset(depth_token)
|
|
75
|
+
if pending_token is not None:
|
|
76
|
+
triggers = _batch_pending.get() or []
|
|
77
|
+
_batch_pending.reset(pending_token)
|
|
78
|
+
for trigger in triggers:
|
|
79
|
+
trigger()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def in_transition() -> bool:
|
|
83
|
+
"""Whether state updates in the current context are transitions."""
|
|
84
|
+
return _transition_var.get()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_in_transition(fn: Callable[[], None]) -> None:
|
|
88
|
+
"""Run ``fn`` with its state updates marked as transitions."""
|
|
89
|
+
token = _transition_var.set(True)
|
|
90
|
+
try:
|
|
91
|
+
fn()
|
|
92
|
+
finally:
|
|
93
|
+
_transition_var.reset(token)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TransitionQueue:
|
|
97
|
+
"""Deferred render triggers and completion callbacks for one reconciler.
|
|
98
|
+
|
|
99
|
+
Triggers added via [`defer`][pythonnative.scheduler.TransitionQueue.defer]
|
|
100
|
+
run together on a later loop turn; callbacks added via
|
|
101
|
+
[`on_complete`][pythonnative.scheduler.TransitionQueue.on_complete]
|
|
102
|
+
run right after them (this is how ``use_transition`` flips its
|
|
103
|
+
``is_pending`` flag back off once the deferred render committed).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
__slots__ = ("_triggers", "_callbacks", "_scheduled")
|
|
107
|
+
|
|
108
|
+
def __init__(self) -> None:
|
|
109
|
+
self._triggers: List[Callable[[], None]] = []
|
|
110
|
+
self._callbacks: List[Callable[[], None]] = []
|
|
111
|
+
self._scheduled = False
|
|
112
|
+
|
|
113
|
+
def defer(self, trigger: Callable[[], None]) -> None:
|
|
114
|
+
"""Queue ``trigger`` for the next flush."""
|
|
115
|
+
if trigger not in self._triggers:
|
|
116
|
+
self._triggers.append(trigger)
|
|
117
|
+
self._ensure_scheduled()
|
|
118
|
+
|
|
119
|
+
def on_complete(self, callback: Callable[[], None]) -> None:
|
|
120
|
+
"""Queue ``callback`` to run after the next flush's triggers."""
|
|
121
|
+
self._callbacks.append(callback)
|
|
122
|
+
self._ensure_scheduled()
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def pending(self) -> bool:
|
|
126
|
+
"""Whether a flush is scheduled or work is queued."""
|
|
127
|
+
return self._scheduled or bool(self._triggers) or bool(self._callbacks)
|
|
128
|
+
|
|
129
|
+
def flush(self) -> None:
|
|
130
|
+
"""Run every deferred trigger, then the completion callbacks."""
|
|
131
|
+
self._scheduled = False
|
|
132
|
+
triggers = self._triggers
|
|
133
|
+
callbacks = self._callbacks
|
|
134
|
+
self._triggers = []
|
|
135
|
+
self._callbacks = []
|
|
136
|
+
for fn in triggers + callbacks:
|
|
137
|
+
try:
|
|
138
|
+
fn()
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
if not diagnostics.report_error(exc, phase="transition"):
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
def clear(self) -> None:
|
|
144
|
+
"""Drop queued work (used on unmount)."""
|
|
145
|
+
self._triggers = []
|
|
146
|
+
self._callbacks = []
|
|
147
|
+
self._scheduled = False
|
|
148
|
+
|
|
149
|
+
def _ensure_scheduled(self) -> None:
|
|
150
|
+
if self._scheduled:
|
|
151
|
+
return
|
|
152
|
+
self._scheduled = True
|
|
153
|
+
from .runtime import get_loop
|
|
154
|
+
|
|
155
|
+
get_loop().call_soon(self._flush_if_scheduled)
|
|
156
|
+
|
|
157
|
+
def _flush_if_scheduled(self) -> None:
|
|
158
|
+
if self._scheduled:
|
|
159
|
+
self.flush()
|
pythonnative/sdk/_components.py
CHANGED
|
@@ -170,9 +170,7 @@ def native_component(
|
|
|
170
170
|
|
|
171
171
|
def decorator(handler_cls: Type[H]) -> Type[H]:
|
|
172
172
|
if not isinstance(handler_cls, type) or not issubclass(handler_cls, ViewHandler):
|
|
173
|
-
raise TypeError(
|
|
174
|
-
f"@native_component({name!r}) must decorate a ViewHandler subclass; " f"got {handler_cls!r}"
|
|
175
|
-
)
|
|
173
|
+
raise TypeError(f"@native_component({name!r}) must decorate a ViewHandler subclass; got {handler_cls!r}")
|
|
176
174
|
register_component(name=name, props=props, handlers={plat: handler_cls() for plat in plats})
|
|
177
175
|
return handler_cls
|
|
178
176
|
|
|
@@ -385,7 +383,7 @@ def element_factory(name: str) -> Callable[..., Element]:
|
|
|
385
383
|
"""
|
|
386
384
|
if name not in _REGISTRY:
|
|
387
385
|
raise KeyError(
|
|
388
|
-
f"No component registered under name {name!r}.
|
|
386
|
+
f"No component registered under name {name!r}. Use @native_component or register_component first."
|
|
389
387
|
)
|
|
390
388
|
|
|
391
389
|
def factory(*children: Element, key: Optional[str] = None, props: Any = None, **kwargs: Any) -> Element:
|
pythonnative/style.py
CHANGED
|
@@ -35,7 +35,7 @@ Example:
|
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
37
|
import difflib
|
|
38
|
-
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
38
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union, get_args
|
|
39
39
|
|
|
40
40
|
from . import diagnostics
|
|
41
41
|
from .hooks import Context, create_context, use_color_scheme, use_context
|
|
@@ -107,6 +107,11 @@ class ShadowOffset(TypedDict):
|
|
|
107
107
|
height: float
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
ShadowOffsetValue = Union[ShadowOffset, Tuple[float, float], List[float]]
|
|
111
|
+
"""``shadow_offset`` / ``text_shadow_offset`` value: an
|
|
112
|
+
[`ShadowOffset`][pythonnative.ShadowOffset] dict or an ``(x, y)`` pair."""
|
|
113
|
+
|
|
114
|
+
|
|
110
115
|
# ----------------------------------------------------------------------
|
|
111
116
|
# Transforms
|
|
112
117
|
# ----------------------------------------------------------------------
|
|
@@ -192,6 +197,7 @@ AlignItems = Literal[
|
|
|
192
197
|
"flex_start",
|
|
193
198
|
"center",
|
|
194
199
|
"flex_end",
|
|
200
|
+
"baseline",
|
|
195
201
|
"auto",
|
|
196
202
|
"start",
|
|
197
203
|
"leading",
|
|
@@ -203,9 +209,18 @@ AlignItems = Literal[
|
|
|
203
209
|
]
|
|
204
210
|
AlignSelf = AlignItems
|
|
205
211
|
Position = Literal["relative", "absolute"]
|
|
212
|
+
Display = Literal["flex", "none"]
|
|
213
|
+
"""``display`` style value. ``"none"`` removes the node and its subtree
|
|
214
|
+
from layout entirely (no size, gap, or margin contribution; every frame
|
|
215
|
+
in the subtree is ``(0, 0, 0, 0)``)."""
|
|
206
216
|
Overflow = Literal["visible", "hidden", "scroll"]
|
|
207
217
|
TextAlign = Literal["left", "center", "right", "justify", "start", "end"]
|
|
208
218
|
TextDecoration = Literal["none", "underline", "line_through"]
|
|
219
|
+
TextTransform = Literal["none", "uppercase", "lowercase", "capitalize"]
|
|
220
|
+
"""``text_transform`` style value. Applied in Python before the string
|
|
221
|
+
reaches the native label, so measurement and rendering agree."""
|
|
222
|
+
MarginValue = Union[Dimension, Literal["auto"]]
|
|
223
|
+
"""A margin length: points, a ``"%"`` string, or ``"auto"`` to absorb free space."""
|
|
209
224
|
FontWeight = Literal[
|
|
210
225
|
"normal",
|
|
211
226
|
"bold",
|
|
@@ -294,6 +309,7 @@ class Style(TypedDict, total=False):
|
|
|
294
309
|
align_self: AlignSelf
|
|
295
310
|
align_content: AlignContent
|
|
296
311
|
direction: LayoutDirection
|
|
312
|
+
display: Display
|
|
297
313
|
|
|
298
314
|
# --- Layout: position ---
|
|
299
315
|
position: Position
|
|
@@ -314,15 +330,15 @@ class Style(TypedDict, total=False):
|
|
|
314
330
|
padding_end: Dimension
|
|
315
331
|
padding_horizontal: Dimension
|
|
316
332
|
padding_vertical: Dimension
|
|
317
|
-
margin:
|
|
318
|
-
margin_top:
|
|
319
|
-
margin_bottom:
|
|
320
|
-
margin_left:
|
|
321
|
-
margin_right:
|
|
322
|
-
margin_start:
|
|
323
|
-
margin_end:
|
|
324
|
-
margin_horizontal:
|
|
325
|
-
margin_vertical:
|
|
333
|
+
margin: Union[MarginValue, EdgeInsets]
|
|
334
|
+
margin_top: MarginValue
|
|
335
|
+
margin_bottom: MarginValue
|
|
336
|
+
margin_left: MarginValue
|
|
337
|
+
margin_right: MarginValue
|
|
338
|
+
margin_start: MarginValue
|
|
339
|
+
margin_end: MarginValue
|
|
340
|
+
margin_horizontal: MarginValue
|
|
341
|
+
margin_vertical: MarginValue
|
|
326
342
|
spacing: float
|
|
327
343
|
gap: float
|
|
328
344
|
row_gap: float
|
|
@@ -362,13 +378,17 @@ class Style(TypedDict, total=False):
|
|
|
362
378
|
italic: bool
|
|
363
379
|
text_align: TextAlign
|
|
364
380
|
text_decoration: TextDecoration
|
|
381
|
+
text_transform: TextTransform
|
|
365
382
|
line_height: float
|
|
366
383
|
letter_spacing: float
|
|
367
384
|
max_lines: int
|
|
385
|
+
text_shadow_color: Color
|
|
386
|
+
text_shadow_offset: ShadowOffsetValue
|
|
387
|
+
text_shadow_radius: float
|
|
368
388
|
|
|
369
389
|
# --- Visual: shadows / effects ---
|
|
370
390
|
shadow_color: Color
|
|
371
|
-
shadow_offset:
|
|
391
|
+
shadow_offset: ShadowOffsetValue
|
|
372
392
|
shadow_opacity: float
|
|
373
393
|
shadow_radius: float
|
|
374
394
|
elevation: float
|
|
@@ -447,6 +467,49 @@ def resolve_style(value: StyleProp) -> Dict[str, Any]:
|
|
|
447
467
|
_KNOWN_STYLE_KEYS = frozenset(Style.__annotations__)
|
|
448
468
|
"""Every key declared on the [`Style`][pythonnative.Style] TypedDict."""
|
|
449
469
|
|
|
470
|
+
_STYLE_VALUE_CHOICES: Dict[str, frozenset] = {
|
|
471
|
+
"display": frozenset(get_args(Display)),
|
|
472
|
+
"position": frozenset(get_args(Position)),
|
|
473
|
+
"direction": frozenset(get_args(LayoutDirection)),
|
|
474
|
+
"text_decoration": frozenset(get_args(TextDecoration)),
|
|
475
|
+
"text_transform": frozenset(get_args(TextTransform)),
|
|
476
|
+
"pointer_events": frozenset(get_args(PointerEvents)),
|
|
477
|
+
}
|
|
478
|
+
"""Keys whose string values are checked against their ``Literal`` choices
|
|
479
|
+
in dev mode. Only keys whose handlers accept exactly the declared
|
|
480
|
+
literals are listed, so friendly aliases (``align_items: "leading"``,
|
|
481
|
+
``font_weight: "semibold"``) never trigger false positives."""
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _is_number(value: Any) -> bool:
|
|
485
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _text_shadow_offset_ok(value: Any) -> bool:
|
|
489
|
+
if isinstance(value, dict):
|
|
490
|
+
return all(_is_number(value.get(k, 0)) for k in ("width", "height"))
|
|
491
|
+
if isinstance(value, (tuple, list)) and len(value) == 2:
|
|
492
|
+
return all(_is_number(v) for v in value)
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _bad_value_message(key: str, value: Any) -> Optional[str]:
|
|
497
|
+
"""Return a description of why ``value`` is invalid for ``key``, or ``None``."""
|
|
498
|
+
choices = _STYLE_VALUE_CHOICES.get(key)
|
|
499
|
+
if choices is not None:
|
|
500
|
+
if not isinstance(value, str) or value not in choices:
|
|
501
|
+
allowed = ", ".join(repr(c) for c in sorted(choices))
|
|
502
|
+
return f"Invalid value {value!r} for style key {key!r}; expected one of {allowed}."
|
|
503
|
+
return None
|
|
504
|
+
if key == "text_shadow_offset" and not _text_shadow_offset_ok(value):
|
|
505
|
+
return (
|
|
506
|
+
f"Invalid value {value!r} for style key 'text_shadow_offset'; "
|
|
507
|
+
"expected {'width': x, 'height': y} or an (x, y) pair."
|
|
508
|
+
)
|
|
509
|
+
if key == "text_shadow_radius" and (not _is_number(value) or value < 0):
|
|
510
|
+
return f"Invalid value {value!r} for style key 'text_shadow_radius'; expected a non-negative number."
|
|
511
|
+
return None
|
|
512
|
+
|
|
450
513
|
|
|
451
514
|
def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
|
|
452
515
|
"""Warn (once per key) about style keys no built-in handler reads.
|
|
@@ -457,6 +520,13 @@ def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
|
|
|
457
520
|
to handlers untouched, so custom components that read extra keys
|
|
458
521
|
keep working (at the cost of one dev warning).
|
|
459
522
|
|
|
523
|
+
A small set of enum-shaped keys (``display``, ``position``,
|
|
524
|
+
``direction``, ``text_decoration``, ``text_transform``,
|
|
525
|
+
``pointer_events``) and the ``text_shadow_*`` keys also have their
|
|
526
|
+
values checked; a bad value warns once per key/value pair and is
|
|
527
|
+
otherwise passed through (the engine and handlers ignore values
|
|
528
|
+
they don't recognize).
|
|
529
|
+
|
|
460
530
|
Args:
|
|
461
531
|
style_dict: The flattened style dict about to be merged into an
|
|
462
532
|
element's props.
|
|
@@ -465,8 +535,16 @@ def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
|
|
|
465
535
|
"""
|
|
466
536
|
if not diagnostics.is_dev() or not style_dict:
|
|
467
537
|
return
|
|
468
|
-
for key in style_dict:
|
|
538
|
+
for key, value in style_dict.items():
|
|
469
539
|
if key in _KNOWN_STYLE_KEYS:
|
|
540
|
+
if value is None:
|
|
541
|
+
continue
|
|
542
|
+
problem = _bad_value_message(key, value)
|
|
543
|
+
if problem is not None:
|
|
544
|
+
diagnostics.warn_once(
|
|
545
|
+
problem + (f" (on {owner})" if owner else ""),
|
|
546
|
+
key=f"style-value:{owner}:{key}:{value!r}",
|
|
547
|
+
)
|
|
470
548
|
continue
|
|
471
549
|
matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1)
|
|
472
550
|
hint = f" Did you mean {matches[0]!r}?" if matches else ""
|
|
@@ -621,7 +699,7 @@ Without a provider, [`use_theme`][pythonnative.use_theme] resolves to
|
|
|
621
699
|
[`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or
|
|
622
700
|
[`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on
|
|
623
701
|
the current appearance. Wrap a subtree in
|
|
624
|
-
[`Provider(
|
|
702
|
+
[`ThemeContext.Provider(my_theme, ...)`][pythonnative.hooks.Context.Provider] to
|
|
625
703
|
pin an explicit theme for that subtree, then read it inside
|
|
626
704
|
descendants via [`use_theme`][pythonnative.use_theme] (or
|
|
627
705
|
[`use_context(ThemeContext)`][pythonnative.use_context]).
|
|
@@ -676,23 +754,27 @@ __all__ = [
|
|
|
676
754
|
"DEFAULT_DARK_THEME",
|
|
677
755
|
"DEFAULT_LIGHT_THEME",
|
|
678
756
|
"Dimension",
|
|
757
|
+
"Display",
|
|
679
758
|
"EdgeInsets",
|
|
680
759
|
"EdgeValue",
|
|
681
760
|
"FlexDirection",
|
|
682
761
|
"FontWeight",
|
|
683
762
|
"JustifyContent",
|
|
684
763
|
"KeyboardType",
|
|
764
|
+
"MarginValue",
|
|
685
765
|
"Overflow",
|
|
686
766
|
"PointerEvents",
|
|
687
767
|
"Position",
|
|
688
768
|
"ReturnKeyType",
|
|
689
769
|
"ScaleType",
|
|
690
770
|
"ShadowOffset",
|
|
771
|
+
"ShadowOffsetValue",
|
|
691
772
|
"Style",
|
|
692
773
|
"StyleProp",
|
|
693
774
|
"StyleSheet",
|
|
694
775
|
"TextAlign",
|
|
695
776
|
"TextDecoration",
|
|
777
|
+
"TextTransform",
|
|
696
778
|
"ThemeContext",
|
|
697
779
|
"TransformEntry",
|
|
698
780
|
"TransformRotate",
|
pythonnative/suspense.py
CHANGED
|
@@ -386,26 +386,22 @@ def lazy(loader: Callable[[], Any]) -> Callable[..., Any]:
|
|
|
386
386
|
)
|
|
387
387
|
```
|
|
388
388
|
"""
|
|
389
|
-
from .
|
|
389
|
+
from .component import Component
|
|
390
390
|
|
|
391
391
|
state: dict = {}
|
|
392
392
|
|
|
393
|
-
|
|
394
|
-
def Lazy(**props: Any) -> Any:
|
|
393
|
+
def Lazy(*children: Any, **props: Any) -> Any:
|
|
395
394
|
resource = state.get("resource")
|
|
396
395
|
if resource is None:
|
|
397
396
|
resource = start_resource(loader)
|
|
398
397
|
state["resource"] = resource
|
|
399
398
|
loaded = resource.read()
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if Lazy.__name__ == "<lambda>":
|
|
407
|
-
Lazy.__name__ = "Lazy"
|
|
408
|
-
return Lazy
|
|
399
|
+
return loaded(*children, **props)
|
|
400
|
+
|
|
401
|
+
name = getattr(loader, "__name__", "Lazy")
|
|
402
|
+
if name == "<lambda>":
|
|
403
|
+
name = "Lazy"
|
|
404
|
+
return Component(Lazy, display_name=name)
|
|
409
405
|
|
|
410
406
|
|
|
411
407
|
__all__ = [
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.pythonnative.android_template
|
|
2
2
|
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
3
4
|
import android.content.Context
|
|
4
5
|
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.os.BatteryManager
|
|
5
8
|
import android.graphics.Color
|
|
6
9
|
import android.graphics.Typeface
|
|
7
10
|
import android.net.ConnectivityManager
|
|
@@ -20,6 +23,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
20
23
|
private val TAG = javaClass.simpleName
|
|
21
24
|
private var pythonReady = false
|
|
22
25
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
26
|
+
private var batteryReceiver: BroadcastReceiver? = null
|
|
23
27
|
|
|
24
28
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
25
29
|
super.onCreate(savedInstanceState)
|
|
@@ -42,7 +46,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
42
46
|
}
|
|
43
47
|
// Import the entry module now so a broken app fails here,
|
|
44
48
|
// with a full traceback, instead of inside the first fragment.
|
|
45
|
-
py.getModule(
|
|
49
|
+
py.getModule(getString(R.string.pn_entry_module))
|
|
46
50
|
pythonReady = true
|
|
47
51
|
} catch (e: Exception) {
|
|
48
52
|
Log.e("PythonNative", "Bootstrap failed", e)
|
|
@@ -54,10 +58,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
54
58
|
intent?.dataString?.let { dispatchUrl(it) }
|
|
55
59
|
|
|
56
60
|
registerNetworkCallback()
|
|
61
|
+
registerBatteryReceiver()
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
override fun onDestroy() {
|
|
60
65
|
super.onDestroy()
|
|
66
|
+
batteryReceiver?.let {
|
|
67
|
+
try {
|
|
68
|
+
unregisterReceiver(it)
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
Log.e("PythonNative", "unregisterReceiver(battery) failed", e)
|
|
71
|
+
}
|
|
72
|
+
batteryReceiver = null
|
|
73
|
+
}
|
|
61
74
|
networkCallback?.let {
|
|
62
75
|
try {
|
|
63
76
|
(getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager)
|
|
@@ -69,6 +82,35 @@ class MainActivity : AppCompatActivity() {
|
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
@Deprecated("Deprecated in Java")
|
|
86
|
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
87
|
+
super.onActivityResult(requestCode, resultCode, data)
|
|
88
|
+
if (!pythonReady) return
|
|
89
|
+
try {
|
|
90
|
+
Python.getInstance()
|
|
91
|
+
.getModule("pythonnative.native_modules")
|
|
92
|
+
.callAttr("dispatch_activity_result", requestCode, resultCode, data)
|
|
93
|
+
} catch (e: Exception) {
|
|
94
|
+
Log.e("PythonNative", "dispatch_activity_result failed", e)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onRequestPermissionsResult(
|
|
99
|
+
requestCode: Int,
|
|
100
|
+
permissions: Array<out String>,
|
|
101
|
+
grantResults: IntArray
|
|
102
|
+
) {
|
|
103
|
+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
104
|
+
if (!pythonReady) return
|
|
105
|
+
try {
|
|
106
|
+
Python.getInstance()
|
|
107
|
+
.getModule("pythonnative.native_modules")
|
|
108
|
+
.callAttr("dispatch_permissions_result", requestCode, permissions.toList(), grantResults.toList())
|
|
109
|
+
} catch (e: Exception) {
|
|
110
|
+
Log.e("PythonNative", "dispatch_permissions_result failed", e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
72
114
|
override fun onNewIntent(intent: Intent) {
|
|
73
115
|
super.onNewIntent(intent)
|
|
74
116
|
setIntent(intent)
|
|
@@ -143,6 +185,48 @@ class MainActivity : AppCompatActivity() {
|
|
|
143
185
|
}
|
|
144
186
|
}
|
|
145
187
|
|
|
188
|
+
// MARK: - Battery forwarding
|
|
189
|
+
|
|
190
|
+
// ACTION_BATTERY_CHANGED is a sticky broadcast, so registering also
|
|
191
|
+
// delivers the current level immediately.
|
|
192
|
+
private fun registerBatteryReceiver() {
|
|
193
|
+
if (!pythonReady || batteryReceiver != null) return
|
|
194
|
+
try {
|
|
195
|
+
val receiver = object : BroadcastReceiver() {
|
|
196
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
197
|
+
if (intent == null) return
|
|
198
|
+
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
|
199
|
+
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
|
200
|
+
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
|
201
|
+
val fraction = if (level >= 0 && scale > 0) level.toDouble() / scale.toDouble() else -1.0
|
|
202
|
+
val state = when (status) {
|
|
203
|
+
BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
|
|
204
|
+
BatteryManager.BATTERY_STATUS_FULL -> "full"
|
|
205
|
+
BatteryManager.BATTERY_STATUS_DISCHARGING,
|
|
206
|
+
BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "unplugged"
|
|
207
|
+
else -> "unknown"
|
|
208
|
+
}
|
|
209
|
+
dispatchBattery(fraction, state)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
|
213
|
+
batteryReceiver = receiver
|
|
214
|
+
} catch (e: Exception) {
|
|
215
|
+
Log.e("PythonNative", "registerReceiver(battery) failed", e)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun dispatchBattery(level: Double, state: String) {
|
|
220
|
+
if (!pythonReady) return
|
|
221
|
+
try {
|
|
222
|
+
Python.getInstance()
|
|
223
|
+
.getModule("pythonnative.native_modules.battery")
|
|
224
|
+
.callAttr("dispatch_battery", level, state)
|
|
225
|
+
} catch (e: Exception) {
|
|
226
|
+
Log.e("PythonNative", "dispatch_battery failed", e)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
146
230
|
// MARK: - Deep links
|
|
147
231
|
|
|
148
232
|
private fun dispatchUrl(url: String) {
|
|
@@ -38,18 +38,17 @@ class ScreenFragment : Fragment() {
|
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
40
|
val py = Python.getInstance()
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
val screenPath = arguments?.getString("screen_path") ?: "app.main"
|
|
41
|
+
// PythonNative imports the entry module (app.entry_point in
|
|
42
|
+
// pythonnative.toml, staged into R.string.pn_entry_module)
|
|
43
|
+
// and looks up its top-level `App` attribute. Pushed screens
|
|
44
|
+
// pass an explicit path in the fragment arguments.
|
|
45
|
+
val screenPath = arguments?.getString("screen_path") ?: getString(R.string.pn_entry_module)
|
|
47
46
|
val argsJson = arguments?.getString("args_json")
|
|
48
47
|
val filesRoot = requireContext().filesDir.absolutePath
|
|
49
48
|
val devRoot = "$filesRoot/pythonnative_dev"
|
|
50
49
|
val hotReload = py.getModule("pythonnative.hot_reload")
|
|
51
50
|
hotReload.callAttr("configure_dev_environment", filesRoot)
|
|
52
|
-
val pnScreen = py.getModule("pythonnative.
|
|
51
|
+
val pnScreen = py.getModule("pythonnative.hosts")
|
|
53
52
|
screen = pnScreen.callAttr("create_screen", screenPath, requireActivity(), argsJson)
|
|
54
53
|
screen?.callAttr("enable_hot_reload", "$devRoot/reload.json", devRoot)
|
|
55
54
|
} catch (e: Exception) {
|
|
@@ -16,9 +16,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
16
16
|
_ application: UIApplication,
|
|
17
17
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
18
18
|
) -> Bool {
|
|
19
|
+
registerBatteryObservers()
|
|
19
20
|
return true
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// MARK: - Battery forwarding
|
|
24
|
+
|
|
25
|
+
// UIDevice only posts battery notifications while monitoring is on;
|
|
26
|
+
// Battery.add_listener subscribers receive {"level", "state"}.
|
|
27
|
+
private func registerBatteryObservers() {
|
|
28
|
+
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
29
|
+
let center = NotificationCenter.default
|
|
30
|
+
for name in [UIDevice.batteryLevelDidChangeNotification, UIDevice.batteryStateDidChangeNotification] {
|
|
31
|
+
center.addObserver(forName: name, object: nil, queue: .main) { _ in
|
|
32
|
+
self.dispatchBattery()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private func dispatchBattery() {
|
|
38
|
+
guard PythonRuntime.shared.started else { return }
|
|
39
|
+
let device = UIDevice.current
|
|
40
|
+
let state: String
|
|
41
|
+
switch device.batteryState {
|
|
42
|
+
case .unplugged: state = "unplugged"
|
|
43
|
+
case .charging: state = "charging"
|
|
44
|
+
case .full: state = "full"
|
|
45
|
+
default: state = "unknown"
|
|
46
|
+
}
|
|
47
|
+
PythonRuntime.shared.notify(
|
|
48
|
+
module: "pythonnative.native_modules.battery",
|
|
49
|
+
function: "dispatch_battery",
|
|
50
|
+
Double(device.batteryLevel),
|
|
51
|
+
state
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
22
55
|
// MARK: - Remote notifications (APNs)
|
|
23
56
|
|
|
24
57
|
func application(
|
|
@@ -392,6 +392,6 @@ final class PythonRuntime {
|
|
|
392
392
|
public func pn_schedule_render_drain() {
|
|
393
393
|
DispatchQueue.main.async {
|
|
394
394
|
guard PythonRuntime.shared.started else { return }
|
|
395
|
-
PythonRuntime.shared.notify(module: "pythonnative.
|
|
395
|
+
PythonRuntime.shared.notify(module: "pythonnative.hosts", function: "drain_ios_scheduled_renders")
|
|
396
396
|
}
|
|
397
397
|
}
|