pythonnative 0.4.0__tar.gz → 0.6.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 (117) hide show
  1. {pythonnative-0.4.0/src/pythonnative.egg-info → pythonnative-0.6.0}/PKG-INFO +20 -19
  2. {pythonnative-0.4.0 → pythonnative-0.6.0}/README.md +18 -16
  3. {pythonnative-0.4.0 → pythonnative-0.6.0}/pyproject.toml +2 -3
  4. pythonnative-0.6.0/src/pythonnative/__init__.py +102 -0
  5. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/cli/pn.py +153 -24
  6. pythonnative-0.6.0/src/pythonnative/components.py +563 -0
  7. pythonnative-0.6.0/src/pythonnative/element.py +53 -0
  8. pythonnative-0.6.0/src/pythonnative/hooks.py +287 -0
  9. pythonnative-0.6.0/src/pythonnative/hot_reload.py +143 -0
  10. pythonnative-0.6.0/src/pythonnative/native_modules/__init__.py +19 -0
  11. pythonnative-0.6.0/src/pythonnative/native_modules/camera.py +105 -0
  12. pythonnative-0.6.0/src/pythonnative/native_modules/file_system.py +131 -0
  13. pythonnative-0.6.0/src/pythonnative/native_modules/location.py +61 -0
  14. pythonnative-0.6.0/src/pythonnative/native_modules/notifications.py +151 -0
  15. pythonnative-0.6.0/src/pythonnative/native_views.py +1334 -0
  16. pythonnative-0.6.0/src/pythonnative/page.py +469 -0
  17. pythonnative-0.6.0/src/pythonnative/reconciler.py +262 -0
  18. pythonnative-0.6.0/src/pythonnative/style.py +115 -0
  19. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/build.gradle +2 -7
  20. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
  21. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/build.gradle +1 -1
  22. pythonnative-0.6.0/src/pythonnative/utils.py +83 -0
  23. {pythonnative-0.4.0 → pythonnative-0.6.0/src/pythonnative.egg-info}/PKG-INFO +20 -19
  24. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/SOURCES.txt +18 -28
  25. pythonnative-0.6.0/tests/test_components.py +300 -0
  26. pythonnative-0.6.0/tests/test_element.py +71 -0
  27. pythonnative-0.6.0/tests/test_hooks.py +433 -0
  28. pythonnative-0.6.0/tests/test_reconciler.py +371 -0
  29. pythonnative-0.6.0/tests/test_smoke.py +51 -0
  30. pythonnative-0.6.0/tests/test_style.py +58 -0
  31. pythonnative-0.4.0/src/pythonnative/__init__.py +0 -74
  32. pythonnative-0.4.0/src/pythonnative/activity_indicator_view.py +0 -71
  33. pythonnative-0.4.0/src/pythonnative/button.py +0 -113
  34. pythonnative-0.4.0/src/pythonnative/collection_view.py +0 -0
  35. pythonnative-0.4.0/src/pythonnative/date_picker.py +0 -76
  36. pythonnative-0.4.0/src/pythonnative/image_view.py +0 -78
  37. pythonnative-0.4.0/src/pythonnative/label.py +0 -133
  38. pythonnative-0.4.0/src/pythonnative/list_view.py +0 -76
  39. pythonnative-0.4.0/src/pythonnative/material_activity_indicator_view.py +0 -71
  40. pythonnative-0.4.0/src/pythonnative/material_bottom_navigation_view.py +0 -0
  41. pythonnative-0.4.0/src/pythonnative/material_button.py +0 -69
  42. pythonnative-0.4.0/src/pythonnative/material_date_picker.py +0 -87
  43. pythonnative-0.4.0/src/pythonnative/material_progress_view.py +0 -70
  44. pythonnative-0.4.0/src/pythonnative/material_search_bar.py +0 -69
  45. pythonnative-0.4.0/src/pythonnative/material_switch.py +0 -69
  46. pythonnative-0.4.0/src/pythonnative/material_time_picker.py +0 -76
  47. pythonnative-0.4.0/src/pythonnative/material_toolbar.py +0 -0
  48. pythonnative-0.4.0/src/pythonnative/page.py +0 -396
  49. pythonnative-0.4.0/src/pythonnative/picker_view.py +0 -69
  50. pythonnative-0.4.0/src/pythonnative/progress_view.py +0 -70
  51. pythonnative-0.4.0/src/pythonnative/scroll_view.py +0 -101
  52. pythonnative-0.4.0/src/pythonnative/search_bar.py +0 -69
  53. pythonnative-0.4.0/src/pythonnative/stack_view.py +0 -199
  54. pythonnative-0.4.0/src/pythonnative/switch.py +0 -68
  55. pythonnative-0.4.0/src/pythonnative/text_field.py +0 -132
  56. pythonnative-0.4.0/src/pythonnative/text_view.py +0 -135
  57. pythonnative-0.4.0/src/pythonnative/time_picker.py +0 -77
  58. pythonnative-0.4.0/src/pythonnative/utils.py +0 -91
  59. pythonnative-0.4.0/src/pythonnative/view.py +0 -173
  60. pythonnative-0.4.0/src/pythonnative/web_view.py +0 -60
  61. pythonnative-0.4.0/tests/test_smoke.py +0 -2
  62. {pythonnative-0.4.0 → pythonnative-0.6.0}/LICENSE +0 -0
  63. {pythonnative-0.4.0 → pythonnative-0.6.0}/setup.cfg +0 -0
  64. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/cli/__init__.py +0 -0
  65. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  66. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
  67. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  68. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
  69. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
  70. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  71. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
  72. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  73. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
  74. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
  75. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  76. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  77. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  78. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  79. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  80. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  81. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  82. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  83. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  84. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  85. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  86. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  87. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  88. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  89. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  90. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  91. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  92. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
  93. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  94. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  95. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  96. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  97. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  98. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  99. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  100. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  101. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  102. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  103. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  104. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  105. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  106. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  107. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
  108. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  109. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  110. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  111. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  112. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  113. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  114. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  115. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/requires.txt +0 -0
  116. {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  117. {pythonnative-0.4.0 → pythonnative-0.6.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -34,12 +34,11 @@ Classifier: Intended Audience :: Developers
34
34
  Classifier: License :: OSI Approved :: MIT License
35
35
  Classifier: Programming Language :: Python :: 3
36
36
  Classifier: Programming Language :: Python :: 3 :: Only
37
- Classifier: Programming Language :: Python :: 3.9
38
37
  Classifier: Programming Language :: Python :: 3.10
39
38
  Classifier: Programming Language :: Python :: 3.11
40
39
  Classifier: Programming Language :: Python :: 3.12
41
40
  Classifier: Topic :: Software Development :: User Interfaces
42
- Requires-Python: >=3.9
41
+ Requires-Python: >=3.10
43
42
  Description-Content-Type: text/markdown
44
43
  License-File: LICENSE
45
44
  Requires-Dist: requests>=2.31.0
@@ -89,18 +88,17 @@ Dynamic: license-file
89
88
 
90
89
  ## Overview
91
90
 
92
- PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a Pythonic API for native UI components, lifecycle events, and device capabilities, powered by Chaquopy on Android and rubicon-objc on iOS. Write your app once in Python and run it on both platforms with genuinely native interfaces.
91
+ PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Describe your UI as a tree of elements, manage state with `set_state()`, and let PythonNative handle creating and updating native views.
93
92
 
94
93
  ## Features
95
94
 
96
- - **Cross-platform native UI:** Build Android and iOS apps from a single Python codebase with truly native rendering.
95
+ - **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
96
+ - **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed — no manual view mutation.
97
+ - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
97
98
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
98
- - **Unified component API:** Components like `Page`, `StackView`, `Label`, `Button`, and `WebView` share a consistent interface across platforms.
99
- - **CLI scaffolding:** `pn init` creates a ready-to-run project structure; `pn run android` and `pn run ios` build and launch your app.
100
- - **Page lifecycle:** Hooks for `on_create`, `on_start`, `on_resume`, `on_pause`, `on_stop`, and `on_destroy`, with state save and restore.
99
+ - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
101
100
  - **Navigation:** Push and pop screens with argument passing for multi-page apps.
102
- - **Rich component set:** Core views (Label, Button, TextField, ImageView, WebView, Switch, DatePicker, and more) plus Material Design variants.
103
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
101
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
104
102
 
105
103
  ## Quick Start
106
104
 
@@ -119,15 +117,18 @@ import pythonnative as pn
119
117
  class MainPage(pn.Page):
120
118
  def __init__(self, native_instance):
121
119
  super().__init__(native_instance)
122
-
123
- def on_create(self):
124
- super().on_create()
125
- stack = pn.StackView()
126
- stack.add_view(pn.Label("Hello from PythonNative!"))
127
- button = pn.Button("Tap me")
128
- button.set_on_click(lambda: print("Button tapped"))
129
- stack.add_view(button)
130
- self.set_root_view(stack)
120
+ self.state = {"count": 0}
121
+
122
+ def render(self):
123
+ return pn.Column(
124
+ pn.Text(f"Count: {self.state['count']}", font_size=24),
125
+ pn.Button(
126
+ "Tap me",
127
+ on_click=lambda: self.set_state(count=self.state["count"] + 1),
128
+ ),
129
+ spacing=12,
130
+ padding=16,
131
+ )
131
132
  ```
132
133
 
133
134
  ## Documentation
@@ -26,18 +26,17 @@
26
26
 
27
27
  ## Overview
28
28
 
29
- PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a Pythonic API for native UI components, lifecycle events, and device capabilities, powered by Chaquopy on Android and rubicon-objc on iOS. Write your app once in Python and run it on both platforms with genuinely native interfaces.
29
+ PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Describe your UI as a tree of elements, manage state with `set_state()`, and let PythonNative handle creating and updating native views.
30
30
 
31
31
  ## Features
32
32
 
33
- - **Cross-platform native UI:** Build Android and iOS apps from a single Python codebase with truly native rendering.
33
+ - **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
34
+ - **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed — no manual view mutation.
35
+ - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
34
36
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
35
- - **Unified component API:** Components like `Page`, `StackView`, `Label`, `Button`, and `WebView` share a consistent interface across platforms.
36
- - **CLI scaffolding:** `pn init` creates a ready-to-run project structure; `pn run android` and `pn run ios` build and launch your app.
37
- - **Page lifecycle:** Hooks for `on_create`, `on_start`, `on_resume`, `on_pause`, `on_stop`, and `on_destroy`, with state save and restore.
37
+ - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
38
38
  - **Navigation:** Push and pop screens with argument passing for multi-page apps.
39
- - **Rich component set:** Core views (Label, Button, TextField, ImageView, WebView, Switch, DatePicker, and more) plus Material Design variants.
40
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
39
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
41
40
 
42
41
  ## Quick Start
43
42
 
@@ -56,15 +55,18 @@ import pythonnative as pn
56
55
  class MainPage(pn.Page):
57
56
  def __init__(self, native_instance):
58
57
  super().__init__(native_instance)
59
-
60
- def on_create(self):
61
- super().on_create()
62
- stack = pn.StackView()
63
- stack.add_view(pn.Label("Hello from PythonNative!"))
64
- button = pn.Button("Tap me")
65
- button.set_on_click(lambda: print("Button tapped"))
66
- stack.add_view(button)
67
- self.set_root_view(stack)
58
+ self.state = {"count": 0}
59
+
60
+ def render(self):
61
+ return pn.Column(
62
+ pn.Text(f"Count: {self.state['count']}", font_size=24),
63
+ pn.Button(
64
+ "Tap me",
65
+ on_click=lambda: self.set_state(count=self.state["count"] + 1),
66
+ ),
67
+ spacing=12,
68
+ padding=16,
69
+ )
68
70
  ```
69
71
 
70
72
  ## Documentation
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.4.0"
7
+ version = "0.6.0"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
11
11
  ]
12
12
  readme = "README.md"
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.10"
14
14
  license = { file = "LICENSE" }
15
15
  classifiers = [
16
16
  "Development Status :: 2 - Pre-Alpha",
@@ -18,7 +18,6 @@ classifiers = [
18
18
  "License :: OSI Approved :: MIT License",
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.9",
22
21
  "Programming Language :: Python :: 3.10",
23
22
  "Programming Language :: Python :: 3.11",
24
23
  "Programming Language :: Python :: 3.12",
@@ -0,0 +1,102 @@
1
+ """PythonNative — declarative native UI for Android and iOS.
2
+
3
+ Public API::
4
+
5
+ import pythonnative as pn
6
+
7
+ @pn.component
8
+ def counter(initial=0):
9
+ count, set_count = pn.use_state(initial)
10
+ return pn.Column(
11
+ pn.Text(f"Count: {count}", font_size=24),
12
+ pn.Button("+", on_click=lambda: set_count(count + 1)),
13
+ spacing=12,
14
+ )
15
+
16
+ class MainPage(pn.Page):
17
+ def __init__(self, native_instance):
18
+ super().__init__(native_instance)
19
+
20
+ def render(self):
21
+ return pn.Column(
22
+ counter(initial=0),
23
+ counter(initial=10),
24
+ spacing=16,
25
+ padding=16,
26
+ )
27
+ """
28
+
29
+ __version__ = "0.6.0"
30
+
31
+ from .components import (
32
+ ActivityIndicator,
33
+ Button,
34
+ Column,
35
+ FlatList,
36
+ Image,
37
+ Modal,
38
+ Pressable,
39
+ ProgressBar,
40
+ Row,
41
+ SafeAreaView,
42
+ ScrollView,
43
+ Slider,
44
+ Spacer,
45
+ Switch,
46
+ Text,
47
+ TextInput,
48
+ View,
49
+ WebView,
50
+ )
51
+ from .element import Element
52
+ from .hooks import (
53
+ Provider,
54
+ component,
55
+ create_context,
56
+ use_callback,
57
+ use_context,
58
+ use_effect,
59
+ use_memo,
60
+ use_ref,
61
+ use_state,
62
+ )
63
+ from .page import Page
64
+ from .style import StyleSheet, ThemeContext
65
+
66
+ __all__ = [
67
+ # Components
68
+ "ActivityIndicator",
69
+ "Button",
70
+ "Column",
71
+ "FlatList",
72
+ "Image",
73
+ "Modal",
74
+ "Pressable",
75
+ "ProgressBar",
76
+ "Row",
77
+ "SafeAreaView",
78
+ "ScrollView",
79
+ "Slider",
80
+ "Spacer",
81
+ "Switch",
82
+ "Text",
83
+ "TextInput",
84
+ "View",
85
+ "WebView",
86
+ # Core
87
+ "Element",
88
+ "Page",
89
+ # Hooks
90
+ "component",
91
+ "create_context",
92
+ "use_callback",
93
+ "use_context",
94
+ "use_effect",
95
+ "use_memo",
96
+ "use_ref",
97
+ "use_state",
98
+ "Provider",
99
+ # Styling
100
+ "StyleSheet",
101
+ "ThemeContext",
102
+ ]
@@ -2,6 +2,7 @@ import argparse
2
2
  import hashlib
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import shutil
6
7
  import subprocess
7
8
  import sys
@@ -41,49 +42,49 @@ def init_project(args: argparse.Namespace) -> None:
41
42
 
42
43
  os.makedirs(app_dir, exist_ok=True)
43
44
 
44
- # Minimal hello world app scaffold (no bootstrap function; host instantiates Page directly)
45
45
  main_page_py = os.path.join(app_dir, "main_page.py")
46
46
  if not os.path.exists(main_page_py) or args.force:
47
47
  with open(main_page_py, "w", encoding="utf-8") as f:
48
- f.write(
49
- """import pythonnative as pn
48
+ f.write("""import pythonnative as pn
50
49
 
51
50
 
52
51
  class MainPage(pn.Page):
53
52
  def __init__(self, native_instance):
54
53
  super().__init__(native_instance)
55
-
56
- def on_create(self):
57
- super().on_create()
58
- stack = (
59
- pn.StackView()
60
- .set_axis("vertical")
61
- .set_spacing(12)
62
- .set_alignment("fill")
63
- .set_padding(all=16)
64
- )
65
- stack.add_view(pn.Label("Hello from PythonNative!").set_text_size(18))
66
- button = pn.Button("Tap me").set_on_click(lambda: print("Button clicked"))
67
- stack.add_view(button)
68
- self.set_root_view(stack.wrap_in_scroll())
69
- """
54
+ self.state = {"count": 0}
55
+
56
+ def increment(self):
57
+ self.set_state(count=self.state["count"] + 1)
58
+
59
+ def render(self):
60
+ return pn.ScrollView(
61
+ pn.Column(
62
+ pn.Text("Hello from PythonNative!", font_size=24, bold=True),
63
+ pn.Text(f"Tapped {self.state['count']} times"),
64
+ pn.Button("Tap me", on_click=self.increment),
65
+ spacing=12,
66
+ padding=16,
67
+ alignment="fill",
70
68
  )
69
+ )
70
+ """)
71
71
 
72
72
  # Create config
73
73
  config = {
74
74
  "name": project_name,
75
75
  "appId": "com.example." + project_name.replace(" ", "").lower(),
76
76
  "entryPoint": "app/main_page.py",
77
+ "pythonVersion": "3.11",
77
78
  "ios": {},
78
79
  "android": {},
79
80
  }
80
81
  with open(config_path, "w", encoding="utf-8") as f:
81
82
  json.dump(config, f, indent=2)
82
83
 
83
- # Requirements
84
+ # Requirements (third-party packages only; pythonnative itself is bundled by the CLI)
84
85
  if not os.path.exists(requirements_path) or args.force:
85
86
  with open(requirements_path, "w", encoding="utf-8") as f:
86
- f.write("pythonnative\n")
87
+ f.write("")
87
88
 
88
89
  # .gitignore
89
90
  default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n"
@@ -157,7 +158,11 @@ def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
157
158
 
158
159
 
159
160
  def _github_json(url: str) -> Any:
160
- req = urllib.request.Request(url, headers={"User-Agent": "pythonnative-cli"})
161
+ headers: dict[str, str] = {"User-Agent": "pythonnative-cli"}
162
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
163
+ if token:
164
+ headers["Authorization"] = f"Bearer {token}"
165
+ req = urllib.request.Request(url, headers=headers)
161
166
  with urllib.request.urlopen(req) as r:
162
167
  return json.loads(r.read().decode("utf-8"))
163
168
 
@@ -212,6 +217,43 @@ def create_ios_project(project_name: str, destination: str) -> None:
212
217
  _copy_bundled_template_dir("ios_template", destination)
213
218
 
214
219
 
220
+ def _read_project_config() -> dict:
221
+ """Read pythonnative.json from the current working directory."""
222
+ config_path = os.path.join(os.getcwd(), "pythonnative.json")
223
+ if os.path.exists(config_path):
224
+ with open(config_path, encoding="utf-8") as f:
225
+ return json.load(f)
226
+ return {}
227
+
228
+
229
+ def _read_requirements(requirements_path: str) -> list[str]:
230
+ """Read a requirements file and return non-empty, non-comment lines.
231
+
232
+ Exits with an error if pythonnative is listed — the CLI bundles it
233
+ directly, so it must not be installed separately via pip/Chaquopy.
234
+ """
235
+ if not os.path.exists(requirements_path):
236
+ return []
237
+ with open(requirements_path, encoding="utf-8") as f:
238
+ lines = f.readlines()
239
+ result: list[str] = []
240
+ for line in lines:
241
+ stripped = line.strip()
242
+ if not stripped or stripped.startswith("#") or stripped.startswith("-"):
243
+ continue
244
+ pkg_name = re.split(r"[\[><=!;]", stripped)[0].strip()
245
+ if pkg_name.lower().replace("-", "_") == "pythonnative":
246
+ print(
247
+ "Error: 'pythonnative' must not be in requirements.txt.\n"
248
+ "The pn CLI automatically bundles the installed pythonnative into your app.\n"
249
+ "requirements.txt is for third-party packages only (e.g. humanize, requests).\n"
250
+ "Remove the pythonnative line from requirements.txt and try again."
251
+ )
252
+ sys.exit(1)
253
+ result.append(stripped)
254
+ return result
255
+
256
+
215
257
  def run_project(args: argparse.Namespace) -> None:
216
258
  """
217
259
  Run the specified project.
@@ -219,9 +261,15 @@ def run_project(args: argparse.Namespace) -> None:
219
261
  # Determine the platform
220
262
  platform: str = args.platform
221
263
  prepare_only: bool = getattr(args, "prepare_only", False)
264
+ hot_reload: bool = getattr(args, "hot_reload", False)
265
+
266
+ # Read project configuration and save project root before any chdir
267
+ project_dir: str = os.getcwd()
268
+ config = _read_project_config()
269
+ python_version: str = config.get("pythonVersion", "3.11")
222
270
 
223
271
  # Define the build directory
224
- build_dir: str = os.path.join(os.getcwd(), "build", platform)
272
+ build_dir: str = os.path.join(project_dir, "build", platform)
225
273
 
226
274
  # Create the build directory if it doesn't exist
227
275
  os.makedirs(build_dir, exist_ok=True)
@@ -265,10 +313,30 @@ def run_project(args: argparse.Namespace) -> None:
265
313
  # Non-fatal; fallback to the packaged PyPI dependency if present
266
314
  pass
267
315
 
268
- # Install any necessary Python packages into the project environment
316
+ # Validate and read the user's requirements.txt
317
+ requirements_path = os.path.join(project_dir, "requirements.txt")
318
+ pip_reqs = _read_requirements(requirements_path)
319
+
320
+ if platform == "android":
321
+ # Patch the Android build.gradle with the configured Python version
322
+ app_build_gradle = os.path.join(build_dir, "android_template", "app", "build.gradle")
323
+ if os.path.exists(app_build_gradle):
324
+ with open(app_build_gradle, encoding="utf-8") as f:
325
+ content = f.read()
326
+ content = content.replace('version "3.11"', f'version "{python_version}"')
327
+ with open(app_build_gradle, "w", encoding="utf-8") as f:
328
+ f.write(content)
329
+ # Copy requirements.txt into the Android project for Chaquopy
330
+ android_reqs_path = os.path.join(build_dir, "android_template", "app", "requirements.txt")
331
+ if os.path.exists(requirements_path):
332
+ shutil.copy2(requirements_path, android_reqs_path)
333
+ else:
334
+ with open(android_reqs_path, "w", encoding="utf-8") as f:
335
+ f.write("")
336
+
337
+ # Install any necessary Python packages into the host environment
269
338
  # Skip installation during prepare-only to avoid network access and speed up scaffolding
270
339
  if not prepare_only:
271
- requirements_path = os.path.join(os.getcwd(), "requirements.txt")
272
340
  if os.path.exists(requirements_path):
273
341
  subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False)
274
342
 
@@ -520,6 +588,29 @@ def run_project(args: argparse.Namespace) -> None:
520
588
  except Exception:
521
589
  # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear
522
590
  pass
591
+ # Install user's pip requirements (pure-Python packages) into the app bundle
592
+ if pip_reqs:
593
+ try:
594
+ reqs_tmp = os.path.join(build_dir, "ios_requirements.txt")
595
+ with open(reqs_tmp, "w", encoding="utf-8") as f:
596
+ f.write("\n".join(pip_reqs) + "\n")
597
+ tmp_reqs_dir = os.path.join(build_dir, "ios_user_packages")
598
+ if os.path.isdir(tmp_reqs_dir):
599
+ shutil.rmtree(tmp_reqs_dir)
600
+ os.makedirs(tmp_reqs_dir, exist_ok=True)
601
+ subprocess.run(
602
+ [sys.executable, "-m", "pip", "install", "-t", tmp_reqs_dir, "-r", reqs_tmp],
603
+ check=False,
604
+ )
605
+ for entry in os.listdir(tmp_reqs_dir):
606
+ src_entry = os.path.join(tmp_reqs_dir, entry)
607
+ dst_entry = os.path.join(platform_site_dir, entry)
608
+ if os.path.isdir(src_entry):
609
+ shutil.copytree(src_entry, dst_entry, dirs_exist_ok=True)
610
+ else:
611
+ shutil.copy2(src_entry, dst_entry)
612
+ except Exception:
613
+ pass
523
614
  # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time.
524
615
  # We copy the XCFramework into the project directory above so Xcode can link it.
525
616
  except Exception:
@@ -566,6 +657,39 @@ def run_project(args: argparse.Namespace) -> None:
566
657
  except Exception:
567
658
  print("Failed to auto-run on Simulator; open the project in Xcode to run.")
568
659
 
660
+ # Hot-reload file watcher
661
+ if hot_reload and not prepare_only:
662
+ _run_hot_reload(platform, project_dir, build_dir)
663
+
664
+
665
+ def _run_hot_reload(platform: str, project_dir: str, build_dir: str) -> None:
666
+ """Watch ``app/`` for changes and push updated files to the device."""
667
+ from .hot_reload import FileWatcher
668
+
669
+ app_dir = os.path.join(project_dir, "app")
670
+
671
+ def on_change(changed_files: List[str]) -> None:
672
+ for fpath in changed_files:
673
+ rel = os.path.relpath(fpath, project_dir)
674
+ print(f"[hot-reload] Changed: {rel}")
675
+ if platform == "android":
676
+ dest = f"/data/data/com.pythonnative.android_template/files/{rel}"
677
+ subprocess.run(["adb", "push", fpath, dest], check=False, capture_output=True)
678
+ elif platform == "ios":
679
+ pass # simctl file push would go here
680
+
681
+ print("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop.")
682
+ watcher = FileWatcher(app_dir, on_change, interval=1.0)
683
+ watcher.start()
684
+ try:
685
+ import time
686
+
687
+ while True:
688
+ time.sleep(1)
689
+ except KeyboardInterrupt:
690
+ watcher.stop()
691
+ print("\n[hot-reload] Stopped.")
692
+
569
693
 
570
694
  def clean_project(args: argparse.Namespace) -> None:
571
695
  """
@@ -600,6 +724,11 @@ def main() -> None:
600
724
  action="store_true",
601
725
  help="Extract templates and stage app without building",
602
726
  )
727
+ parser_run.add_argument(
728
+ "--hot-reload",
729
+ action="store_true",
730
+ help="Watch app/ for changes and push updates to the running app",
731
+ )
603
732
  parser_run.set_defaults(func=run_project)
604
733
 
605
734
  # Create a new command 'clean' that calls clean_project