Skip to content
41 changes: 41 additions & 0 deletions test/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Purpose: tests related to dependencies
e.g.: hvplot is optional, so it shouldn't be imported by default.
Related issues: #1224, #1539
"""
import subprocess
import sys


def _assert_not_imported_after_import_uxarray(module_name):
"""Run ``import uxarray`` in a fresh interpreter and assert that
``module_name`` was not imported as a side effect.

A subprocess is used so the check is immune to test ordering: other tests
in the same session may have already imported optional deps (e.g. plotting
tests import ``hvplot``), which would pollute this process's ``sys.modules``.
"""
code = (
"import sys; import uxarray; "
f"assert {module_name!r} not in sys.modules, "
f"'{module_name} was imported by `import uxarray`'"
)
result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"{module_name} should not be imported by default:\n{result.stderr}"
)


def test_hvplot_optional():
"""Test that hvplot is actually optional and not imported by default.
(hvplot in particular is "slow" to import (~1s to import hvplot.pandas),
so it should not be imported until actually plotting something.)
"""
_assert_not_imported_after_import_uxarray("hvplot")


# TODO: similar tests for cartopy, holoviews, and other optional deps.
39 changes: 36 additions & 3 deletions uxarray/plot/accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,41 @@
from uxarray.core.dataset import UxDataset
from uxarray.grid import Grid

import hvplot.pandas
import hvplot.xarray

from uxarray.plot.utils import backend as plotting_backend

# import speedup trick:
# code here uses obj.hvplot, which requires import hvplot.pandas and/or hvplot.xarray.
# But, those lines are "slow", so instead of doing them at top-level here,
# only run them when actually creating a plot accessor, inside the code later in this file.
# (timing test: import uxarray takes ~1.7 seconds before change; 0.7 seconds after change.)

_IMPORTED_HVPLOT = False


def _ensure_hvplot_imported() -> None:
"""Import hvplot.pandas and hvplot.xarray if not already imported.
(importing hvplot.pandas may output horizontal gray bar(s) in Jupyter, so only ever do it once.)
"""
global _IMPORTED_HVPLOT
if not _IMPORTED_HVPLOT:
# workaround for hvplot issue #1735;
# import hvplot.pandas and hvplot.xarray always adjust the hvplot.extension().
# To respect previously-setup extension value, need to remember and restore it.
# NB: the active backend lives on holoviews.Store (hvplot has no ``Store``),
# and we only restore a backend that was actually loaded -- on a fresh import
# Store.current_backend defaults to "matplotlib" while Store.registry is empty,
# and restoring that unloaded backend would break rendering.
from holoviews import Store as _store

_backend_orig = _store.current_backend
import hvplot.pandas
import hvplot.xarray

if _backend_orig in _store.registry:
_store.set_current_backend(_backend_orig)

_IMPORTED_HVPLOT = True


class GridPlotAccessor:
"""Plotting accessor for :class:`Grid`.
Expand All @@ -36,6 +66,7 @@ class GridPlotAccessor:
__slots__ = ("_uxgrid",)

def __init__(self, uxgrid: Grid) -> None:
_ensure_hvplot_imported()
self._uxgrid = uxgrid

def __call__(self, **kwargs) -> Any:
Expand Down Expand Up @@ -340,6 +371,7 @@ class UxDataArrayPlotAccessor:
__slots__ = ("_uxda",)

def __init__(self, uxda: UxDataArray) -> None:
_ensure_hvplot_imported()
self._uxda = uxda

def __call__(self, **kwargs) -> Any:
Expand Down Expand Up @@ -521,6 +553,7 @@ class UxDatasetPlotAccessor:
__slots__ = ("_uxds",)

def __init__(self, uxds: UxDataset) -> None:
_ensure_hvplot_imported()
self._uxds = uxds

def __call__(self, **kwargs) -> Any:
Expand Down
Loading