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.
Files changed (113) hide show
  1. {pythonnative-0.12.0/src/pythonnative.egg-info → pythonnative-0.13.0}/PKG-INFO +4 -3
  2. {pythonnative-0.12.0 → pythonnative-0.13.0}/README.md +3 -2
  3. {pythonnative-0.12.0 → pythonnative-0.13.0}/pyproject.toml +2 -1
  4. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/__init__.py +3 -3
  5. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/cli/pn.py +34 -9
  6. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/hooks.py +22 -13
  7. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/hot_reload.py +188 -22
  8. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/navigation.py +178 -35
  9. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/platform_metrics.py +9 -9
  10. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/reconciler.py +7 -7
  11. pythonnative-0.12.0/src/pythonnative/page.py → pythonnative-0.13.0/src/pythonnative/screen.py +251 -67
  12. {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
  13. pythonnative-0.13.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +43 -0
  14. 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
  15. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +6 -6
  16. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +31 -26
  17. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/utils.py +5 -5
  18. {pythonnative-0.12.0 → pythonnative-0.13.0/src/pythonnative.egg-info}/PKG-INFO +4 -3
  19. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/SOURCES.txt +3 -3
  20. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_cli.py +12 -10
  21. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_hooks.py +2 -2
  22. pythonnative-0.13.0/tests/test_hot_reload.py +303 -0
  23. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_metric_hooks.py +3 -3
  24. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_navigation.py +266 -12
  25. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_reconciler.py +1 -1
  26. pythonnative-0.12.0/tests/test_page.py → pythonnative-0.13.0/tests/test_screen.py +28 -28
  27. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_smoke.py +1 -1
  28. pythonnative-0.12.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -26
  29. pythonnative-0.12.0/tests/test_hot_reload.py +0 -97
  30. {pythonnative-0.12.0 → pythonnative-0.13.0}/LICENSE +0 -0
  31. {pythonnative-0.12.0 → pythonnative-0.13.0}/setup.cfg +0 -0
  32. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/_ios_log.py +0 -0
  33. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/alerts.py +0 -0
  34. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/animated.py +0 -0
  35. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/cli/__init__.py +0 -0
  36. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/components.py +0 -0
  37. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/element.py +0 -0
  38. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/layout.py +0 -0
  39. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/__init__.py +0 -0
  40. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/camera.py +0 -0
  41. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/file_system.py +0 -0
  42. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/location.py +0 -0
  43. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_modules/notifications.py +0 -0
  44. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/__init__.py +0 -0
  45. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/android.py +0 -0
  46. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/base.py +0 -0
  47. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/native_views/ios.py +0 -0
  48. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/platform.py +0 -0
  49. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/style.py +0 -0
  50. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
  51. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  52. {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
  53. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  54. {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
  55. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  56. {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
  57. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  58. {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
  59. {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
  60. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  61. {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
  62. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  63. {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
  64. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  65. {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
  66. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  67. {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
  68. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  69. {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
  70. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  71. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  72. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  73. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  74. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  75. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  76. {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
  77. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
  78. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  79. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  80. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  81. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  82. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  83. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  84. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  85. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  86. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  87. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  88. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  89. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  90. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  91. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  92. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  93. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  94. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  95. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  96. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  97. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  98. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  99. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/requires.txt +0 -0
  100. {pythonnative-0.12.0 → pythonnative-0.13.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  101. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_alert.py +0 -0
  102. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_animated.py +0 -0
  103. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_components.py +0 -0
  104. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_element.py +0 -0
  105. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_ios_log.py +0 -0
  106. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_layout.py +0 -0
  107. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_native_views.py +0 -0
  108. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_new_components.py +0 -0
  109. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_platform.py +0 -0
  110. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_platform_metrics.py +0 -0
  111. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_ref.py +0 -0
  112. {pythonnative-0.12.0 → pythonnative-0.13.0}/tests/test_style.py +0 -0
  113. {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.12.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
- - **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
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 MainPage():
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
- - **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
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 MainPage():
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.12.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.12.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
- "create_page",
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/main_page.py`, `pythonnative.json`,
36
- `requirements.txt`, and `.gitignore`. Refuses to overwrite
37
- existing files unless `--force` is passed.
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
- main_page_py = os.path.join(app_dir, "main_page.py")
72
- if not os.path.exists(main_page_py) or args.force:
73
- with open(main_page_py, "w", encoding="utf-8") as f:
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 MainPage():
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/main_page.py",
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
- "PageFragment:V",
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 page host whenever the platform reports a new
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
- page host has run its first layout pass.
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 without
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(DetailScreen, params={"id": 42}),
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, page: Any, params: Optional[Dict[str, Any]] = None) -> None:
741
- """Push `page` onto the navigation stack.
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
- page: Either a `@component` function or a dotted Python
745
- path (e.g., `"app.detail.DetailScreen"`).
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(page, params)
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
- [`create_page`][pythonnative.create_page].
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 page. "
780
- "Ensure your component is rendered via create_page()."
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 `app/` directory for `.py` changes and
7
- triggers a callback (typically `adb push` on Android or a
8
- `simctl` file copy on iOS).
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 `importlib.reload` and asks the page
12
- host to re-render the current tree.
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 `pn run --hot-reload`:
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/main_page.py` shadows the copy bundled into the native
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.main_page"`).
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.pages.home"`), or
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 reload_page(page_instance: Any, module_names: Optional[Sequence[str]] = None) -> None:
277
- """Force a page re-render after a module reload.
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
- page_instance: An `_AppHost` instance (or duck-typed
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
- page hosts use this to refresh imports before re-render.
297
+ screen hosts use this to refresh imports before re-render.
284
298
  """
285
- reload_fn = getattr(page_instance, "reload", None)
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 .page import _request_render
304
+ from .screen import _request_render
291
305
 
292
- if hasattr(page_instance, "_reconciler") and page_instance._reconciler is not None:
293
- _request_render(page_instance)
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
- page_instance: Any,
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
- page_instance: Page host to refresh.
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 page host.
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.reload_page(page_instance, [str(module) for module in modules])
494
+ ModuleReloader.reload_screen(screen_instance, [str(module) for module in modules])
329
495
  return version