pythonnative 0.7.0__tar.gz → 0.9.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.7.0/src/pythonnative.egg-info → pythonnative-0.9.0}/PKG-INFO +1 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0}/pyproject.toml +1 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/__init__.py +22 -1
- pythonnative-0.9.0/src/pythonnative/_ios_log.py +94 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/cli/pn.py +131 -11
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/components.py +78 -21
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/hooks.py +135 -29
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/hot_reload.py +2 -2
- pythonnative-0.9.0/src/pythonnative/native_views/__init__.py +87 -0
- pythonnative-0.9.0/src/pythonnative/native_views/android.py +832 -0
- pythonnative-0.9.0/src/pythonnative/native_views/base.py +150 -0
- pythonnative-0.9.0/src/pythonnative/native_views/ios.py +777 -0
- pythonnative-0.9.0/src/pythonnative/navigation.py +571 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/page.py +77 -17
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/reconciler.py +89 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +19 -25
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/utils.py +40 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0/src/pythonnative.egg-info}/PKG-INFO +1 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/SOURCES.txt +11 -2
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_cli.py +28 -4
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_components.py +86 -3
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_hooks.py +265 -6
- pythonnative-0.9.0/tests/test_ios_log.py +147 -0
- pythonnative-0.9.0/tests/test_native_views.py +252 -0
- pythonnative-0.9.0/tests/test_navigation.py +846 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_reconciler.py +194 -1
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_smoke.py +10 -0
- pythonnative-0.9.0/tests/test_utils.py +70 -0
- pythonnative-0.7.0/src/pythonnative/native_views.py +0 -1404
- {pythonnative-0.7.0 → pythonnative-0.9.0}/LICENSE +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/README.md +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/setup.cfg +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/element.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/__init__.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/camera.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/file_system.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/location.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/notifications.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/style.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_element.py +0 -0
- {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_style.py +0 -0
|
@@ -14,12 +14,13 @@ Public API::
|
|
|
14
14
|
)
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "0.
|
|
17
|
+
__version__ = "0.9.0"
|
|
18
18
|
|
|
19
19
|
from .components import (
|
|
20
20
|
ActivityIndicator,
|
|
21
21
|
Button,
|
|
22
22
|
Column,
|
|
23
|
+
ErrorBoundary,
|
|
23
24
|
FlatList,
|
|
24
25
|
Image,
|
|
25
26
|
Modal,
|
|
@@ -39,6 +40,7 @@ from .components import (
|
|
|
39
40
|
from .element import Element
|
|
40
41
|
from .hooks import (
|
|
41
42
|
Provider,
|
|
43
|
+
batch_updates,
|
|
42
44
|
component,
|
|
43
45
|
create_context,
|
|
44
46
|
use_callback,
|
|
@@ -46,9 +48,18 @@ from .hooks import (
|
|
|
46
48
|
use_effect,
|
|
47
49
|
use_memo,
|
|
48
50
|
use_navigation,
|
|
51
|
+
use_reducer,
|
|
49
52
|
use_ref,
|
|
50
53
|
use_state,
|
|
51
54
|
)
|
|
55
|
+
from .navigation import (
|
|
56
|
+
NavigationContainer,
|
|
57
|
+
create_drawer_navigator,
|
|
58
|
+
create_stack_navigator,
|
|
59
|
+
create_tab_navigator,
|
|
60
|
+
use_focus_effect,
|
|
61
|
+
use_route,
|
|
62
|
+
)
|
|
52
63
|
from .page import create_page
|
|
53
64
|
from .style import StyleSheet, ThemeContext
|
|
54
65
|
|
|
@@ -57,6 +68,7 @@ __all__ = [
|
|
|
57
68
|
"ActivityIndicator",
|
|
58
69
|
"Button",
|
|
59
70
|
"Column",
|
|
71
|
+
"ErrorBoundary",
|
|
60
72
|
"FlatList",
|
|
61
73
|
"Image",
|
|
62
74
|
"Modal",
|
|
@@ -76,16 +88,25 @@ __all__ = [
|
|
|
76
88
|
"Element",
|
|
77
89
|
"create_page",
|
|
78
90
|
# Hooks
|
|
91
|
+
"batch_updates",
|
|
79
92
|
"component",
|
|
80
93
|
"create_context",
|
|
81
94
|
"use_callback",
|
|
82
95
|
"use_context",
|
|
83
96
|
"use_effect",
|
|
97
|
+
"use_focus_effect",
|
|
84
98
|
"use_memo",
|
|
85
99
|
"use_navigation",
|
|
100
|
+
"use_reducer",
|
|
86
101
|
"use_ref",
|
|
102
|
+
"use_route",
|
|
87
103
|
"use_state",
|
|
88
104
|
"Provider",
|
|
105
|
+
# Navigation
|
|
106
|
+
"NavigationContainer",
|
|
107
|
+
"create_drawer_navigator",
|
|
108
|
+
"create_stack_navigator",
|
|
109
|
+
"create_tab_navigator",
|
|
89
110
|
# Styling
|
|
90
111
|
"StyleSheet",
|
|
91
112
|
"ThemeContext",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Route Python ``sys.stdout``/``sys.stderr`` through fd 2 on iOS.
|
|
2
|
+
|
|
3
|
+
Why
|
|
4
|
+
---
|
|
5
|
+
When an app is launched via ``xcrun simctl launch --console-pty`` (what
|
|
6
|
+
``pn run ios`` does), the simulator attaches the caller's terminal to
|
|
7
|
+
the app's stderr, which is the same channel ``NSLog`` / ``os_log``
|
|
8
|
+
writes to. Python ``print()`` calls, however, go to ``sys.stdout``
|
|
9
|
+
(fd 1), and for reasons specific to how CPython's embedded framework
|
|
10
|
+
is started on the iOS Simulator that descriptor does not reach the
|
|
11
|
+
attached console. As a result users see Swift-side ``NSLog`` output
|
|
12
|
+
but never their own ``print()`` output.
|
|
13
|
+
|
|
14
|
+
Redirecting ``sys.stdout`` / ``sys.stderr`` at a Python level to write
|
|
15
|
+
straight to fd 2 is a small, reliable fix: fd 2 *is* visible to
|
|
16
|
+
``simctl`` (that's exactly how ``NSLog`` reaches the terminal), so
|
|
17
|
+
Python output lands next to the Swift logs with correct ordering.
|
|
18
|
+
|
|
19
|
+
This module is intentionally self-contained: no rubicon-objc or
|
|
20
|
+
platform-specific C bindings required, so it's safe to import early
|
|
21
|
+
during ``pythonnative`` package initialization.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Iterable
|
|
29
|
+
|
|
30
|
+
_STDERR_FD = 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _StderrStream:
|
|
34
|
+
"""Minimal text-mode file-like that writes UTF-8 bytes to fd 2.
|
|
35
|
+
|
|
36
|
+
It's write-through (no buffering) so a ``print()`` call appears in
|
|
37
|
+
the terminal immediately, which matches user expectations for an
|
|
38
|
+
interactive "run on simulator" log stream.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
encoding = "utf-8"
|
|
42
|
+
errors = "replace"
|
|
43
|
+
mode = "w"
|
|
44
|
+
name = "<stderr>"
|
|
45
|
+
|
|
46
|
+
def write(self, s: str) -> int:
|
|
47
|
+
if not s:
|
|
48
|
+
return 0
|
|
49
|
+
data = s.encode(self.encoding, self.errors)
|
|
50
|
+
try:
|
|
51
|
+
return os.write(_STDERR_FD, data)
|
|
52
|
+
except OSError:
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
def writelines(self, lines: Iterable[str]) -> None:
|
|
56
|
+
for line in lines:
|
|
57
|
+
self.write(line)
|
|
58
|
+
|
|
59
|
+
def flush(self) -> None:
|
|
60
|
+
# os.write is unbuffered; nothing to flush.
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def isatty(self) -> bool:
|
|
64
|
+
try:
|
|
65
|
+
return os.isatty(_STDERR_FD)
|
|
66
|
+
except OSError:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def fileno(self) -> int:
|
|
70
|
+
return _STDERR_FD
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
# Don't actually close fd 2.
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def closed(self) -> bool:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_installed = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def install() -> None:
|
|
85
|
+
"""Swap ``sys.stdout`` / ``sys.stderr`` for fd-2 writers.
|
|
86
|
+
|
|
87
|
+
Safe to call multiple times; only the first call has effect.
|
|
88
|
+
"""
|
|
89
|
+
global _installed
|
|
90
|
+
if _installed:
|
|
91
|
+
return
|
|
92
|
+
sys.stdout = _StderrStream()
|
|
93
|
+
sys.stderr = _StderrStream()
|
|
94
|
+
_installed = True
|
|
@@ -246,6 +246,55 @@ def _read_requirements(requirements_path: str) -> list[str]:
|
|
|
246
246
|
return result
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
ANDROID_LOGCAT_FILTERS: list[str] = [
|
|
250
|
+
"python.stdout:V",
|
|
251
|
+
"python.stderr:V",
|
|
252
|
+
"MainActivity:V",
|
|
253
|
+
"PageFragment:V",
|
|
254
|
+
"Navigator:V",
|
|
255
|
+
"PythonNative:V",
|
|
256
|
+
"AndroidRuntime:E",
|
|
257
|
+
"System.err:W",
|
|
258
|
+
"*:S",
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
IOS_BUNDLE_ID: str = "com.pythonnative.ios-template"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _start_android_log_stream() -> Optional[subprocess.Popen]:
|
|
265
|
+
"""Clear logcat and stream Python-relevant log tags to the terminal.
|
|
266
|
+
|
|
267
|
+
Returns the ``adb logcat`` subprocess, or ``None`` when ``adb`` is
|
|
268
|
+
unavailable. Python's ``print()`` output reaches logcat via Chaquopy,
|
|
269
|
+
which redirects ``sys.stdout``/``sys.stderr`` to the ``python.stdout``
|
|
270
|
+
and ``python.stderr`` tags.
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
subprocess.run(["adb", "logcat", "-c"], check=False, capture_output=True)
|
|
274
|
+
except FileNotFoundError:
|
|
275
|
+
print("Note: 'adb' not found on PATH; skipping log streaming.")
|
|
276
|
+
return None
|
|
277
|
+
try:
|
|
278
|
+
proc = subprocess.Popen(["adb", "logcat", *ANDROID_LOGCAT_FILTERS])
|
|
279
|
+
except FileNotFoundError:
|
|
280
|
+
return None
|
|
281
|
+
print("Streaming Python logs from device (Ctrl+C to stop)...")
|
|
282
|
+
return proc
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
|
|
286
|
+
"""Politely stop a subprocess, escalating to SIGKILL if needed."""
|
|
287
|
+
if proc is None:
|
|
288
|
+
return
|
|
289
|
+
if proc.poll() is not None:
|
|
290
|
+
return
|
|
291
|
+
proc.terminate()
|
|
292
|
+
try:
|
|
293
|
+
proc.wait(timeout=3)
|
|
294
|
+
except subprocess.TimeoutExpired:
|
|
295
|
+
proc.kill()
|
|
296
|
+
|
|
297
|
+
|
|
249
298
|
def run_project(args: argparse.Namespace) -> None:
|
|
250
299
|
"""
|
|
251
300
|
Run the specified project.
|
|
@@ -254,6 +303,7 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
254
303
|
platform: str = args.platform
|
|
255
304
|
prepare_only: bool = getattr(args, "prepare_only", False)
|
|
256
305
|
hot_reload: bool = getattr(args, "hot_reload", False)
|
|
306
|
+
show_logs: bool = not getattr(args, "no_logs", False)
|
|
257
307
|
|
|
258
308
|
# Read project configuration and save project root before any chdir
|
|
259
309
|
project_dir: str = os.getcwd()
|
|
@@ -371,6 +421,18 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
371
421
|
],
|
|
372
422
|
check=True,
|
|
373
423
|
)
|
|
424
|
+
|
|
425
|
+
# Stream Python logs from logcat unless the user opted out or requested
|
|
426
|
+
# hot-reload (hot-reload handles its own log tailing below).
|
|
427
|
+
if show_logs and not hot_reload:
|
|
428
|
+
logcat_proc = _start_android_log_stream()
|
|
429
|
+
if logcat_proc is not None:
|
|
430
|
+
try:
|
|
431
|
+
logcat_proc.wait()
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
print()
|
|
434
|
+
_terminate_subprocess(logcat_proc)
|
|
435
|
+
print("Stopped log streaming.")
|
|
374
436
|
elif platform == "ios":
|
|
375
437
|
# Attempt to build and run on iOS Simulator (best-effort)
|
|
376
438
|
ios_project_dir: str = os.path.join(build_dir, "ios_template")
|
|
@@ -640,22 +702,64 @@ def run_project(args: argparse.Namespace) -> None:
|
|
|
640
702
|
return
|
|
641
703
|
|
|
642
704
|
udid = preferred.get("udid")
|
|
643
|
-
# Boot (no-op if already booted)
|
|
644
|
-
|
|
645
|
-
#
|
|
705
|
+
# Boot (no-op if already booted). simctl returns non-zero and
|
|
706
|
+
# prints to stderr when the device is already Booted; we
|
|
707
|
+
# don't care about that case, so swallow its output.
|
|
708
|
+
subprocess.run(["xcrun", "simctl", "boot", udid], check=False, capture_output=True)
|
|
709
|
+
# Install
|
|
646
710
|
subprocess.run(["xcrun", "simctl", "install", udid, app_path], check=False)
|
|
647
|
-
|
|
648
|
-
|
|
711
|
+
if show_logs and not hot_reload:
|
|
712
|
+
# Attach the app's stdout/stderr to this terminal so Python
|
|
713
|
+
# print() calls and exceptions are visible. SIMCTL_CHILD_*
|
|
714
|
+
# env vars are forwarded to the launched process.
|
|
715
|
+
sim_env = os.environ.copy()
|
|
716
|
+
sim_env["SIMCTL_CHILD_PYTHONUNBUFFERED"] = "1"
|
|
717
|
+
print("Launched iOS app on Simulator. Streaming logs (Ctrl+C to stop)...")
|
|
718
|
+
try:
|
|
719
|
+
subprocess.run(
|
|
720
|
+
[
|
|
721
|
+
"xcrun",
|
|
722
|
+
"simctl",
|
|
723
|
+
"launch",
|
|
724
|
+
"--console-pty",
|
|
725
|
+
"--terminate-running-process",
|
|
726
|
+
udid,
|
|
727
|
+
IOS_BUNDLE_ID,
|
|
728
|
+
],
|
|
729
|
+
env=sim_env,
|
|
730
|
+
check=False,
|
|
731
|
+
)
|
|
732
|
+
except KeyboardInterrupt:
|
|
733
|
+
print()
|
|
734
|
+
subprocess.run(
|
|
735
|
+
["xcrun", "simctl", "terminate", udid, IOS_BUNDLE_ID],
|
|
736
|
+
check=False,
|
|
737
|
+
capture_output=True,
|
|
738
|
+
)
|
|
739
|
+
print("Stopped log streaming.")
|
|
740
|
+
else:
|
|
741
|
+
subprocess.run(["xcrun", "simctl", "launch", udid, IOS_BUNDLE_ID], check=False)
|
|
742
|
+
print("Launched iOS app on Simulator (best-effort).")
|
|
743
|
+
if show_logs and hot_reload:
|
|
744
|
+
print(
|
|
745
|
+
"Note: live Python log streaming on iOS is disabled while --hot-reload is active; "
|
|
746
|
+
"use Console.app or Xcode to view logs."
|
|
747
|
+
)
|
|
649
748
|
except Exception:
|
|
650
749
|
print("Failed to auto-run on Simulator; open the project in Xcode to run.")
|
|
651
750
|
|
|
652
751
|
# Hot-reload file watcher
|
|
653
752
|
if hot_reload and not prepare_only:
|
|
654
|
-
_run_hot_reload(platform, project_dir, build_dir)
|
|
753
|
+
_run_hot_reload(platform, project_dir, build_dir, show_logs=show_logs)
|
|
754
|
+
|
|
655
755
|
|
|
756
|
+
def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs: bool = True) -> None:
|
|
757
|
+
"""Watch ``app/`` for changes and push updated files to the device.
|
|
656
758
|
|
|
657
|
-
|
|
658
|
-
|
|
759
|
+
When ``show_logs`` is true and targeting Android, ``adb logcat`` is
|
|
760
|
+
streamed in parallel so Python print/exception output stays visible
|
|
761
|
+
alongside hot-reload notifications.
|
|
762
|
+
"""
|
|
659
763
|
from .hot_reload import FileWatcher
|
|
660
764
|
|
|
661
765
|
app_dir = os.path.join(project_dir, "app")
|
|
@@ -673,12 +777,23 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str) -> None:
|
|
|
673
777
|
print("[hot-reload] Watching app/ for changes. Press Ctrl+C to stop.")
|
|
674
778
|
watcher = FileWatcher(app_dir, on_change, interval=1.0)
|
|
675
779
|
watcher.start()
|
|
780
|
+
|
|
781
|
+
logcat_proc: Optional[subprocess.Popen] = None
|
|
782
|
+
if show_logs and platform == "android":
|
|
783
|
+
logcat_proc = _start_android_log_stream()
|
|
784
|
+
|
|
676
785
|
try:
|
|
677
|
-
|
|
786
|
+
if logcat_proc is not None:
|
|
787
|
+
logcat_proc.wait()
|
|
788
|
+
else:
|
|
789
|
+
import time
|
|
678
790
|
|
|
679
|
-
|
|
680
|
-
|
|
791
|
+
while True:
|
|
792
|
+
time.sleep(1)
|
|
681
793
|
except KeyboardInterrupt:
|
|
794
|
+
pass
|
|
795
|
+
finally:
|
|
796
|
+
_terminate_subprocess(logcat_proc)
|
|
682
797
|
watcher.stop()
|
|
683
798
|
print("\n[hot-reload] Stopped.")
|
|
684
799
|
|
|
@@ -721,6 +836,11 @@ def main() -> None:
|
|
|
721
836
|
action="store_true",
|
|
722
837
|
help="Watch app/ for changes and push updates to the running app",
|
|
723
838
|
)
|
|
839
|
+
parser_run.add_argument(
|
|
840
|
+
"--no-logs",
|
|
841
|
+
action="store_true",
|
|
842
|
+
help="Don't attach to the app's stdout/stderr after launching (default: stream logs)",
|
|
843
|
+
)
|
|
724
844
|
parser_run.set_defaults(func=run_project)
|
|
725
845
|
|
|
726
846
|
# Create a new command 'clean' that calls clean_project
|
|
@@ -9,12 +9,18 @@ which accepts a dict or a list of dicts (later entries override earlier).
|
|
|
9
9
|
|
|
10
10
|
Layout properties supported by all components::
|
|
11
11
|
|
|
12
|
-
width, height, flex,
|
|
13
|
-
max_height, align_self
|
|
12
|
+
width, height, flex, flex_grow, flex_shrink, margin,
|
|
13
|
+
min_width, max_width, min_height, max_height, align_self
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Flex container properties (View / Column / Row)::
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
flex_direction, justify_content, align_items, overflow,
|
|
18
|
+
spacing, padding
|
|
19
|
+
|
|
20
|
+
``View`` is the universal flex container (like React Native's ``View``).
|
|
21
|
+
It defaults to ``flex_direction: "column"``. ``Column`` and ``Row``
|
|
22
|
+
are convenience wrappers that fix the direction to ``"column"`` and
|
|
23
|
+
``"row"`` respectively.
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
26
|
from typing import Any, Callable, Dict, List, Optional
|
|
@@ -204,15 +210,48 @@ def Slider(
|
|
|
204
210
|
# ======================================================================
|
|
205
211
|
|
|
206
212
|
|
|
213
|
+
def View(
|
|
214
|
+
*children: Element,
|
|
215
|
+
style: StyleValue = None,
|
|
216
|
+
key: Optional[str] = None,
|
|
217
|
+
) -> Element:
|
|
218
|
+
"""Universal flex container (like React Native's ``View``).
|
|
219
|
+
|
|
220
|
+
Defaults to ``flex_direction: "column"``. Override via ``style``::
|
|
221
|
+
|
|
222
|
+
pn.View(child_a, child_b, style={"flex_direction": "row"})
|
|
223
|
+
|
|
224
|
+
Flex container properties (inside ``style``):
|
|
225
|
+
|
|
226
|
+
- ``flex_direction`` — ``"column"`` (default), ``"row"``,
|
|
227
|
+
``"column_reverse"``, ``"row_reverse"``
|
|
228
|
+
- ``justify_content`` — main-axis distribution:
|
|
229
|
+
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
230
|
+
``"space_between"``, ``"space_around"``, ``"space_evenly"``
|
|
231
|
+
- ``align_items`` — cross-axis alignment:
|
|
232
|
+
``"stretch"`` (default), ``"flex_start"``, ``"center"``,
|
|
233
|
+
``"flex_end"``
|
|
234
|
+
- ``overflow`` — ``"visible"`` (default) or ``"hidden"``
|
|
235
|
+
- ``spacing``, ``padding``, ``background_color``
|
|
236
|
+
"""
|
|
237
|
+
props: Dict[str, Any] = {"flex_direction": "column"}
|
|
238
|
+
props.update(resolve_style(style))
|
|
239
|
+
return Element("View", props, list(children), key=key)
|
|
240
|
+
|
|
241
|
+
|
|
207
242
|
def Column(
|
|
208
243
|
*children: Element,
|
|
209
244
|
style: StyleValue = None,
|
|
210
245
|
key: Optional[str] = None,
|
|
211
246
|
) -> Element:
|
|
212
|
-
"""Arrange children vertically.
|
|
247
|
+
"""Arrange children vertically (``flex_direction: "column"``).
|
|
248
|
+
|
|
249
|
+
Convenience wrapper around :func:`View`. The direction is fixed;
|
|
250
|
+
use :func:`View` directly if you need ``flex_direction: "row"``.
|
|
213
251
|
|
|
214
252
|
Style properties: ``spacing``, ``padding``, ``align_items``,
|
|
215
|
-
``justify_content``, ``background_color``,
|
|
253
|
+
``justify_content``, ``background_color``, ``overflow``,
|
|
254
|
+
plus common layout props.
|
|
216
255
|
|
|
217
256
|
``align_items`` controls cross-axis (horizontal) alignment:
|
|
218
257
|
``"stretch"`` (default), ``"flex_start"``/``"leading"``,
|
|
@@ -222,8 +261,9 @@ def Column(
|
|
|
222
261
|
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
223
262
|
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
|
|
224
263
|
"""
|
|
225
|
-
props: Dict[str, Any] = {}
|
|
264
|
+
props: Dict[str, Any] = {"flex_direction": "column"}
|
|
226
265
|
props.update(resolve_style(style))
|
|
266
|
+
props["flex_direction"] = "column"
|
|
227
267
|
return Element("Column", props, list(children), key=key)
|
|
228
268
|
|
|
229
269
|
|
|
@@ -232,10 +272,14 @@ def Row(
|
|
|
232
272
|
style: StyleValue = None,
|
|
233
273
|
key: Optional[str] = None,
|
|
234
274
|
) -> Element:
|
|
235
|
-
"""Arrange children horizontally.
|
|
275
|
+
"""Arrange children horizontally (``flex_direction: "row"``).
|
|
276
|
+
|
|
277
|
+
Convenience wrapper around :func:`View`. The direction is fixed;
|
|
278
|
+
use :func:`View` directly if you need ``flex_direction: "column"``.
|
|
236
279
|
|
|
237
280
|
Style properties: ``spacing``, ``padding``, ``align_items``,
|
|
238
|
-
``justify_content``, ``background_color``,
|
|
281
|
+
``justify_content``, ``background_color``, ``overflow``,
|
|
282
|
+
plus common layout props.
|
|
239
283
|
|
|
240
284
|
``align_items`` controls cross-axis (vertical) alignment:
|
|
241
285
|
``"stretch"`` (default), ``"flex_start"``/``"top"``,
|
|
@@ -245,8 +289,9 @@ def Row(
|
|
|
245
289
|
``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
|
|
246
290
|
``"space_between"``, ``"space_around"``, ``"space_evenly"``.
|
|
247
291
|
"""
|
|
248
|
-
props: Dict[str, Any] = {}
|
|
292
|
+
props: Dict[str, Any] = {"flex_direction": "row"}
|
|
249
293
|
props.update(resolve_style(style))
|
|
294
|
+
props["flex_direction"] = "row"
|
|
250
295
|
return Element("Row", props, list(children), key=key)
|
|
251
296
|
|
|
252
297
|
|
|
@@ -263,17 +308,6 @@ def ScrollView(
|
|
|
263
308
|
return Element("ScrollView", props, children, key=key)
|
|
264
309
|
|
|
265
310
|
|
|
266
|
-
def View(
|
|
267
|
-
*children: Element,
|
|
268
|
-
style: StyleValue = None,
|
|
269
|
-
key: Optional[str] = None,
|
|
270
|
-
) -> Element:
|
|
271
|
-
"""Generic container view (``UIView`` / ``android.view.View``)."""
|
|
272
|
-
props: Dict[str, Any] = {}
|
|
273
|
-
props.update(resolve_style(style))
|
|
274
|
-
return Element("View", props, list(children), key=key)
|
|
275
|
-
|
|
276
|
-
|
|
277
311
|
def SafeAreaView(
|
|
278
312
|
*children: Element,
|
|
279
313
|
style: StyleValue = None,
|
|
@@ -323,6 +357,29 @@ def Pressable(
|
|
|
323
357
|
return Element("Pressable", props, children, key=key)
|
|
324
358
|
|
|
325
359
|
|
|
360
|
+
def ErrorBoundary(
|
|
361
|
+
child: Optional[Element] = None,
|
|
362
|
+
*,
|
|
363
|
+
fallback: Optional[Any] = None,
|
|
364
|
+
key: Optional[str] = None,
|
|
365
|
+
) -> Element:
|
|
366
|
+
"""Catch render errors in *child* and display *fallback* instead.
|
|
367
|
+
|
|
368
|
+
*fallback* may be an ``Element`` or a callable that receives the
|
|
369
|
+
exception and returns an ``Element``::
|
|
370
|
+
|
|
371
|
+
pn.ErrorBoundary(
|
|
372
|
+
MyRiskyComponent(),
|
|
373
|
+
fallback=lambda err: pn.Text(f"Error: {err}"),
|
|
374
|
+
)
|
|
375
|
+
"""
|
|
376
|
+
props: Dict[str, Any] = {}
|
|
377
|
+
if fallback is not None:
|
|
378
|
+
props["__fallback__"] = fallback
|
|
379
|
+
children = [child] if child is not None else []
|
|
380
|
+
return Element("__ErrorBoundary__", props, children, key=key)
|
|
381
|
+
|
|
382
|
+
|
|
326
383
|
def FlatList(
|
|
327
384
|
*,
|
|
328
385
|
data: Optional[List[Any]] = None,
|