pythonnative 0.5.0__tar.gz → 0.7.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 (92) hide show
  1. {pythonnative-0.5.0/src/pythonnative.egg-info → pythonnative-0.7.0}/PKG-INFO +18 -38
  2. {pythonnative-0.5.0 → pythonnative-0.7.0}/README.md +16 -35
  3. {pythonnative-0.5.0 → pythonnative-0.7.0}/pyproject.toml +2 -3
  4. pythonnative-0.7.0/src/pythonnative/__init__.py +92 -0
  5. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/cli/pn.py +150 -30
  6. pythonnative-0.7.0/src/pythonnative/components.py +351 -0
  7. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/element.py +14 -8
  8. pythonnative-0.7.0/src/pythonnative/hooks.py +334 -0
  9. pythonnative-0.7.0/src/pythonnative/hot_reload.py +143 -0
  10. pythonnative-0.7.0/src/pythonnative/native_modules/__init__.py +19 -0
  11. pythonnative-0.7.0/src/pythonnative/native_modules/camera.py +105 -0
  12. pythonnative-0.7.0/src/pythonnative/native_modules/file_system.py +131 -0
  13. pythonnative-0.7.0/src/pythonnative/native_modules/location.py +61 -0
  14. pythonnative-0.7.0/src/pythonnative/native_modules/notifications.py +151 -0
  15. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/native_views.py +638 -34
  16. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/page.py +138 -171
  17. pythonnative-0.7.0/src/pythonnative/reconciler.py +262 -0
  18. pythonnative-0.7.0/src/pythonnative/style.py +135 -0
  19. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/build.gradle +2 -7
  20. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -9
  21. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/build.gradle +1 -1
  22. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +7 -20
  23. {pythonnative-0.5.0 → pythonnative-0.7.0/src/pythonnative.egg-info}/PKG-INFO +18 -38
  24. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative.egg-info/SOURCES.txt +11 -4
  25. {pythonnative-0.5.0 → pythonnative-0.7.0}/tests/test_cli.py +1 -1
  26. {pythonnative-0.5.0 → pythonnative-0.7.0}/tests/test_components.py +136 -10
  27. pythonnative-0.7.0/tests/test_hooks.py +465 -0
  28. {pythonnative-0.5.0 → pythonnative-0.7.0}/tests/test_reconciler.py +99 -8
  29. {pythonnative-0.5.0 → pythonnative-0.7.0}/tests/test_smoke.py +22 -1
  30. pythonnative-0.7.0/tests/test_style.py +80 -0
  31. pythonnative-0.5.0/src/pythonnative/__init__.py +0 -54
  32. pythonnative-0.5.0/src/pythonnative/collection_view.py +0 -0
  33. pythonnative-0.5.0/src/pythonnative/components.py +0 -241
  34. pythonnative-0.5.0/src/pythonnative/material_bottom_navigation_view.py +0 -0
  35. pythonnative-0.5.0/src/pythonnative/material_toolbar.py +0 -0
  36. pythonnative-0.5.0/src/pythonnative/reconciler.py +0 -129
  37. {pythonnative-0.5.0 → pythonnative-0.7.0}/LICENSE +0 -0
  38. {pythonnative-0.5.0 → pythonnative-0.7.0}/setup.cfg +0 -0
  39. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/cli/__init__.py +0 -0
  40. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  41. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
  42. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  43. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
  44. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
  45. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  46. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
  47. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  48. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
  49. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
  50. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  51. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  52. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  53. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  54. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  55. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  56. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  57. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  58. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  59. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  60. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  61. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  62. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  63. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  64. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  65. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  66. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  67. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
  68. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  69. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  70. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  71. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  72. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  73. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  74. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  75. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  76. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  77. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  78. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  79. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  80. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  81. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  82. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  83. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  84. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  85. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  86. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  87. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative/utils.py +0 -0
  88. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  89. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  90. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative.egg-info/requires.txt +0 -0
  91. {pythonnative-0.5.0 → pythonnative-0.7.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  92. {pythonnative-0.5.0 → pythonnative-0.7.0}/tests/test_element.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.5.0
3
+ Version: 0.7.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,17 +88,18 @@ 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 **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.
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 hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
93
92
 
94
93
  ## Features
95
94
 
96
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.
97
- - **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed no manual view mutation.
96
+ - **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
97
+ - **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
98
98
  - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
99
99
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
100
100
  - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
101
- - **Navigation:** Push and pop screens with argument passing for multi-page apps.
102
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
101
+ - **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
102
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
103
103
 
104
104
  ## Quick Start
105
105
 
@@ -115,39 +115,19 @@ pip install pythonnative
115
115
  import pythonnative as pn
116
116
 
117
117
 
118
- class MainPage(pn.Page):
119
- def __init__(self, native_instance):
120
- super().__init__(native_instance)
121
- self.state = {"count": 0}
122
-
123
- def render(self):
124
- return pn.Column(
125
- pn.Text(f"Count: {self.state['count']}", font_size=24),
126
- pn.Button(
127
- "Tap me",
128
- on_click=lambda: self.set_state(count=self.state["count"] + 1),
129
- ),
130
- spacing=12,
131
- padding=16,
132
- )
118
+ @pn.component
119
+ def MainPage():
120
+ count, set_count = pn.use_state(0)
121
+ return pn.Column(
122
+ pn.Text(f"Count: {count}", style={"font_size": 24}),
123
+ pn.Button(
124
+ "Tap me",
125
+ on_click=lambda: set_count(count + 1),
126
+ ),
127
+ style={"spacing": 12, "padding": 16},
128
+ )
133
129
  ```
134
130
 
135
- ### Available Components
136
-
137
- | Component | Description |
138
- |---|---|
139
- | `Text` | Display text |
140
- | `Button` | Tappable button with `on_click` callback |
141
- | `Column` / `Row` | Vertical / horizontal layout containers |
142
- | `ScrollView` | Scrollable wrapper |
143
- | `TextInput` | Text entry field with `on_change` callback |
144
- | `Image` | Display images |
145
- | `Switch` | Toggle with `on_change` callback |
146
- | `ProgressBar` | Determinate progress (0.0–1.0) |
147
- | `ActivityIndicator` | Indeterminate loading spinner |
148
- | `WebView` | Embedded web content |
149
- | `Spacer` | Empty space |
150
-
151
131
  ## Documentation
152
132
 
153
133
  Visit [docs.pythonnative.com](https://docs.pythonnative.com/) for the full documentation, including getting started guides, platform-specific instructions for Android and iOS, API reference, and working examples.
@@ -26,17 +26,18 @@
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 **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.
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 hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
30
30
 
31
31
  ## Features
32
32
 
33
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.
34
+ - **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
35
+ - **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
35
36
  - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
36
37
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
37
38
  - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
38
- - **Navigation:** Push and pop screens with argument passing for multi-page apps.
39
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
39
+ - **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
40
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
40
41
 
41
42
  ## Quick Start
42
43
 
@@ -52,39 +53,19 @@ pip install pythonnative
52
53
  import pythonnative as pn
53
54
 
54
55
 
55
- class MainPage(pn.Page):
56
- def __init__(self, native_instance):
57
- super().__init__(native_instance)
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
- )
56
+ @pn.component
57
+ def MainPage():
58
+ count, set_count = pn.use_state(0)
59
+ return pn.Column(
60
+ pn.Text(f"Count: {count}", style={"font_size": 24}),
61
+ pn.Button(
62
+ "Tap me",
63
+ on_click=lambda: set_count(count + 1),
64
+ ),
65
+ style={"spacing": 12, "padding": 16},
66
+ )
70
67
  ```
71
68
 
72
- ### Available Components
73
-
74
- | Component | Description |
75
- |---|---|
76
- | `Text` | Display text |
77
- | `Button` | Tappable button with `on_click` callback |
78
- | `Column` / `Row` | Vertical / horizontal layout containers |
79
- | `ScrollView` | Scrollable wrapper |
80
- | `TextInput` | Text entry field with `on_change` callback |
81
- | `Image` | Display images |
82
- | `Switch` | Toggle with `on_change` callback |
83
- | `ProgressBar` | Determinate progress (0.0–1.0) |
84
- | `ActivityIndicator` | Indeterminate loading spinner |
85
- | `WebView` | Embedded web content |
86
- | `Spacer` | Empty space |
87
-
88
69
  ## Documentation
89
70
 
90
71
  Visit [docs.pythonnative.com](https://docs.pythonnative.com/) for the full documentation, including getting started guides, platform-specific instructions for Android and iOS, API reference, and working examples.
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.5.0"
7
+ version = "0.7.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,92 @@
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 App():
9
+ count, set_count = pn.use_state(0)
10
+ return pn.Column(
11
+ pn.Text(f"Count: {count}", style={"font_size": 24}),
12
+ pn.Button("+", on_click=lambda: set_count(count + 1)),
13
+ style={"spacing": 12},
14
+ )
15
+ """
16
+
17
+ __version__ = "0.7.0"
18
+
19
+ from .components import (
20
+ ActivityIndicator,
21
+ Button,
22
+ Column,
23
+ FlatList,
24
+ Image,
25
+ Modal,
26
+ Pressable,
27
+ ProgressBar,
28
+ Row,
29
+ SafeAreaView,
30
+ ScrollView,
31
+ Slider,
32
+ Spacer,
33
+ Switch,
34
+ Text,
35
+ TextInput,
36
+ View,
37
+ WebView,
38
+ )
39
+ from .element import Element
40
+ from .hooks import (
41
+ Provider,
42
+ component,
43
+ create_context,
44
+ use_callback,
45
+ use_context,
46
+ use_effect,
47
+ use_memo,
48
+ use_navigation,
49
+ use_ref,
50
+ use_state,
51
+ )
52
+ from .page import create_page
53
+ from .style import StyleSheet, ThemeContext
54
+
55
+ __all__ = [
56
+ # Components
57
+ "ActivityIndicator",
58
+ "Button",
59
+ "Column",
60
+ "FlatList",
61
+ "Image",
62
+ "Modal",
63
+ "Pressable",
64
+ "ProgressBar",
65
+ "Row",
66
+ "SafeAreaView",
67
+ "ScrollView",
68
+ "Slider",
69
+ "Spacer",
70
+ "Switch",
71
+ "Text",
72
+ "TextInput",
73
+ "View",
74
+ "WebView",
75
+ # Core
76
+ "Element",
77
+ "create_page",
78
+ # Hooks
79
+ "component",
80
+ "create_context",
81
+ "use_callback",
82
+ "use_context",
83
+ "use_effect",
84
+ "use_memo",
85
+ "use_navigation",
86
+ "use_ref",
87
+ "use_state",
88
+ "Provider",
89
+ # Styling
90
+ "StyleSheet",
91
+ "ThemeContext",
92
+ ]
@@ -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
@@ -44,47 +45,38 @@ def init_project(args: argparse.Namespace) -> None:
44
45
  main_page_py = os.path.join(app_dir, "main_page.py")
45
46
  if not os.path.exists(main_page_py) or args.force:
46
47
  with open(main_page_py, "w", encoding="utf-8") as f:
47
- f.write(
48
- """import pythonnative as pn
49
-
50
-
51
- class MainPage(pn.Page):
52
- def __init__(self, native_instance):
53
- super().__init__(native_instance)
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",
68
- )
48
+ f.write("""import pythonnative as pn
49
+
50
+
51
+ @pn.component
52
+ def MainPage():
53
+ count, set_count = pn.use_state(0)
54
+ return pn.ScrollView(
55
+ pn.Column(
56
+ pn.Text("Hello from PythonNative!", style={"font_size": 24, "bold": True}),
57
+ pn.Text(f"Tapped {count} times"),
58
+ pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
59
+ style={"spacing": 12, "padding": 16, "align_items": "stretch"},
69
60
  )
70
- """
71
- )
61
+ )
62
+ """)
72
63
 
73
64
  # Create config
74
65
  config = {
75
66
  "name": project_name,
76
67
  "appId": "com.example." + project_name.replace(" ", "").lower(),
77
68
  "entryPoint": "app/main_page.py",
69
+ "pythonVersion": "3.11",
78
70
  "ios": {},
79
71
  "android": {},
80
72
  }
81
73
  with open(config_path, "w", encoding="utf-8") as f:
82
74
  json.dump(config, f, indent=2)
83
75
 
84
- # Requirements
76
+ # Requirements (third-party packages only; pythonnative itself is bundled by the CLI)
85
77
  if not os.path.exists(requirements_path) or args.force:
86
78
  with open(requirements_path, "w", encoding="utf-8") as f:
87
- f.write("pythonnative\n")
79
+ f.write("")
88
80
 
89
81
  # .gitignore
90
82
  default_gitignore = "# PythonNative\n" "__pycache__/\n" "*.pyc\n" ".venv/\n" "build/\n" ".DS_Store\n"
@@ -158,7 +150,11 @@ def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
158
150
 
159
151
 
160
152
  def _github_json(url: str) -> Any:
161
- req = urllib.request.Request(url, headers={"User-Agent": "pythonnative-cli"})
153
+ headers: dict[str, str] = {"User-Agent": "pythonnative-cli"}
154
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
155
+ if token:
156
+ headers["Authorization"] = f"Bearer {token}"
157
+ req = urllib.request.Request(url, headers=headers)
162
158
  with urllib.request.urlopen(req) as r:
163
159
  return json.loads(r.read().decode("utf-8"))
164
160
 
@@ -213,6 +209,43 @@ def create_ios_project(project_name: str, destination: str) -> None:
213
209
  _copy_bundled_template_dir("ios_template", destination)
214
210
 
215
211
 
212
+ def _read_project_config() -> dict:
213
+ """Read pythonnative.json from the current working directory."""
214
+ config_path = os.path.join(os.getcwd(), "pythonnative.json")
215
+ if os.path.exists(config_path):
216
+ with open(config_path, encoding="utf-8") as f:
217
+ return json.load(f)
218
+ return {}
219
+
220
+
221
+ def _read_requirements(requirements_path: str) -> list[str]:
222
+ """Read a requirements file and return non-empty, non-comment lines.
223
+
224
+ Exits with an error if pythonnative is listed — the CLI bundles it
225
+ directly, so it must not be installed separately via pip/Chaquopy.
226
+ """
227
+ if not os.path.exists(requirements_path):
228
+ return []
229
+ with open(requirements_path, encoding="utf-8") as f:
230
+ lines = f.readlines()
231
+ result: list[str] = []
232
+ for line in lines:
233
+ stripped = line.strip()
234
+ if not stripped or stripped.startswith("#") or stripped.startswith("-"):
235
+ continue
236
+ pkg_name = re.split(r"[\[><=!;]", stripped)[0].strip()
237
+ if pkg_name.lower().replace("-", "_") == "pythonnative":
238
+ print(
239
+ "Error: 'pythonnative' must not be in requirements.txt.\n"
240
+ "The pn CLI automatically bundles the installed pythonnative into your app.\n"
241
+ "requirements.txt is for third-party packages only (e.g. humanize, requests).\n"
242
+ "Remove the pythonnative line from requirements.txt and try again."
243
+ )
244
+ sys.exit(1)
245
+ result.append(stripped)
246
+ return result
247
+
248
+
216
249
  def run_project(args: argparse.Namespace) -> None:
217
250
  """
