pythonnative 0.6.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 +9 -19
- pythonnative/cli/pn.py +10 -18
- pythonnative/components.py +128 -340
- pythonnative/hooks.py +49 -2
- pythonnative/native_views.py +86 -16
- pythonnative/page.py +138 -172
- pythonnative/style.py +27 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
- pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +17 -20
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +15 -15
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.6.0.dist-info → pythonnative-0.7.0.dist-info}/top_level.txt +0 -0
pythonnative/page.py
CHANGED
|
@@ -1,161 +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
|
-
element = page.render()
|
|
127
|
-
page._root_native_view = page._reconciler.mount(element)
|
|
128
|
-
page._attach_root(page._root_native_view)
|
|
72
|
+
host._reconciler = Reconciler(get_registry())
|
|
73
|
+
host._reconciler._page_re_render = lambda: _re_render(host)
|
|
129
74
|
|
|
75
|
+
nav_handle = NavigationHandle(host)
|
|
76
|
+
app_element = host._component()
|
|
77
|
+
provider_element = Provider(_NavigationContext, nav_handle, app_element)
|
|
130
78
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
new_root = page._reconciler.reconcile(element)
|
|
134
|
-
if new_root is not page._root_native_view:
|
|
135
|
-
page._detach_root(page._root_native_view)
|
|
136
|
-
page._root_native_view = new_root
|
|
137
|
-
page._attach_root(new_root)
|
|
79
|
+
host._root_native_view = host._reconciler.mount(provider_element)
|
|
80
|
+
host._attach_root(host._root_native_view)
|
|
138
81
|
|
|
139
82
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return f"{module}.{name}"
|
|
147
|
-
cls = page_ref.__class__
|
|
148
|
-
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)
|
|
149
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)
|
|
150
95
|
|
|
151
|
-
|
|
96
|
+
|
|
97
|
+
def _set_args(host: Any, args: Any) -> None:
|
|
152
98
|
if isinstance(args, str):
|
|
153
99
|
try:
|
|
154
|
-
|
|
100
|
+
host._args = json.loads(args) or {}
|
|
155
101
|
except Exception:
|
|
156
|
-
|
|
102
|
+
host._args = {}
|
|
157
103
|
return
|
|
158
|
-
|
|
104
|
+
host._args = args if isinstance(args, dict) else {}
|
|
159
105
|
|
|
160
106
|
|
|
161
107
|
# ======================================================================
|
|
@@ -165,21 +111,14 @@ def _set_args(page: Any, args: Optional[dict]) -> None:
|
|
|
165
111
|
if IS_ANDROID:
|
|
166
112
|
from java import jclass
|
|
167
113
|
|
|
168
|
-
class
|
|
169
|
-
"""Android
|
|
114
|
+
class _AppHost:
|
|
115
|
+
"""Android host backed by an Activity and Fragment navigation."""
|
|
170
116
|
|
|
171
|
-
def __init__(self, native_instance: Any) -> None:
|
|
172
|
-
super().__init__()
|
|
173
|
-
self.native_class = jclass("android.app.Activity")
|
|
117
|
+
def __init__(self, native_instance: Any, component_func: Any) -> None:
|
|
174
118
|
self.native_instance = native_instance
|
|
119
|
+
self._component = component_func
|
|
175
120
|
set_android_context(native_instance)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def render(self) -> Any:
|
|
179
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
180
|
-
|
|
181
|
-
def set_state(self, **updates: Any) -> None:
|
|
182
|
-
_set_state(self, **updates)
|
|
121
|
+
_init_host_common(self)
|
|
183
122
|
|
|
184
123
|
def on_create(self) -> None:
|
|
185
124
|
_on_create(self)
|
|
@@ -208,16 +147,19 @@ if IS_ANDROID:
|
|
|
208
147
|
def on_restore_instance_state(self) -> None:
|
|
209
148
|
pass
|
|
210
149
|
|
|
211
|
-
def set_args(self, args:
|
|
150
|
+
def set_args(self, args: Any) -> None:
|
|
212
151
|
_set_args(self, args)
|
|
213
152
|
|
|
214
|
-
def
|
|
215
|
-
|
|
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)
|
|
216
158
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
217
159
|
args_json = json.dumps(args) if args else None
|
|
218
160
|
Navigator.push(self.native_instance, page_path, args_json)
|
|
219
161
|
|
|
220
|
-
def
|
|
162
|
+
def _pop(self) -> None:
|
|
221
163
|
try:
|
|
222
164
|
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
223
165
|
Navigator.pop(self.native_instance)
|
|
@@ -265,10 +207,10 @@ else:
|
|
|
265
207
|
|
|
266
208
|
_IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
|
|
267
209
|
|
|
268
|
-
def _ios_register_page(vc_instance: Any,
|
|
210
|
+
def _ios_register_page(vc_instance: Any, host_obj: Any) -> None:
|
|
269
211
|
try:
|
|
270
212
|
ptr = int(vc_instance.ptr)
|
|
271
|
-
_IOS_PAGE_REGISTRY[ptr] =
|
|
213
|
+
_IOS_PAGE_REGISTRY[ptr] = host_obj
|
|
272
214
|
except Exception:
|
|
273
215
|
pass
|
|
274
216
|
|
|
@@ -280,38 +222,31 @@ else:
|
|
|
280
222
|
pass
|
|
281
223
|
|
|
282
224
|
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
283
|
-
"""Forward a lifecycle event from Swift ViewController to the registered
|
|
284
|
-
|
|
285
|
-
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:
|
|
286
228
|
return
|
|
287
|
-
handler = getattr(
|
|
229
|
+
handler = getattr(host, event, None)
|
|
288
230
|
if handler:
|
|
289
231
|
handler()
|
|
290
232
|
|
|
291
233
|
if _rubicon_available:
|
|
292
234
|
|
|
293
|
-
class
|
|
294
|
-
"""iOS
|
|
235
|
+
class _AppHost:
|
|
236
|
+
"""iOS host backed by a UIViewController."""
|
|
295
237
|
|
|
296
|
-
def __init__(self, native_instance: Any) -> None:
|
|
297
|
-
super().__init__()
|
|
298
|
-
self.native_class = ObjCClass("UIViewController")
|
|
238
|
+
def __init__(self, native_instance: Any, component_func: Any) -> None:
|
|
299
239
|
if isinstance(native_instance, int):
|
|
300
240
|
try:
|
|
301
241
|
native_instance = ObjCInstance(native_instance)
|
|
302
242
|
except Exception:
|
|
303
243
|
native_instance = None
|
|
304
244
|
self.native_instance = native_instance
|
|
305
|
-
|
|
245
|
+
self._component = component_func
|
|
246
|
+
_init_host_common(self)
|
|
306
247
|
if self.native_instance is not None:
|
|
307
248
|
_ios_register_page(self.native_instance, self)
|
|
308
249
|
|
|
309
|
-
def render(self) -> Any:
|
|
310
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
311
|
-
|
|
312
|
-
def set_state(self, **updates: Any) -> None:
|
|
313
|
-
_set_state(self, **updates)
|
|
314
|
-
|
|
315
250
|
def on_create(self) -> None:
|
|
316
251
|
_on_create(self)
|
|
317
252
|
|
|
@@ -340,11 +275,14 @@ else:
|
|
|
340
275
|
def on_restore_instance_state(self) -> None:
|
|
341
276
|
pass
|
|
342
277
|
|
|
343
|
-
def set_args(self, args:
|
|
278
|
+
def set_args(self, args: Any) -> None:
|
|
344
279
|
_set_args(self, args)
|
|
345
280
|
|
|
346
|
-
def
|
|
347
|
-
|
|
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)
|
|
348
286
|
ViewController = None
|
|
349
287
|
try:
|
|
350
288
|
ViewController = ObjCClass("ViewController")
|
|
@@ -373,11 +311,11 @@ else:
|
|
|
373
311
|
nav = getattr(self.native_instance, "navigationController", None)
|
|
374
312
|
if nav is None:
|
|
375
313
|
raise RuntimeError(
|
|
376
|
-
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
314
|
+
"No UINavigationController available; " "ensure template embeds root in navigation controller"
|
|
377
315
|
)
|
|
378
316
|
nav.pushViewController_animated_(next_vc, True)
|
|
379
317
|
|
|
380
|
-
def
|
|
318
|
+
def _pop(self) -> None:
|
|
381
319
|
nav = getattr(self.native_instance, "navigationController", None)
|
|
382
320
|
if nav is not None:
|
|
383
321
|
nav.popViewControllerAnimated_(True)
|
|
@@ -408,23 +346,17 @@ else:
|
|
|
408
346
|
|
|
409
347
|
else:
|
|
410
348
|
|
|
411
|
-
class
|
|
349
|
+
class _AppHost:
|
|
412
350
|
"""Desktop stub — no native runtime available.
|
|
413
351
|
|
|
414
352
|
Fully functional for testing with a mock backend via
|
|
415
353
|
``native_views.set_registry()``.
|
|
416
354
|
"""
|
|
417
355
|
|
|
418
|
-
def __init__(self, native_instance: Any = None) -> None:
|
|
419
|
-
super().__init__()
|
|
356
|
+
def __init__(self, native_instance: Any = None, component_func: Any = None) -> None:
|
|
420
357
|
self.native_instance = native_instance
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def render(self) -> Any:
|
|
424
|
-
raise NotImplementedError("Page subclass must implement render()")
|
|
425
|
-
|
|
426
|
-
def set_state(self, **updates: Any) -> None:
|
|
427
|
-
_set_state(self, **updates)
|
|
358
|
+
self._component = component_func
|
|
359
|
+
_init_host_common(self)
|
|
428
360
|
|
|
429
361
|
def on_create(self) -> None:
|
|
430
362
|
_on_create(self)
|
|
@@ -453,13 +385,16 @@ else:
|
|
|
453
385
|
def on_restore_instance_state(self) -> None:
|
|
454
386
|
pass
|
|
455
387
|
|
|
456
|
-
def set_args(self, args:
|
|
388
|
+
def set_args(self, args: Any) -> None:
|
|
457
389
|
_set_args(self, args)
|
|
458
390
|
|
|
459
|
-
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:
|
|
460
395
|
raise RuntimeError("push() requires a native runtime (iOS or Android)")
|
|
461
396
|
|
|
462
|
-
def
|
|
397
|
+
def _pop(self) -> None:
|
|
463
398
|
raise RuntimeError("pop() requires a native runtime (iOS or Android)")
|
|
464
399
|
|
|
465
400
|
def _attach_root(self, native_view: Any) -> None:
|
|
@@ -467,3 +402,34 @@ else:
|
|
|
467
402
|
|
|
468
403
|
def _detach_root(self, native_view: Any) -> None:
|
|
469
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
|
pythonnative/style.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""StyleSheet and theming support.
|
|
1
|
+
"""StyleSheet, style resolution, and theming support.
|
|
2
2
|
|
|
3
3
|
Provides a :class:`StyleSheet` helper for creating and composing
|
|
4
|
-
reusable style dictionaries,
|
|
4
|
+
reusable style dictionaries, a :func:`resolve_style` utility for
|
|
5
|
+
flattening the ``style`` prop, and built-in theme contexts.
|
|
5
6
|
|
|
6
7
|
Usage::
|
|
7
8
|
|
|
@@ -12,20 +13,39 @@ Usage::
|
|
|
12
13
|
container={"padding": 16, "spacing": 12},
|
|
13
14
|
)
|
|
14
15
|
|
|
15
|
-
pn.Text("Hello",
|
|
16
|
-
pn.Column(...,
|
|
16
|
+
pn.Text("Hello", style=styles["title"])
|
|
17
|
+
pn.Column(..., style=styles["container"])
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
from typing import Any, Dict
|
|
20
|
+
from typing import Any, Dict, List, Optional, Union
|
|
20
21
|
|
|
21
22
|
from .hooks import Context, create_context
|
|
22
23
|
|
|
24
|
+
_StyleDict = Dict[str, Any]
|
|
25
|
+
StyleValue = Union[None, _StyleDict, List[Optional[_StyleDict]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_style(style: StyleValue) -> _StyleDict:
|
|
29
|
+
"""Flatten a ``style`` prop into a single dict.
|
|
30
|
+
|
|
31
|
+
Accepts ``None``, a single dict, or a list of dicts (later entries
|
|
32
|
+
override earlier ones, mirroring React Native's array style pattern).
|
|
33
|
+
"""
|
|
34
|
+
if style is None:
|
|
35
|
+
return {}
|
|
36
|
+
if isinstance(style, dict):
|
|
37
|
+
return dict(style)
|
|
38
|
+
result: _StyleDict = {}
|
|
39
|
+
for entry in style:
|
|
40
|
+
if entry:
|
|
41
|
+
result.update(entry)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
# ======================================================================
|
|
24
46
|
# StyleSheet
|
|
25
47
|
# ======================================================================
|
|
26
48
|
|
|
27
|
-
_StyleDict = Dict[str, Any]
|
|
28
|
-
|
|
29
49
|
|
|
30
50
|
class StyleSheet:
|
|
31
51
|
"""Utility for creating and composing style dictionaries."""
|
|
@@ -25,15 +25,8 @@ class PageFragment : Fragment() {
|
|
|
25
25
|
val py = Python.getInstance()
|
|
26
26
|
val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
|
|
27
27
|
val argsJson = arguments?.getString("args_json")
|
|
28
|
-
val
|
|
29
|
-
|
|
30
|
-
val pyModule = py.getModule(moduleName)
|
|
31
|
-
val pageClass = pyModule.get(className)
|
|
32
|
-
// Pass the hosting Activity as native_instance for context
|
|
33
|
-
page = pageClass?.call(requireActivity())
|
|
34
|
-
if (!argsJson.isNullOrEmpty()) {
|
|
35
|
-
page?.callAttr("set_args", argsJson)
|
|
36
|
-
}
|
|
28
|
+
val pnPage = py.getModule("pythonnative.page")
|
|
29
|
+
page = pnPage.callAttr("create_page", pagePath, requireActivity(), argsJson)
|
|
37
30
|
} catch (e: Exception) {
|
|
38
31
|
Log.e(TAG, "Failed to instantiate page", e)
|
|
39
32
|
}
|
|
@@ -85,28 +85,15 @@ class ViewController: UIViewController {
|
|
|
85
85
|
// Determine which Python page to load
|
|
86
86
|
let pagePath: String = requestedPagePath ?? "app.main_page.MainPage"
|
|
87
87
|
do {
|
|
88
|
-
let
|
|
89
|
-
let className = String(pagePath.split(separator: ".").last ?? "MainPage")
|
|
90
|
-
let pyModule = try Python.attemptImport(moduleName)
|
|
91
|
-
// Resolve class by name via builtins.getattr to avoid subscripting issues
|
|
92
|
-
let builtins = Python.import("builtins")
|
|
93
|
-
let getattrFn = builtins.getattr
|
|
94
|
-
let pageClass = try getattrFn.throwing.dynamicallyCall(withArguments: [pyModule, className])
|
|
95
|
-
// Pass native pointer so Python Page can wrap via rubicon.objc
|
|
88
|
+
let pnPage = try Python.attemptImport("pythonnative.page")
|
|
96
89
|
let ptr = Unmanaged.passUnretained(self).toOpaque()
|
|
97
90
|
let addr = UInt(bitPattern: ptr)
|
|
98
|
-
let
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
_ = try page.set_args.throwing.dynamicallyCall(withArguments: [args])
|
|
105
|
-
} catch {
|
|
106
|
-
NSLog("[PN] Failed to decode requestedPageArgsJSON: \(error)")
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Call on_create immediately so Python can insert its root view
|
|
91
|
+
let argsJson: PythonObject = (requestedPageArgsJSON != nil)
|
|
92
|
+
? PythonObject(requestedPageArgsJSON!)
|
|
93
|
+
: Python.None
|
|
94
|
+
let page = try pnPage.create_page.throwing.dynamicallyCall(
|
|
95
|
+
withArguments: [pagePath, addr, argsJson]
|
|
96
|
+
)
|
|
110
97
|
_ = try page.on_create.throwing.dynamicallyCall(withArguments: [])
|
|
111
98
|
return
|
|
112
99
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -88,17 +88,18 @@ Dynamic: license-file
|
|
|
88
88
|
|
|
89
89
|
## Overview
|
|
90
90
|
|
|
91
|
-
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS.
|
|
91
|
+
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
|
|
92
92
|
|
|
93
93
|
## Features
|
|
94
94
|
|
|
95
95
|
- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
|
|
96
|
-
- **
|
|
96
|
+
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
97
|
+
- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
|
|
97
98
|
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
98
99
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
99
100
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
100
|
-
- **Navigation:** Push and pop screens with argument passing
|
|
101
|
-
- **Bundled templates:** Android Gradle and iOS Xcode templates are included
|
|
101
|
+
- **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
|
|
102
|
+
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
|
|
102
103
|
|
|
103
104
|
## Quick Start
|
|
104
105
|
|
|
@@ -114,21 +115,17 @@ pip install pythonnative
|
|
|
114
115
|
import pythonnative as pn
|
|
115
116
|
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
),
|
|
129
|
-
spacing=12,
|
|
130
|
-
padding=16,
|
|
131
|
-
)
|
|
118
|
+
@pn.component
|
|
119
|
+
def MainPage():
|
|
120
|
+
count, set_count = pn.use_state(0)
|
|
121
|
+
return pn.Column(
|
|
122
|
+
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
123
|
+
pn.Button(
|
|
124
|
+
"Tap me",
|
|
125
|
+
on_click=lambda: set_count(count + 1),
|
|
126
|
+
),
|
|
127
|
+
style={"spacing": 12, "padding": 16},
|
|
128
|
+
)
|
|
132
129
|
```
|
|
133
130
|
|
|
134
131
|
## Documentation
|