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,85 @@
1
+ """Screen hosts: the bridge between a native screen and the reconciler.
2
+
3
+ Native templates create one host per screen:
4
+
5
+ ```python
6
+ host = pythonnative.hosts.create_screen("app.main", native_instance, args_json)
7
+ host.on_create()
8
+ ```
9
+
10
+ and forward lifecycle events to it. The concrete class depends on the
11
+ runtime ([`AndroidScreenHost`][pythonnative.hosts.android.AndroidScreenHost],
12
+ [`IOSScreenHost`][pythonnative.hosts.ios.IOSScreenHost],
13
+ [`DesktopScreenHost`][pythonnative.hosts.desktop.DesktopScreenHost]);
14
+ the headless base [`ScreenHost`][pythonnative.hosts.ScreenHost] is used
15
+ in unit tests with a fake backend.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Optional, Type
21
+
22
+ from ..utils import IS_ANDROID, IS_DESKTOP, IS_IOS
23
+ from .base import ScreenHost, import_component
24
+
25
+ __all__ = ["ScreenHost", "create_screen", "host_class", "import_component"]
26
+
27
+
28
+ def host_class() -> Type[ScreenHost]:
29
+ """The host class for the current runtime."""
30
+ if IS_ANDROID:
31
+ from .android import AndroidScreenHost
32
+
33
+ return AndroidScreenHost
34
+ if IS_DESKTOP:
35
+ from .desktop import DesktopScreenHost
36
+
37
+ return DesktopScreenHost
38
+ if IS_IOS:
39
+ from .ios import IOSScreenHost
40
+
41
+ return IOSScreenHost
42
+ return ScreenHost
43
+
44
+
45
+ def create_screen(component_path: str, native_instance: Any = None, args_json: Optional[str] = None) -> ScreenHost:
46
+ """Create the screen host for a root component.
47
+
48
+ Args:
49
+ component_path: ``"app.main"`` (the module's ``App`` is used) or a
50
+ dotted path like ``"app.main.RootScreen"``. Imported lazily
51
+ so the dev server can reload it.
52
+ native_instance: The platform object owning the screen
53
+ (``Activity``, ``UIViewController`` pointer, ``DesktopApp``).
54
+ args_json: Optional JSON launch arguments (pushed screens
55
+ receive their navigation history here).
56
+
57
+ Returns:
58
+ A host ready for ``on_create`` and the other lifecycle calls.
59
+ """
60
+ component = import_component(component_path)
61
+ host = host_class()(native_instance, component_path, component)
62
+ if args_json:
63
+ host.set_args(args_json)
64
+ return host
65
+
66
+
67
+ def drain_ios_scheduled_renders() -> None:
68
+ """Drain deferred iOS renders (called by the Swift template on the main thread)."""
69
+ from .ios import drain_ios_scheduled_renders as drain
70
+
71
+ drain()
72
+
73
+
74
+ def forward_lifecycle(native_addr: int, event: str) -> None:
75
+ """Forward a Swift view-controller lifecycle event to its host."""
76
+ from .ios import forward_lifecycle as forward
77
+
78
+ forward(native_addr, event)
79
+
80
+
81
+ def drain_desktop_scheduled_renders() -> None:
82
+ """Drain deferred desktop renders (called by the preview's Tk loop)."""
83
+ from .desktop import drain_desktop_scheduled_renders as drain
84
+
85
+ drain()
@@ -0,0 +1,269 @@
1
+ """Android screen host: a fragment inside the template's ``NavHostFragment``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ from java import dynamic_proxy, jclass
10
+
11
+ from ..utils import get_android_fragment_container, set_android_context
12
+ from .base import ScreenHost, flush_hosts
13
+
14
+ __all__ = ["AndroidScreenHost"]
15
+
16
+ _SCHEDULED: Dict[int, ScreenHost] = {}
17
+ _handler: Any = None
18
+ _runnable: Any = None
19
+ _main_looper: Any = None
20
+
21
+
22
+ def _is_main_thread() -> bool:
23
+ global _main_looper
24
+ try:
25
+ Looper = jclass("android.os.Looper")
26
+ if _main_looper is None:
27
+ _main_looper = Looper.getMainLooper()
28
+ return Looper.myLooper() == _main_looper
29
+ except Exception:
30
+ return threading.current_thread() is threading.main_thread()
31
+
32
+
33
+ def _flush_scheduled() -> None:
34
+ hosts = list(_SCHEDULED.values())
35
+ _SCHEDULED.clear()
36
+ flush_hosts(hosts)
37
+
38
+
39
+ def _publish_window_insets(view: Any) -> None:
40
+ """Publish system-bar and IME insets from ``view`` to ``platform_metrics`` (best-effort)."""
41
+ try:
42
+ from .. import platform_metrics
43
+ except Exception:
44
+ return
45
+ try:
46
+ insets_obj = view.getRootWindowInsets()
47
+ if insets_obj is None:
48
+ return
49
+ density = float(view.getResources().getDisplayMetrics().density) or 1.0
50
+ Type: Any = None
51
+ try:
52
+ WindowInsets = jclass("android.view.WindowInsets")
53
+ Type = WindowInsets.Type
54
+ typed = insets_obj.getInsets(Type.systemBars())
55
+ top_px, left_px, bottom_px, right_px = int(typed.top), int(typed.left), int(typed.bottom), int(typed.right)
56
+ except Exception:
57
+ top_px = int(insets_obj.getSystemWindowInsetTop() or 0)
58
+ left_px = int(insets_obj.getSystemWindowInsetLeft() or 0)
59
+ bottom_px = int(insets_obj.getSystemWindowInsetBottom() or 0)
60
+ right_px = int(insets_obj.getSystemWindowInsetRight() or 0)
61
+ platform_metrics.set_safe_area_insets(
62
+ top_px / density, left_px / density, bottom_px / density, right_px / density
63
+ )
64
+ # The keyboard overlaps the navigation bar, so the visible
65
+ # keyboard height is the IME inset minus the bar inset.
66
+ try:
67
+ ime = insets_obj.getInsets(Type.ime())
68
+ platform_metrics.set_keyboard_height(max(0, int(ime.bottom) - bottom_px) / density)
69
+ except Exception:
70
+ pass
71
+ except Exception:
72
+ pass
73
+
74
+
75
+ def _publish_color_scheme(activity: Any) -> None:
76
+ try:
77
+ from .. import appearance
78
+
79
+ Configuration = jclass("android.content.res.Configuration")
80
+ ui_mode = int(activity.getResources().getConfiguration().uiMode)
81
+ night = ui_mode & int(Configuration.UI_MODE_NIGHT_MASK)
82
+ appearance.set_system_color_scheme("dark" if night == int(Configuration.UI_MODE_NIGHT_YES) else "light")
83
+ except Exception:
84
+ pass
85
+
86
+
87
+ class AndroidScreenHost(ScreenHost):
88
+ """Host owned by ``ScreenFragment.kt``; ``native_instance`` is the activity."""
89
+
90
+ def __init__(self, native_instance: Any, component_path: str, component: Any) -> None:
91
+ set_android_context(native_instance)
92
+ super().__init__(native_instance, component_path, component)
93
+ self._layout_listener: Any = None # retained to prevent GC
94
+ self._insets_listener: Any = None
95
+
96
+ # -- lifecycle ------------------------------------------------------
97
+
98
+ def on_create(self) -> None:
99
+ """Publish the system color scheme from the activity, then mount the root component."""
100
+ _publish_color_scheme(self.native_instance)
101
+ super().on_create()
102
+
103
+ def on_resume(self) -> None:
104
+ """Refresh the system color scheme, then mark the screen focused."""
105
+ _publish_color_scheme(self.native_instance)
106
+ super().on_resume()
107
+
108
+ def on_activity_result(self, request_code: int, result_code: int, data: Any) -> None:
109
+ """Forward ``Activity.onActivityResult`` to the native-module dispatcher."""
110
+ from ..native_modules import dispatch_activity_result
111
+
112
+ dispatch_activity_result(int(request_code), int(result_code), data)
113
+
114
+ def on_request_permissions_result(self, request_code: int, permissions: Any, grant_results: Any) -> None:
115
+ """Forward ``Activity.onRequestPermissionsResult`` to the native-module dispatcher."""
116
+ from ..native_modules import dispatch_permissions_result
117
+
118
+ dispatch_permissions_result(int(request_code), list(permissions or []), list(grant_results or []))
119
+
120
+ # -- platform primitives -------------------------------------------
121
+
122
+ def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
123
+ try:
124
+ metrics = self.native_instance.getResources().getDisplayMetrics()
125
+ density = float(metrics.density) or 1.0
126
+ return (metrics.widthPixels / density, metrics.heightPixels / density)
127
+ except Exception:
128
+ return None
129
+
130
+ def _schedule_render_async(self) -> bool:
131
+ global _handler, _runnable
132
+ if self._render_scheduled:
133
+ return True
134
+ if _is_main_thread():
135
+ return False
136
+ self._render_scheduled = True
137
+ _SCHEDULED[id(self)] = self
138
+ try:
139
+ if _handler is None:
140
+ Handler = jclass("android.os.Handler")
141
+ Looper = jclass("android.os.Looper")
142
+ Runnable = jclass("java.lang.Runnable")
143
+ _handler = Handler(Looper.getMainLooper())
144
+
145
+ class _RenderRunnable(dynamic_proxy(Runnable)): # type: ignore[misc]
146
+ def run(self) -> None:
147
+ _flush_scheduled()
148
+
149
+ _runnable = _RenderRunnable()
150
+ _handler.post(_runnable)
151
+ return True
152
+ except Exception:
153
+ self._render_scheduled = False
154
+ _SCHEDULED.pop(id(self), None)
155
+ return False
156
+
157
+ def _navigator(self) -> Any:
158
+ return jclass(f"{self.native_instance.getPackageName()}.Navigator")
159
+
160
+ def _native_push(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
161
+ self._navigator().push(self.native_instance, component_path, json.dumps(args) if args else None)
162
+
163
+ def _native_pop(self, count: int) -> None:
164
+ try:
165
+ navigator = self._navigator()
166
+ for _ in range(count):
167
+ navigator.pop(self.native_instance)
168
+ except Exception:
169
+ self.native_instance.finish()
170
+
171
+ def _native_pop_to_root(self) -> None:
172
+ try:
173
+ self._navigator().popToRoot(self.native_instance)
174
+ except Exception:
175
+ pass
176
+
177
+ def _native_set_options(self, options: Dict[str, Any]) -> None:
178
+ title = options.get("title")
179
+ try:
180
+ if title is not None and hasattr(self.native_instance, "setTitle"):
181
+ self.native_instance.setTitle(str(title))
182
+ except Exception:
183
+ pass
184
+
185
+ def _attach_root(self, native_view: Any) -> None:
186
+ container = None
187
+ try:
188
+ container = get_android_fragment_container()
189
+ try:
190
+ container.removeAllViews()
191
+ except Exception:
192
+ pass
193
+ # A root from a prior mount may still be parented under the
194
+ # old (destroyed) FrameLayout; addView throws otherwise.
195
+ try:
196
+ old_parent = native_view.getParent()
197
+ if old_parent is not None:
198
+ old_parent.removeView(native_view)
199
+ except Exception:
200
+ pass
201
+ LayoutParams = jclass("android.view.ViewGroup$LayoutParams")
202
+ container.addView(native_view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
203
+ except Exception:
204
+ self.native_instance.setContentView(native_view)
205
+ container = native_view
206
+ if container is not None:
207
+ self._register_layout_listener(container)
208
+ self._register_insets_listener(container)
209
+ self._push_initial_viewport(container)
210
+
211
+ def _detach_root(self, native_view: Any) -> None:
212
+ # Never clear the shared fragment container: a popped fragment's
213
+ # detach runs after the screen below has re-attached its root.
214
+ if native_view is None:
215
+ return
216
+ try:
217
+ parent = native_view.getParent()
218
+ if parent is not None:
219
+ parent.removeView(native_view)
220
+ except Exception:
221
+ pass
222
+
223
+ # -- viewport plumbing ---------------------------------------------
224
+
225
+ def _register_layout_listener(self, view: Any) -> None:
226
+ host = self
227
+ try:
228
+ View = jclass("android.view.View")
229
+
230
+ class _LayoutListener(dynamic_proxy(View.OnLayoutChangeListener)): # type: ignore[misc]
231
+ def onLayoutChange(self, v: Any, left: int, top: int, right: int, bottom: int, *_: Any) -> None:
232
+ try:
233
+ _publish_window_insets(v)
234
+ density = float(v.getResources().getDisplayMetrics().density) or 1.0
235
+ host.set_viewport_size((right - left) / density, (bottom - top) / density)
236
+ except Exception:
237
+ pass
238
+
239
+ self._layout_listener = _LayoutListener()
240
+ view.addOnLayoutChangeListener(self._layout_listener)
241
+ except Exception:
242
+ pass
243
+
244
+ def _register_insets_listener(self, view: Any) -> None:
245
+ try:
246
+ View = jclass("android.view.View")
247
+
248
+ class _InsetsListener(dynamic_proxy(View.OnApplyWindowInsetsListener)): # type: ignore[misc]
249
+ def onApplyWindowInsets(self, v: Any, insets: Any) -> Any:
250
+ _publish_window_insets(v)
251
+ return insets
252
+
253
+ self._insets_listener = _InsetsListener()
254
+ view.setOnApplyWindowInsetsListener(self._insets_listener)
255
+ except Exception:
256
+ pass
257
+
258
+ def _push_initial_viewport(self, view: Any) -> None:
259
+ try:
260
+ _publish_window_insets(view)
261
+ metrics = view.getResources().getDisplayMetrics()
262
+ density = float(metrics.density) or 1.0
263
+ w, h = int(view.getWidth() or 0), int(view.getHeight() or 0)
264
+ if w > 0 and h > 0:
265
+ self.set_viewport_size(w / density, h / density)
266
+ else:
267
+ self.set_viewport_size(metrics.widthPixels / density, metrics.heightPixels / density)
268
+ except Exception:
269
+ pass