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

"""Imperative, awaitable system alerts.

Inspired by React Native's ``Alert.alert()`` but designed around
``async`` / ``await`` instead of per-button callbacks. There are three
entry points:

- [`Alert.show`][pythonnative.alerts.Alert.show]: fire-and-forget
  one-button notice (no return value).
- [`Alert.confirm`][pythonnative.alerts.Alert.confirm]: awaitable
  two-button yes/no, resolves to a ``bool``.
- [`Alert.choose`][pythonnative.alerts.Alert.choose]: awaitable
  multi-button picker / action sheet, resolves to the selected
  label (or ``None`` if dismissed).

Example:
    ```python
    import pythonnative as pn


    async def maybe_delete():
        if await pn.Alert.confirm(
            title="Delete item?",
            message="This action cannot be undone.",
            confirm_label="Delete",
            cancel_label="Keep",
        ):
            await delete_item()
    ```
"""

from __future__ import annotations

import asyncio
from typing import Any, Dict, List, Optional, Sequence

from .platform import Platform
from .runtime import resolve_future

# ======================================================================
# Internal dispatch helpers
# ======================================================================


def _dispatch_alert(
    *,
    title: str,
    message: Optional[str],
    buttons: List[Dict[str, Any]],
    style: str,
    on_result: Any,
) -> None:
    """Route an alert request to the active platform presenter.

    ``buttons`` is a list of ``{"label": str, "style":
    "default"|"cancel"|"destructive"}`` dicts. The presenter must
    invoke ``on_result(index)`` exactly once when the user picks a
    button, or ``on_result(-1)`` if the dialog is dismissed without a
    selection. ``on_result`` may run on any thread.
    """
    if Platform.is_ios:
        try:
            from .native_views.ios import _present_alert as _ios_present_alert

            _ios_present_alert(
                title=title,
                message=message,
                buttons=buttons,
                style=style,
                on_result=on_result,
            )
            return
        except Exception:
            on_result(-1)
            return

    if Platform.is_android:
        try:
            from .native_views.android import _present_alert as _android_present_alert

            _android_present_alert(
                title=title,
                message=message,
                buttons=buttons,
                style=style,
                on_result=on_result,
            )
            return
        except Exception:
            on_result(-1)
            return

    # Test backend: record the call so unit tests can assert on it,
    # then deliver the configured response.
    Alert._test_log.append(
        {
            "title": title,
            "message": message,
            "buttons": list(buttons),
            "style": style,
        }
    )
    response = Alert._next_test_response()
    on_result(response)


# ======================================================================
# Public Alert API
# ======================================================================


class Alert:
    """Imperative alert / action-sheet helper.

    All methods are static. Use [`show`][pythonnative.alerts.Alert.show]
    for a fire-and-forget single-button notice,
    [`confirm`][pythonnative.alerts.Alert.confirm] for an awaitable
    yes/no dialog, and
    [`choose`][pythonnative.alerts.Alert.choose] for a multi-option
    picker.
    """

    #: Records every alert call when running off-device. Tests reset
    #: this between cases via ``Alert._test_log.clear()``. Each entry
    #: contains ``title``, ``message``, ``buttons``, and ``style``.
    _test_log: List[Dict[str, Any]] = []

    #: Queue of indices to deliver to upcoming alerts in tests. Set via
    #: [`Alert.set_test_response`][pythonnative.alerts.Alert.set_test_response].
    #: A negative value (or empty queue) simulates a dismiss.
    _test_responses: List[int] = []

    @staticmethod
    def set_test_response(*indices: int) -> None:
        """Queue indices to return from upcoming test alerts.

        Use in async tests to script the user's choices: each pending
        call to [`confirm`][pythonnative.alerts.Alert.confirm] or
        [`choose`][pythonnative.alerts.Alert.choose] pops the next
        queued index. Pass ``-1`` to simulate a dismiss.

        Args:
            *indices: Sequence of button indices to deliver, oldest
                first. Calls beyond the queue length resolve to ``-1``.
        """
        Alert._test_responses[:] = list(indices)

    @staticmethod
    def _next_test_response() -> int:
        if Alert._test_responses:
            return Alert._test_responses.pop(0)
        return -1

    @staticmethod
    def show(
        title: str,
        message: Optional[str] = None,
        *,
        button: str = "OK",
    ) -> None:
        """Display a simple, one-button alert and return immediately.

        Args:
            title: Dialog title.
            message: Optional body text.
            button: Label for the single dismiss button (default
                ``"OK"``).

        This is fire-and-forget. To know what the user did, use
        [`confirm`][pythonnative.alerts.Alert.confirm] or
        [`choose`][pythonnative.alerts.Alert.choose] and ``await``
        the result.
        """
        _dispatch_alert(
            title=title,
            message=message,
            buttons=[{"label": button, "style": "default"}],
            ,
            on_result=lambda _idx: None,
        )

    @staticmethod
    async def confirm(
        title: str,
        message: Optional[str] = None,
        *,
        confirm_label: str = "OK",
        cancel_label: str = "Cancel",
    ) -> bool:
        """Present a two-button yes/no dialog and wait for the choice.

        Args:
            title: Dialog title.
            message: Optional body text.
            confirm_label: Label for the "yes" button (default
                ``"OK"``).
            cancel_label: Label for the "no" button (default
                ``"Cancel"``).

        Returns:
            ``True`` if the user pressed the confirm button, ``False``
            for the cancel button or a dismiss.

        Example:
            ```python
            if await pn.Alert.confirm("Save changes?"):
                await save()
            ```
        """
        loop = asyncio.get_running_loop()
        future: asyncio.Future[bool] = loop.create_future()

        def _on_result(index: int) -> None:
            resolve_future(future, index == 1)

        _dispatch_alert(
            title=title,
            message=message,
            buttons=[
                {"label": cancel_label, "style": "cancel"},
                {"label": confirm_label, "style": "default"},
            ],
            ,
            on_result=_on_result,
        )
        return await future

    @staticmethod
    async def choose(
        title: str,
        options: Sequence[str],
        *,
        message: Optional[str] = None,
        cancel_label: Optional[str] = None,
        style: str = "action_sheet",
        destructive_labels: Sequence[str] = (),
    ) -> Optional[str]:
        """Present a multi-option picker and wait for the user's choice.

        Args:
            title: Dialog title.
            options: Sequence of option labels (in display order).
            message: Optional body text.
            cancel_label: If provided, adds a "cancel" button with
                this label. Selecting it resolves to ``None``.
            style: ``"action_sheet"`` (default) for an iOS-style
                sheet, or ``"alert"`` for a stacked alert dialog.
            destructive_labels: Labels in ``options`` that should be
                styled destructively (red on iOS).

        Returns:
            The selected label, or ``None`` if the user dismissed or
            tapped the cancel button.

        Example:
            ```python
            choice = await pn.Alert.choose(
                "Photo source",
                options=["Camera", "Gallery"],
                cancel_label="Cancel",
            )
            if choice == "Camera":
                ...
            ```
        """
        if not options:
            raise ValueError("Alert.choose requires at least one option")

        loop = asyncio.get_running_loop()
        future: asyncio.Future[Optional[str]] = loop.create_future()

        destructive = set(destructive_labels)
        buttons: List[Dict[str, Any]] = [
            {
                "label": opt,
                "style": "destructive" if opt in destructive else "default",
            }
            for opt in options
        ]
        if cancel_label is not None:
            buttons.append({"label": cancel_label, "style": "cancel"})

        def _on_result(index: int) -> None:
            if 0 

Web Proxy Viewer  |  New URL  |  Original Page