218
251
  Run the specified project.
@@ -220,9 +253,15 @@ def run_project(args: argparse.Namespace) -> None:
220
253
  # Determine the platform
221
254
  platform: str = args.platform
222
255
  prepare_only: bool = getattr(args, "prepare_only", False)
256
+ hot_reload: bool = getattr(args, "hot_reload", False)
257
+
258
+ # Read project configuration and save project root before any chdir
259
+ project_dir: str = os.getcwd()
260
+ config = _read_project_config()
261
+ python_version: str = config.get("pythonVersion", "3.11")
223
262
 
224
263
  # Define the build directory
225
- build_dir: str = os.path.join(os.getcwd(), "build", platform)
264
+ build_dir: str = os.path.join(project_dir, "build", platform)
226
265
 
227
266
  # Create the build directory if it doesn't exist
228
267
  os.makedirs(build_dir, exist_ok=True)
@@ -266,10 +305,30 @@ def run_project(args: argparse.Namespace) -> None:
266
305
  # Non-fatal; fallback to the packaged PyPI dependency if present
267
306
  pass
268
307
 
269
- # Install any necessary Python packages into the project environment
308
+ # Validate and read the user's requirements.txt
309
+ requirements_path = os.path.join(project_dir, "requirements.txt")
310
+ pip_reqs = _read_requirements(requirements_path)
311
+
312
+ if platform == "android":
313
+ # Patch the Android build.gradle with the configured Python version
314
+ app_build_gradle = os.path.join(build_dir, "android_template", "app", "build.gradle")
315
+ if os.path.exists(app_build_gradle):
316
+ with open(app_build_gradle, encoding="utf-8") as f:
317
+ content = f.read()
318
+ content = content.replace('version "3.11"', f'version "{python_version}"')
319
+ with open(app_build_gradle, "w", encoding="utf-8") as f:
320
+ f.write(content)
321
+ # Copy requirements.txt into the Android project for Chaquopy
322
+ android_reqs_path = os.path.join(build_dir, "android_template", "app", "requirements.txt")
323
+ if os.path.exists(requirements_path):
324
+ shutil.copy2(requirements_path, android_reqs_path)
325
+ else:
326
+ with open(android_reqs_path, "w", encoding="utf-8") as f:
327
+ f.write("")
328
+
329
+ # Install any necessary Python packages into the host environment
270
330
  # Skip installation during prepare-only to avoid network access and speed up scaffolding
