pythonnative 0.9.0__tar.gz → 0.10.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.9.0/src/pythonnative.egg-info → pythonnative-0.10.0}/PKG-INFO +3 -1
- {pythonnative-0.9.0 → pythonnative-0.10.0}/pyproject.toml +29 -3
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/__init__.py +28 -3
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/_ios_log.py +20 -22
- pythonnative-0.10.0/src/pythonnative/cli/__init__.py +7 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/cli/pn.py +275 -45
- pythonnative-0.10.0/src/pythonnative/components.py +649 -0
- pythonnative-0.10.0/src/pythonnative/element.py +77 -0
- pythonnative-0.10.0/src/pythonnative/hooks.py +706 -0
- pythonnative-0.10.0/src/pythonnative/hot_reload.py +329 -0
- pythonnative-0.10.0/src/pythonnative/native_modules/__init__.py +25 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_modules/camera.py +32 -16
- pythonnative-0.10.0/src/pythonnative/native_modules/file_system.py +241 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_modules/location.py +32 -9
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_modules/notifications.py +51 -18
- pythonnative-0.10.0/src/pythonnative/native_views/__init__.py +172 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_views/android.py +14 -7
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_views/base.py +68 -11
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/native_views/ios.py +13 -6
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/navigation.py +288 -63
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/page.py +232 -47
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/reconciler.py +59 -25
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/style.py +79 -18
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +4 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +23 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +34 -1
- pythonnative-0.10.0/src/pythonnative/utils.py +184 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0/src/pythonnative.egg-info}/PKG-INFO +3 -1
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative.egg-info/SOURCES.txt +2 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative.egg-info/requires.txt +2 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_cli.py +73 -0
- pythonnative-0.10.0/tests/test_hot_reload.py +97 -0
- pythonnative-0.10.0/tests/test_page.py +68 -0
- pythonnative-0.9.0/src/pythonnative/cli/__init__.py +0 -0
- pythonnative-0.9.0/src/pythonnative/components.py +0 -408
- pythonnative-0.9.0/src/pythonnative/element.py +0 -53
- pythonnative-0.9.0/src/pythonnative/hooks.py +0 -440
- pythonnative-0.9.0/src/pythonnative/hot_reload.py +0 -143
- pythonnative-0.9.0/src/pythonnative/native_modules/__init__.py +0 -19
- pythonnative-0.9.0/src/pythonnative/native_modules/file_system.py +0 -131
- pythonnative-0.9.0/src/pythonnative/native_views/__init__.py +0 -87
- pythonnative-0.9.0/src/pythonnative/utils.py +0 -122
- {pythonnative-0.9.0 → pythonnative-0.10.0}/LICENSE +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/README.md +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/setup.cfg +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_components.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_element.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_hooks.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_ios_log.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_native_views.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_navigation.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_reconciler.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_smoke.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_style.py +0 -0
- {pythonnative-0.9.0 → pythonnative-0.10.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonnative
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Cross-platform native UI toolkit for Android and iOS
|
|
5
5
|
Author: Owen Carey
|
|
6
6
|
License: MIT License
|
|
@@ -48,6 +48,8 @@ Provides-Extra: docs
|
|
|
48
48
|
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
49
49
|
Requires-Dist: mkdocs-material[imaging]>=9.5; extra == "docs"
|
|
50
50
|
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
51
|
+
Requires-Dist: mkdocs-autorefs>=1.0; extra == "docs"
|
|
52
|
+
Requires-Dist: pymdown-extensions>=10.7; extra == "docs"
|
|
51
53
|
Provides-Extra: dev
|
|
52
54
|
Requires-Dist: black>=24.0; extra == "dev"
|
|
53
55
|
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonnative"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "Cross-platform native UI toolkit for Android and iOS"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Owen Carey" }
|
|
@@ -35,6 +35,8 @@ docs = [
|
|
|
35
35
|
"mkdocs>=1.5",
|
|
36
36
|
"mkdocs-material[imaging]>=9.5",
|
|
37
37
|
"mkdocstrings[python]>=0.24",
|
|
38
|
+
"mkdocs-autorefs>=1.0",
|
|
39
|
+
"pymdown-extensions>=10.7",
|
|
38
40
|
]
|
|
39
41
|
dev = [
|
|
40
42
|
"black>=24.0",
|
|
@@ -81,8 +83,32 @@ extend-exclude = [
|
|
|
81
83
|
]
|
|
82
84
|
|
|
83
85
|
[tool.ruff.lint]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
# Docstring (D) rules use the Google convention; see [tool.ruff.lint.pydocstyle].
|
|
87
|
+
# Selectively enabled so tests/examples/templates aren't penalized.
|
|
88
|
+
select = ["E", "F", "I", "D"]
|
|
89
|
+
# D107 (missing __init__ docstring): mkdocstrings is configured to merge
|
|
90
|
+
# __init__ into the class docstring, so the class docstring is the source of truth.
|
|
91
|
+
# D105 (magic method docstring): most are self-explanatory (__repr__, __eq__, etc.).
|
|
92
|
+
# D203/D213: conflict with Google convention defaults; ruff auto-suppresses
|
|
93
|
+
# these via `convention = "google"`, but we list them defensively.
|
|
94
|
+
ignore = ["D107", "D105", "D203", "D213"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.per-file-ignores]
|
|
97
|
+
"tests/**/*.py" = ["D"]
|
|
98
|
+
"examples/**/*.py" = ["D"]
|
|
99
|
+
"src/pythonnative/templates/**/*.py" = ["D"]
|
|
100
|
+
"setup.py" = ["D"]
|
|
101
|
+
"conftest.py" = ["D"]
|
|
102
|
+
# Platform handler subclasses implement the ViewHandler ABC defined in
|
|
103
|
+
# native_views/base.py, which carries the canonical docstrings for the
|
|
104
|
+
# protocol. The concrete classes are internal (registered by name in
|
|
105
|
+
# NativeViewRegistry, never imported by users), so requiring per-method
|
|
106
|
+
# docstrings would only add boilerplate that repeats the ABC.
|
|
107
|
+
"src/pythonnative/native_views/android.py" = ["D101", "D102"]
|
|
108
|
+
"src/pythonnative/native_views/ios.py" = ["D101", "D102"]
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.pydocstyle]
|
|
111
|
+
convention = "google"
|
|
86
112
|
|
|
87
113
|
[tool.black]
|
|
88
114
|
line-length = 120
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
"""PythonNative
|
|
1
|
+
"""PythonNative: declarative native UI for Android and iOS.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PythonNative is a cross-platform toolkit that turns Python ``@component``
|
|
4
|
+
functions into real, native Android and iOS views. The component model
|
|
5
|
+
is React-like (function components plus hooks), but rendering happens
|
|
6
|
+
through direct platform bindings: Chaquopy on Android (Java) and
|
|
7
|
+
rubicon-objc on iOS (Objective-C). There is no JavaScript bridge.
|
|
4
8
|
|
|
9
|
+
Key building blocks:
|
|
10
|
+
|
|
11
|
+
- **Element factories** ([`Text`][pythonnative.Text],
|
|
12
|
+
[`Button`][pythonnative.Button], [`Column`][pythonnative.Column], etc.)
|
|
13
|
+
return immutable [`Element`][pythonnative.Element] descriptors.
|
|
14
|
+
- **Hooks** ([`use_state`][pythonnative.use_state],
|
|
15
|
+
[`use_effect`][pythonnative.use_effect],
|
|
16
|
+
[`use_reducer`][pythonnative.use_reducer], etc.) manage state, side
|
|
17
|
+
effects, and context inside `@component` functions.
|
|
18
|
+
- **Navigation** is built from
|
|
19
|
+
[`NavigationContainer`][pythonnative.NavigationContainer] plus one of
|
|
20
|
+
the [`create_stack_navigator`][pythonnative.create_stack_navigator],
|
|
21
|
+
[`create_tab_navigator`][pythonnative.create_tab_navigator], or
|
|
22
|
+
[`create_drawer_navigator`][pythonnative.create_drawer_navigator]
|
|
23
|
+
factories.
|
|
24
|
+
- **Styling** uses a single ``style`` dict per element (or a list of
|
|
25
|
+
dicts), composable via [`StyleSheet`][pythonnative.StyleSheet].
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
5
29
|
import pythonnative as pn
|
|
6
30
|
|
|
7
31
|
@pn.component
|
|
@@ -12,9 +36,10 @@ Public API::
|
|
|
12
36
|
pn.Button("+", on_click=lambda: set_count(count + 1)),
|
|
13
37
|
style={"spacing": 12},
|
|
14
38
|
)
|
|
39
|
+
```
|
|
15
40
|
"""
|
|
16
41
|
|
|
17
|
-
__version__ = "0.
|
|
42
|
+
__version__ = "0.10.0"
|
|
18
43
|
|
|
19
44
|
from .components import (
|
|
20
45
|
ActivityIndicator,
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
"""Route Python
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Redirecting ``sys.stdout`` / ``sys.stderr`` at a Python level to write
|
|
1
|
+
"""Route Python `sys.stdout`/`sys.stderr` through fd 2 on iOS.
|
|
2
|
+
|
|
3
|
+
When an app is launched via `xcrun simctl launch --console-pty`
|
|
4
|
+
(what `pn run ios` does), the simulator attaches the caller's
|
|
5
|
+
terminal to the app's stderr, which is the same channel `NSLog` and
|
|
6
|
+
`os_log` write to. Python `print()` calls, however, go to
|
|
7
|
+
`sys.stdout` (fd 1), and for reasons specific to how CPython's
|
|
8
|
+
embedded framework is started on the iOS Simulator that descriptor
|
|
9
|
+
does not reach the attached console. As a result, users see
|
|
10
|
+
Swift-side `NSLog` output but never their own `print()` output.
|
|
11
|
+
|
|
12
|
+
Redirecting `sys.stdout` and `sys.stderr` at a Python level to write
|
|
15
13
|
straight to fd 2 is a small, reliable fix: fd 2 *is* visible to
|
|
16
|
-
|
|
14
|
+
`simctl` (that is exactly how `NSLog` reaches the terminal), so
|
|
17
15
|
Python output lands next to the Swift logs with correct ordering.
|
|
18
16
|
|
|
19
|
-
This module is intentionally self-contained
|
|
20
|
-
platform-specific C bindings required, so it
|
|
21
|
-
during
|
|
17
|
+
This module is intentionally self-contained (no rubicon-objc or
|
|
18
|
+
platform-specific C bindings required), so it is safe to import
|
|
19
|
+
early during `pythonnative` package initialization.
|
|
22
20
|
"""
|
|
23
21
|
|
|
24
22
|
from __future__ import annotations
|
|
@@ -33,8 +31,8 @@ _STDERR_FD = 2
|
|
|
33
31
|
class _StderrStream:
|
|
34
32
|
"""Minimal text-mode file-like that writes UTF-8 bytes to fd 2.
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Write-through (no buffering), so a `print()` call appears in the
|
|
35
|
+
terminal immediately. That matches user expectations for an
|
|
38
36
|
interactive "run on simulator" log stream.
|
|
39
37
|
"""
|
|
40
38
|
|
|
@@ -82,9 +80,9 @@ _installed = False
|
|
|
82
80
|
|
|
83
81
|
|
|
84
82
|
def install() -> None:
|
|
85
|
-
"""Swap
|
|
83
|
+
"""Swap `sys.stdout` and `sys.stderr` for fd-2 writers.
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
Idempotent: only the first call has effect.
|
|
88
86
|
"""
|
|
89
87
|
global _installed
|
|
90
88
|
if _installed:
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
"""`pn` CLI: scaffold, run, and clean PythonNative projects.
|
|
2
|
+
|
|
3
|
+
The console script `pn` (declared in `pyproject.toml` under
|
|
4
|
+
`[project.scripts]`) dispatches to one of three subcommands:
|
|
5
|
+
|
|
6
|
+
- `pn init [name]`: scaffold a new project in the current directory.
|
|
7
|
+
- `pn run android|ios`: stage code into a native template, build it,
|
|
8
|
+
install it, and stream logs back to the terminal.
|
|
9
|
+
- `pn clean`: remove the local `build/` directory.
|
|
10
|
+
|
|
11
|
+
The implementation here is intentionally side-effect heavy: it shells
|
|
12
|
+
out to `gradle`, `xcodebuild`, `adb`, and `xcrun simctl`. Errors from
|
|
13
|
+
those tools are usually surfaced inline so the developer sees the
|
|
14
|
+
underlying message.
|
|
15
|
+
"""
|
|
16
|
+
|
|
1
17
|
import argparse
|
|
2
18
|
import hashlib
|
|
3
19
|
import json
|
|
@@ -7,15 +23,25 @@ import shutil
|
|
|
7
23
|
import subprocess
|
|
8
24
|
import sys
|
|
9
25
|
import sysconfig
|
|
26
|
+
import time
|
|
10
27
|
import urllib.request
|
|
11
28
|
from importlib import resources
|
|
12
29
|
from typing import Any, Dict, List, Optional
|
|
13
30
|
|
|
14
31
|
|
|
15
32
|
def init_project(args: argparse.Namespace) -> None:
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
Creates `app
|
|
33
|
+
"""Scaffold a new PythonNative project in the current directory.
|
|
34
|
+
|
|
35
|
+
Creates `app/main_page.py`, `pythonnative.json`,
|
|
36
|
+
`requirements.txt`, and `.gitignore`. Refuses to overwrite
|
|
37
|
+
existing files unless `--force` is passed.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
args: The parsed argparse namespace. Recognized attributes:
|
|
41
|
+
|
|
42
|
+
- `name` (`str`, optional): Project name (defaults to the
|
|
43
|
+
current directory name).
|
|
44
|
+
- `force` (`bool`): Overwrite existing files.
|
|
19
45
|
"""
|
|
20
46
|
project_name: str = getattr(args, "name", None) or os.path.basename(os.getcwd())
|
|
21
47
|
cwd: str = os.getcwd()
|
|
@@ -88,16 +114,29 @@ def MainPage():
|
|
|
88
114
|
|
|
89
115
|
|
|
90
116
|
def _copy_dir(src: str, dst: str) -> None:
|
|
117
|
+
"""Recursively copy `src` into `dst`, creating parents as needed."""
|
|
91
118
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
92
119
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
93
120
|
|
|
94
121
|
|
|
95
122
|
def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
"""Copy a bundled template directory into `destination`.
|
|
124
|
+
|
|
125
|
+
Search order:
|
|
126
|
+
|
|
127
|
+
1. Local source checkout (`src/pythonnative/templates/<name>`).
|
|
128
|
+
2. Repository `templates/<name>` (used when running from a clone).
|
|
129
|
+
3. Installed package data via `importlib.resources`.
|
|
130
|
+
4. `sysconfig` data/site directories (last resort).
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
template_dir: The bundled template subdirectory to copy
|
|
134
|
+
(e.g., `"android_template"`).
|
|
135
|
+
destination: Parent directory; the template lands at
|
|
136
|
+
`<destination>/<template_dir>`.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
FileNotFoundError: If no bundled copy can be located.
|
|
101
140
|
"""
|
|
102
141
|
dest_path = os.path.join(destination, template_dir)
|
|
103
142
|
|
|
@@ -150,6 +189,11 @@ def _copy_bundled_template_dir(template_dir: str, destination: str) -> None:
|
|
|
150
189
|
|
|
151
190
|
|
|
152
191
|
def _github_json(url: str) -> Any:
|
|
192
|
+
"""Fetch a GitHub JSON endpoint, optionally authenticated.
|
|
193
|
+
|
|
194
|
+
Reads `GITHUB_TOKEN` or `GH_TOKEN` from the environment to raise
|
|
195
|
+
the unauthenticated rate limit.
|
|
196
|
+
"""
|
|
153
197
|
headers: dict[str, str] = {"User-Agent": "pythonnative-cli"}
|
|
154
198
|
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
155
199
|
if token:
|
|
@@ -162,10 +206,21 @@ def _github_json(url: str) -> Any:
|
|
|
162
206
|
def _resolve_python_apple_support_asset(
|
|
163
207
|
py_major_minor: str = "3.11", preferred_name: str = "Python-3.11-iOS-support.b7.tar.gz"
|
|
164
208
|
) -> Optional[str]:
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
Prefers an exact name match
|
|
168
|
-
asset whose name contains
|
|
209
|
+
"""Resolve a download URL for a `Python-Apple-support` release asset.
|
|
210
|
+
|
|
211
|
+
Prefers an exact name match for `preferred_name`; otherwise falls
|
|
212
|
+
back to the newest asset whose name contains
|
|
213
|
+
`Python-{py_major_minor}-iOS-support` and ends with `.tar.gz`.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
py_major_minor: Python version string used in the asset name
|
|
217
|
+
(e.g., `"3.11"`).
|
|
218
|
+
preferred_name: Exact filename to prefer when multiple matching
|
|
219
|
+
assets exist.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
A `browser_download_url` string, or `None` if the GitHub API
|
|
223
|
+
call fails or no matching asset is found.
|
|
169
224
|
"""
|
|
170
225
|
try:
|
|
171
226
|
releases = _github_json("https://api.github.com/repos/beeware/Python-Apple-support/releases?per_page=100")
|
|
@@ -188,29 +243,33 @@ def _resolve_python_apple_support_asset(
|
|
|
188
243
|
|
|
189
244
|
|
|
190
245
|
def create_android_project(project_name: str, destination: str) -> None:
|
|
191
|
-
"""
|
|
192
|
-
Create a new Android project using a template.
|
|
246
|
+
"""Stage the bundled Android template into `destination`.
|
|
193
247
|
|
|
194
|
-
:
|
|
195
|
-
|
|
248
|
+
Args:
|
|
249
|
+
project_name: Project name (currently informational; the
|
|
250
|
+
template uses fixed package IDs).
|
|
251
|
+
destination: Directory to receive the staged project.
|
|
196
252
|
"""
|
|
197
|
-
# Copy the Android template project directory
|
|
198
253
|
_copy_bundled_template_dir("android_template", destination)
|
|
199
254
|
|
|
200
255
|
|
|
201
256
|
def create_ios_project(project_name: str, destination: str) -> None:
|
|
202
|
-
"""
|
|
203
|
-
Create a new iOS project using a template.
|
|
257
|
+
"""Stage the bundled iOS template into `destination`.
|
|
204
258
|
|
|
205
|
-
:
|
|
206
|
-
|
|
259
|
+
Args:
|
|
260
|
+
project_name: Project name (currently informational; the
|
|
261
|
+
template uses fixed bundle IDs).
|
|
262
|
+
destination: Directory to receive the staged project.
|
|
207
263
|
"""
|
|
208
|
-
# Copy the iOS template project directory
|
|
209
264
|
_copy_bundled_template_dir("ios_template", destination)
|
|
210
265
|
|
|
211
266
|
|
|
212
267
|
def _read_project_config() -> dict:
|
|
213
|
-
"""Read pythonnative.json from the current working directory.
|
|
268
|
+
"""Read `pythonnative.json` from the current working directory.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The parsed config dict, or `{}` if the file is missing.
|
|
272
|
+
"""
|
|
214
273
|
config_path = os.path.join(os.getcwd(), "pythonnative.json")
|
|
215
274
|
if os.path.exists(config_path):
|
|
216
275
|
with open(config_path, encoding="utf-8") as f:
|
|
@@ -221,8 +280,15 @@ def _read_project_config() -> dict:
|
|
|
221
280
|
def _read_requirements(requirements_path: str) -> list[str]:
|
|
222
281
|
"""Read a requirements file and return non-empty, non-comment lines.
|
|
223
282
|
|
|
224
|
-
Exits with an error if pythonnative is listed
|
|
225
|
-
directly, so it must not be installed separately via pip
|
|
283
|
+
Exits with an error if `pythonnative` is listed: the CLI bundles
|
|
284
|
+
it directly, so it must not be installed separately via pip or
|
|
285
|
+
Chaquopy.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
requirements_path: Path to a `requirements.txt` file.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
A list of requirement specifier strings, in file order.
|
|
226
292
|
"""
|
|
227
293
|
if not os.path.exists(requirements_path):
|
|
228
294
|
return []
|
|
@@ -246,6 +312,9 @@ def _read_requirements(requirements_path: str) -> list[str]:
|
|
|
246
312
|
return result
|
|
247
313
|
|
|
248
314
|
|
|
315
|
+
ANDROID_PACKAGE_ID: str = "com.pythonnative.android_template"
|
|
316
|
+
HOT_RELOAD_DEV_ROOT: str = "pythonnative_dev"
|
|
317
|
+
|
|
249
318
|
ANDROID_LOGCAT_FILTERS: list[str] = [
|
|
250
319
|
"python.stdout:V",
|
|
251
320
|
"python.stderr:V",
|
|
@@ -264,10 +333,13 @@ IOS_BUNDLE_ID: str = "com.pythonnative.ios-template"
|
|
|
264
333
|
def _start_android_log_stream() -> Optional[subprocess.Popen]:
|
|
265
334
|
"""Clear logcat and stream Python-relevant log tags to the terminal.
|
|
266
335
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
336
|
+
Python's `print()` output reaches logcat via Chaquopy, which
|
|
337
|
+
redirects `sys.stdout`/`sys.stderr` to the `python.stdout` and
|
|
338
|
+
`python.stderr` tags.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
The `adb logcat` subprocess, or `None` when `adb` is
|
|
342
|
+
unavailable on `PATH`.
|
|
271
343
|
"""
|
|
272
344
|
try:
|
|
273
345
|
subprocess.run(["adb", "logcat", "-c"], check=False, capture_output=True)
|
|
@@ -283,7 +355,10 @@ def _start_android_log_stream() -> Optional[subprocess.Popen]:
|
|
|
283
355
|
|
|
284
356
|
|
|
285
357
|
def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
|
|
286
|
-
"""Politely stop a subprocess, escalating to SIGKILL if needed.
|
|
358
|
+
"""Politely stop a subprocess, escalating to `SIGKILL` if needed.
|
|
359
|
+
|
|
360
|
+
A no-op when `proc` is `None` or has already exited.
|
|
361
|
+
"""
|
|
287
362
|
if proc is None:
|
|
288
363
|
return
|
|
289
364
|
if proc.poll() is not None:
|
|
@@ -295,9 +370,141 @@ def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
|
|
|
295
370
|
proc.kill()
|
|
296
371
|
|
|
297
372
|
|
|
373
|
+
def _hot_reload_manifest_payload(
|
|
374
|
+
changed_files: List[str],
|
|
375
|
+
project_dir: str,
|
|
376
|
+
*,
|
|
377
|
+
version: Optional[str] = None,
|
|
378
|
+
) -> Dict[str, Any]:
|
|
379
|
+
"""Build the reload manifest consumed by the running app."""
|
|
380
|
+
from pythonnative.hot_reload import ModuleReloader
|
|
381
|
+
|
|
382
|
+
rel_files = sorted(os.path.relpath(path, project_dir) for path in changed_files)
|
|
383
|
+
return {
|
|
384
|
+
"version": version or str(time.time_ns()),
|
|
385
|
+
"files": rel_files,
|
|
386
|
+
"modules": ModuleReloader.modules_from_files(rel_files),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _write_hot_reload_manifest(changed_files: List[str], project_dir: str, build_dir: str) -> str:
|
|
391
|
+
"""Write a local hot-reload manifest and return its path."""
|
|
392
|
+
manifest_dir = os.path.join(build_dir, "hot_reload")
|
|
393
|
+
os.makedirs(manifest_dir, exist_ok=True)
|
|
394
|
+
manifest_path = os.path.join(manifest_dir, "reload.json")
|
|
395
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
396
|
+
json.dump(_hot_reload_manifest_payload(changed_files, project_dir), f)
|
|
397
|
+
return manifest_path
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _android_hot_reload_dest(rel_path: str) -> str:
|
|
401
|
+
"""Return a `run-as` relative destination for an app source file."""
|
|
402
|
+
return os.path.join("files", HOT_RELOAD_DEV_ROOT, rel_path)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _push_android_hot_reload_file(local_path: str, rel_path: str) -> bool:
|
|
406
|
+
"""Push one file into the Android app's writable hot-reload overlay."""
|
|
407
|
+
tmp_path = f"/data/local/tmp/pythonnative-hot-reload-{os.getpid()}-{os.path.basename(local_path)}"
|
|
408
|
+
dest_path = _android_hot_reload_dest(rel_path)
|
|
409
|
+
dest_dir = os.path.dirname(dest_path)
|
|
410
|
+
push = subprocess.run(["adb", "push", local_path, tmp_path], check=False, capture_output=True)
|
|
411
|
+
if push.returncode != 0:
|
|
412
|
+
return False
|
|
413
|
+
subprocess.run(
|
|
414
|
+
["adb", "shell", "run-as", ANDROID_PACKAGE_ID, "mkdir", "-p", dest_dir],
|
|
415
|
+
check=False,
|
|
416
|
+
capture_output=True,
|
|
417
|
+
)
|
|
418
|
+
copy = subprocess.run(
|
|
419
|
+
["adb", "shell", "run-as", ANDROID_PACKAGE_ID, "cp", tmp_path, dest_path],
|
|
420
|
+
check=False,
|
|
421
|
+
capture_output=True,
|
|
422
|
+
)
|
|
423
|
+
subprocess.run(["adb", "shell", "rm", "-f", tmp_path], check=False, capture_output=True)
|
|
424
|
+
return copy.returncode == 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _ios_data_container() -> Optional[str]:
|
|
428
|
+
"""Return the booted simulator's app data container, if available."""
|
|
429
|
+
try:
|
|
430
|
+
result = subprocess.run(
|
|
431
|
+
["xcrun", "simctl", "get_app_container", "booted", IOS_BUNDLE_ID, "data"],
|
|
432
|
+
check=False,
|
|
433
|
+
capture_output=True,
|
|
434
|
+
text=True,
|
|
435
|
+
)
|
|
436
|
+
except FileNotFoundError:
|
|
437
|
+
return None
|
|
438
|
+
if result.returncode != 0:
|
|
439
|
+
return None
|
|
440
|
+
container = result.stdout.strip()
|
|
441
|
+
return container or None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _push_ios_hot_reload_file(local_path: str, rel_path: str) -> bool:
|
|
445
|
+
"""Copy one file into the booted iOS Simulator's hot-reload overlay."""
|
|
446
|
+
container = _ios_data_container()
|
|
447
|
+
if container is None:
|
|
448
|
+
return False
|
|
449
|
+
dest_path = os.path.join(container, "Documents", HOT_RELOAD_DEV_ROOT, rel_path)
|
|
450
|
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
451
|
+
shutil.copy2(local_path, dest_path)
|
|
452
|
+
return True
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _clear_android_hot_reload_overlay() -> bool:
|
|
456
|
+
"""Remove stale Android hot-reload files before launching."""
|
|
457
|
+
result = subprocess.run(
|
|
458
|
+
["adb", "shell", "run-as", ANDROID_PACKAGE_ID, "rm", "-rf", f"files/{HOT_RELOAD_DEV_ROOT}"],
|
|
459
|
+
check=False,
|
|
460
|
+
capture_output=True,
|
|
461
|
+
)
|
|
462
|
+
return result.returncode == 0
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _clear_ios_hot_reload_overlay() -> bool:
|
|
466
|
+
"""Remove stale iOS Simulator hot-reload files before launching."""
|
|
467
|
+
container = _ios_data_container()
|
|
468
|
+
if container is None:
|
|
469
|
+
return False
|
|
470
|
+
shutil.rmtree(os.path.join(container, "Documents", HOT_RELOAD_DEV_ROOT), ignore_errors=True)
|
|
471
|
+
return True
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _clear_hot_reload_overlay(platform: str) -> bool:
|
|
475
|
+
"""Remove stale hot-reload overlay files for `platform`."""
|
|
476
|
+
if platform == "android":
|
|
477
|
+
return _clear_android_hot_reload_overlay()
|
|
478
|
+
if platform == "ios":
|
|
479
|
+
return _clear_ios_hot_reload_overlay()
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _push_hot_reload_file(platform: str, local_path: str, rel_path: str) -> bool:
|
|
484
|
+
"""Push a changed source file to the running app."""
|
|
485
|
+
if platform == "android":
|
|
486
|
+
return _push_android_hot_reload_file(local_path, rel_path)
|
|
487
|
+
if platform == "ios":
|
|
488
|
+
return _push_ios_hot_reload_file(local_path, rel_path)
|
|
489
|
+
return False
|
|
490
|
+
|
|
491
|
+
|
|
298
492
|
def run_project(args: argparse.Namespace) -> None:
|
|
299
|
-
"""
|
|
300
|
-
|
|
493
|
+
"""Build and run the project on the requested platform.
|
|
494
|
+
|
|
495
|
+
Stages templates, copies the user's `app/` into the platform
|
|
496
|
+
project, optionally installs Python requirements, and (unless
|
|
497
|
+
`--prepare-only` is set) builds and launches the app on a
|
|
498
|
+
connected device or simulator. With `--hot-reload`, also watches
|
|
499
|
+
`app/` for changes and pushes updates to the device.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
args: Parsed argparse namespace. Recognized attributes:
|
|
503
|
+
|
|
504
|
+
- `platform` (`"android"` | `"ios"`): Build target.
|
|
505
|
+
- `prepare_only` (`bool`): Stage files but skip the build.
|
|
506
|
+
- `hot_reload` (`bool`): Watch `app/` and push changes.
|
|
507
|
+
- `no_logs` (`bool`): Don't stream device logs after launch.
|
|
301
508
|
"""
|
|
302
509
|
# Determine the platform
|
|
303
510
|
platform: str = args.platform
|
|
@@ -407,6 +614,8 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
407
614
|
pass
|
|
408
615
|
subprocess.run(["./gradlew", "installDebug"], check=True, env=env)
|
|
409
616
|
|
|
617
|
+
_clear_hot_reload_overlay(platform)
|
|
618
|
+
|
|
410
619
|
# Run the Android app
|
|
411
620
|
# Assumes that the package name of your app is "com.example.myapp" and the main activity is "MainActivity"
|
|
412
621
|
# Replace "com.example.myapp" and ".MainActivity" with your actual package name and main activity
|
|
@@ -417,7 +626,7 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
417
626
|
"am",
|
|
418
627
|
"start",
|
|
419
628
|
"-n",
|
|
420
|
-
"
|
|
629
|
+
f"{ANDROID_PACKAGE_ID}/.MainActivity",
|
|
421
630
|
],
|
|
422
631
|
check=True,
|
|
423
632
|
)
|
|
@@ -708,6 +917,7 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
708
917
|
subprocess.run(["xcrun", "simctl", "boot", udid], check=False, capture_output=True)
|
|
709
918
|
# Install
|
|
710
919
|
subprocess.run(["xcrun", "simctl", "install", udid, app_path], check=False)
|
|
920
|
+
_clear_hot_reload_overlay(platform)
|
|
711
921
|
if show_logs and not hot_reload:
|
|
712
922
|
# Attach the app's stdout/stderr to this terminal so Python
|
|
713
923
|
# print() calls and exceptions are visible. SIMCTL_CHILD_*
|
|
@@ -754,25 +964,37 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
754
964
|
|
|
755
965
|
|
|
756
966
|
def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs: bool = True) -> None:
|
|
757
|
-
"""Watch
|
|
967
|
+
"""Watch `app/` for changes and push updated files to the device.
|
|
758
968
|
|
|
759
|
-
When
|
|
760
|
-
streamed in parallel so Python print
|
|
761
|
-
alongside hot-reload notifications.
|
|
969
|
+
When `show_logs` is true and targeting Android, `adb logcat` is
|
|
970
|
+
streamed in parallel so Python print and exception output stays
|
|
971
|
+
visible alongside hot-reload notifications.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
platform: Either `"android"` or `"ios"`.
|
|
975
|
+
project_dir: Absolute path to the user's project root.
|
|
976
|
+
build_dir: Absolute path to the staged build directory.
|
|
977
|
+
show_logs: Whether to stream device logs in parallel.
|
|
762
978
|
"""
|
|
763
|
-
from
|
|
979
|
+
from ..hot_reload import FileWatcher
|
|
764
980
|
|
|
765
981
|
app_dir = os.path.join(project_dir, "app")
|
|
766
982
|
|
|
767
983
|
def on_change(changed_files: List[str]) -> None:
|
|
984
|
+
pushed: List[str] = []
|
|
768
985
|
for fpath in changed_files:
|
|
769
986
|
rel = os.path.relpath(fpath, project_dir)
|
|
770
987
|
print(f"[hot-reload] Changed: {rel}")
|
|
771
|
-
if platform
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
988
|
+
if _push_hot_reload_file(platform, fpath, rel):
|
|
989
|
+
pushed.append(fpath)
|
|
990
|
+
else:
|
|
991
|
+
print(f"[hot-reload] Failed to push {rel}")
|
|
992
|
+
if pushed:
|
|
993
|
+
manifest = _write_hot_reload_manifest(pushed, project_dir, build_dir)
|
|
994
|
+
if _push_hot_reload_file(platform, manifest, "reload.json"):
|
|
995
|
+
print(f"[hot-reload] Signaled reload for {len(pushed)} file(s).")
|
|
996
|
+
else:
|
|
997
|
+
print("[hot-reload] Failed to signal reload; app will not refresh automatically.")
|
|
776
998
|
|
|
777
999
|
print("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop.")
|
|
778
1000
|
watcher = FileWatcher(app_dir, on_change, interval=1.0)
|
|
@@ -799,8 +1021,11 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs:
|
|
|
799
1021
|
|
|
800
1022
|
|
|
801
1023
|
def clean_project(args: argparse.Namespace) -> None:
|
|
802
|
-
"""
|
|
803
|
-
|
|
1024
|
+
"""Remove the local `build/` directory.
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
args: Parsed argparse namespace (unused; accepted for the
|
|
1028
|
+
`set_defaults(func=...)` dispatch shape).
|
|
804
1029
|
"""
|
|
805
1030
|
# Define the build directory
|
|
806
1031
|
build_dir: str = os.path.join(os.getcwd(), "build")
|
|
@@ -814,6 +1039,11 @@ def clean_project(args: argparse.Namespace) -> None:
|
|
|
814
1039
|
|
|
815
1040
|
|
|
816
1041
|
def main() -> None:
|
|
1042
|
+
"""Entry point for the `pn` console script.
|
|
1043
|
+
|
|
1044
|
+
Wires up the `init`, `run`, and `clean` subcommands and dispatches
|
|
1045
|
+
to the corresponding handler.
|
|
1046
|
+
"""
|
|
817
1047
|
parser = argparse.ArgumentParser(prog="pn", description="PythonNative CLI")
|
|
818
1048
|
subparsers = parser.add_subparsers()
|
|
819
1049
|
|