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.
- {pythonnative-0.4.0/src/pythonnative.egg-info → pythonnative-0.6.0}/PKG-INFO +20 -19
- {pythonnative-0.4.0 → pythonnative-0.6.0}/README.md +18 -16
- {pythonnative-0.4.0 → pythonnative-0.6.0}/pyproject.toml +2 -3
- pythonnative-0.6.0/src/pythonnative/__init__.py +102 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/cli/pn.py +153 -24
- pythonnative-0.6.0/src/pythonnative/components.py +563 -0
- pythonnative-0.6.0/src/pythonnative/element.py +53 -0
- pythonnative-0.6.0/src/pythonnative/hooks.py +287 -0
- pythonnative-0.6.0/src/pythonnative/hot_reload.py +143 -0
- pythonnative-0.6.0/src/pythonnative/native_modules/__init__.py +19 -0
- pythonnative-0.6.0/src/pythonnative/native_modules/camera.py +105 -0
- pythonnative-0.6.0/src/pythonnative/native_modules/file_system.py +131 -0
- pythonnative-0.6.0/src/pythonnative/native_modules/location.py +61 -0
- pythonnative-0.6.0/src/pythonnative/native_modules/notifications.py +151 -0
- pythonnative-0.6.0/src/pythonnative/native_views.py +1334 -0
- pythonnative-0.6.0/src/pythonnative/page.py +469 -0
- pythonnative-0.6.0/src/pythonnative/reconciler.py +262 -0
- pythonnative-0.6.0/src/pythonnative/style.py +115 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/build.gradle +2 -7
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative-0.6.0/src/pythonnative/utils.py +83 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0/src/pythonnative.egg-info}/PKG-INFO +20 -19
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/SOURCES.txt +18 -28
- pythonnative-0.6.0/tests/test_components.py +300 -0
- pythonnative-0.6.0/tests/test_element.py +71 -0
- pythonnative-0.6.0/tests/test_hooks.py +433 -0
- pythonnative-0.6.0/tests/test_reconciler.py +371 -0
- pythonnative-0.6.0/tests/test_smoke.py +51 -0
- pythonnative-0.6.0/tests/test_style.py +58 -0
- pythonnative-0.4.0/src/pythonnative/__init__.py +0 -74
- pythonnative-0.4.0/src/pythonnative/activity_indicator_view.py +0 -71
- pythonnative-0.4.0/src/pythonnative/button.py +0 -113
- pythonnative-0.4.0/src/pythonnative/collection_view.py +0 -0
- pythonnative-0.4.0/src/pythonnative/date_picker.py +0 -76
- pythonnative-0.4.0/src/pythonnative/image_view.py +0 -78
- pythonnative-0.4.0/src/pythonnative/label.py +0 -133
- pythonnative-0.4.0/src/pythonnative/list_view.py +0 -76
- pythonnative-0.4.0/src/pythonnative/material_activity_indicator_view.py +0 -71
- pythonnative-0.4.0/src/pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative-0.4.0/src/pythonnative/material_button.py +0 -69
- pythonnative-0.4.0/src/pythonnative/material_date_picker.py +0 -87
- pythonnative-0.4.0/src/pythonnative/material_progress_view.py +0 -70
- pythonnative-0.4.0/src/pythonnative/material_search_bar.py +0 -69
- pythonnative-0.4.0/src/pythonnative/material_switch.py +0 -69
- pythonnative-0.4.0/src/pythonnative/material_time_picker.py +0 -76
- pythonnative-0.4.0/src/pythonnative/material_toolbar.py +0 -0
- pythonnative-0.4.0/src/pythonnative/page.py +0 -396
- pythonnative-0.4.0/src/pythonnative/picker_view.py +0 -69
- pythonnative-0.4.0/src/pythonnative/progress_view.py +0 -70
- pythonnative-0.4.0/src/pythonnative/scroll_view.py +0 -101
- pythonnative-0.4.0/src/pythonnative/search_bar.py +0 -69
- pythonnative-0.4.0/src/pythonnative/stack_view.py +0 -199
- pythonnative-0.4.0/src/pythonnative/switch.py +0 -68
- pythonnative-0.4.0/src/pythonnative/text_field.py +0 -132
- pythonnative-0.4.0/src/pythonnative/text_view.py +0 -135
- pythonnative-0.4.0/src/pythonnative/time_picker.py +0 -77
- pythonnative-0.4.0/src/pythonnative/utils.py +0 -91
- pythonnative-0.4.0/src/pythonnative/view.py +0 -173
- pythonnative-0.4.0/src/pythonnative/web_view.py +0 -60
- pythonnative-0.4.0/tests/test_smoke.py +0 -2
- {pythonnative-0.4.0 → pythonnative-0.6.0}/LICENSE +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/setup.cfg +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {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
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {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
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {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
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.4.0 → pythonnative-0.6.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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("
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|