[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/andevsr/pythonnative/main/src/pythonnative/utils.py [Back]  [Original]

"""Platform detection and shared helpers.

This module is imported early by most other modules, so it avoids
importing platform-specific packages at module level. The detection
results are cached the first time
[`IS_ANDROID`][pythonnative.utils.IS_ANDROID] and
[`IS_IOS`][pythonnative.utils.IS_IOS] are read.

The Android variants also expose a small global registry for the
current `Activity` / `Context` and the screen's fragment container view;
both are populated by the bundled Android template before any
PythonNative code runs.

Attributes:
    IS_ANDROID: `True` when running inside an Android process (either
        because `ANDROID_*` env vars are present or because Chaquopy's
        `java` module imports successfully).
    IS_IOS: `True` when running inside an iOS app bundle (signaled by
        `PN_PLATFORM=ios`, `sys.platform == "ios"`, or a Simulator
        `HOME` path). Importing `rubicon-objc` alone is intentionally
        not enough to trigger this flag.
    IS_DESKTOP: `True` when running the desktop preview backend
        (signaled by `PN_PLATFORM=desktop`, set by ``pn preview``).
        This drives the Tkinter native-view registry so a PythonNative
        app can render in a real OS window for fast local iteration.
"""

import os
import sys
from typing import Any, Optional

# ======================================================================
# Platform detection
# ======================================================================

_is_android: Optional[bool] = None
_is_ios: Optional[bool] = None
_is_desktop: Optional[bool] = None


def _detect_android() -> bool:
    """Return whether we're running inside an Android process."""
    env = os.environ
    if "ANDROID_BOOTLOGO" in env or "ANDROID_ROOT" in env or "ANDROID_DATA" in env or "ANDROID_ARGUMENT" in env:
        return True
    try:
        from java import jclass  # noqa: F401

        return True
    except Exception:
        pass
    return False


def _detect_ios() -> bool:
    """Detect whether we're running inside an iOS app bundle.

    Signals, in priority order:

    - Explicit `PN_PLATFORM=ios` env var (set by the iOS template's
      `ViewController.swift` before Python starts). This is the
      canonical signal and survives even on hosts where `sys.platform`
      is generic `darwin`.
    - `sys.platform == "ios"` (CPython 3.13+ native iOS builds).
    - `/CoreSimulator/Devices/` in `$HOME` (iOS Simulator fallback if
      the template signal is missing for some reason).

    Crucially, having `rubicon-objc` importable is *not* enough:
    developers frequently install it on macOS via the `[ios]` extra,
    and treating that as iOS would cause subtle side effects
    (e.g., stdout redirection) on desktop machines.
    """
    if os.environ.get("PN_PLATFORM") == "ios":
        return True
    if sys.platform == "ios":
        return True
    home = os.environ.get("HOME", "")
    if "/CoreSimulator/Devices/" in home:
        return True
    return False


def _detect_desktop() -> bool:
    """Detect whether we're running the desktop (Tkinter) preview backend.

    The only signal is the explicit ``PN_PLATFORM=desktop`` env var,
    set by ``pn preview`` before importing PythonNative. Desktop is a
    *development* target: it renders the app in a native OS window via
    the pure-Python Tkinter registry so the inner dev loop doesn't
    require a device build. Off-device unit tests deliberately leave
    this flag ``False`` so they keep using an injected mock registry
    and ``Platform.OS == "test"``.
    """
    return os.environ.get("PN_PLATFORM") == "desktop"


def _ensure_platform_detection() -> None:
    """Populate `_is_android` / `_is_ios` / `_is_desktop` once, then reuse."""
    global _is_android, _is_ios, _is_desktop
    if _is_android is None:
        _is_android = _detect_android()
    if _is_ios is None:
        _is_ios = (not _is_android) and _detect_ios()
    if _is_desktop is None:
        _is_desktop = (not _is_android) and (not _is_ios) and _detect_desktop()


def _get_is_android() -> bool:
    """Return the cached Android-detection result."""
    _ensure_platform_detection()
    assert _is_android is not None
    return _is_android


def _get_is_ios() -> bool:
    """Return the cached iOS-detection result."""
    _ensure_platform_detection()
    assert _is_ios is not None
    return _is_ios


def _get_is_desktop() -> bool:
    """Return the cached desktop-detection result."""
    _ensure_platform_detection()
    assert _is_desktop is not None
    return _is_desktop


IS_ANDROID: bool = _get_is_android()
"""``True`` when running inside an Android process.

The flag is computed once at import time, by checking for `ANDROID_*`
environment variables and trying to import Chaquopy's `java` module.
"""

IS_IOS: bool = _get_is_ios()
"""``True`` when running inside an iOS app bundle.

The flag is computed once at import time, by checking
`PN_PLATFORM=ios`, `sys.platform == "ios"`, and the iOS Simulator
`HOME` path.
"""

IS_DESKTOP: bool = _get_is_desktop()
"""``True`` when running the desktop (Tkinter) preview backend.

Set by ``pn preview`` via ``PN_PLATFORM=desktop``. Mutually exclusive
with `IS_ANDROID` / `IS_IOS`. Off-device unit tests leave this
``False`` and inject a mock registry instead.
"""

# ======================================================================
# Android context management
# ======================================================================

_android_context: Any = None
_android_fragment_container: Any = None


def set_android_context(context: Any) -> None:
    """Record the current Android `Activity`/`Context`.

    Called by the bundled Android template before any view is created;
    most app code should not call this directly.

    Args:
        context: A Java `android.content.Context` (typically the
            current `Activity`).
    """
    global _android_context
    _android_context = context


def set_android_fragment_container(container_view: Any) -> None:
    """Record the current `Fragment` root container `ViewGroup`.

    Args:
        container_view: A Java `android.view.ViewGroup` that holds the
            screen's view tree.
    """
    global _android_fragment_container
    _android_fragment_container = container_view


def get_android_context() -> Any:
    """Return the current Android `Activity`/`Context`.

    Returns:
        The most recently recorded Android context.

    Raises:
        RuntimeError: If called on a non-Android platform, or before
            the template has registered a context.
    """
    if not IS_ANDROID:
        raise RuntimeError("get_android_context() called on non-Android platform")
    if _android_context is None:
        raise RuntimeError(
            "Android context not set. Ensure the screen host is initialized from an Activity before constructing views."
        )
    return _android_context


def get_android_fragment_container() -> Any:
    """Return the current `Fragment` container `ViewGroup`.

    Returns:
        The most recently recorded fragment container view.

    Raises:
        RuntimeError: If called on a non-Android platform, or before
            `ScreenFragment` has registered its container.
    """
    if not IS_ANDROID:
        raise RuntimeError("get_android_fragment_container() called on non-Android platform")
    if _android_fragment_container is None:
        raise RuntimeError(
            "Android fragment container not set. Ensure ScreenFragment has been created before set_root_view."
        )
    return _android_fragment_container

Web Proxy Viewer  |  New URL  |  Original Page