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.
Files changed (28) hide show
  1. pythonnative/__init__.py +53 -15
  2. pythonnative/cli/pn.py +150 -30
  3. pythonnative/components.py +217 -107
  4. pythonnative/element.py +14 -8
  5. pythonnative/hooks.py +334 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +638 -34
  13. pythonnative/page.py +138 -171
  14. pythonnative/reconciler.py +153 -20
  15. pythonnative/style.py +135 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
  20. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/METADATA +18 -38
  21. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/RECORD +25 -20
  22. pythonnative/collection_view.py +0 -0
  23. pythonnative/material_bottom_navigation_view.py +0 -0
  24. pythonnative/material_toolbar.py +0 -0
  25. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/WHEEL +0 -0
  26. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/entry_points.txt +0 -0
  27. {pythonnative-0.5.0.dist-info → pythonnative-0.7.0.dist-info}/licenses/LICENSE +0 -0
  28. {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 root component that bridges native lifecycle and declarative UI.
1
+ """Page host — the bridge between native lifecycle and function components.
2
2
 
3
- A ``Page`` subclass is the entry point for each screen. It owns a
4
- :class:`~pythonnative.reconciler.Reconciler` and automatically mounts /
5
- re-renders the element tree returned by :meth:`render` whenever state
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
- class MainPage(pn.Page):
13
- def __init__(self, native_instance):
14
- super().__init__(native_instance)
15
- self.state = {"count": 0}
16
-
17
- def increment(self):
18
- self.set_state(count=self.state["count"] + 1)
19
-
20
- def render(self):
21
- return pn.Column(
22
- pn.Text(f"Count: {self.state['count']}", font_size=24),
23
- pn.Button("Increment", on_click=self.increment),
24
- spacing=12,
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 abc import ABC, abstractmethod
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
- # Base class (platform-independent)
33
+ # Component path resolution
37
34
  # ======================================================================
38
35
 
39
36
 
40
- class PageBase(ABC):
41
- """Abstract base defining the Page interface."""
42
-
43
- @abstractmethod
44
- def __init__(self) -> None:
45
- super().__init__()
46
-
47
- @abstractmethod
48
- def render(self) -> Any:
49
- """Return an Element tree describing this page's UI."""
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
- def navigate_to(self, page: Any) -> None:
98
- self.push(page)
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 declarative rendering helpers
57
+ # Shared helpers
103
58
  # ======================================================================
104
59
 
105
60
 
106
- def _init_page_common(page: Any) -> None:
107
- """Common initialisation shared by both platform Page classes."""
108
- page.state = {}
109
- page._args = {}
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 _set_state(page: Any, **updates: Any) -> None:
115
- page.state.update(updates)
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
- page._reconciler = Reconciler(get_registry())
125
- element = page.render()
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
- def _re_render(page: Any) -> None:
131
- element = page.render()
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 _resolve_page_path(page_ref: Union[str, Any]) -> str:
140
- if isinstance(page_ref, str):
141
- return page_ref
142
- module = getattr(page_ref, "__module__", None)
143
- name = getattr(page_ref, "__name__", None)
144
- if module and name:
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
- def _set_args(page: Any, args: Optional[dict]) -> None:
96
+
97
+ def _set_args(host: Any, args: Any) -> None:
151
98
  if isinstance(args, str):
152
99
  try:
153
- page._args = json.loads(args) or {}
100
+ host._args = json.loads(args) or {}
154
101
  except Exception:
155
- page._args = {}
102
+ host._args = {}
156
103
  return
157
- page._args = args or {}
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 Page(PageBase):
168
- """Android Page backed by an Activity and Fragment navigation."""
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
- _init_page_common(self)
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: Optional[dict]) -> None:
150
+ def set_args(self, args: Any) -> None:
211
151
  _set_args(self, args)
212
152
 
213
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
214
- page_path = _resolve_page_path(page)
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 pop(self) -> None:
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, page_obj: Any) -> None:
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] = page_obj
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 Page."""
283
- page = _IOS_PAGE_REGISTRY.get(int(native_addr))
284
- if page is None:
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(page, event, None)
229
+ handler = getattr(host, event, None)
287
230
  if handler:
288
231
  handler()
289
232
 
290
233
  if _rubicon_available:
291
234
 
292
- class Page(PageBase):
293
- """iOS Page backed by a UIViewController."""
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
- _init_page_common(self)
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: Optional[dict]) -> None:
278
+ def set_args(self, args: Any) -> None:
343
279
  _set_args(self, args)
344
280
 
345
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
346
- page_path = _resolve_page_path(page)
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 pop(self) -> None:
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 Page(PageBase):
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
- _init_page_common(self)
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: Optional[dict]) -> None:
388
+ def set_args(self, args: Any) -> None:
456
389
  _set_args(self, args)
457
390
 
458
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
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 pop(self) -> None:
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