pythonnative 0.12.0__tar.gz → 0.13.0__tar.gz
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-0.12.0/src/pythonnative.egg-info → pythonnative-0.13.0}/PKG-INFO +4 -3
- {pythonnative-0.12.0 → pythonnative-0.13.0}/README.md +3 -2
- {pythonnative-0.12.0 → pythonnative-0.13.0}/pyproject.toml +2 -1
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/__init__.py +3 -3
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/cli/pn.py +34 -9
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/hooks.py +22 -13
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/hot_reload.py +188 -22
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/navigation.py +178 -35
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/platform_metrics.py +9 -9
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/reconciler.py +7 -7
- pythonnative-0.12.0/src/pythonnative/page.py → pythonnative-0.13.0/src/pythonnative/screen.py +251 -67
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +4 -4
- pythonnative-0.13.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +43 -0
- pythonnative-0.12.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt → pythonnative-0.13.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +23 -18
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +6 -6
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +31 -26
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/utils.py +5 -5
- {pythonnative-0.12.0 → pythonnative-0.13.0/src/pythonnative.egg-info}/PKG-INFO +4 -3
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/SOURCES.txt +3 -3
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_cli.py +12 -10
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_hooks.py +2 -2
- pythonnative-0.13.0/tests/test_hot_reload.py +303 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_metric_hooks.py +3 -3
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_navigation.py +266 -12
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_reconciler.py +1 -1
- pythonnative-0.12.0/tests/test_page.py → pythonnative-0.13.0/tests/test_screen.py +28 -28
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_smoke.py +1 -1
- pythonnative-0.12.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -26
- pythonnative-0.12.0/tests/test_hot_reload.py +0 -97
- {pythonnative-0.12.0 → pythonnative-0.13.0}/LICENSE +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/setup.cfg +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/_ios_log.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/alerts.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/animated.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/components.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/element.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/layout.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/__init__.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/camera.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/file_system.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/location.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/notifications.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/__init__.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/android.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/base.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/ios.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/platform.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/style.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_alert.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_animated.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_components.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_element.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_ios_log.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_layout.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_native_views.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_new_components.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_platform.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_platform_metrics.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_ref.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_style.py +0 -0
- {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -101,7 +101,8 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
101
101
|
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
102
102
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
103
103
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
104
|
-
- **
|
|
104
|
+
- **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.
|
|
105
|
+
- **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.
|
|
105
106
|
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
|
|
106
107
|
|
|
107
108
|
## Quick Start
|
|
@@ -119,7 +120,7 @@ import pythonnative as pn
|
|
|
119
120
|
|
|
120
121
|
|
|
121
122
|
@pn.component
|
|
122
|
-
def
|
|
123
|
+
def App():
|
|
123
124
|
count, set_count = pn.use_state(0)
|
|
124
125
|
return pn.Column(
|
|
125
126
|
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
@@ -37,7 +37,8 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
|
|
|
37
37
|
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
|
|
38
38
|
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
|
|
39
39
|
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
|
|
40
|
-
- **
|
|
40
|
+
- **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.
|
|
41
|
+
- **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.
|
|
41
42
|
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
|
|
42
43
|
|
|
43
44
|
## Quick Start
|
|
@@ -55,7 +56,7 @@ import pythonnative as pn
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@pn.component
|
|
58
|
-
def
|
|
59
|
+
def App():
|
|
59
60
|
count, set_count = pn.use_state(0)
|
|
60
61
|
return pn.Column(
|
|
61
62
|
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonnative"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.0"
|
|
8
8
|
description = "Cross-platform native UI toolkit for Android and iOS"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Owen Carey" }
|
|
@@ -61,6 +61,7 @@ Issues = "https://github.com/pythonnative/pythonnative/issues"
|
|
|
61
61
|
Documentation = "https://docs.pythonnative.com/"
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
|
|
64
65
|
[tool.setuptools.packages.find]
|
|
65
66
|
where = ["src"]
|
|
66
67
|
|
|
@@ -41,7 +41,7 @@ Example:
|
|
|
41
41
|
```
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
|
-
__version__ = "0.
|
|
44
|
+
__version__ = "0.13.0"
|
|
45
45
|
|
|
46
46
|
from .alerts import Alert
|
|
47
47
|
from .animated import Animated, AnimatedValue
|
|
@@ -98,8 +98,8 @@ from .navigation import (
|
|
|
98
98
|
use_focus_effect,
|
|
99
99
|
use_route,
|
|
100
100
|
)
|
|
101
|
-
from .page import create_page
|
|
102
101
|
from .platform import Platform
|
|
102
|
+
from .screen import create_screen
|
|
103
103
|
from .style import StyleSheet, ThemeContext
|
|
104
104
|
|
|
105
105
|
__all__ = [
|
|
@@ -130,7 +130,7 @@ __all__ = [
|
|
|
130
130
|
"WebView",
|
|
131
131
|
# Core
|
|
132
132
|
"Element",
|
|
133
|
-
"
|
|
133
|
+
"create_screen",
|
|
134
134
|
# Hooks
|
|
135
135
|
"batch_updates",
|
|
136
136
|
"component",
|
|
@@ -32,9 +32,9 @@ from typing import Any, Dict, List, Optional
|
|
|
32
32
|
def init_project(args: argparse.Namespace) -> None:
|
|
33
33
|
"""Scaffold a new PythonNative project in the current directory.
|
|
34
34
|
|
|
35
|
-
Creates `app/
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
Creates `app/main.py`, `pythonnative.json`, `requirements.txt`,
|
|
36
|
+
and `.gitignore`. Refuses to overwrite existing files unless
|
|
37
|
+
`--force` is passed.
|
|
38
38
|
|
|
39
39
|
Args:
|
|
40
40
|
args: The parsed argparse namespace. Recognized attributes:
|
|
@@ -68,30 +68,55 @@ def init_project(args: argparse.Namespace) -> None:
|
|
|
68
68
|
|
|
69
69
|
os.makedirs(app_dir, exist_ok=True)
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
if not os.path.exists(
|
|
73
|
-
with open(
|
|
71
|
+
main_py = os.path.join(app_dir, "main.py")
|
|
72
|
+
if not os.path.exists(main_py) or args.force:
|
|
73
|
+
with open(main_py, "w", encoding="utf-8") as f:
|
|
74
74
|
f.write("""import pythonnative as pn
|
|
75
75
|
|
|
76
|
+
Stack = pn.create_stack_navigator()
|
|
77
|
+
|
|
76
78
|
|
|
77
79
|
@pn.component
|
|
78
|
-
def
|
|
80
|
+
def HomeScreen():
|
|
79
81
|
count, set_count = pn.use_state(0)
|
|
82
|
+
nav = pn.use_navigation()
|
|
80
83
|
return pn.ScrollView(
|
|
81
84
|
pn.Column(
|
|
82
85
|
pn.Text("Hello from PythonNative!", style={"font_size": 24, "bold": True}),
|
|
83
86
|
pn.Text(f"Tapped {count} times"),
|
|
84
87
|
pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
|
|
88
|
+
pn.Button("Open detail", on_click=lambda: nav.navigate("Detail", {"count": count})),
|
|
85
89
|
style={"spacing": 12, "padding": 16, "align_items": "stretch"},
|
|
86
90
|
)
|
|
87
91
|
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pn.component
|
|
95
|
+
def DetailScreen():
|
|
96
|
+
nav = pn.use_navigation()
|
|
97
|
+
params = pn.use_route()
|
|
98
|
+
return pn.Column(
|
|
99
|
+
pn.Text(f"Detail: count was {params.get('count', 0)}", style={"font_size": 20}),
|
|
100
|
+
pn.Button("Back", on_click=nav.go_back),
|
|
101
|
+
style={"spacing": 12, "padding": 16},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pn.component
|
|
106
|
+
def App():
|
|
107
|
+
return pn.NavigationContainer(
|
|
108
|
+
Stack.Navigator(
|
|
109
|
+
Stack.Screen("Home", component=HomeScreen, options={"title": "Home"}),
|
|
110
|
+
Stack.Screen("Detail", component=DetailScreen, options={"title": "Detail"}),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
88
113
|
""")
|
|
89
114
|
|
|
90
115
|
# Create config
|
|
91
116
|
config = {
|
|
92
117
|
"name": project_name,
|
|
93
118
|
"appId": "com.example." + project_name.replace(" ", "").lower(),
|
|
94
|
-
"entryPoint": "app/
|
|
119
|
+
"entryPoint": "app/main.py",
|
|
95
120
|
"pythonVersion": "3.11",
|
|
96
121
|
"ios": {},
|
|
97
122
|
"android": {},
|
|
@@ -319,7 +344,7 @@ ANDROID_LOGCAT_FILTERS: list[str] = [
|
|
|
319
344
|
"python.stdout:V",
|
|
320
345
|
"python.stderr:V",
|
|
321
346
|
"MainActivity:V",
|
|
322
|
-
"
|
|
347
|
+
"ScreenFragment:V",
|
|
323
348
|
"Navigator:V",
|
|
324
349
|
"PythonNative:V",
|
|
325
350
|
"AndroidRuntime:E",
|
|
@@ -511,13 +511,13 @@ def use_window_dimensions() -> Dict[str, float]:
|
|
|
511
511
|
"""Return the current viewport size and re-render when it changes.
|
|
512
512
|
|
|
513
513
|
Equivalent to React Native's ``useWindowDimensions``. The values
|
|
514
|
-
are pushed by the
|
|
514
|
+
are pushed by the screen host whenever the platform reports a new
|
|
515
515
|
size (initial layout, rotation, multitasking split-view).
|
|
516
516
|
|
|
517
517
|
Returns:
|
|
518
518
|
A dict with ``"width"`` and ``"height"`` floats in layout
|
|
519
519
|
units (pt on iOS, dp on Android). Both are ``0.0`` until the
|
|
520
|
-
|
|
520
|
+
screen host has run its first layout pass.
|
|
521
521
|
|
|
522
522
|
Raises:
|
|
523
523
|
RuntimeError: If called outside a `@component` function.
|
|
@@ -717,8 +717,13 @@ _NavigationContext: Context = create_context(None)
|
|
|
717
717
|
class NavigationHandle:
|
|
718
718
|
"""Handle returned by [`use_navigation`][pythonnative.use_navigation].
|
|
719
719
|
|
|
720
|
-
Wraps the host's push/pop primitives so screens can navigate
|
|
721
|
-
knowing the underlying native navigation stack.
|
|
720
|
+
Wraps the host's push/pop primitives so screens can navigate
|
|
721
|
+
without knowing the underlying native navigation stack. The
|
|
722
|
+
typical user-facing surface is the declarative handle returned by
|
|
723
|
+
a [`Stack`][pythonnative.create_stack_navigator] — this class is
|
|
724
|
+
the lower-level fallback used when no navigator is rendered (and
|
|
725
|
+
as the bridge that declarative navigators delegate to when they
|
|
726
|
+
need to push real native screens).
|
|
722
727
|
|
|
723
728
|
Example:
|
|
724
729
|
```python
|
|
@@ -729,7 +734,7 @@ class NavigationHandle:
|
|
|
729
734
|
nav = pn.use_navigation()
|
|
730
735
|
return pn.Button(
|
|
731
736
|
"Open Detail",
|
|
732
|
-
on_click=lambda: nav.navigate(
|
|
737
|
+
on_click=lambda: nav.navigate("Detail", {"id": 42}),
|
|
733
738
|
)
|
|
734
739
|
```
|
|
735
740
|
"""
|
|
@@ -737,16 +742,20 @@ class NavigationHandle:
|
|
|
737
742
|
def __init__(self, host: Any) -> None:
|
|
738
743
|
self._host = host
|
|
739
744
|
|
|
740
|
-
def navigate(self,
|
|
741
|
-
"""Push
|
|
745
|
+
def navigate(self, component: Any, params: Optional[Dict[str, Any]] = None) -> None:
|
|
746
|
+
"""Push ``component`` onto the navigation stack.
|
|
742
747
|
|
|
743
748
|
Args:
|
|
744
|
-
|
|
745
|
-
path (e.g
|
|
749
|
+
component: A ``@component`` function or a dotted Python
|
|
750
|
+
path (e.g. ``"app.detail.DetailScreen"``). When a
|
|
751
|
+
Stack navigator is the root of the app, prefer the
|
|
752
|
+
declarative ``nav.navigate("Detail", params)`` form
|
|
753
|
+
returned by ``use_navigation()`` (it pushes by route
|
|
754
|
+
name and the host re-uses its own ``App`` component).
|
|
746
755
|
params: Optional dict of arguments serialized into the
|
|
747
756
|
target screen.
|
|
748
757
|
"""
|
|
749
|
-
self._host._push(
|
|
758
|
+
self._host._push(component, params)
|
|
750
759
|
|
|
751
760
|
def go_back(self) -> None:
|
|
752
761
|
"""Pop the current screen and return to the previous one."""
|
|
@@ -771,13 +780,13 @@ def use_navigation() -> NavigationHandle:
|
|
|
771
780
|
|
|
772
781
|
Raises:
|
|
773
782
|
RuntimeError: If called outside a component rendered via
|
|
774
|
-
[`
|
|
783
|
+
[`create_screen`][pythonnative.create_screen].
|
|
775
784
|
"""
|
|
776
785
|
handle = use_context(_NavigationContext)
|
|
777
786
|
if handle is None:
|
|
778
787
|
raise RuntimeError(
|
|
779
|
-
"use_navigation() called outside a PythonNative
|
|
780
|
-
"Ensure your component is rendered via
|
|
788
|
+
"use_navigation() called outside a PythonNative screen. "
|
|
789
|
+
"Ensure your component is rendered via create_screen()."
|
|
781
790
|
)
|
|
782
791
|
return handle
|
|
783
792
|
|
|
@@ -3,16 +3,30 @@
|
|
|
3
3
|
Two cooperating pieces:
|
|
4
4
|
|
|
5
5
|
- **Host-side**: [`FileWatcher`][pythonnative.hot_reload.FileWatcher]
|
|
6
|
-
polls the developer's
|
|
7
|
-
triggers a callback (typically
|
|
8
|
-
|
|
6
|
+
polls the developer's ``app/`` directory for ``.py`` changes and
|
|
7
|
+
triggers a callback (typically ``adb push`` on Android or a
|
|
8
|
+
``simctl`` file copy on iOS).
|
|
9
9
|
- **Device-side**:
|
|
10
10
|
[`ModuleReloader`][pythonnative.hot_reload.ModuleReloader] reloads
|
|
11
|
-
changed Python modules using
|
|
12
|
-
host to re-render
|
|
11
|
+
changed Python modules using ``importlib`` and asks the screen
|
|
12
|
+
host to re-render its current tree.
|
|
13
|
+
|
|
14
|
+
Two strategies share the device-side surface:
|
|
15
|
+
|
|
16
|
+
- **Fast Refresh** (default): after reloading the changed modules
|
|
17
|
+
the reconciler tree is walked and every component function whose
|
|
18
|
+
module was reloaded is swapped in place. Hook state, navigation
|
|
19
|
+
state, and even scroll positions survive because the underlying
|
|
20
|
+
``VNode`` objects are reused — the next render simply calls the
|
|
21
|
+
new function bodies through the old slots.
|
|
22
|
+
- **Full remount**: when the in-place swap fails (e.g. the new
|
|
23
|
+
module raised at import time, or a render exception bubbled out
|
|
24
|
+
while running the new function), the host falls back to building
|
|
25
|
+
a brand-new reconciler tree. State is lost but the app keeps
|
|
26
|
+
running.
|
|
13
27
|
|
|
14
28
|
Example:
|
|
15
|
-
Integrated into
|
|
29
|
+
Integrated into ``pn run --hot-reload``:
|
|
16
30
|
|
|
17
31
|
```python
|
|
18
32
|
from pythonnative.hot_reload import FileWatcher
|
|
@@ -33,7 +47,7 @@ import os
|
|
|
33
47
|
import sys
|
|
34
48
|
import threading
|
|
35
49
|
import time
|
|
36
|
-
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
50
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set
|
|
37
51
|
|
|
38
52
|
DEV_ROOT_DIR = "pythonnative_dev"
|
|
39
53
|
"""Name of the writable on-device directory that shadows bundled app code."""
|
|
@@ -46,7 +60,7 @@ def configure_dev_environment(writable_root: str) -> str:
|
|
|
46
60
|
"""Create and prioritize the writable hot-reload source overlay.
|
|
47
61
|
|
|
48
62
|
The returned directory is inserted at the front of `sys.path`, so a
|
|
49
|
-
pushed `app/
|
|
63
|
+
pushed `app/main.py` shadows the copy bundled into the native
|
|
50
64
|
application. Templates call this before importing user code.
|
|
51
65
|
|
|
52
66
|
Args:
|
|
@@ -191,7 +205,7 @@ class ModuleReloader:
|
|
|
191
205
|
"""Reload a single module by its dotted name.
|
|
192
206
|
|
|
193
207
|
Args:
|
|
194
|
-
module_name: Dotted module name (e.g., `"app.
|
|
208
|
+
module_name: Dotted module name (e.g., `"app.main"`).
|
|
195
209
|
|
|
196
210
|
Returns:
|
|
197
211
|
`True` if the module imported successfully from the current
|
|
@@ -250,7 +264,7 @@ class ModuleReloader:
|
|
|
250
264
|
If empty, `file_path` is treated as already relative.
|
|
251
265
|
|
|
252
266
|
Returns:
|
|
253
|
-
The dotted module name (e.g., `"app.
|
|
267
|
+
The dotted module name (e.g., `"app.screens.home"`), or
|
|
254
268
|
`None` for an empty path.
|
|
255
269
|
"""
|
|
256
270
|
rel = os.path.relpath(file_path, base_dir) if base_dir else file_path
|
|
@@ -273,28 +287,180 @@ class ModuleReloader:
|
|
|
273
287
|
return modules
|
|
274
288
|
|
|
275
289
|
@staticmethod
|
|
276
|
-
def
|
|
277
|
-
"""Force a
|
|
290
|
+
def reload_screen(screen_instance: Any, module_names: Optional[Sequence[str]] = None) -> None:
|
|
291
|
+
"""Force a screen re-render after a module reload.
|
|
278
292
|
|
|
279
293
|
Args:
|
|
280
|
-
|
|
294
|
+
screen_instance: A `_ScreenHost` instance (or duck-typed
|
|
281
295
|
equivalent) that exposes a `_reconciler` attribute.
|
|
282
296
|
module_names: Optional modules that changed. Reload-aware
|
|
283
|
-
|
|
297
|
+
screen hosts use this to refresh imports before re-render.
|
|
284
298
|
"""
|
|
285
|
-
reload_fn = getattr(
|
|
299
|
+
reload_fn = getattr(screen_instance, "reload", None)
|
|
286
300
|
if callable(reload_fn):
|
|
287
301
|
reload_fn(list(module_names or []))
|
|
288
302
|
return
|
|
289
303
|
|
|
290
|
-
from .
|
|
304
|
+
from .screen import _request_render
|
|
291
305
|
|
|
292
|
-
if hasattr(
|
|
293
|
-
_request_render(
|
|
306
|
+
if hasattr(screen_instance, "_reconciler") and screen_instance._reconciler is not None:
|
|
307
|
+
_request_render(screen_instance)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def find_replacement_function(old_fn: Any) -> Optional[Any]:
|
|
311
|
+
"""Locate a function's post-reload counterpart by qualname.
|
|
312
|
+
|
|
313
|
+
Functions decorated with [`component`][pythonnative.component]
|
|
314
|
+
store the user's original function on the wrapper's
|
|
315
|
+
``__wrapped__`` attribute and forward ``__module__`` /
|
|
316
|
+
``__qualname__`` so that the reconciler's stored
|
|
317
|
+
``element.type`` (the unwrapped function) still has the
|
|
318
|
+
information needed to re-resolve after a module reload.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
old_fn: The function captured in an
|
|
322
|
+
[`Element`][pythonnative.Element]'s ``type`` slot.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
The reloaded module's matching function, ``None`` if no
|
|
326
|
+
replacement was found, or the original function itself
|
|
327
|
+
when the module has not been reloaded (so callers can
|
|
328
|
+
skip the swap).
|
|
329
|
+
"""
|
|
330
|
+
module_name = getattr(old_fn, "__module__", None)
|
|
331
|
+
qualname = getattr(old_fn, "__qualname__", None) or getattr(old_fn, "__name__", None)
|
|
332
|
+
if not module_name or not qualname:
|
|
333
|
+
return None
|
|
334
|
+
if "<locals>" in qualname:
|
|
335
|
+
return None # nested functions are not addressable from the module surface
|
|
336
|
+
|
|
337
|
+
module = sys.modules.get(module_name)
|
|
338
|
+
if module is None:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
obj: Any = module
|
|
342
|
+
for part in qualname.split("."):
|
|
343
|
+
obj = getattr(obj, part, None)
|
|
344
|
+
if obj is None:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
if getattr(obj, "_pn_component", False):
|
|
348
|
+
obj = getattr(obj, "__wrapped__", obj)
|
|
349
|
+
|
|
350
|
+
if obj is old_fn:
|
|
351
|
+
return None
|
|
352
|
+
return obj
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def build_replacement_map(reconciler: Any, reloaded_modules: Iterable[str]) -> Dict[Any, Any]:
|
|
356
|
+
"""Compute ``{old_function: new_function}`` for one tree.
|
|
357
|
+
|
|
358
|
+
The reconciler's stored tree references the *pre-reload*
|
|
359
|
+
component functions through ``VNode.element.type``. This
|
|
360
|
+
method walks the tree, collects every callable type whose
|
|
361
|
+
``__module__`` was just reloaded, and asks
|
|
362
|
+
[`find_replacement_function`][pythonnative.hot_reload.ModuleReloader.find_replacement_function]
|
|
363
|
+
for its successor.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
reconciler: The reconciler whose
|
|
367
|
+
``_tree`` should be inspected.
|
|
368
|
+
reloaded_modules: Set of module names that were just
|
|
369
|
+
reloaded (only callables from these modules are
|
|
370
|
+
considered).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
A mapping suitable for passing to
|
|
374
|
+
[`swap_components_in_tree`][pythonnative.hot_reload.ModuleReloader.swap_components_in_tree].
|
|
375
|
+
"""
|
|
376
|
+
modules: Set[str] = {m for m in reloaded_modules if m}
|
|
377
|
+
if not modules or reconciler is None or getattr(reconciler, "_tree", None) is None:
|
|
378
|
+
return {}
|
|
379
|
+
|
|
380
|
+
seen: Set[int] = set()
|
|
381
|
+
mapping: Dict[Any, Any] = {}
|
|
382
|
+
|
|
383
|
+
def visit(vnode: Any) -> None:
|
|
384
|
+
if vnode is None:
|
|
385
|
+
return
|
|
386
|
+
elem = getattr(vnode, "element", None)
|
|
387
|
+
if elem is not None and callable(elem.type):
|
|
388
|
+
fn = elem.type
|
|
389
|
+
fn_id = id(fn)
|
|
390
|
+
if fn_id not in seen:
|
|
391
|
+
seen.add(fn_id)
|
|
392
|
+
if getattr(fn, "__module__", None) in modules:
|
|
393
|
+
replacement = ModuleReloader.find_replacement_function(fn)
|
|
394
|
+
if replacement is not None and replacement is not fn:
|
|
395
|
+
mapping[fn] = replacement
|
|
396
|
+
for child in getattr(vnode, "children", []) or []:
|
|
397
|
+
visit(child)
|
|
398
|
+
|
|
399
|
+
visit(reconciler._tree)
|
|
400
|
+
return mapping
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def swap_components_in_tree(reconciler: Any, replacement_map: Dict[Any, Any]) -> int:
|
|
404
|
+
"""Apply a ``{old: new}`` map to every node in the reconciler tree.
|
|
405
|
+
|
|
406
|
+
Mutates ``vnode.element.type`` directly so the NEXT diff sees
|
|
407
|
+
identical types and reuses VNodes (preserving hook state).
|
|
408
|
+
Pending ``Element`` trees stored on ``vnode._rendered`` are
|
|
409
|
+
rewritten too because the reconciler reads from them when
|
|
410
|
+
comparing keys across renders.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
The number of element type references that were rewritten.
|
|
414
|
+
"""
|
|
415
|
+
if not replacement_map or reconciler is None or getattr(reconciler, "_tree", None) is None:
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
rewrites = 0
|
|
419
|
+
|
|
420
|
+
def rewrite_element_tree(element: Any) -> None:
|
|
421
|
+
nonlocal rewrites
|
|
422
|
+
if element is None:
|
|
423
|
+
return
|
|
424
|
+
new_type = replacement_map.get(element.type)
|
|
425
|
+
if new_type is not None:
|
|
426
|
+
element.type = new_type
|
|
427
|
+
rewrites += 1
|
|
428
|
+
for child in element.children or []:
|
|
429
|
+
rewrite_element_tree(child)
|
|
430
|
+
|
|
431
|
+
def visit(vnode: Any) -> None:
|
|
432
|
+
if vnode is None:
|
|
433
|
+
return
|
|
434
|
+
if getattr(vnode, "element", None) is not None:
|
|
435
|
+
rewrite_element_tree(vnode.element)
|
|
436
|
+
rendered = getattr(vnode, "_rendered", None)
|
|
437
|
+
if rendered is not None:
|
|
438
|
+
rewrite_element_tree(rendered)
|
|
439
|
+
for child in getattr(vnode, "children", []) or []:
|
|
440
|
+
visit(child)
|
|
441
|
+
|
|
442
|
+
visit(reconciler._tree)
|
|
443
|
+
return rewrites
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def refresh_in_place(reconciler: Any, reloaded_modules: Iterable[str]) -> bool:
|
|
447
|
+
"""Try a state-preserving Fast Refresh for one reconciler.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
``True`` if any component function was replaced (callers
|
|
451
|
+
should then trigger a re-render). ``False`` means the
|
|
452
|
+
tree already references the latest functions (or has no
|
|
453
|
+
nodes from the reloaded modules at all).
|
|
454
|
+
"""
|
|
455
|
+
replacement_map = ModuleReloader.build_replacement_map(reconciler, reloaded_modules)
|
|
456
|
+
if not replacement_map:
|
|
457
|
+
return False
|
|
458
|
+
rewrites = ModuleReloader.swap_components_in_tree(reconciler, replacement_map)
|
|
459
|
+
return rewrites > 0
|
|
294
460
|
|
|
295
461
|
@staticmethod
|
|
296
462
|
def reload_from_manifest(
|
|
297
|
-
|
|
463
|
+
screen_instance: Any,
|
|
298
464
|
manifest_path: str,
|
|
299
465
|
*,
|
|
300
466
|
last_version: Optional[str] = None,
|
|
@@ -302,9 +468,9 @@ class ModuleReloader:
|
|
|
302
468
|
"""Apply a reload manifest if it is newer than `last_version`.
|
|
303
469
|
|
|
304
470
|
Args:
|
|
305
|
-
|
|
471
|
+
screen_instance: Screen host to refresh.
|
|
306
472
|
manifest_path: JSON manifest written by the CLI.
|
|
307
|
-
last_version: Version already applied by this
|
|
473
|
+
last_version: Version already applied by this screen host.
|
|
308
474
|
|
|
309
475
|
Returns:
|
|
310
476
|
The manifest version after applying, or `last_version` when
|
|
@@ -325,5 +491,5 @@ class ModuleReloader:
|
|
|
325
491
|
files = manifest.get("files", [])
|
|
326
492
|
modules = ModuleReloader.modules_from_files(files if isinstance(files, list) else [])
|
|
327
493
|
|
|
328
|
-
ModuleReloader.
|
|
494
|
+
ModuleReloader.reload_screen(screen_instance, [str(module) for module in modules])
|
|
329
495
|
return version
|