pythonnative 0.22.0__py3-none-any.whl → 0.23.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 (53) hide show
  1. pythonnative/__init__.py +16 -2
  2. pythonnative/animated.py +6 -6
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +1 -1
  5. pythonnative/components.py +718 -48
  6. pythonnative/events.py +5 -5
  7. pythonnative/gestures.py +3 -3
  8. pythonnative/hooks.py +44 -3
  9. pythonnative/hot_reload.py +4 -4
  10. pythonnative/images.py +196 -0
  11. pythonnative/layout.py +3 -3
  12. pythonnative/mutations.py +4 -13
  13. pythonnative/native_modules/camera.py +1 -1
  14. pythonnative/native_modules/haptics.py +2 -2
  15. pythonnative/native_modules/location.py +1 -1
  16. pythonnative/native_modules/permissions.py +2 -2
  17. pythonnative/native_modules/secure_store.py +1 -1
  18. pythonnative/native_views/__init__.py +1 -7
  19. pythonnative/native_views/android.py +592 -64
  20. pythonnative/native_views/base.py +3 -3
  21. pythonnative/native_views/desktop.py +85 -26
  22. pythonnative/native_views/ios.py +762 -74
  23. pythonnative/navigation.py +4 -4
  24. pythonnative/net.py +3 -3
  25. pythonnative/platform.py +1 -1
  26. pythonnative/platform_metrics.py +5 -5
  27. pythonnative/preview.py +34 -3
  28. pythonnative/project/builder.py +1 -1
  29. pythonnative/project/config.py +4 -12
  30. pythonnative/project/doctor.py +2 -2
  31. pythonnative/project/icons.py +1 -1
  32. pythonnative/project/ios.py +1 -1
  33. pythonnative/project/permissions.py +2 -2
  34. pythonnative/project/runtime_assets.py +3 -3
  35. pythonnative/reconciler.py +9 -9
  36. pythonnative/runtime.py +8 -8
  37. pythonnative/screen.py +90 -5
  38. pythonnative/sdk/_components.py +1 -1
  39. pythonnative/storage.py +3 -3
  40. pythonnative/style.py +66 -7
  41. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  42. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  43. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  44. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  45. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +2 -2
  46. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +1 -1
  47. pythonnative/virtual_rows.py +137 -0
  48. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/METADATA +13 -13
  49. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/RECORD +53 -47
  50. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
  51. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
  52. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
  53. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
pythonnative/events.py CHANGED
@@ -10,7 +10,7 @@ the bridge. This module replaces that with a single dispatch channel:
10
10
  - Handlers wire their platform listener **once** at view creation; the
11
11
  listener calls [`dispatch_event`][pythonnative.events.dispatch_event]
12
12
  with the view's tag and the event name.
13
- - Re-renders only mutate this Python-side registry no native call is
13
+ - Re-renders only mutate this Python-side registry; no native call is
14
14
  made when just a callback identity changes.
15
15
 
16
16
  The set of event names present on an element is forwarded to handlers
@@ -78,7 +78,7 @@ class EventRegistry:
78
78
 
79
79
  Returns:
80
80
  ``True`` when a callback existed and was invoked (even if
81
- it raised exceptions are swallowed so a buggy app
81
+ it raised: exceptions are swallowed so a buggy app
82
82
  callback can't crash the platform's UI thread), ``False``
83
83
  when nothing is registered.
84
84
  """
@@ -115,7 +115,7 @@ def dispatch_event(tag: int, name: str, *args: Any) -> bool:
115
115
 
116
116
  Args:
117
117
  tag: The view's reconciler-assigned tag.
118
- name: Event name the original prop name (``"on_click"``,
118
+ name: Event name, the original prop name (``"on_click"``,
119
119
  ``"on_change"``, …) or a gesture channel (``"gesture:0"``).
120
120
  *args: Positional arguments forwarded to the user callback,
121
121
  preserving each prop's documented signature.
@@ -144,8 +144,8 @@ def extract_events(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Cal
144
144
  - ``gestures`` lists of gesture descriptors are serialized to plain
145
145
  dicts (handlers wire recognizers from them) while their callbacks
146
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
147
+ - The resulting payload carries ``_pn_events`` (a frozenset of the
148
+ event names present), so handlers can wire listeners
149
149
  conditionally and the prop differ can detect listener
150
150
  addition/removal without comparing closures.
151
151
 
pythonnative/gestures.py CHANGED
@@ -351,7 +351,7 @@ def serialize_gestures(
351
351
  # guarantees identical semantics on both backends.
352
352
 
353
353
  EmitFn = Callable[[int, Dict[str, Any]], None]
354
- """``emit(gesture_index, payload)`` the arbiter's output channel."""
354
+ """``emit(gesture_index, payload)``: the arbiter's output channel."""
355
355
 
