Source code for phenotypic.tools_.panel_

"""Shared Panel toolkit for interactive dashboards.

Centralises Panel availability checks, Jupyter environment detection,
and ``pn.extension()`` initialization so that every module using Panel
imports from one place instead of reimplementing these patterns.
"""

from __future__ import annotations

import importlib.util
import os

__all__ = [
    "PANEL_AVAILABLE",
    "PANEL_IMPORT_ERROR",
    "PLOTLY_AVAILABLE",
    "display_or_return",
    "ensure_panel_extension",
    "in_ipython",
    "in_jupyter",
    "require_panel",
]

# ---------------------------------------------------------------------------
# Availability
# ---------------------------------------------------------------------------

PANEL_AVAILABLE: bool = importlib.util.find_spec("panel") is not None
"""``True`` when the ``panel`` package is importable."""

PLOTLY_AVAILABLE: bool = importlib.util.find_spec("plotly") is not None
"""``True`` when the ``plotly`` package is importable."""

PANEL_IMPORT_ERROR: str = (
    "Panel is required for interactive dashboards. "
    "Install it with: pip install 'phenotypic[gui]'"
)
"""User-facing error message when Panel is missing."""

# ---------------------------------------------------------------------------
# Guards
# ---------------------------------------------------------------------------


[docs] def require_panel() -> None: """Raise :class:`ImportError` with an actionable message if Panel is missing. Raises: ImportError: If ``panel`` is not installed. """ if not PANEL_AVAILABLE: raise ImportError(PANEL_IMPORT_ERROR)
# --------------------------------------------------------------------------- # Environment detection # ---------------------------------------------------------------------------
[docs] def in_ipython() -> bool: """Detect whether code is running inside any IPython session. Returns: ``True`` when an IPython kernel or terminal is active. """ try: get_ipython() # type: ignore[name-defined] # noqa: F821 return True except NameError: return False
[docs] def in_jupyter() -> bool: """Detect whether code is running inside a Jupyter notebook. This is stricter than :func:`in_ipython` — it returns ``True`` only for notebook kernels (``ZMQInteractiveShell``), not plain IPython terminals. Returns: ``True`` when running in a Jupyter notebook kernel. """ try: from IPython import get_ipython as _get_ipython shell = _get_ipython() return shell is not None and shell.__class__.__name__ == "ZMQInteractiveShell" except ImportError: return False
# --------------------------------------------------------------------------- # Extension initialisation # --------------------------------------------------------------------------- _panel_initialized: bool = False def _detect_comms() -> str: """Pick the Panel ``comms`` transport for the current Jupyter host. PyCharm's embedded Jupyter server, VSCode notebooks, and Google Colab fail to deliver widget events under Panel's ``"default"`` comms and require ``"ipywidgets"``. Standard JupyterLab and the classic Notebook work correctly with ``"default"``. Returns: ``"ipywidgets"`` for PyCharm / VSCode / Colab, ``"default"`` otherwise. """ if "PYCHARM_HOSTED" in os.environ: return "ipywidgets" if "VSCODE_PID" in os.environ or "VSCODE_CWD" in os.environ: return "ipywidgets" try: import google.colab # type: ignore[import-not-found] # noqa: F401 return "ipywidgets" except ImportError: pass return "default"
[docs] def ensure_panel_extension(*extensions: str, **kwargs) -> None: """Initialise ``pn.extension()`` once, only when inside IPython. Safe to call multiple times — the extension is loaded at most once per process. Outside IPython the call is a silent no-op. When :mod:`plotly` is importable it is automatically included as an extension so Plotly figures render correctly in Panel layouts. The ``comms`` kwarg is auto-detected via :func:`_detect_comms` unless the caller supplies one explicitly. Args: *extensions: Extra Panel extensions to load (e.g. ``"tabulator"``). ``"plotly"`` is added automatically when Plotly is installed. **kwargs: Forwarded to ``pn.extension()`` on the first call (e.g. ``inline=True``). An explicit ``comms`` value wins over the auto-detected default. """ global _panel_initialized if _panel_initialized: return if not in_ipython(): return if not PANEL_AVAILABLE: return import panel as pn ext_set = list(dict.fromkeys(extensions)) if PLOTLY_AVAILABLE and "plotly" not in ext_set: ext_set.append("plotly") kwargs.setdefault("comms", _detect_comms()) pn.extension(*ext_set, **kwargs) _panel_initialized = True
# --------------------------------------------------------------------------- # Display helper # ---------------------------------------------------------------------------
[docs] def display_or_return(layout, *, show: bool = True): """Display a Panel layout in Jupyter or return it for programmatic use. When *show* is ``True`` and the session is a Jupyter notebook the layout is passed to ``IPython.display.display`` (after ensuring the Panel extension is loaded). Otherwise the layout is returned as-is. Args: layout: A Panel layout object (e.g. ``pn.Column``). show: If ``True``, attempt to display interactively. Returns: The *layout* object (always returned, even after display). """ if show and in_jupyter(): ensure_panel_extension() from IPython.display import display display(layout) return layout