pythonnative 0.18.0__tar.gz → 0.20.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 (137) hide show
  1. {pythonnative-0.18.0/src/pythonnative.egg-info → pythonnative-0.20.0}/PKG-INFO +3 -2
  2. {pythonnative-0.18.0 → pythonnative-0.20.0}/README.md +2 -1
  3. {pythonnative-0.18.0 → pythonnative-0.20.0}/pyproject.toml +4 -1
  4. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/__init__.py +1 -1
  5. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/cli/pn.py +107 -1
  6. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/hooks.py +30 -6
  7. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_views/__init__.py +18 -5
  8. pythonnative-0.20.0/src/pythonnative/native_views/desktop.py +1489 -0
  9. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/platform.py +17 -8
  10. pythonnative-0.20.0/src/pythonnative/preview.py +471 -0
  11. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/reconciler.py +285 -3
  12. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/runtime.py +26 -1
  13. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/screen.py +207 -31
  14. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/utils.py +38 -2
  15. {pythonnative-0.18.0 → pythonnative-0.20.0/src/pythonnative.egg-info}/PKG-INFO +3 -2
  16. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/SOURCES.txt +4 -0
  17. pythonnative-0.20.0/tests/test_desktop_backend.py +363 -0
  18. pythonnative-0.20.0/tests/test_incremental_render.py +371 -0
  19. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_screen.py +43 -0
  20. {pythonnative-0.18.0 → pythonnative-0.20.0}/LICENSE +0 -0
  21. {pythonnative-0.18.0 → pythonnative-0.20.0}/setup.cfg +0 -0
  22. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/_ios_log.py +0 -0
  23. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/alerts.py +0 -0
  24. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/animated.py +0 -0
  25. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/cli/__init__.py +0 -0
  26. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/components.py +0 -0
  27. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/element.py +0 -0
  28. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/hot_reload.py +0 -0
  29. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/layout.py +0 -0
  30. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/__init__.py +0 -0
  31. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/app_state.py +0 -0
  32. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/battery.py +0 -0
  33. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/biometrics.py +0 -0
  34. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/camera.py +0 -0
  35. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/clipboard.py +0 -0
  36. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/file_system.py +0 -0
  37. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/haptics.py +0 -0
  38. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/linking.py +0 -0
  39. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/location.py +0 -0
  40. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/net_info.py +0 -0
  41. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/notifications.py +0 -0
  42. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/permissions.py +0 -0
  43. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/secure_store.py +0 -0
  44. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_modules/share.py +0 -0
  45. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_views/android.py +0 -0
  46. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_views/base.py +0 -0
  47. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/native_views/ios.py +0 -0
  48. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/navigation.py +0 -0
  49. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/net.py +0 -0
  50. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/platform_metrics.py +0 -0
  51. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/sdk/__init__.py +0 -0
  52. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/sdk/_components.py +0 -0
  53. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/storage.py +0 -0
  54. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/style.py +0 -0
  55. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
  56. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  57. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
  58. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  59. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
  60. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
  61. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
  62. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
  63. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  64. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
  65. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  66. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
  67. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
  68. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  69. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  70. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  71. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  72. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  73. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  74. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  75. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  76. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  77. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  78. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  79. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  80. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  81. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  82. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  83. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  84. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  85. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
  86. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
  87. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  88. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  89. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  90. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  91. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  92. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  93. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  94. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  95. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  96. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  97. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  98. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  99. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  100. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  101. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
  102. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  103. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  104. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  105. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  106. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  107. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  108. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  109. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/requires.txt +0 -0
  110. {pythonnative-0.18.0 → pythonnative-0.20.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  111. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_alert.py +0 -0
  112. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_animated.py +0 -0
  113. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_async_hooks.py +0 -0
  114. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_cli.py +0 -0
  115. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_components.py +0 -0
  116. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_element.py +0 -0
  117. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_extended_components.py +0 -0
  118. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_hooks.py +0 -0
  119. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_hot_reload.py +0 -0
  120. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_ios_log.py +0 -0
  121. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_layout.py +0 -0
  122. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_metric_hooks.py +0 -0
  123. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_native_modules.py +0 -0
  124. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_native_views.py +0 -0
  125. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_navigation.py +0 -0
  126. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_net.py +0 -0
  127. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_new_components.py +0 -0
  128. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_platform.py +0 -0
  129. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_platform_metrics.py +0 -0
  130. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_reconciler.py +0 -0
  131. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_ref.py +0 -0
  132. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_runtime.py +0 -0
  133. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_sdk.py +0 -0
  134. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_smoke.py +0 -0
  135. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_storage.py +0 -0
  136. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_style.py +0 -0
  137. {pythonnative-0.18.0 → pythonnative-0.20.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.18.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.
@@ -36,10 +36,11 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
36
36
  - **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
37
37
  - **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`.
38
38
  - **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.
39
- - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
39
+ - **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.
40
40
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
41
41
  - **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.
42
42
  - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
43
+ - **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.
43
44
  - **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.
44
45
  - **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.
45
46
  - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.18.0"
7
+ version = "0.20.0"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
@@ -71,6 +71,8 @@ Documentation = "https://docs.pythonnative.com/"
71
71
 
72
72
 
73
73
 
74
+
75
+
74
76
  [tool.setuptools.packages.find]
75
77
  where = ["src"]
76
78
 
@@ -117,6 +119,7 @@ ignore = ["D107", "D105", "D203", "D213"]
117
119
  # docstrings would only add boilerplate that repeats the ABC.
118
120
  "src/pythonnative/native_views/android.py" = ["D101", "D102"]
119
121
  "src/pythonnative/native_views/ios.py" = ["D101", "D102"]
122
+ "src/pythonnative/native_views/desktop.py" = ["D101", "D102"]
120
123
 
121
124
  [tool.ruff.lint.pydocstyle]
122
125
  convention = "google"
@@ -51,7 +51,7 @@ Example:
51
51
  ```
52
52
  """
53
53
 
54
- __version__ = "0.18.0"
54
+ __version__ = "0.20.0"
55
55
 
56
56
  from . import runtime, sdk
57
57
  from .alerts import Alert
@@ -1,9 +1,12 @@
1
1
  """`pn` CLI: scaffold, run, and clean PythonNative projects.
2
2
 
3
3
  The console script `pn` (declared in `pyproject.toml` under
4
- `[project.scripts]`) dispatches to one of three subcommands:
4
+ `[project.scripts]`) dispatches to one of four subcommands:
5
5
 
6
6
  - `pn init [name]`: scaffold a new project in the current directory.
7
+ - `pn preview [component]`: render the app in a desktop (Tkinter)
8
+ window with instant Fast Refresh — the fast inner dev loop, no
9
+ device or simulator required.
7
10
  - `pn run android|ios`: stage code into a native template, build it,
8
11
  install it, and stream logs back to the terminal.
9
12
  - `pn clean`: remove the local `build/` directory.
@@ -1119,6 +1122,90 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs:
1119
1122
  print("\n[hot-reload] Stopped.")
1120
1123
 
1121
1124
 
1125
+ def _entrypoint_to_module(entry_point: str) -> str:
1126
+ """Convert a config ``entryPoint`` path into an importable module path.
1127
+
1128
+ ``"app/main.py"`` → ``"app.main"``. Returns ``"app.main"`` for
1129
+ empty / unusable input so ``pn preview`` always has a sane default.
1130
+ """
1131
+ normalized = entry_point.strip().replace("\\", "/")
1132
+ if normalized.endswith(".py"):
1133
+ normalized = normalized[:-3]
1134
+ normalized = normalized.strip("/").replace("/", ".")
1135
+ return normalized or "app.main"
1136
+
1137
+
1138
+ def preview_project(args: argparse.Namespace) -> None:
1139
+ """Render the project in a desktop preview window (Tkinter).
1140
+
1141
+ Sets ``PN_PLATFORM=desktop`` (so PythonNative selects the Tkinter
1142
+ backend) and hands off to ``pythonnative.preview.run_preview``,
1143
+ which opens a window, mounts the app, and Fast Refreshes on every
1144
+ file save until the window is closed.
1145
+
1146
+ Args:
1147
+ args: Parsed argparse namespace. Recognized attributes:
1148
+
1149
+ - `component` (`str`, optional): Module path like
1150
+ ``"app.main"`` (its ``App`` is used) or a dotted
1151
+ ``module.Component`` path. Defaults to the project's
1152
+ configured ``entryPoint``.
1153
+ - `width` / `height` (`int`): Initial window size in points.
1154
+ - `title` (`str`): Window title.
1155
+ - `no_hot_reload` (`bool`): Disable file watching.
1156
+ """
1157
+ # The desktop backend is selected at *import time* from the
1158
+ # ``PN_PLATFORM`` environment variable (see ``pythonnative.utils`` and
1159
+ # the host selection in ``pythonnative.screen``). Because the ``pn``
1160
+ # console entry point lives inside the ``pythonnative`` package,
1161
+ # importing it already loaded the package under the default,
1162
+ # non-desktop platform before this handler ever runs. Re-exec a fresh
1163
+ # interpreter with the variable set so every module binds to the
1164
+ # Tkinter backend; the re-execed child sees ``PN_PLATFORM=desktop`` and
1165
+ # skips this branch, so there is no exec loop.
1166
+ if os.environ.get("PN_PLATFORM") != "desktop":
1167
+ try:
1168
+ completed = subprocess.run(
1169
+ [sys.executable, "-m", "pythonnative.cli.pn", *sys.argv[1:]],
1170
+ env={**os.environ, "PN_PLATFORM": "desktop"},
1171
+ )
1172
+ except KeyboardInterrupt:
1173
+ sys.exit(130)
1174
+ sys.exit(completed.returncode)
1175
+
1176
+ project_dir = os.getcwd()
1177
+ component: Optional[str] = getattr(args, "component", None)
1178
+ if not component:
1179
+ config = _read_project_config()
1180
+ component = _entrypoint_to_module(config.get("entryPoint", "app/main.py"))
1181
+
1182
+ try:
1183
+ from pythonnative.preview import run_preview
1184
+ except Exception as exc: # pragma: no cover - environment dependent
1185
+ print(f"Error: could not start the desktop preview: {exc}")
1186
+ print(
1187
+ "The desktop preview needs Tkinter (Python's standard GUI toolkit).\n"
1188
+ "On macOS: brew install python-tk\n"
1189
+ "On Debian/Ubuntu: sudo apt-get install python3-tk\n"
1190
+ "On Windows: reinstall Python with the 'tcl/tk' option checked."
1191
+ )
1192
+ sys.exit(1)
1193
+
1194
+ print(f"Starting PythonNative preview for {component} (Ctrl+C or close the window to stop).")
1195
+ try:
1196
+ run_preview(
1197
+ component,
1198
+ project_root=project_dir,
1199
+ width=getattr(args, "width", 390),
1200
+ height=getattr(args, "height", 844),
1201
+ title=getattr(args, "title", "PythonNative Preview"),
1202
+ hot_reload=not getattr(args, "no_hot_reload", False),
1203
+ )
1204
+ except RuntimeError as exc:
1205
+ print(f"Error: {exc}")
1206
+ sys.exit(1)
1207
+
1208
+
1122
1209
  def clean_project(args: argparse.Namespace) -> None:
1123
1210
  """Remove the local `build/` directory.
1124
1211
 
@@ -1152,6 +1239,25 @@ def main() -> None:
1152
1239
  parser_init.add_argument("--force", action="store_true", help="Overwrite existing files if present")
1153
1240
  parser_init.set_defaults(func=init_project)
1154
1241
 
1242
+ # Create a new command 'preview' that calls preview_project
1243
+ parser_preview = subparsers.add_parser("preview")
1244
+ parser_preview.add_argument(
1245
+ "component",
1246
+ nargs="?",
1247
+ help="Module path (e.g. app.main) or dotted component path; defaults to the project entry point",
1248
+ )
1249
+ parser_preview.add_argument("--width", type=int, default=390, help="Initial window width in points (default: 390)")
1250
+ parser_preview.add_argument(
1251
+ "--height", type=int, default=844, help="Initial window height in points (default: 844)"
1252
+ )
1253
+ parser_preview.add_argument("--title", default="PythonNative Preview", help="Preview window title")
1254
+ parser_preview.add_argument(
1255
+ "--no-hot-reload",
1256
+ action="store_true",
1257
+ help="Disable file watching / Fast Refresh",
1258
+ )
1259
+ parser_preview.set_defaults(func=preview_project)
1260
+
1155
1261
  # Create a new command 'run' that calls run_project
1156
1262
  parser_run = subparsers.add_parser("run")
1157
1263
  parser_run.add_argument("platform", choices=["android", "ios"])
@@ -76,6 +76,8 @@ class HookState:
76
76
  "_trigger_render",
77
77
  "_pending_effects",
78
78
  "_dirty",
79
+ "_vnode",
80
+ "_reconciler",
79
81
  )
80
82
 
81
83
  def __init__(self) -> None:
@@ -95,6 +97,13 @@ class HookState:
95
97
  # knows that a memoized component still needs to re-render even
96
98
  # when its props didn't change.
97
99
  self._dirty: bool = False
100
+ # Back-references wired by the reconciler so a state setter can
101
+ # mark *its own* component subtree dirty for a local re-render
102
+ # (instead of forcing a whole-app re-render from the root). Both
103
+ # stay ``None`` until the component is mounted, and are cleared
104
+ # again when it unmounts.
105
+ self._vnode: Any = None
106
+ self._reconciler: Any = None
98
107
 
99
108
  def reset_index(self) -> None:
100
109
  """Reset every per-hook cursor to ``0``.
@@ -182,6 +191,25 @@ def _schedule_trigger(trigger: Callable[[], None]) -> None:
182
191
  trigger()
183
192
 
184
193
 
194
+ def _notify_state_changed(ctx: "HookState") -> None:
195
+ """Mark ``ctx``'s component dirty and schedule a render after a state change.
196
+
197
+ Enqueuing the owning ``VNode`` in the reconciler's dirty set is what
198
+ makes the subsequent render *local*: the screen host's trigger calls
199
+ ``flush_dirty``, which re-renders only the components marked here
200
+ rather than the whole app. The dirty mark is eager (so several
201
+ setters coalesce), while the render trigger respects
202
+ [`batch_updates`][pythonnative.batch_updates].
203
+ """
204
+ ctx._dirty = True
205
+ reconciler = ctx._reconciler
206
+ vnode = ctx._vnode
207
+ if reconciler is not None and vnode is not None:
208
+ reconciler.mark_dirty(vnode)
209
+ if ctx._trigger_render:
210
+ _schedule_trigger(ctx._trigger_render)
211
+
212
+
185
213
  @contextmanager
186
214
  def batch_updates() -> Generator[None, None, None]:
187
215
  """Coalesce multiple state updates into a single re-render.
@@ -272,9 +300,7 @@ def use_state(initial: Any = None) -> Tuple[Any, Callable]:
272
300
  new_value = new_value(ctx.states[idx])
273
301
  if ctx.states[idx] is not new_value and ctx.states[idx] != new_value:
274
302
  ctx.states[idx] = new_value
275
- ctx._dirty = True
276
- if ctx._trigger_render:
277
- _schedule_trigger(ctx._trigger_render)
303
+ _notify_state_changed(ctx)
278
304
 
279
305
  return current, setter
280
306
 
@@ -339,9 +365,7 @@ def use_reducer(reducer: Callable[[Any, Any], Any], initial_state: Any) -> Tuple
339
365
  new_state = reducer(ctx.states[idx], action)
340
366
  if ctx.states[idx] is not new_state and ctx.states[idx] != new_state:
341
367
  ctx.states[idx] = new_state
342
- ctx._dirty = True
343
- if ctx._trigger_render:
344
- _schedule_trigger(ctx._trigger_render)
368
+ _notify_state_changed(ctx)
345
369
 
346
370
  return current, dispatch
347
371
 
@@ -238,18 +238,31 @@ _registry: Optional[NativeViewRegistry] = None
238
238
 
239
239
 
240
240
  def _active_platform_name() -> str:
241
- """Return ``"android"`` or ``"ios"`` for the active runtime."""
242
- from ..utils import IS_ANDROID
241
+ """Return ``"android"``, ``"desktop"``, or ``"ios"`` for the active runtime."""
242
+ from ..utils import IS_ANDROID, IS_DESKTOP
243
243
 
244
- return "android" if IS_ANDROID else "ios"
244
+ if IS_ANDROID:
245
+ return "android"
246
+ if IS_DESKTOP:
247
+ return "desktop"
248
+ return "ios"
245
249
 
246
250
 
247
251
  def _register_builtin_handlers(registry: NativeViewRegistry) -> None:
248
- """Register every built-in handler for the active platform."""
249
- from ..utils import IS_ANDROID
252
+ """Register every built-in handler for the active platform.
253
+
254
+ The desktop (Tkinter) backend is selected when ``pn preview`` sets
255
+ ``PN_PLATFORM=desktop``; otherwise this picks Android (on device) or
256
+ iOS (the default off-device path, exercised by the iOS templates and
257
+ by tests that install the ``[ios]`` extra). Off-device unit tests
258
+ typically inject a mock registry via ``set_registry`` instead.
259
+ """
260
+ from ..utils import IS_ANDROID, IS_DESKTOP
250
261
 
251
262
  if IS_ANDROID:
252
263
  from .android import register_handlers
264
+ elif IS_DESKTOP:
265
+ from .desktop import register_handlers
253
266
  else:
254
267
  from .ios import register_handlers
255
268
  register_handlers(registry)