356
356
 
357
357
  class _VelocityTracker:
@@ -400,7 +400,7 @@ class _Recognizer:
400
400
  def kind(self) -> str:
401
401
  return str(self.config.get("kind", ""))
402
402
 
403
- # Event hooks ``pointers`` maps pointer id -> (x, y).
403
+ # Event hooks: ``pointers`` maps pointer id -> (x, y).
404
404
  def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
405
405
  pass
406
406
 
@@ -805,7 +805,7 @@ class GestureArbiter:
805
805
 
806
806
  One arbiter serves one view. The host backend feeds it normalized
807
807
  pointer events (positions in the view's coordinate space, times in
808
- seconds any monotonic clock) and provides an ``emit`` callback
808
+ seconds, any monotonic clock) and provides an ``emit`` callback
809
809
  that forwards ``(gesture_index, payload)`` pairs to
810
810
  [`dispatch_event`][pythonnative.events.dispatch_event].
811
811
 
pythonnative/hooks.py CHANGED
@@ -613,7 +613,7 @@ def use_query(
613
613
  Args:
614
614
  fetcher: Zero-arg ``async`` callable that resolves to the
615
615
  current data.
616
- deps: Dependency list refetches whenever any entry changes.
616
+ deps: Dependency list. Refetches whenever any entry changes.
617
617
  initial: Optional starting value for ``data`` before the
618
618
  first fetch completes.
619
619
 
@@ -698,7 +698,7 @@ class MutationCall(Generic[T]):
698
698
  Returned by the second element of the
699
699
  [`use_mutation`][pythonnative.use_mutation] tuple. Awaiting the
700
700
  handle resolves to the mutator's return value (or re-raises its
701
- exception); discarding the handle is safe Python won't warn
701
+ exception); discarding the handle is safe. Python won't warn
702
702
  about an unawaited coroutine because this is a plain object.
703
703
 
704
704
  Example:
@@ -899,6 +899,47 @@ def use_keyboard_height() -> float:
899
899
  return platform_metrics.get_keyboard_height()
900
900
 
901
901
 
902
+ def use_color_scheme() -> str:
903
+ """Return the effective color scheme and re-render when it changes.
904
+
905
+ Equivalent to React Native's ``useColorScheme``. The system value
906
+ is published by the screen host; an app-level override set through
907
+ [`appearance.set_color_scheme`][pythonnative.appearance.set_color_scheme]
908
+ takes precedence.
909
+
910
+ Returns:
911
+ ``"light"`` or ``"dark"``.
912
+
913
+ Raises:
914
+ RuntimeError: If called outside a `@component` function.
915
+
916
+ Example:
917
+ ```python
918
+ import pythonnative as pn
919
+
920
+ @pn.component
921
+ def Banner():
922
+ scheme = pn.use_color_scheme()
923
+ bg = "#000000" if scheme == "dark" else "#FFFFFF"
924
+ return pn.View(style=pn.style(background_color=bg))
925
+ ```
926
+ """
927
+ from . import appearance
928
+
929
+ ctx = _get_hook_state()
930
+ if ctx is None:
931
+ raise RuntimeError("use_color_scheme must be called inside a @component function")
932
+
933
+ _, set_tick = use_state(0)
934
+
935
+ def subscribe() -> Callable[[], None]:
936
+ return appearance.subscribe(lambda: set_tick(lambda n: n + 1))
937
+
938
+ use_effect(subscribe, [])
939
+
940
+ return appearance.get_color_scheme()
941
+
942
+
902
943
  # ======================================================================
903
944
  # Context
904
945
  # ======================================================================
@@ -1068,7 +1109,7 @@ class NavigationHandle:
1068
1109
  Wraps the host's push/pop primitives so screens can navigate
1069
1110
  without knowing the underlying native navigation stack. The
