Source code for turbograph.backend.factory

"""Factory module for selecting and managing graph backends.

This module provides functions to:

- Check the availability of graph backends (``networkx`` and ``igraph``).
- Automatically determine the best available backend.
- Retrieve the appropriate backend wrapper class for graph operations.

"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Literal, NoReturn

if TYPE_CHECKING:  # pragma: no cover
    from .igraph_backend import IGraphWrapper
    from .networkx_backend import NetworkXWrapper

Backend = Literal["networkx", "igraph"]
"""Enumeration of supported graph backends."""

BACKENDS: tuple[Backend, ...] = ("networkx", "igraph")
"""Tuple of available graph backends.

The first available backend is selected as the default unless explicitly specified.
"""

backend_to_module: dict[Backend, str] = {
    "igraph": "igraph",
    "networkx": "networkx",
}
"""Mapping of backend names to their corresponding module names.

This mapping is used to dynamically check the availability of backend libraries.
"""


def _raise_invalid_backend(
    backend: Backend, origin: Exception | None = None
) -> NoReturn:
    """Raise an error for an invalid backend name.

    Args:
        backend: The name of the backend.
        origin: The original exception that triggered the error (if any).

    Raises:
        ValueError: Always.

    """
    msg = f"Backend {backend!r} is invalid. Valid backends are: " + ", ".join(
        backend_to_module.keys()
    )
    raise ValueError(msg) from origin


def _is_available(backend: Backend) -> bool:
    r"""Check if the specified backend is installed and available for use.

    Args:
        backend: The name of the backend (``"igraph"`` or ``"networkx"``).

    Returns:
        True if the backend's Python package is installed, False otherwise.

    Raises:
        ValueError: If an unrecognized backend name is provided.

    Example:
        >>> _is_available("igraph")
        True  # If 'igraph' is installed

    """
    import importlib.util

    try:
        module_name = backend_to_module[backend]
    except KeyError as error:
        _raise_invalid_backend(backend, error)

    return importlib.util.find_spec(module_name) is not None


[docs] def get_backend_auto() -> Backend: """Automatically select the best available graph backend. Returns: The first available backend from the predefined list (:py:data:`BACKENDS`). Raises: ImportError: If no supported backend is installed. Example: >>> get_backend_auto() 'networkx' # If 'networkx' is installed """ for backend in BACKENDS: if _is_available(backend): return backend msg = ( "No available graph backend. " "Please install either 'networkx' or 'igraph' library." ) raise ImportError(msg)
[docs] def get_graph_backend( backend: Literal["igraph", "networkx"] | None = None, ) -> type[NetworkXWrapper[Any] | IGraphWrapper[Any]]: """Retrieve the appropriate backend wrapper class for graph operations. Args: backend: The backend to use. If `None`, the best available backend is selected automatically using :py:func:`get_backend_auto`. Returns: The backend wrapper class. Raises: ValueError: If an unrecognized backend name is provided. ImportError: If no supported backend is installed. """ if backend is None: backend = get_backend_auto() if backend == "igraph": from .igraph_backend import IGraphWrapper return IGraphWrapper elif backend == "networkx": from .networkx_backend import NetworkXWrapper return NetworkXWrapper else: _raise_invalid_backend(backend)