pythonnative 0.18.0__py3-none-any.whl → 0.20.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 +1 -1
- pythonnative/cli/pn.py +107 -1
- pythonnative/hooks.py +30 -6
- pythonnative/native_views/__init__.py +18 -5
- pythonnative/native_views/desktop.py +1489 -0
- pythonnative/platform.py +17 -8
- pythonnative/preview.py +471 -0
- pythonnative/reconciler.py +285 -3
- pythonnative/runtime.py +26 -1
- pythonnative/screen.py +207 -31
- pythonnative/utils.py +38 -2
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/METADATA +3 -2
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/RECORD +17 -15
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.18.0.dist-info → pythonnative-0.20.0.dist-info}/top_level.txt +0 -0
pythonnative/screen.py
CHANGED
|
@@ -50,7 +50,7 @@ import sys
|
|
|
50
50
|
import threading
|
|
51
51
|
from typing import Any, Dict, Optional, Sequence
|
|
52
52
|
|
|
53
|
-
from .utils import IS_ANDROID, IS_IOS, set_android_context
|
|
53
|
+
from .utils import IS_ANDROID, IS_DESKTOP, IS_IOS, set_android_context
|
|
54
54
|
|
|
55
55
|
_MAX_RENDER_PASSES = 25
|
|
56
56
|
_DEBUG_ENV = "PYTHONNATIVE_DEBUG"
|
|
@@ -87,6 +87,20 @@ def _resolve_component_path(component_ref: Any) -> str:
|
|
|
87
87
|
raise ValueError(f"Cannot resolve component path for {component_ref!r}")
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def _missing_module_is_target(exc: ModuleNotFoundError, dotted: str) -> bool:
|
|
91
|
+
"""Return ``True`` when ``exc`` means ``dotted`` itself is absent.
|
|
92
|
+
|
|
93
|
+
Distinguishes "the component module/package cannot be found" (so the
|
|
94
|
+
caller should fall through to the next resolution strategy) from "the
|
|
95
|
+
module exists but raised :class:`ModuleNotFoundError` while importing
|
|
96
|
+
one of *its own* dependencies". The latter must propagate so the
|
|
97
|
+
developer sees the real missing import (e.g. ``No module named
|
|
98
|
+
'emoji'``) instead of a misleading "could not resolve component".
|
|
99
|
+
"""
|
|
100
|
+
missing = exc.name or ""
|
|
101
|
+
return missing == dotted or dotted.startswith(missing + ".")
|
|
102
|
+
|
|
103
|
+
|
|
90
104
|
def _import_component(component_path: str) -> Any:
|
|
91
105
|
"""Import a component by module or dotted-attribute path.
|
|
92
106
|
|
|
@@ -117,11 +131,16 @@ def _import_component(component_path: str) -> Any:
|
|
|
117
131
|
The resolved component callable.
|
|
118
132
|
|
|
119
133
|
Raises:
|
|
120
|
-
ImportError: If
|
|
134
|
+
ImportError: If the module or dotted path cannot be found.
|
|
135
|
+
Errors raised *inside* a resolvable module (such as a
|
|
136
|
+
missing third-party dependency it imports) propagate
|
|
137
|
+
unchanged so the real cause stays visible.
|
|
121
138
|
"""
|
|
122
139
|
try:
|
|
123
140
|
module = importlib.import_module(component_path)
|
|
124
|
-
except ModuleNotFoundError:
|
|
141
|
+
except ModuleNotFoundError as exc:
|
|
142
|
+
if not _missing_module_is_target(exc, component_path):
|
|
143
|
+
raise
|
|
125
144
|
module = None
|
|
126
145
|
if module is not None:
|
|
127
146
|
component = getattr(module, "App", None)
|
|
@@ -132,7 +151,9 @@ def _import_component(component_path: str) -> Any:
|
|
|
132
151
|
module_path, attr = component_path.rsplit(".", 1)
|
|
133
152
|
try:
|
|
134
153
|
parent = importlib.import_module(module_path)
|
|
135
|
-
except ModuleNotFoundError:
|
|
154
|
+
except ModuleNotFoundError as exc:
|
|
155
|
+
if not _missing_module_is_target(exc, module_path):
|
|
156
|
+
raise
|
|
136
157
|
parent = None
|
|
137
158
|
if parent is not None:
|
|
138
159
|
component = getattr(parent, attr, None)
|
|
@@ -301,53 +322,49 @@ def _request_render(host: Any) -> None:
|
|
|
301
322
|
|
|
302
323
|
|
|
303
324
|
def _re_render(host: Any) -> None:
|
|
304
|
-
"""Run one render pass, then drain any renders queued during it.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
325
|
+
"""Run one *local* render pass, then drain any renders queued during it.
|
|
326
|
+
|
|
327
|
+
State setters mark only their own component subtree dirty (see
|
|
328
|
+
[`mark_dirty`][pythonnative.reconciler.Reconciler.mark_dirty]), so
|
|
329
|
+
this drains the reconciler's dirty set via
|
|
330
|
+
[`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty]
|
|
331
|
+
instead of re-running the whole ``App`` from the root. The app's
|
|
332
|
+
element tree is only rebuilt from scratch on mount, navigation, and
|
|
333
|
+
hot reload.
|
|
334
|
+
"""
|
|
335
|
+
_log_pn("_re_render: starting local render pass")
|
|
308
336
|
host._is_rendering = True
|
|
309
337
|
try:
|
|
310
338
|
host._render_queued = False
|
|
311
|
-
|
|
312
|
-
app_element = _render_app(host)
|
|
313
|
-
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
314
|
-
|
|
315
|
-
new_root = host._reconciler.reconcile(provider_element)
|
|
316
|
-
if new_root is not host._root_native_view:
|
|
317
|
-
_log_pn(f"_re_render: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
318
|
-
host._detach_root(host._root_native_view)
|
|
319
|
-
host._root_native_view = new_root
|
|
320
|
-
host._attach_root(new_root)
|
|
321
|
-
|
|
339
|
+
_commit_dirty(host)
|
|
322
340
|
_drain_renders(host)
|
|
323
341
|
finally:
|
|
324
342
|
host._is_rendering = False
|
|
325
343
|
_log_pn("_re_render: done")
|
|
326
344
|
|
|
327
345
|
|
|
346
|
+
def _commit_dirty(host: Any) -> None:
|
|
347
|
+
"""Flush the reconciler's dirty components and re-attach the root if it changed."""
|
|
348
|
+
new_root = host._reconciler.flush_dirty()
|
|
349
|
+
if new_root is not host._root_native_view:
|
|
350
|
+
_log_pn(f"_commit_dirty: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
351
|
+
host._detach_root(host._root_native_view)
|
|
352
|
+
host._root_native_view = new_root
|
|
353
|
+
host._attach_root(new_root)
|
|
354
|
+
|
|
355
|
+
|
|
328
356
|
def _drain_renders(host: Any) -> None:
|
|
329
357
|
"""Flush additional renders queued by effects that set state.
|
|
330
358
|
|
|
331
359
|
Capped at `_MAX_RENDER_PASSES` to break runaway feedback loops
|
|
332
360
|
(e.g., an effect that unconditionally calls a setter).
|
|
333
361
|
"""
|
|
334
|
-
from .hooks import Provider, _NavigationContext
|
|
335
|
-
|
|
336
362
|
for i in range(_MAX_RENDER_PASSES):
|
|
337
363
|
if not host._render_queued:
|
|
338
364
|
break
|
|
339
365
|
_log_pn(f"_drain_renders: draining pass #{i + 1}")
|
|
340
366
|
host._render_queued = False
|
|
341
|
-
|
|
342
|
-
app_element = _render_app(host)
|
|
343
|
-
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
344
|
-
|
|
345
|
-
new_root = host._reconciler.reconcile(provider_element)
|
|
346
|
-
if new_root is not host._root_native_view:
|
|
347
|
-
_log_pn(f"_drain_renders: ROOT VIEW CHANGED ({id(host._root_native_view)} -> {id(new_root)}); reattaching")
|
|
348
|
-
host._detach_root(host._root_native_view)
|
|
349
|
-
host._root_native_view = new_root
|
|
350
|
-
host._attach_root(new_root)
|
|
367
|
+
_commit_dirty(host)
|
|
351
368
|
|
|
352
369
|
|
|
353
370
|
def _set_args(host: Any, args: Any) -> None:
|
|
@@ -868,6 +885,165 @@ if IS_ANDROID:
|
|
|
868
885
|
"""Public hook for native code to push viewport sizes (Maestro/tests)."""
|
|
869
886
|
_push_viewport_size(self, width, height)
|
|
870
887
|
|
|
888
|
+
elif IS_DESKTOP:
|
|
889
|
+
# ------------------------------------------------------------------
|
|
890
|
+
# Desktop preview host (Tkinter), driven by ``pn preview``.
|
|
891
|
+
#
|
|
892
|
+
# The screen host owns the reconciler + lifecycle just like the
|
|
893
|
+
# device hosts; placement of the root view and the navigation stack
|
|
894
|
+
# are delegated to the ``DesktopApp`` controller in
|
|
895
|
+
# ``pythonnative.preview`` (passed in as ``native_instance``). The
|
|
896
|
+
# controller runs the Tk event loop on the main thread and polls
|
|
897
|
+
# ``drain_desktop_scheduled_renders`` so renders requested from the
|
|
898
|
+
# asyncio worker thread are applied on the main thread.
|
|
899
|
+
# ------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
_DESKTOP_SCHEDULED_RENDER_HOSTS: Dict[int, Any] = {}
|
|
902
|
+
_desktop_render_lock = threading.Lock()
|
|
903
|
+
|
|
904
|
+
def _schedule_render_async(host: Any) -> bool:
|
|
905
|
+
"""Queue an off-main-thread render for the Tk poll loop to drain.
|
|
906
|
+
|
|
907
|
+
Renders requested on the Tk main thread (button handlers, etc.)
|
|
908
|
+
run synchronously (returns ``False``); requests from the asyncio
|
|
909
|
+
worker thread are queued and applied by
|
|
910
|
+
[`drain_desktop_scheduled_renders`][pythonnative.screen.drain_desktop_scheduled_renders].
|
|
911
|
+
"""
|
|
912
|
+
if not IS_DESKTOP:
|
|
913
|
+
return False
|
|
914
|
+
if threading.current_thread() is threading.main_thread():
|
|
915
|
+
return False
|
|
916
|
+
if getattr(host, "_render_scheduled", False):
|
|
917
|
+
return True
|
|
918
|
+
host._render_scheduled = True
|
|
919
|
+
with _desktop_render_lock:
|
|
920
|
+
_DESKTOP_SCHEDULED_RENDER_HOSTS[id(host)] = host
|
|
921
|
+
return True
|
|
922
|
+
|
|
923
|
+
def drain_desktop_scheduled_renders() -> None:
|
|
924
|
+
"""Apply renders queued from worker threads (called on the main thread)."""
|
|
925
|
+
with _desktop_render_lock:
|
|
926
|
+
hosts = list(_DESKTOP_SCHEDULED_RENDER_HOSTS.values())
|
|
927
|
+
_DESKTOP_SCHEDULED_RENDER_HOSTS.clear()
|
|
928
|
+
_flush_scheduled_renders(hosts)
|
|
929
|
+
|
|
930
|
+
class _ScreenHost:
|
|
931
|
+
"""Desktop host backed by a Tk window and an in-process nav stack.
|
|
932
|
+
|
|
933
|
+
Created by ``pythonnative.preview`` for
|
|
934
|
+
each screen on the navigation stack. ``native_instance`` is the
|
|
935
|
+
``DesktopApp`` controller, which provides the stage frame,
|
|
936
|
+
viewport size, and push/pop primitives.
|
|
937
|
+
"""
|
|
938
|
+
|
|
939
|
+
def __init__(self, native_instance: Any = None, component_path: str = "", component_func: Any = None) -> None:
|
|
940
|
+
self.native_instance = native_instance
|
|
941
|
+
_init_host_common(self, component_path, component_func)
|
|
942
|
+
|
|
943
|
+
def on_create(self) -> None:
|
|
944
|
+
_on_create(self)
|
|
945
|
+
|
|
946
|
+
def on_start(self) -> None:
|
|
947
|
+
pass
|
|
948
|
+
|
|
949
|
+
def on_resume(self) -> None:
|
|
950
|
+
_set_host_focused(self, True)
|
|
951
|
+
|
|
952
|
+
def on_layout(self) -> None:
|
|
953
|
+
pass
|
|
954
|
+
|
|
955
|
+
def on_pause(self) -> None:
|
|
956
|
+
_set_host_focused(self, False)
|
|
957
|
+
|
|
958
|
+
def on_stop(self) -> None:
|
|
959
|
+
pass
|
|
960
|
+
|
|
961
|
+
def on_destroy(self) -> None:
|
|
962
|
+
pass
|
|
963
|
+
|
|
964
|
+
def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
|
|
965
|
+
_enable_hot_reload(self, manifest_path)
|
|
966
|
+
|
|
967
|
+
def hot_reload_tick(self) -> bool:
|
|
968
|
+
return _hot_reload_tick(self)
|
|
969
|
+
|
|
970
|
+
def reload(self, changed_modules: Optional[Sequence[str]] = None) -> None:
|
|
971
|
+
_reload_host(self, changed_modules)
|
|
972
|
+
|
|
973
|
+
def on_restart(self) -> None:
|
|
974
|
+
pass
|
|
975
|
+
|
|
976
|
+
def on_save_instance_state(self) -> None:
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
def on_restore_instance_state(self) -> None:
|
|
980
|
+
pass
|
|
981
|
+
|
|
982
|
+
def set_args(self, args: Any) -> None:
|
|
983
|
+
_set_args(self, args)
|
|
984
|
+
|
|
985
|
+
def _get_nav_args(self) -> Dict[str, Any]:
|
|
986
|
+
return self._args
|
|
987
|
+
|
|
988
|
+
def _push(self, component: Any, args: Optional[Dict[str, Any]] = None) -> None:
|
|
989
|
+
screen_path = _resolve_component_path(component)
|
|
990
|
+
app = self.native_instance
|
|
991
|
+
if app is None or not hasattr(app, "push_screen"):
|
|
992
|
+
raise RuntimeError("desktop navigation requires a running `pn preview` session")
|
|
993
|
+
app.push_screen(screen_path, args)
|
|
994
|
+
|
|
995
|
+
def _pop(self) -> None:
|
|
996
|
+
app = self.native_instance
|
|
997
|
+
if app is not None and hasattr(app, "pop_screen"):
|
|
998
|
+
app.pop_screen()
|
|
999
|
+
|
|
1000
|
+
def _reset_to_root(self) -> None:
|
|
1001
|
+
app = self.native_instance
|
|
1002
|
+
if app is not None and hasattr(app, "reset_to_root"):
|
|
1003
|
+
try:
|
|
1004
|
+
app.reset_to_root()
|
|
1005
|
+
except Exception:
|
|
1006
|
+
pass
|
|
1007
|
+
|
|
1008
|
+
def _set_screen_options(self, options: Dict[str, Any]) -> None:
|
|
1009
|
+
title = options.get("title") if isinstance(options, dict) else None
|
|
1010
|
+
app = self.native_instance
|
|
1011
|
+
if title and app is not None and hasattr(app, "set_title"):
|
|
1012
|
+
try:
|
|
1013
|
+
app.set_title(str(title))
|
|
1014
|
+
except Exception:
|
|
1015
|
+
pass
|
|
1016
|
+
|
|
1017
|
+
def _attach_root(self, native_view: Any) -> None:
|
|
1018
|
+
from .native_views import desktop as _desktop_backend
|
|
1019
|
+
|
|
1020
|
+
stage = _desktop_backend.get_root_container()
|
|
1021
|
+
if stage is not None and native_view is not None:
|
|
1022
|
+
try:
|
|
1023
|
+
native_view.place(in_=stage, x=0, y=0, relwidth=1.0, relheight=1.0)
|
|
1024
|
+
native_view.lift()
|
|
1025
|
+
except Exception:
|
|
1026
|
+
pass
|
|
1027
|
+
app = self.native_instance
|
|
1028
|
+
if app is not None and hasattr(app, "viewport_size"):
|
|
1029
|
+
try:
|
|
1030
|
+
width, height = app.viewport_size()
|
|
1031
|
+
if width > 0 and height > 0:
|
|
1032
|
+
_push_viewport_size(self, float(width), float(height))
|
|
1033
|
+
except Exception:
|
|
1034
|
+
pass
|
|
1035
|
+
|
|
1036
|
+
def _detach_root(self, native_view: Any) -> None:
|
|
1037
|
+
if native_view is not None:
|
|
1038
|
+
try:
|
|
1039
|
+
native_view.place_forget()
|
|
1040
|
+
except Exception:
|
|
1041
|
+
pass
|
|
1042
|
+
|
|
1043
|
+
def set_viewport_size(self, width: float, height: float) -> None:
|
|
1044
|
+
"""Push a viewport-size change (called on window resize)."""
|
|
1045
|
+
_push_viewport_size(self, width, height)
|
|
1046
|
+
|
|
871
1047
|
else:
|
|
872
1048
|
from typing import Dict as _Dict
|
|
873
1049
|
|
pythonnative/utils.py
CHANGED
|
@@ -19,6 +19,10 @@ Attributes:
|
|
|
19
19
|
`PN_PLATFORM=ios`, `sys.platform == "ios"`, or a Simulator
|
|
20
20
|
`HOME` path). Importing `rubicon-objc` alone is intentionally
|
|
21
21
|
not enough to trigger this flag.
|
|
22
|
+
IS_DESKTOP: `True` when running the desktop preview backend
|
|
23
|
+
(signaled by `PN_PLATFORM=desktop`, set by ``pn preview``).
|
|
24
|
+
This drives the Tkinter native-view registry so a PythonNative
|
|
25
|
+
app can render in a real OS window for fast local iteration.
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
import os
|
|
@@ -31,6 +35,7 @@ from typing import Any, Optional
|
|
|
31
35
|
|
|
32
36
|
_is_android: Optional[bool] = None
|
|
33
37
|
_is_ios: Optional[bool] = None
|
|
38
|
+
_is_desktop: Optional[bool] = None
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
def _detect_android() -> bool:
|
|
@@ -75,13 +80,29 @@ def _detect_ios() -> bool:
|
|
|
75
80
|
return False
|
|
76
81
|
|
|
77
82
|
|
|
83
|
+
def _detect_desktop() -> bool:
|
|
84
|
+
"""Detect whether we're running the desktop (Tkinter) preview backend.
|
|
85
|
+
|
|
86
|
+
The only signal is the explicit ``PN_PLATFORM=desktop`` env var,
|
|
87
|
+
set by ``pn preview`` before importing PythonNative. Desktop is a
|
|
88
|
+
*development* target: it renders the app in a native OS window via
|
|
89
|
+
the pure-Python Tkinter registry so the inner dev loop doesn't
|
|
90
|
+
require a device build. Off-device unit tests deliberately leave
|
|
91
|
+
this flag ``False`` so they keep using an injected mock registry
|
|
92
|
+
and ``Platform.OS == "test"``.
|
|
93
|
+
"""
|
|
94
|
+
return os.environ.get("PN_PLATFORM") == "desktop"
|
|
95
|
+
|
|
96
|
+
|
|
78
97
|
def _ensure_platform_detection() -> None:
|
|
79
|
-
"""Populate `_is_android` / `_is_ios` once, then reuse."""
|
|
80
|
-
global _is_android, _is_ios
|
|
98
|
+
"""Populate `_is_android` / `_is_ios` / `_is_desktop` once, then reuse."""
|
|
99
|
+
global _is_android, _is_ios, _is_desktop
|
|
81
100
|
if _is_android is None:
|
|
82
101
|
_is_android = _detect_android()
|
|
83
102
|
if _is_ios is None:
|
|
84
103
|
_is_ios = (not _is_android) and _detect_ios()
|
|
104
|
+
if _is_desktop is None:
|
|
105
|
+
_is_desktop = (not _is_android) and (not _is_ios) and _detect_desktop()
|
|
85
106
|
|
|
86
107
|
|
|
87
108
|
def _get_is_android() -> bool:
|
|
@@ -98,6 +119,13 @@ def _get_is_ios() -> bool:
|
|
|
98
119
|
return _is_ios
|
|
99
120
|
|
|
100
121
|
|
|
122
|
+
def _get_is_desktop() -> bool:
|
|
123
|
+
"""Return the cached desktop-detection result."""
|
|
124
|
+
_ensure_platform_detection()
|
|
125
|
+
assert _is_desktop is not None
|
|
126
|
+
return _is_desktop
|
|
127
|
+
|
|
128
|
+
|
|
101
129
|
IS_ANDROID: bool = _get_is_android()
|
|
102
130
|
"""``True`` when running inside an Android process.
|
|
103
131
|
|
|
@@ -113,6 +141,14 @@ The flag is computed once at import time, by checking
|
|
|
113
141
|
`HOME` path.
|
|
114
142
|
"""
|
|
115
143
|
|
|
144
|
+
IS_DESKTOP: bool = _get_is_desktop()
|
|
145
|
+
"""``True`` when running the desktop (Tkinter) preview backend.
|
|
146
|
+
|
|
147
|
+
Set by ``pn preview`` via ``PN_PLATFORM=desktop``. Mutually exclusive
|
|
148
|
+
with `IS_ANDROID` / `IS_IOS`. Off-device unit tests leave this
|
|
149
|
+
``False`` and inject a mock registry instead.
|
|
150
|
+
"""
|
|
151
|
+
|
|
116
152
|
# ======================================================================
|
|
117
153
|
# Android context management
|
|
118
154
|
# ======================================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -102,10 +102,11 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
102
102
|
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
|
|
103
103
|
- **Typed `style` prop:** Pass all visual and layout properties through a single `style` dict, fully described by the `pn.Style` `TypedDict` and the ergonomic `pn.style(...)` helper for IDE autocomplete and static checking. Compose reusable styles with `StyleSheet`.
|
|
104
104
|
- **Cross-platform flexbox engine:** A pure-Python, Yoga-style layout engine computes frames once and applies them to native views, so `flex`, `padding`, `aspect_ratio`, and `position: "absolute"` produce the same geometry on Android and iOS.
|
|
105
|
-
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
105
|
+
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. State updates re-render **locally** — only the component whose state changed (and its subtree) re-runs, and unchanged leaves reuse cached intrinsic measurements — so deep UIs stay responsive instead of re-rendering the whole app from the root on every tap.
|
|
106
106
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
107
107
|
- **Custom-component SDK:** Wrap any platform widget as a first-class element with type-checked props via `pythonnative.sdk` (`Props`, `@native_component`, `element_factory`). Plugins distributed on PyPI auto-register through the `pythonnative.handlers` entry-point group.
|
|
108
108
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
109
|
+
- **Instant desktop preview:** `pn preview` renders your app in a native desktop window via Tkinter with Fast Refresh on every save — iterate on layout, state, and navigation in milliseconds without booting a simulator or device. The reconciler, hooks, layout engine, and navigation are the same code that ships to the phone.
|
|
109
110
|
- **Native-backed navigation:** Declarative `Stack`, `Tab`, and `Drawer` navigators inspired by React Navigation. The root stack drives the platform's native navigation controller (`UINavigationController` on iOS, AndroidX Navigation Component on Android), so transitions, back gestures, and the hardware back button match what users expect.
|
|
110
111
|
- **Fast Refresh hot reload:** `pn run --hot-reload` watches `app/` and patches edits into the running app on save, preserving component state across most changes.
|
|
111
112
|
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
pythonnative/__init__.py,sha256=
|
|
1
|
+
pythonnative/__init__.py,sha256=tfRm60oMZNS2TXgluc8QfIi2uV3fHncDYVNB9ir8voY,8042
|
|
2
2
|
pythonnative/_ios_log.py,sha256=Oi7V28VxcVoZyrpAirvLeEmUW18McqnU87V4d37Zzlw,2582
|
|
3
3
|
pythonnative/alerts.py,sha256=mIANysFlaHwL5EqKnvNcyiJN9rGiZi9XDrD9Jpz1RFM,9340
|
|
4
4
|
pythonnative/animated.py,sha256=bAgG_sGODAdl5eVQjX_vryaKI1hyjI92QH1PNx7Tqyg,24491
|
|
5
5
|
pythonnative/components.py,sha256=7vkoMaKVTVKZNBut1UKET3xawnJzuIc-Zt7m__3w9X8,72503
|
|
6
6
|
pythonnative/element.py,sha256=W9varJj0Cl9HpckL8BcsC1u4ryUQOPVMrvetro4ilAE,2725
|
|
7
|
-
pythonnative/hooks.py,sha256=
|
|
7
|
+
pythonnative/hooks.py,sha256=_XkoyK0aTqu3A7BiiXalN3R-qXTiOh1UbL_ZmUcIaDA,38996
|
|
8
8
|
pythonnative/hot_reload.py,sha256=j7z2c7o2Hdoyd-p4nQY15LTW7CBH_1z0TSAzLCer-aA,25036
|
|
9
9
|
pythonnative/layout.py,sha256=siU7PeVOjL_G1f-1q31ssrKWlxz2UBmvMwNXtmqyOxw,36586
|
|
10
10
|
pythonnative/navigation.py,sha256=skMZFh3AXEJgTU6qQpATFN1Lp4GB94K_ACNNXZjEOEE,35579
|
|
11
11
|
pythonnative/net.py,sha256=UI-39-BmGYWLE_vMAFoAbkzWZvfhIFj9yX_gexp1loc,8091
|
|
12
|
-
pythonnative/platform.py,sha256=
|
|
12
|
+
pythonnative/platform.py,sha256=cqG37hPY1fh6QzgO_AfHObg8R1fiMZmDeMu_2TIXqO0,4997
|
|
13
13
|
pythonnative/platform_metrics.py,sha256=m2u8M8x52n5THNsYdspcaI9mlWWMbfSJWai1svjD0NM,8976
|
|
14
|
-
pythonnative/
|
|
15
|
-
pythonnative/
|
|
16
|
-
pythonnative/
|
|
14
|
+
pythonnative/preview.py,sha256=ZtDupXfQdW07i2JD-fnoikAd3zHKVPYoPSMob1K8KQs,16990
|
|
15
|
+
pythonnative/reconciler.py,sha256=cy6OtPXlxHzMafpNuMUfTMqIVobrxCnkwYd2MaiqOXw,56815
|
|
16
|
+
pythonnative/runtime.py,sha256=8xQvhZvMQJYJ0eozWTwKvC-H1GN5vikl6OCxt8f5uI0,18267
|
|
17
|
+
pythonnative/screen.py,sha256=Vm6tdc9YM9P1eORmEo-6clODeaTf6rF0aVFDgSxg6NE,64745
|
|
17
18
|
pythonnative/storage.py,sha256=hLgSI44ADq6wj29eeYbHaAUNpxYPzJ2ZLn1L7AHkPZY,12010
|
|
18
19
|
pythonnative/style.py,sha256=yDJv-G6iZIgrscpc-IZS_cbEQvY2o7R02PTQZ4BV8RA,15162
|
|
19
|
-
pythonnative/utils.py,sha256
|
|
20
|
+
pythonnative/utils.py,sha256=-hwe_YS19ebpjeygdl3dGeVsYzO4G74rYD53svSi0rI,7593
|
|
20
21
|
pythonnative/cli/__init__.py,sha256=NM1psvKe8jT0vzp8Ak4MMoygZz4P_msk5g-YEsY8xLk,232
|
|
21
|
-
pythonnative/cli/pn.py,sha256=
|
|
22
|
+
pythonnative/cli/pn.py,sha256=wd6mpPxO5nMzt_jJWbw_Vp4Rz-BceQZHHu2o3CaE6KE,53795
|
|
22
23
|
pythonnative/native_modules/__init__.py,sha256=pgigpHuzT-rqwcjlwJvu93_4L8Nozal1HJid0S_JlmM,2636
|
|
23
24
|
pythonnative/native_modules/app_state.py,sha256=EnfChi_YWEgUpZosUiBQh0CmblBZFw8ZGaW1NKIJ4WM,2666
|
|
24
25
|
pythonnative/native_modules/battery.py,sha256=gOU9aN5fCmWfHgTXPD-BgvatdQnyUjfVfsJM7PQ_ARA,4317
|
|
@@ -34,9 +35,10 @@ pythonnative/native_modules/notifications.py,sha256=WVtzdimc_aGfnxU6syCFPkjHF9YR
|
|
|
34
35
|
pythonnative/native_modules/permissions.py,sha256=adRiO_LGxZ1PSJaCoJx_3nheuh9lLWNrPIb47O_A6-Y,6710
|
|
35
36
|
pythonnative/native_modules/secure_store.py,sha256=YC25T2n2Nkfgks_qrCM1CR232AeSkh1_78NOJEBQ800,5956
|
|
36
37
|
pythonnative/native_modules/share.py,sha256=B9ovknmnjeQvGQ8MQ14Hmq6E1x-JA469APMburJ0KUg,4727
|
|
37
|
-
pythonnative/native_views/__init__.py,sha256=
|
|
38
|
+
pythonnative/native_views/__init__.py,sha256=PCb2twVEIwEgMS5NZxsjoe4DeZwm1LRh0Q4QKYHS--A,12347
|
|
38
39
|
pythonnative/native_views/android.py,sha256=z2ypAqMcgL9WNjVcTw-oINgbcFZJR27awC2jS_cWqJ0,114036
|
|
39
40
|
pythonnative/native_views/base.py,sha256=LXDQYRM8wJa3MmGPwslkVyvlu36_s1_J6aO7wwhwYpA,6173
|
|
41
|
+
pythonnative/native_views/desktop.py,sha256=Z4r-72B3zbuvnqbJ5um35UqPM_iTj8wrEz61qcg0SZA,54931
|
|
40
42
|
pythonnative/native_views/ios.py,sha256=F-DTStyva9fsESHAR5qZkNwrQKH_q2BoSGzgKPKJCBA,142727
|
|
41
43
|
pythonnative/sdk/__init__.py,sha256=btIRfW2yy2d2LzjdpFnlc6ym-G3iJj9sVUbb2IlFMOI,3384
|
|
42
44
|
pythonnative/sdk/_components.py,sha256=Hw0cqiyJ1NEzUrhOIT8zsC_mnVtf0jgpPUsireQG2qM,14644
|
|
@@ -92,9 +94,9 @@ pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/x
|
|
|
92
94
|
pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift,sha256=YnwzZx7yXB13xKAXEGNgz17VuhWeqkHTRTtBJ2Vu3_E,1238
|
|
93
95
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift,sha256=l2Pwa50F_rv-qPu2go6e4bQernM6PTQJeNPFl_c4ivY,1387
|
|
94
96
|
pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift,sha256=f5JrG0uVtLMeJQy26Yyz7Om-JUkT220osqcbeIVkj2g,815
|
|
95
|
-
pythonnative-0.
|
|
96
|
-
pythonnative-0.
|
|
97
|
-
pythonnative-0.
|
|
98
|
-
pythonnative-0.
|
|
99
|
-
pythonnative-0.
|
|
100
|
-
pythonnative-0.
|
|
97
|
+
pythonnative-0.20.0.dist-info/licenses/LICENSE,sha256=A69iG7TIAe6KkGQf6xoVHkc5JSZtOr5eRSvC5iuivnI,1067
|
|
98
|
+
pythonnative-0.20.0.dist-info/METADATA,sha256=XlOYAungaXEGKHMhG9xRQV2IUCcWrgKoHCci7hwou54,9233
|
|
99
|
+
pythonnative-0.20.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
100
|
+
pythonnative-0.20.0.dist-info/entry_points.txt,sha256=iUtDawWSAJAEyWTycpZxDuYz73ol31butpzDIEAgPO0,48
|
|
101
|
+
pythonnative-0.20.0.dist-info/top_level.txt,sha256=kT4SEATY2ywzrZ2Pgea6_zxyym44Q_PbOsUoOYjJLFE,13
|
|
102
|
+
pythonnative-0.20.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|