1070
1111
  typical user-facing surface is the declarative handle returned by
1071
- a [`Stack`][pythonnative.create_stack_navigator] this class is
1112
+ a [`Stack`][pythonnative.create_stack_navigator]; this class is
1072
1113
  the lower-level fallback used when no navigator is rendered (and
1073
1114
  as the bridge that declarative navigators delegate to when they
1074
1115
  need to push real native screens).
@@ -17,7 +17,7 @@ Two strategies share the device-side surface:
17
17
  the reconciler tree is walked and every component function whose
18
18
  module was reloaded is swapped in place. Hook state, navigation
19
19
  state, and even scroll positions survive because the underlying
20
- ``VNode`` objects are reused the next render simply calls the
20
+ ``VNode`` objects are reused; the next render simply calls the
21
21
  new function bodies through the old slots.
22
22
  - **Full remount**: when the in-place swap fails (e.g. the new
23
23
  module raised at import time, or a render exception bubbled out
@@ -197,8 +197,8 @@ class ModuleReloader:
197
197
 
198
198
  Designed to be invoked from device-side glue when a hot-reload
199
199
  push completes. All public methods are static; the class holds a
200
- single piece of process-wide state the manifest version that
201
- has most recently been applied to ``sys.modules`` so that
200
+ single piece of process-wide state (the manifest version that
201
+ has most recently been applied to ``sys.modules``) so that
202
202
  multiple screen hosts polling the same manifest do not each
203
203
  re-execute the user-app modules. The first host to see a new
204
204
  version pays the ``reload_modules`` cost; subsequent hosts on the
@@ -302,7 +302,7 @@ class ModuleReloader:
302
302
 
303
303
  Returns:
304
304
  The list of module names that are currently fresh in
305
- ``sys.modules`` either freshly reloaded by this call, or
305
+ ``sys.modules``, either freshly reloaded by this call, or
306
306
  already reloaded by an earlier host for the same version.
307
307
  """
308
308
  with ModuleReloader._reload_lock:
pythonnative/images.py ADDED
@@ -0,0 +1,196 @@
1
+ """Shared image pipeline: async fetch with memory and disk caching.
2
+
3
+ Every platform's ``Image`` handler routes remote sources through this
4
+ module. The pipeline downloads on a daemon thread (never blocking the
5
+ UI), stores the raw bytes in a platform-appropriate disk cache keyed
6
+ by URL hash, deduplicates concurrent requests for the same URL, and
7
+ keeps a small in-memory LRU of recently fetched byte payloads so
8
+ scrolling back to an image doesn't touch the filesystem again.
9
+
10
+ Decoding stays platform-native (``BitmapFactory`` / ``UIImage`` /
11
+ ``PhotoImage``): callbacks receive a *local file path*, which each
12
+ handler decodes with its platform's downsampling facilities.
13
+
14
+ Callbacks are delivered on the platform main thread via
15
+ [`call_on_main_thread`][pythonnative.runtime.call_on_main_thread], so
16
+ handlers can touch native views directly.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import os
23
+ import tempfile
24
+ import threading
25
+ import urllib.request
26
+ from collections import OrderedDict
27
+ from typing import Callable, Dict, List, Optional, Tuple
28
+
29
+ _DOWNLOAD_TIMEOUT_S = 30.0
30
+ _MEMORY_CACHE_MAX_BYTES = 16 * 1024 * 1024
31
+
32
+
33
+ def _default_cache_dir() -> str:
34
+ """Resolve the on-disk cache directory for the current platform.
35
+
36
+ Android: the app's ``Context.getCacheDir()`` (purged by the OS
37
+ under storage pressure). iOS: ``~/Library/Caches`` (excluded from
38
+ backups). Desktop: a per-user directory under the system temp dir.
39
+ """
40
+ try:
41
+ from .utils import IS_ANDROID
42
+
43
+ if IS_ANDROID:
44
+ from .utils import get_android_context
45
+
46
+ base = str(get_android_context().getCacheDir().getAbsolutePath())
47
+ return os.path.join(base, "pn_images")
48
+ except Exception:
49
+ pass
50
+ home = os.path.expanduser("~")
51
+ caches = os.path.join(home, "Library", "Caches")
52
+ if os.path.isdir(caches): # iOS / macOS
53
+ return os.path.join(caches, "pn_images")
54
+ return os.path.join(tempfile.gettempdir(), "pn_images")
55
+
56
+
57
+ class _ByteLru:
58
+ """Tiny thread-safe LRU for raw image bytes."""
59
+
60
+ def __init__(self, max_bytes: int) -> None:
61
+ self._max = max_bytes
62
+ self._size = 0
63
+ self._items: "OrderedDict[str, bytes]" = OrderedDict()
64
+ self._lock = threading.Lock()
65
+
66
+ def get(self, key: str) -> Optional[bytes]:
67
+ with self._lock:
68
+ data = self._items.get(key)
69
+ if data is not None:
70
+ self._items.move_to_end(key)
71
+ return data
72
+
73
+ def put(self, key: str, data: bytes) -> None:
74
+ if len(data) > self._max:
75
+ return
76
+ with self._lock:
77
+ old = self._items.pop(key, None)
78
+ if old is not None:
79
+ self._size -= len(old)
80
+ self._items[key] = data
81
+ self._size += len(data)
82
+ while self._size > self._max and self._items:
83
+ _, evicted = self._items.popitem(last=False)
84
+ self._size -= len(evicted)
85
+
86
+ def clear(self) -> None:
87
+ with self._lock:
88
+ self._items.clear()
89
+ self._size = 0
90
+
91
+
92
+ _memory_cache = _ByteLru(_MEMORY_CACHE_MAX_BYTES)
93
+ _cache_dir: Optional[str] = None
94
+ # URL -> callbacks waiting on an in-flight download.
95
+ _in_flight: Dict[str, List[Tuple[Callable[[str], None], Callable[[str], None]]]] = {}
96
+ _in_flight_lock = threading.Lock()
97
+
98
+
99
+ def _get_cache_dir() -> str:
100
+ global _cache_dir
101
+ if _cache_dir is None:
102
+ _cache_dir = _default_cache_dir()
103
+ os.makedirs(_cache_dir, exist_ok=True)
104
+ return _cache_dir
105
+
106
+
107
+ def _cache_path(url: str) -> str:
108
+ digest = hashlib.sha256(url.encode("utf-8")).hexdigest()
109
+ ext = os.path.splitext(url.split("?", 1)[0])[1]
110
+ if len(ext) > 8 or "/" in ext:
111
+ ext = ""
112
+ return os.path.join(_get_cache_dir(), digest + ext)
113
+
114
+
115
+ def _dispatch_main(fn: Callable[[], None]) -> None:
116
+ try:
117
+ from .runtime import call_on_main_thread
118
+
119
+ call_on_main_thread(fn)
120
+ except Exception:
121
+ try:
122
+ fn()
123
+ except Exception:
124
+ pass
125
+
126
+
127
+ def fetch(
128
+ url: str,
129
+ on_ready: Callable[[str], None],
130
+ on_error: Optional[Callable[[str], None]] = None,
131
+ ) -> None:
132
+ """Fetch ``url`` into the cache and deliver a local file path.
133
+
134
+ Cache hits (memory or disk) still deliver asynchronously-consistent
135
+ behavior but resolve without a network round trip. Concurrent
136
+ requests for the same URL share one download. Callbacks run on the
137
+ platform main thread.
138
+
139
+ Args:
140
+ url: The ``http(s)`` image URL.
141
+ on_ready: Called with the local file path once available.
142
+ on_error: Called with an error message if the download fails.
143
+ """
144
+ path = _cache_path(url)
145
+ if _memory_cache.get(url) is not None or os.path.isfile(path):
146
+ _dispatch_main(lambda: on_ready(path))
147
+ return
148
+
149
+ err = on_error or (lambda _msg: None)
150
+ with _in_flight_lock:
151
+ waiters = _in_flight.get(url)
152
+ if waiters is not None:
153
+ waiters.append((on_ready, err))
154
+ return
155
+ _in_flight[url] = [(on_ready, err)]
156
+
157
+ def _worker() -> None:
158
+ error_msg: Optional[str] = None
159
+ try:
160
+ req = urllib.request.Request(url, headers={"User-Agent": "PythonNative"})
161
+ with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT_S) as resp:
162
+ data = resp.read()
163
+ _memory_cache.put(url, data)
164
+ tmp = path + ".part"
165
+ with open(tmp, "wb") as fh:
166
+ fh.write(data)
167
+ os.replace(tmp, path)
168
+ except Exception as exc:
169
+ error_msg = str(exc)
170
+ with _in_flight_lock:
171
+ callbacks = _in_flight.pop(url, [])
172
+
173
+ def _notify(ready_cb: Callable[[str], None], error_cb: Callable[[str], None], msg: Optional[str]) -> None:
174
+ if msg is None:
175
+ _dispatch_main(lambda: ready_cb(path))
176
+ else:
177
+ _dispatch_main(lambda: error_cb(msg))
178
+
179
+ for ready_cb, error_cb in callbacks:
180
+ _notify(ready_cb, error_cb, error_msg)
181
+
182
+ threading.Thread(target=_worker, name=f"pn-image-{path[-12:]}", daemon=True).start()
183
+
184
+
185
+ def clear_cache() -> None:
186
+ """Empty the memory cache and delete all cached image files."""
187
+ _memory_cache.clear()
188
+ try:
189
+ directory = _get_cache_dir()
190
+ for name in os.listdir(directory):
191
+ try:
192
+ os.remove(os.path.join(directory, name))
193
+ except OSError:
194
+ pass
195
+ except OSError:
196
+ pass
pythonnative/layout.py CHANGED
@@ -9,7 +9,7 @@ The engine is invoked by the reconciler after each commit pass:
9
9
 