271
331
  if not prepare_only:
272
- requirements_path = os.path.join(os.getcwd(), "requirements.txt")
273
332
  if os.path.exists(requirements_path):
274
333
  subprocess.run([sys.executable, "-m", "pip", "install", "-r", requirements_path], check=False)
275
334
 
@@ -521,6 +580,29 @@ def run_project(args: argparse.Namespace) -> None:
521
580
  except Exception:
522
581
  # Non-fatal; if metadata isn't present, rubicon import may fail and fallback UI will appear
523
582
  pass
583
+ # Install user's pip requirements (pure-Python packages) into the app bundle
584
+ if pip_reqs:
585
+ try:
586
+ reqs_tmp = os.path.join(build_dir, "ios_requirements.txt")
587
+ with open(reqs_tmp, "w", encoding="utf-8") as f:
588
+ f.write("\n".join(pip_reqs) + "\n")
589
+ tmp_reqs_dir = os.path.join(build_dir, "ios_user_packages")
590
+ if os.path.isdir(tmp_reqs_dir):
591
+ shutil.rmtree(tmp_reqs_dir)
592
+ os.makedirs(tmp_reqs_dir, exist_ok=True)
593
+ subprocess.run(
594
+ [sys.executable, "-m", "pip", "install", "-t", tmp_reqs_dir, "-r", reqs_tmp],
595
+ check=False,
596
+ )
597
+ for entry in os.listdir(tmp_reqs_dir):
598
+ src_entry = os.path.join(tmp_reqs_dir, entry)
599
+ dst_entry = os.path.join(platform_site_dir, entry)
600
+ if os.path.isdir(src_entry):
601
+ shutil.copytree(src_entry, dst_entry, dirs_exist_ok=True)
602
+ else:
603
+ shutil.copy2(src_entry, dst_entry)
604
+ except Exception:
605
+ pass
524
606
  # Note: Python.xcframework provides a static library for Simulator; it must be linked at build time.
