Source code for spine.utils.conditional

"""Lazy access to optional third-party dependencies.

The symbols exported here preserve the previous
``from spine.utils.conditional import torch`` style while avoiding eager imports
and unrelated warnings. Availability flags are cheap import-spec checks. The
actual package import is deferred until an attribute on the proxy is used.
"""

import importlib
import importlib.util
import os
import sys
from typing import TYPE_CHECKING, Any, Protocol, TypeGuard

__all__ = [
    "ROOT",
    "larcv",
    "torch",
    "ME",
    "MF",
    "ROOT_AVAILABLE",
    "LARCV_AVAILABLE",
    "TORCH_AVAILABLE",
    "ME_AVAILABLE",
    "SparseTensorLike",
]


[docs] class SparseTensorLike(Protocol): """Structural type for MinkowskiEngine sparse tensors. MinkowskiEngine is an optional runtime dependency exposed through a lazy proxy, so ``ME.SparseTensor`` cannot be used directly in type expressions. This protocol captures the sparse tensor surface used by SPINE without making static analysis depend on MinkowskiEngine stubs. """ dtype: Any device: Any F: Any C: Any coordinate_map_key: Any coordinate_manager: Any def __len__(self) -> int: ... def __getitem__(self, index: Any) -> Any: ...
[docs] def features_at(self, batch_id: int) -> Any: ...
[docs] def coordinates_at(self, batch_id: int) -> Any: ...
def is_sparse_tensor_like(obj: object) -> TypeGuard[SparseTensorLike]: """Check whether an object exposes the sparse tensor API SPINE uses.""" return ( hasattr(obj, "dtype") and hasattr(obj, "F") and hasattr(obj, "C") and hasattr(obj, "coordinate_map_key") and hasattr(obj, "coordinate_manager") ) class _MissingType: """Placeholder type used for annotations and simple availability checks.""" class _MissingNamespace: """Annotation-safe namespace for unavailable optional modules. This allows type-hint evaluation of chained names such as ``torch.utils.data.Dataset`` without importing the real dependency. Any apparent runtime use still raises the original import error when called. """ def __init__(self, error: str) -> None: self._error = error def __getattr__(self, name: str) -> Any: if name and name[0].isupper(): return _MissingType return self def __call__(self, *args: Any, **kwargs: Any) -> Any: raise ImportError(self._error) class _LazyModule: """Proxy that imports an optional module on first real use.""" def __init__(self, import_name: str, display_name: str, error: str) -> None: self._import_name = import_name self._display_name = display_name self._error = error self._module = None def _load(self) -> Any: if self._module is None: try: self._module = importlib.import_module(self._import_name) except ModuleNotFoundError as exc: raise ImportError(self._error) from exc return self._module def __getattr__(self, name: str) -> Any: return getattr(self._load(), name) def __repr__(self) -> str: if self._module is None: return f"<lazy module {self._display_name}>" return repr(self._module) class _LazyAttribute: """Proxy for an attribute imported from an optional module.""" def __init__( self, module_name: str, attr_name: str, display_name: str, error: str ) -> None: self._module_name = module_name self._attr_name = attr_name self._display_name = display_name self._error = error self._attr = None def _load(self) -> Any: if self._attr is None: try: module = importlib.import_module(self._module_name) except ModuleNotFoundError as exc: raise ImportError(self._error) from exc self._attr = getattr(module, self._attr_name) return self._attr def __getattr__(self, name: str) -> Any: return getattr(self._load(), name) def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._load()(*args, **kwargs) def __repr__(self) -> str: if self._attr is None: return f"<lazy attribute {self._display_name}>" return repr(self._attr) class _MissingTorch(_LazyModule): """Torch proxy with a Tensor placeholder when torch is unavailable.""" Tensor = _MissingType BoolTensor = _MissingType LongTensor = _MissingType FloatTensor = _MissingType def __getattr__(self, name: str) -> Any: if name and name[0].isupper(): return _MissingType return _MissingNamespace(self._error) class _MissingME(_LazyModule): """MinkowskiEngine proxy with a SparseTensor placeholder when unavailable.""" SparseTensor = _MissingType def __getattr__(self, name: str) -> Any: if name and name[0].isupper(): return _MissingType return _MissingNamespace(self._error) def _module_available(module_name: str) -> bool: if os.getenv("SPINE_DOC_BUILD") and module_name in { "torch", "MinkowskiEngine", "MinkowskiFunctional", "larcv", "ROOT", }: return False if module_name in sys.modules: module = sys.modules[module_name] if module is None: return False # Sphinx autodoc mock imports can populate `sys.modules` with # lightweight placeholders that do not correspond to a real importable # package. Only treat preloaded modules as available when they carry a # real module spec. if getattr(module, "__spec__", None) is not None: return True try: if importlib.util.find_spec(module_name) is not None: return True except (ImportError, ValueError): pass # PyROOT/LArCV can be importable even when importlib metadata discovery # fails in some HPC/container/module environments. For these modules only, # fall back to an actual import probe before declaring them unavailable. if module_name not in {"ROOT", "larcv"}: return False try: module = importlib.import_module(module_name) except ImportError: return False if module_name == "larcv": try: getattr(module, "larcv") except AttributeError: return False return True ROOT_AVAILABLE = _module_available("ROOT") LARCV_AVAILABLE = _module_available("larcv") TORCH_AVAILABLE = _module_available("torch") ME_AVAILABLE = _module_available("MinkowskiEngine") if TYPE_CHECKING: import MinkowskiEngine as ME import MinkowskiFunctional as MF import ROOT import torch from larcv import larcv else: ROOT = _LazyModule("ROOT", "ROOT", "ROOT is required to parse LArCV data.") larcv = _LazyAttribute( "larcv", "larcv", "larcv.larcv", "larcv is required to parse LArCV data." ) if TORCH_AVAILABLE: torch = _LazyModule("torch", "torch", "PyTorch is required.") else: torch = _MissingTorch("torch", "torch", "PyTorch is required.") if ME_AVAILABLE: ME = _LazyModule( "MinkowskiEngine", "MinkowskiEngine", "MinkowskiEngine is required." ) MF = _LazyModule( "MinkowskiFunctional", "MinkowskiFunctional", "MinkowskiFunctional is required.", ) else: ME = _MissingME( "MinkowskiEngine", "MinkowskiEngine", "MinkowskiEngine is required." ) MF = _LazyModule( "MinkowskiFunctional", "MinkowskiFunctional", "MinkowskiFunctional is required.", )