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

"""Platform-aware constants and selectors.

A small, RN-style helper for branching app code on the host platform.
The public surface is the [`Platform`][pythonnative.Platform] class
exposing ``OS``, ``Version``, ``is_ios``, ``is_android``, ``is_web``,
and ``select`` so user code can write ``Platform.select({"ios": ..., ...})``
without importing the ``IS_*`` flags directly.

Example:
    ```python
    import pythonnative as pn

    title_size = pn.Platform.select({"ios": 17, "android": 16, "default": 16})

    @pn.component
    def App():
        return pn.Text(
            f"Running on {pn.Platform.OS} {pn.Platform.Version}",
            style={"font_size": title_size},
        )
    ```
"""

from __future__ import annotations

import os
import sys
from typing import Any, Dict, Optional

from .utils import IS_ANDROID, IS_IOS, IS_WEB


def _detect_os() -> str:
    if IS_ANDROID:
        return "android"
    if IS_IOS:
        return "ios"
    if IS_WEB:
        return "web"
    return "test"


def _detect_version() -> str:
    """Return a human-readable platform version string.

    On device this asks the native ``Device`` module (``UIDevice
    .systemVersion`` / ``Build.VERSION.RELEASE``); the browser preview
    answers with the browser's user agent family. Off-device (the test
    environment), returns the host Python's version so user code can
    still introspect *something*.
    """
    if IS_IOS or IS_ANDROID or IS_WEB:
        try:
            from .native_modules.registry import native_module

            info = native_module("Device").call("info")
            if isinstance(info, dict) and info.get("os_version"):
                return str(info["os_version"])
        except Exception:
            pass
        sim_version = os.environ.get("SIMULATOR_RUNTIME_VERSION")
        if sim_version:
            return sim_version
    return f"python-{sys.version_info.major}.{sys.version_info.minor}"


class _LazyVersion:
    """Resolve ``Platform.Version`` on first access (it needs the bridge on device)."""

    _value: Optional[str] = None

    def __get__(self, obj: object, owner: Optional[type] = None) -> str:
        if _LazyVersion._value is None:
            _LazyVersion._value = _detect_version()
        return _LazyVersion._value


class Platform:
    """Platform-aware constants and the ``select`` dispatcher.

    All attributes are read at import time. ``OS`` is one of
    ``"ios"``, ``"android"``, ``"web"`` (the browser preview), or
    ``"test"`` (when running off-device, e.g., in unit tests).
    """

    OS: str = _detect_os()
    """``"ios"``, ``"android"``, ``"web"``, or ``"test"``."""

    Version: str = _LazyVersion()  # type: ignore[assignment]
    """Best-effort OS version string (``"17.4"``, ``"14"``, ``"python-3.11"``)."""

    is_ios: bool = IS_IOS
    """``True`` when running inside an iOS app bundle."""

    is_android: bool = IS_ANDROID
    """``True`` when running inside an Android process."""

    is_web: bool = IS_WEB
    """``True`` when running the browser preview (``pn preview``)."""

    is_test: bool = OS == "test"
    """``True`` when running off-device (no native runtime)."""

    @staticmethod
    def supports(name: str, member: str | None = None) -> bool:
        """Check whether a compiled native contract supports this platform.

        Args:
            name: Native component or service name, including installed plugins.
            member: Optional property, command, or service method to inspect.

        Returns:
            Whether the contract declares support. Hardware permission and
            availability are checked separately by the service when called.
        """
        from .sdk.schema import COMPONENTS, MODULES

        component = COMPONENTS.get(name)
        if component is not None:
            if Platform.OS not in component.platforms:
                return False
            if member is None:
                return True
            field = component.props.get(member)
            if field is not None:
                return Platform.OS in field.get("native", {}).get("platforms", component.platforms)
            return member in component.commands
        module = MODULES.get(name)
        if module is None or Platform.OS not in {"ios", "android", "web"}:
            return False
        if member is None:
            return any(
                Platform.OS in method.get("platforms", ("ios", "android", "web")) for method in module.methods.values()
            )
        method = module.methods.get(member)
        return method is not None and Platform.OS in method.get("platforms", ("ios", "android", "web"))

    @staticmethod
    def select(spec: Dict[str, Any], default: Any = None) -> Any:
        """Pick the value matching the current platform.

        Looks up ``spec[Platform.OS]``, then falls back to
        ``spec["native"]`` (matches iOS and Android, *not* the browser
        preview, which is a development surface), then to
        ``spec["default"]``, then to the explicit ``default`` argument.

        Args:
            spec: Mapping from platform name to value. Recognized keys:
                ``"ios"``, ``"android"``, ``"web"``, ``"test"``,
                ``"native"``, ``"default"``.
            default: Value returned when ``spec`` has no matching key
                and no ``"default"`` entry.

        Returns:
            The matching value, or ``default`` when nothing matches.

        Example:
            ```python
            font = pn.Platform.select(
                {"ios": "Helvetica", "android": "Roboto", "default": None}
            )
            ```
        """
        if Platform.OS in spec:
            return spec[Platform.OS]
        if (Platform.is_ios or Platform.is_android) and "native" in spec:
            return spec["native"]
        if "default" in spec:
            return spec["default"]
        return default


def get_platform() -> str:
    """Return the active platform name.

    Equivalent to reading ``Platform.OS``, exposed as a function for
    introspection from non-component code.
    """
    return Platform.OS


def _set_platform_for_test(name: Optional[str]) -> None:
    """Override ``Platform.OS`` for unit tests.

    Production code should not call this. Tests can pass ``"ios"``,
    ``"android"``, ``"web"``, ``"test"``, or ``None`` (to reset to
    autodetect).
    """
    if name is None:
        Platform.OS = _detect_os()
        Platform.is_ios = IS_IOS
        Platform.is_android = IS_ANDROID
        Platform.is_web = IS_WEB
        Platform.is_test = Platform.OS == "test"
        return
    Platform.OS = name
    Platform.is_ios = name == "ios"
    Platform.is_android = name == "android"
    Platform.is_web = name == "web"
    Platform.is_test = name == "test"

Web Proxy Viewer  |  New URL  |  Original Page