pythonnative 0.5.0__py3-none-any.whl → 0.7.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 +53 -15
- pythonnative/cli/pn.py +150 -30
- pythonnative/components.py +217 -107
- pythonnative/element.py +14 -8
- pythonnative/hooks.py +334 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +638 -34
- pythonnative/page.py +138 -171
- pythonnative/reconciler.py +153 -20
- pythonnative/style.py +135 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
- pythonnative/collection_view.py +0 -0
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
pythonnative/page.py
CHANGED
|
@@ -1,160 +1,107 @@
|
|
|
1
|
-
"""Page — the
|
|
1
|
+
"""Page host — the bridge between native lifecycle and function components.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
changes.
|
|
3
|
+
Users no longer subclass ``Page``. Instead they write ``@component``
|
|
4
|
+
functions and the native template calls :func:`create_page` to obtain
|
|
5
|
+
an :class:`_AppHost` that manages the reconciler and lifecycle.
|
|
7
6
|
|
|
8
|
-
Usage::
|
|
7
|
+
Usage (user code)::
|
|
9
8
|
|
|
10
9
|
import pythonnative as pn
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
padding=16,
|
|
26
|
-
)
|
|
11
|
+
@pn.component
|
|
12
|
+
def MainPage():
|
|
13
|
+
count, set_count = pn.use_state(0)
|
|
14
|
+
return pn.Column(
|
|
15
|
+
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
16
|
+
pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
|
|
17
|
+
style={"spacing": 12, "padding": 16},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
The native template calls::
|
|
21
|
+
|
|
22
|
+
host = pythonnative.page.create_page("app.main_page.MainPage", native_instance)
|
|
23
|
+
host.on_create()
|
|
27
24
|
"""
|
|
28
25
|
|
|
26
|
+
import importlib
|
|
29
27
|
import json
|
|
30
|
-
from
|
|
31
|
-
from typing import Any, Optional, Union
|
|
28
|
+
from typing import Any, Dict, Optional
|
|
32
29
|
|
|
33
30
|
from .utils import IS_ANDROID, set_android_context
|
|
34
31
|
|
|
35
32
|
# ======================================================================
|
|
36
|
-
#
|
|
33
|
+
# Component path resolution
|
|
37
34
|
# ======================================================================
|
|
38
35
|
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def set_state(self, **updates: Any) -> None:
|
|
52
|
-
"""Merge *updates* into ``self.state`` and trigger a re-render."""
|
|
53
|
-
|
|
54
|
-
def on_create(self) -> None:
|
|
55
|
-
"""Called when the page is first created. Triggers initial render."""
|
|
56
|
-
|
|
57
|
-
def on_start(self) -> None:
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def on_resume(self) -> None:
|
|
61
|
-
pass
|
|
62
|
-
|
|
63
|
-
def on_pause(self) -> None:
|
|
64
|
-
pass
|
|
65
|
-
|
|
66
|
-
def on_stop(self) -> None:
|
|
67
|
-
pass
|
|
68
|
-
|
|
69
|
-
def on_destroy(self) -> None:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
def on_restart(self) -> None:
|
|
73
|
-
pass
|
|
74
|
-
|
|
75
|
-
def on_save_instance_state(self) -> None:
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
def on_restore_instance_state(self) -> None:
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
|
-
@abstractmethod
|
|
82
|
-
def set_args(self, args: Optional[dict]) -> None:
|
|
83
|
-
pass
|
|
84
|
-
|
|
85
|
-
@abstractmethod
|
|
86
|
-
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
@abstractmethod
|
|
90
|
-
def pop(self) -> None:
|
|
91
|
-
pass
|
|
37
|
+
def _resolve_component_path(page_ref: Any) -> str:
|
|
38
|
+
"""Resolve a component function to a ``module.name`` path string."""
|
|
39
|
+
if isinstance(page_ref, str):
|
|
40
|
+
return page_ref
|
|
41
|
+
func = getattr(page_ref, "__wrapped__", page_ref)
|
|
42
|
+
module = getattr(func, "__module__", None)
|
|
43
|
+
name = getattr(func, "__name__", None)
|
|
44
|
+
if module and name:
|
|
45
|
+
return f"{module}.{name}"
|
|
46
|
+
raise ValueError(f"Cannot resolve component path for {page_ref!r}")
|
|
92
47
|
|
|
93
|
-
def get_args(self) -> dict:
|
|
94
|
-
"""Return navigation arguments (empty dict if none)."""
|
|
95
|
-
return getattr(self, "_args", {})
|
|
96
48
|
|
|
97
|
-
|
|
98
|
-
|
|
49
|
+
def _import_component(component_path: str) -> Any:
|
|
50
|
+
"""Import and return the component function from a dotted path."""
|
|
51
|
+
module_path, component_name = component_path.rsplit(".", 1)
|
|
52
|
+
module = importlib.import_module(module_path)
|
|
53
|
+
return getattr(module, component_name)
|
|
99
54
|
|
|
100
55
|
|
|
101
56
|
# ======================================================================
|
|
102
|
-
# Shared
|
|
57
|
+
# Shared helpers
|
|
103
58
|
# ======================================================================
|
|
104
59
|
|
|
105
60
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
page._reconciler = None
|
|
111
|
-
page._root_native_view = None
|
|
61
|
+
def _init_host_common(host: Any) -> None:
|
|
62
|
+
host._args = {}
|
|
63
|
+
host._reconciler = None
|
|
64
|
+
host._root_native_view = None
|
|
112
65
|
|
|
113
66
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
if page._reconciler is not None:
|
|
117
|
-
_re_render(page)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _on_create(page: Any) -> None:
|
|
67
|
+
def _on_create(host: Any) -> None:
|
|
68
|
+
from .hooks import NavigationHandle, Provider, _NavigationContext
|
|
121
69
|
from .native_views import get_registry
|
|
122
70
|
from .reconciler import Reconciler
|
|
123
71
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
page._root_native_view = page._reconciler.mount(element)
|
|
127
|
-
page._attach_root(page._root_native_view)
|
|
72
|
+
host._reconciler = Reconciler(get_registry())
|
|
73
|
+
host._reconciler._page_re_render = lambda: _re_render(host)
|
|
128
74
|
|
|
75
|
+
nav_handle = NavigationHandle(host)
|
|
76
|
+
app_element = host._component()
|
|
77
|
+
provider_element = Provider(_NavigationContext, nav_handle, app_element)
|
|
129
78
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
new_root = page._reconciler.reconcile(element)
|
|
133
|
-
if new_root is not page._root_native_view:
|
|
134
|
-
page._detach_root(page._root_native_view)
|
|
135
|
-
page._root_native_view = new_root
|
|
136
|
-
page._attach_root(new_root)
|
|
79
|
+
host._root_native_view = host._reconciler.mount(provider_element)
|
|
80
|
+
host._attach_root(host._root_native_view)
|
|
137
81
|
|
|
138
82
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return f"{module}.{name}"
|
|
146
|
-
cls = page_ref.__class__
|
|
147
|
-
return f"{cls.__module__}.{cls.__name__}"
|
|
83
|
+
def _re_render(host: Any) -> None:
|
|
84
|
+
from .hooks import NavigationHandle, Provider, _NavigationContext
|
|
85
|
+
|
|
86
|
+
nav_handle = NavigationHandle(host)
|
|
87
|
+
app_element = host._component()
|
|
88
|
+
provider_element = Provider(_NavigationContext, nav_handle, app_element)
|
|
148
89
|
|
|
90
|
+
new_root = host._reconciler.reconcile(provider_element)
|
|
91
|
+
if new_root is not host._root_native_view:
|
|
92
|
+
host._detach_root(host._root_native_view)
|
|
93
|
+
host._root_native_view = new_root
|
|
94
|
+
host._attach_root(new_root)
|
|
149
95
|
|
|
150
|
-
|
|
96
|
+
|
|
97
|
+
def _set_args(host: Any, args: Any) -> None:
|
|
151
98
|
if isinstance(args, str):
|
|
152
99
|
try:
|
|
153
|
-
|
|
100
|
+
host._args = json.loads(args) or {}
|
|
154
101
|
except Exception:
|
|
155
|
-
|
|
102
|
+
host._args = {}
|
|
156
103
|
return
|
|
157
|
-
|
|
104
|
+
host._args = args if isinstance(args, dict) else {}
|
|
158
105
|
|
|
159
106
|
|
|
160
107
|
# ======================================================================
|
|
@@ -164,21 +111,14 @@ def _set_args(page: Any, args: Optional[dict]) -> None:
|
|
|
164
111
|
if IS_ANDROID:
|
|
165
112
|
from java import jclass
|
|
166
113
|
|
|
167
|
-
class
|
|
168
|
-
"""Android
|
|
114
|
+
class _AppHost:
|
|
115
|
+
"""Android host backed by an Activity and Fragment navigation."""
|
|
169
116
|
|
|
170
|
-
def __init__(self, native_instance: Any) -> None:
|
|
171
|
-
super().__init__()
|
|
172
|
-
self.native_class = jclass("android.app.Activity")
|
|
117
|
+
def __init__(self, native_instance: Any, component_func: Any) -> None:
|
|
173
118
|
self.native_instance = native_instance
|
|
119
|
+
self._component = component_func
|
|
174
120
|
set_android_context(native_instance)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def render(self) -> Any:
|
|
178
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
179
|
-
|
|
180
|
-
def set_state(self, **updates: Any) -> None:
|
|
181
|
-
_set_state(self, **updates)
|
|
121
|
+
_init_host_common(self)
|
|
182
122
|
|
|
183
123
|
def on_create(self) -> None:
|
|
184
124
|
_on_create(self)
|
|
@@ -207,16 +147,19 @@ if IS_ANDROID:
|
|
|
207
147
|
def on_restore_instance_state(self) -> None:
|
|
208
148
|
pass
|
|
209
149
|
|
|
210
|
-
def set_args(self, args:
|
|
150
|
+
def set_args(self, args: Any) -> None:
|
|
211
151
|
_set_args(self, args)
|
|
212
152
|
|
|
213
|
-
def
|
|
214
|
-
|
|
153
|
+
def _get_nav_args(self) -> Dict[str, Any]:
|
|
154
|
+
return self._args
|
|
155
|
+
|
|
156
|
+
def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
|
|
157
|
+
page_path = _resolve_component_path(page)
|
|
215
158
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
216
159
|
args_json = json.dumps(args) if args else None
|
|
217
160
|
Navigator.push(self.native_instance, page_path, args_json)
|
|
218
161
|
|
|
219
|
-
def
|
|
162
|
+
def _pop(self) -> None:
|
|
220
163
|
try:
|
|
221
164
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
222
165
|
Navigator.pop(self.native_instance)
|
|
@@ -264,10 +207,10 @@ else:
|
|
|
264
207
|
|
|
265
208
|
_IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
|
|
266
209
|
|
|
267
|
-
def _ios_register_page(vc_instance: Any,
|
|
210
|
+
def _ios_register_page(vc_instance: Any, host_obj: Any) -> None:
|
|
268
211
|
try:
|
|
269
212
|
ptr = int(vc_instance.ptr)
|
|
270
|
-
_IOS_PAGE_REGISTRY[ptr] =
|
|
213
|
+
_IOS_PAGE_REGISTRY[ptr] = host_obj
|
|
271
214
|
except Exception:
|
|
272
215
|
pass
|
|
273
216
|
|
|
@@ -279,38 +222,31 @@ else:
|
|
|
279
222
|
pass
|
|
280
223
|
|
|
281
224
|
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
282
|
-
"""Forward a lifecycle event from Swift ViewController to the registered
|
|
283
|
-
|
|
284
|
-
if
|
|
225
|
+
"""Forward a lifecycle event from Swift ViewController to the registered host."""
|
|
226
|
+
host = _IOS_PAGE_REGISTRY.get(int(native_addr))
|
|
227
|
+
if host is None:
|
|
285
228
|
return
|
|
286
|
-
handler = getattr(
|
|
229
|
+
handler = getattr(host, event, None)
|
|
287
230
|
if handler:
|
|
288
231
|
handler()
|
|
289
232
|
|
|
290
233
|
if _rubicon_available:
|
|
291
234
|
|
|
292
|
-
class
|
|
293
|
-
"""iOS
|
|
235
|
+
class _AppHost:
|
|
236
|
+
"""iOS host backed by a UIViewController."""
|
|
294
237
|
|
|
295
|
-
def __init__(self, native_instance: Any) -> None:
|
|
296
|
-
super().__init__()
|
|
297
|
-
self.native_class = ObjCClass("UIViewController")
|
|
238
|
+
def __init__(self, native_instance: Any, component_func: Any) -> None:
|
|
298
239
|
if isinstance(native_instance, int):
|
|
299
240
|
try:
|
|
300
241
|
native_instance = ObjCInstance(native_instance)
|
|
301
242
|
except Exception:
|
|
302
243
|
native_instance = None
|
|
303
244
|
self.native_instance = native_instance
|
|
304
|
-
|
|
245
|
+
self._component = component_func
|
|
246
|
+
_init_host_common(self)
|
|
305
247
|
if self.native_instance is not None:
|
|
306
248
|
_ios_register_page(self.native_instance, self)
|
|
307
249
|
|
|
308
|
-
def render(self) -> Any:
|
|
309
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
310
|
-
|
|
311
|
-
def set_state(self, **updates: Any) -> None:
|
|
312
|
-
_set_state(self, **updates)
|
|
313
|
-
|
|
314
250
|
def on_create(self) -> None:
|
|
315
251
|
_on_create(self)
|
|
316
252
|
|
|
@@ -339,11 +275,14 @@ else:
|
|
|
339
275
|
def on_restore_instance_state(self) -> None:
|
|
340
276
|
pass
|
|
341
277
|
|
|
342
|
-
def set_args(self, args:
|
|
278
|
+
def set_args(self, args: Any) -> None:
|
|
343
279
|
_set_args(self, args)
|
|
344
280
|
|
|
345
|
-
def
|
|
346
|
-
|
|
281
|
+
def _get_nav_args(self) -> Dict[str, Any]:
|
|
282
|
+
return self._args
|
|
283
|
+
|
|
284
|
+
def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
|
|
285
|
+
page_path = _resolve_component_path(page)
|
|
347
286
|
ViewController = None
|
|
348
287
|
try:
|
|
349
288
|
ViewController = ObjCClass("ViewController")
|
|
@@ -372,11 +311,11 @@ else:
|
|
|
372
311
|
nav = getattr(self.native_instance, "navigationController", None)
|
|
373
312
|
if nav is None:
|
|
374
313
|
raise RuntimeError(
|
|
375
|
-
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
314
|
+
"No UINavigationController available; " "ensure template embeds root in navigation controller"
|
|
376
315
|
)
|
|
377
316
|
nav.pushViewController_animated_(next_vc, True)
|
|
378
317
|
|
|
379
|
-
def
|
|
318
|
+
def _pop(self) -> None:
|
|
380
319
|
nav = getattr(self.native_instance, "navigationController", None)
|
|
381
320
|
if nav is not None:
|
|
382
321
|
nav.popViewControllerAnimated_(True)
|
|
@@ -407,23 +346,17 @@ else:
|
|
|
407
346
|
|
|
408
347
|
else:
|
|
409
348
|
|
|
410
|
-
class
|
|
349
|
+
class _AppHost:
|
|
411
350
|
"""Desktop stub — no native runtime available.
|
|
412
351
|
|
|
413
352
|
Fully functional for testing with a mock backend via
|
|
414
353
|
``native_views.set_registry()``.
|
|
415
354
|
"""
|
|
416
355
|
|
|
417
|
-
def __init__(self, native_instance: Any = None) -> None:
|
|
418
|
-
super().__init__()
|
|
356
|
+
def __init__(self, native_instance: Any = None, component_func: Any = None) -> None:
|
|
419
357
|
self.native_instance = native_instance
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def render(self) -> Any:
|
|
423
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
424
|
-
|
|
425
|
-
def set_state(self, **updates: Any) -> None:
|
|
426
|
-
_set_state(self, **updates)
|
|
358
|
+
self._component = component_func
|
|
359
|
+
_init_host_common(self)
|
|
427
360
|
|
|
428
361
|
def on_create(self) -> None:
|
|
429
362
|
_on_create(self)
|
|
@@ -452,13 +385,16 @@ else:
|
|
|
452
385
|
def on_restore_instance_state(self) -> None:
|
|
453
386
|
pass
|
|
454
387
|
|
|
455
|
-
def set_args(self, args:
|
|
388
|
+
def set_args(self, args: Any) -> None:
|
|
456
389
|
_set_args(self, args)
|
|
457
390
|
|
|
458
|
-
def
|
|
391
|
+
def _get_nav_args(self) -> Dict[str, Any]:
|
|
392
|
+
return self._args
|
|
393
|
+
|
|
394
|
+
def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
|
|
459
395
|
raise RuntimeError("push() requires a native runtime (iOS or Android)")
|
|
460
396
|
|
|
461
|
-
def
|
|
397
|
+
def _pop(self) -> None:
|
|
462
398
|
raise RuntimeError("pop() requires a native runtime (iOS or Android)")
|
|
463
399
|
|
|
464
400
|
def _attach_root(self, native_view: Any) -> None:
|
|
@@ -466,3 +402,34 @@ else:
|
|
|
466
402
|
|
|
467
403
|
def _detach_root(self, native_view: Any) -> None:
|
|
468
404
|
pass
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ======================================================================
|
|
408
|
+
# Public factory
|
|
409
|
+
# ======================================================================
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def create_page(
|
|
413
|
+
component_path: str,
|
|
414
|
+
native_instance: Any = None,
|
|
415
|
+
args_json: Optional[str] = None,
|
|
416
|
+
) -> _AppHost:
|
|
417
|
+
"""Create a page host for a function component.
|
|
418
|
+
|
|
419
|
+
Called by native templates (PageFragment.kt / ViewController.swift)
|
|
420
|
+
to bridge the native lifecycle to a ``@component`` function.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
component_path:
|
|
425
|
+
Dotted Python path to the component, e.g. ``"app.main_page.MainPage"``.
|
|
426
|
+
native_instance:
|
|
427
|
+
The native Activity (Android) or ViewController pointer (iOS).
|
|
428
|
+
args_json:
|
|
429
|
+
Optional JSON string of navigation arguments.
|
|
430
|
+
"""
|
|
431
|
+
component_func = _import_component(component_path)
|
|
432
|
+
host = _AppHost(native_instance, component_func)
|
|
433
|
+
if args_json:
|
|
434
|
+
_set_args(host, args_json)
|
|
435
|
+
return host
|