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.
Files changed (96) hide show
  1. {pythonnative-0.7.0/src/pythonnative.egg-info → pythonnative-0.9.0}/PKG-INFO +1 -1
  2. {pythonnative-0.7.0 → pythonnative-0.9.0}/pyproject.toml +1 -1
  3. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/__init__.py +22 -1
  4. pythonnative-0.9.0/src/pythonnative/_ios_log.py +94 -0
  5. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/cli/pn.py +131 -11
  6. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/components.py +78 -21
  7. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/hooks.py +135 -29
  8. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/hot_reload.py +2 -2
  9. pythonnative-0.9.0/src/pythonnative/native_views/__init__.py +87 -0
  10. pythonnative-0.9.0/src/pythonnative/native_views/android.py +832 -0
  11. pythonnative-0.9.0/src/pythonnative/native_views/base.py +150 -0
  12. pythonnative-0.9.0/src/pythonnative/native_views/ios.py +777 -0
  13. pythonnative-0.9.0/src/pythonnative/navigation.py +571 -0
  14. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/page.py +77 -17
  15. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/reconciler.py +89 -1
  16. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +19 -25
  17. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/utils.py +40 -1
  18. {pythonnative-0.7.0 → pythonnative-0.9.0/src/pythonnative.egg-info}/PKG-INFO +1 -1
  19. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/SOURCES.txt +11 -2
  20. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_cli.py +28 -4
  21. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_components.py +86 -3
  22. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_hooks.py +265 -6
  23. pythonnative-0.9.0/tests/test_ios_log.py +147 -0
  24. pythonnative-0.9.0/tests/test_native_views.py +252 -0
  25. pythonnative-0.9.0/tests/test_navigation.py +846 -0
  26. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_reconciler.py +194 -1
  27. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_smoke.py +10 -0
  28. pythonnative-0.9.0/tests/test_utils.py +70 -0
  29. pythonnative-0.7.0/src/pythonnative/native_views.py +0 -1404
  30. {pythonnative-0.7.0 → pythonnative-0.9.0}/LICENSE +0 -0
  31. {pythonnative-0.7.0 → pythonnative-0.9.0}/README.md +0 -0
  32. {pythonnative-0.7.0 → pythonnative-0.9.0}/setup.cfg +0 -0
  33. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/cli/__init__.py +0 -0
  34. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/element.py +0 -0
  35. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/__init__.py +0 -0
  36. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/camera.py +0 -0
  37. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/file_system.py +0 -0
  38. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/location.py +0 -0
  39. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/native_modules/notifications.py +0 -0
  40. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/style.py +0 -0
  41. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
  42. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  43. {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
  44. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  45. {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
  46. {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
  47. {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
  48. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  49. {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
  50. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  51. {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
  52. {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
  53. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  54. {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
  55. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  56. {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
  57. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  58. {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
  59. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  60. {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
  61. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  62. {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
  63. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  64. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  65. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  66. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  67. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  68. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  69. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  70. {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
  71. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
  72. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  73. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  74. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  75. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  76. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  77. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  78. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  79. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  80. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  81. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  82. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  83. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  84. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  85. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  86. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  87. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  88. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  89. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  90. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  91. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  92. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  93. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/requires.txt +0 -0
  94. {pythonnative-0.7.0 → pythonnative-0.9.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  95. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_element.py +0 -0
  96. {pythonnative-0.7.0 → pythonnative-0.9.0}/tests/test_style.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.7.0"
7
+ version = "0.9.0"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
@@ -14,12 +14,13 @@ Public API::
14
14
  )
15
15
  """
16
16
 
17
- __version__ = "0.7.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
- subprocess.run(["xcrun", "simctl", "boot", udid], check=False)
645
- # Install and launch
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
- subprocess.run(["xcrun", "simctl", "launch", udid, "com.pythonnative.ios-template"], check=False)
648
- print("Launched iOS app on Simulator (best-effort).")
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
- def _run_hot_reload(platform: str, project_dir: str, build_dir: str) -> None:
658
- """Watch ``app/`` for changes and push updated files to the device."""
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
- import time
786
+ if logcat_proc is not None:
787
+ logcat_proc.wait()
788
+ else:
789
+ import time
678
790
 
679
- while True:
680
- time.sleep(1)
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, margin, min_width, max_width, min_height,
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
- Container-specific layout properties (Column / Row)::
15
+ Flex container properties (View / Column / Row)::
16
16
 
17
- spacing, padding, align_items, justify_content
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``, plus common layout props.
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``, plus common layout props.
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,