pythonnative 0.10.0__py3-none-any.whl → 0.11.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -39,7 +39,7 @@ Example:
39
39
  ```
40
40
  """
41
41
 
42
- __version__ = "0.10.0"
42
+ __version__ = "0.11.0"
43
43
 
44
44
  from .components import (
45
45
  ActivityIndicator,
pythonnative/cli/pn.py CHANGED
@@ -354,6 +354,76 @@ def _start_android_log_stream() -> Optional[subprocess.Popen]:
354
354
  return proc
355
355
 
356
356
 
357
+ def _booted_ios_udid() -> Optional[str]:
358
+ """Return a booted iOS Simulator's UDID, or `None` if none is booted.
359
+
360
+ Used by `_start_ios_log_stream` so the hot-reload path doesn't
361
+ need to thread the UDID through from the install step.
362
+ """
363
+ try:
364
+ result = subprocess.run(
365
+ ["xcrun", "simctl", "list", "devices", "booted", "--json"],
366
+ check=False,
367
+ capture_output=True,
368
+ text=True,
369
+ )
370
+ except FileNotFoundError:
371
+ return None
372
+ try:
373
+ data = json.loads(result.stdout or "{}")
374
+ except json.JSONDecodeError:
375
+ return None
376
+ for _runtime, devices in (data.get("devices") or {}).items():
377
+ for device in devices or []:
378
+ if device.get("state") == "Booted":
379
+ udid = device.get("udid")
380
+ if udid:
381
+ return str(udid)
382
+ return None
383
+
384
+
385
+ def _start_ios_log_stream() -> Optional[subprocess.Popen]:
386
+ """Re-launch the iOS app with a console PTY so its stdio streams here.
387
+
388
+ Mirrors the approach `pn run ios` (without `--hot-reload`) takes:
389
+ ``xcrun simctl launch --console-pty`` attaches the parent
390
+ terminal to the app's stderr, which is where Python ``print()``
391
+ output is routed (see `pythonnative._ios_log`) and where Swift
392
+ ``NSLog`` calls land. Unlike ``log stream``, this *only*
393
+ surfaces what the app writes itself — none of UIKit's verbose
394
+ ``os_log`` chatter.
395
+
396
+ Returns:
397
+ The launched subprocess (output inherits the parent
398
+ terminal), or `None` when no simulator is booted or
399
+ `xcrun` is unavailable.
400
+ """
401
+ udid = _booted_ios_udid()
402
+ if udid is None:
403
+ print("Note: no booted iOS Simulator found; skipping log streaming.")
404
+ return None
405
+ sim_env = os.environ.copy()
406
+ sim_env["SIMCTL_CHILD_PYTHONUNBUFFERED"] = "1"
407
+ try:
408
+ proc = subprocess.Popen(
409
+ [
410
+ "xcrun",
411
+ "simctl",
412
+ "launch",
413
+ "--console-pty",
414
+ "--terminate-running-process",
415
+ udid,
416
+ IOS_BUNDLE_ID,
417
+ ],
418
+ env=sim_env,
419
+ )
420
+ except FileNotFoundError:
421
+ print("Note: 'xcrun' not found on PATH; skipping iOS log streaming.")
422
+ return None
423
+ print("Streaming iOS app logs from the simulator (Ctrl+C to stop)...")
424
+ return proc
425
+
426
+
357
427
  def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
358
428
  """Politely stop a subprocess, escalating to `SIGKILL` if needed.
359
429
 
@@ -947,14 +1017,15 @@ def run_project(args: argparse.Namespace) -> None:
947
1017
  capture_output=True,
948
1018
  )
949
1019
  print("Stopped log streaming.")
1020
+ elif hot_reload:
1021
+ # Skip launching here; ``_run_hot_reload`` will
1022
+ # spawn the app via ``simctl launch --console-pty``
1023
+ # so its ``print()`` / ``NSLog`` output streams to
1024
+ # the parent terminal alongside the file watcher.
1025
+ pass
950
1026
  else:
951
1027
  subprocess.run(["xcrun", "simctl", "launch", udid, IOS_BUNDLE_ID], check=False)
952
1028
  print("Launched iOS app on Simulator (best-effort).")
953
- if show_logs and hot_reload:
954
- print(
955
- "Note: live Python log streaming on iOS is disabled while --hot-reload is active; "
956
- "use Console.app or Xcode to view logs."
957
- )
958
1029
  except Exception:
959
1030
  print("Failed to auto-run on Simulator; open the project in Xcode to run.")
960
1031
 
@@ -1000,13 +1071,16 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs:
1000
1071
  watcher = FileWatcher(app_dir, on_change, interval=1.0)
1001
1072
  watcher.start()
1002
1073
 
1003
- logcat_proc: Optional[subprocess.Popen] = None
1004
- if show_logs and platform == "android":
1005
- logcat_proc = _start_android_log_stream()
1074
+ log_proc: Optional[subprocess.Popen] = None
1075
+ if show_logs:
1076
+ if platform == "android":
1077
+ log_proc = _start_android_log_stream()
1078
+ elif platform == "ios":
1079
+ log_proc = _start_ios_log_stream()
1006
1080
 
1007
1081
  try:
1008
- if logcat_proc is not None:
1009
- logcat_proc.wait()
1082
+ if log_proc is not None:
1083
+ log_proc.wait()
1010
1084
  else:
1011
1085
  import time
1012
1086
 
@@ -1015,7 +1089,7 @@ def _run_hot_reload(platform: str, project_dir: str, build_dir: str, show_logs:
1015
1089
  except KeyboardInterrupt:
1016
1090
  pass
1017
1091
  finally:
1018
- _terminate_subprocess(logcat_proc)
1092
+ _terminate_subprocess(log_proc)
1019
1093
  watcher.stop()
1020
1094
  print("\n[hot-reload] Stopped.")
1021
1095
 
@@ -287,7 +287,14 @@ def Spacer(
287
287
  """
288
288
  props: Dict[str, Any] = {}
289
289
  if size is not None:
290
+ # The layout engine sees ``width`` / ``height`` only, so a fixed
291
+ # ``size`` is mirrored on both axes. Whichever axis the parent
292
+ # container's ``flex_direction`` chooses as main becomes the
293
+ # actual gap; the cross axis is constrained by the parent's
294
+ # ``align_items`` (typically ``stretch``) anyway.
290
295
  props["size"] = size
296
+ props["width"] = size
297
+ props["height"] = size
291
298
  if flex is not None:
292
299
  props["flex"] = flex
293
300
  return Element("Spacer", props, [], key=key)