525
607
  # We copy the XCFramework into the project directory above so Xcode can link it.
526
608
  except Exception:
@@ -567,6 +649,39 @@ def run_project(args: argparse.Namespace) -> None:
567
649
  except Exception:
568
650
  print("Failed to auto-run on Simulator; open the project in Xcode to run.")
569
651
 
652
+ # Hot-reload file watcher
653
+ if hot_reload and not prepare_only:
654
+ _run_hot_reload(platform, project_dir, build_dir)
655
+
656
+
657
+ def _run_hot_reload(platform: str, project_dir: str, build_dir: str) -> None:
658
+ """Watch ``app/`` for changes and push updated files to the device."""
659
+ from .hot_reload import FileWatcher
660
+
661
+ app_dir = os.path.join(project_dir, "app")
662
+
663
+ def on_change(changed_files: List[str]) -> None:
664
+ for fpath in changed_files:
665
+ rel = os.path.relpath(fpath, project_dir)
666
+ print(f"[hot-reload] Changed: {rel}")
667
+ if platform == "android":
668
+ dest = f"/data/data/com.pythonnative.android_template/files/{rel}"
669
+ subprocess.run(["adb", "push", fpath, dest], check=False, capture_output=True)
670
+ elif platform == "ios":
671
+ pass # simctl file push would go here
672
+
673
+ print("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop.")
674
+ watcher = FileWatcher(app_dir, on_change, interval=1.0)
675
+ watcher.start()
676
+ try:
677
+ import time
678
+
679
+ while True:
680
+ time.sleep(1)
681
+ except KeyboardInterrupt:
682
+ watcher.stop()
683
+ print("\n[hot-reload] Stopped.")
684
+
570
685
 
571
686
  def clean_project(args: argparse.Namespace) -> None:
572
687
  """
@@ -601,6 +716,11 @@ def main() -> None:
601
716
  action="store_true",
602
717
  help="Extract templates and stage app without building",
603
718
  )
719
+ parser_run.add_argument(
720
+ "--hot-reload",
721
+ action="store_true",
722
+ help="Watch app/ for changes and push updates to the running app",
723
+ )
604
724
  parser_run.set_defaults(func=run_project)
605
725
 
606
726
  # Create a new command 'clean' that calls clean_project