10
10
  1. The reconciler maintains a parallel
11
11
  [`LayoutNode`][pythonnative.layout.LayoutNode] tree (cached across
12
- passes clean subtrees keep their nodes, dirty ones are rebuilt).
12
+ passes: clean subtrees keep their nodes, dirty ones are rebuilt).
13
13
  2. [`calculate_layout`][pythonnative.layout.calculate_layout] is called
14
14
  with the viewport size; it recursively determines each node's
15
15
  ``(x, y, width, height)`` relative to its parent's coordinate space.
@@ -618,7 +618,7 @@ def _measure_node(
618
618
  resolved_direction = _resolve_direction(style, direction)
619
619
 
620
620
  # Incremental-layout memo: a clean node measured under identical
621
- # inputs reuses its previous result without recursing its whole
621
+ # inputs reuses its previous result without recursing; its whole
622
622
  # subtree keeps the sizes from the prior pass.
623
623
  memo = node._measure_memo
624
624
  if (
@@ -863,7 +863,7 @@ def _layout_flex_children(
863
863
  their line's cross size. The computed line structure is stored on
864
864
  ``parent._lines`` for the positioning pass.
865
865
 
866
- Returns ``(used_main, used_cross)`` the total content size used
866
+ Returns ``(used_main, used_cross)``, the total content size used
867
867
  by the in-flow children, including inter-child gaps but excluding
868
868
  the parent's own padding. The caller adds padding back in for the
869
869
  container's outer size.
pythonnative/mutations.py CHANGED
@@ -22,8 +22,8 @@ Op ordering rules (the reconciler guarantees these):
22
22
 
23
23
  1. A `CreateOp` for a tag precedes any other op referencing that tag.
24
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.
25
+ 3. `DestroyOp` ops are emitted children-first; handlers detach the view
26
+ from its parent as part of destruction.
27
27
  4. `SetFrameOp` ops are only emitted for frames that actually changed
28
28
  since the last layout pass (frame diffing).
29
29
  """
@@ -35,7 +35,6 @@ __all__ = [
35
35
  "CreateOp",
36
36
  "UpdateOp",
37
37
  "InsertOp",
38
- "RemoveOp",
39
38
  "DestroyOp",
40
39
  "SetFrameOp",
41
40
  "Mutation",
@@ -49,7 +48,7 @@ class CreateOp:
49
48
  Attributes:
50
49
  tag: Unique integer identity assigned by the reconciler.
51
50
  type_name: Element type name (e.g. ``"Text"``).
52
- props: Initial *clean* props callables have already been
51
+ props: Initial *clean* props; callables have already been
53
52
  routed to the [`EventRegistry`][pythonnative.events.EventRegistry]
54
53
  and replaced by the ``_pn_events`` name set.
55
54
  """
@@ -86,14 +85,6 @@ class InsertOp:
86
85
  index: int
87
86
 
88
87
 
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
88
  @dataclass(frozen=True)
98
89
  class DestroyOp:
99
90
  """Release the native view registered under ``tag``.
@@ -126,5 +117,5 @@ class SetFrameOp:
126
117
  return (self.x, self.y, self.width, self.height)
127
118
 
128
119
 
129
- Mutation = Union[CreateOp, UpdateOp, InsertOp, RemoveOp, DestroyOp, SetFrameOp]
120
+ Mutation = Union[CreateOp, UpdateOp, InsertOp, DestroyOp, SetFrameOp]
130
121
  """Union of every op type carried by a commit transaction."""
@@ -152,7 +152,7 @@ def _ios_launch_picker(on_result: Callable[[Optional[str]], None], source: str)
152
152
  _pending_delegates.pop(id(delegate), None)
153
153
  on_result(None)
154
154
 
155
- # Reference SEL/objc_method so the lint pass keeps the import
155
+ # Reference SEL/objc_method so the lint pass keeps the import;
156
156
  # they're needed for the delegate class above.
157
157
  _ = (SEL, objc_method)
158
158
  except Exception:
@@ -2,11 +2,11 @@
2
2
 
3
3
  Two interfaces live here:
4
4
 
5
- - [`Haptics`][pythonnative.Haptics] semantic, iOS-style feedback
5
+ - [`Haptics`][pythonnative.Haptics]: semantic, iOS-style feedback
6
6
  (impact / notification / selection) backed by
7
7
  ``UIFeedbackGenerator`` on iOS and ``VibrationEffect`` patterns on
8
8
  Android.
9
- - [`Vibration`][pythonnative.Vibration] a blunt "buzz for N
9
+ - [`Vibration`][pythonnative.Vibration]: a blunt "buzz for N
10
10
  milliseconds" interface for cases where you want an explicit
11
11
  duration.
12
12
 
@@ -167,7 +167,7 @@ def _android_get(on_result: Callable[[Optional[Coords]], None]) -> None:
167
167
  Context = jclass("android.content.Context")
168
168
  lm = ctx.getSystemService(Context.LOCATION_SERVICE)
169
169
 
170
- # Try the most recent known fix first it's instant and avoids
170
+ # Try the most recent known fix first; it's instant and avoids
171
171
  # the GPS warm-up delay.
172
172
  try:
173
173
  for provider in ("gps", "network", "passive"):
@@ -3,9 +3,9 @@
3
3
  [`Permissions`][pythonnative.Permissions] normalizes the very different
4
4
  iOS and Android permission models behind two calls:
5
5
 
6
- - ``check(permission)`` synchronous, returns a status string without
6
+ - ``check(permission)``: synchronous, returns a status string without
7
7
  prompting.
8
- - ``request(permission)`` a coroutine that shows the system prompt
8
+ - ``request(permission)``: a coroutine that shows the system prompt
9
9
  (if needed) and resolves to the resulting status.
10
10
 
11
11
  Statuses are ``"granted"``, ``"denied"``, ``"blocked"`` (denied with
@@ -1,7 +1,7 @@
1
1
  """Encrypted key/value storage for secrets (tokens, credentials).
2
2
 
3
3
  [`SecureStore`][pythonnative.SecureStore] persists small string values
4
- in the iOS Keychain and Android ``EncryptedSharedPreferences`` the
4
+ in the iOS Keychain and Android ``EncryptedSharedPreferences``, the
5
5
  right place for auth tokens and other secrets that
6
6
  [`AsyncStorage`][pythonnative.AsyncStorage] (plain, unencrypted) should
7
7
  never hold.
@@ -37,7 +37,7 @@ import threading
37
37
  import time
38
38
  from typing import Any, Dict, Optional, Sequence, Tuple
39
39
 
40
- from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, RemoveOp, SetFrameOp, UpdateOp
40
+ from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, SetFrameOp, UpdateOp
41
41
  from .base import ViewHandler
42
42
 
43
43
  # ======================================================================
@@ -192,12 +192,6 @@ class NativeViewRegistry:
192
192
  if parent is not None and child is not None:
193
193
  parent.handler.insert_child(parent.view, child.view, op.index)
194
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
195
  if isinstance(op, DestroyOp):
202
196
  record = self._records.pop(op.tag, None)
203
197
  if record is not None: