From 687820bbd19f7143e84d6b3eb14dedfc63ed4a00 Mon Sep 17 00:00:00 2001 From: Sam Evans Date: Mon, 6 Jul 2026 13:39:44 -0400 Subject: [PATCH 1/3] Refactor dependencies to make more optional Related to #1224; addresses user feedback asking to handle dependencies in a more elegant manner. Many previously-required dependencies are now optional. Also may speed up "import uxarray" slightly, as now-optional dependencies are moved to no longer be at top of files; instead, they are imported the first time any relevant method gets called. See upcoming associated PR for more notes / reasoning / discussion. --- pyproject.toml | 31 ++++++++++++++++--------------- uxarray/core/dataarray.py | 5 +++-- uxarray/cross_sections/sample.py | 3 ++- uxarray/grid/geometry.py | 2 -- uxarray/grid/grid.py | 8 ++++++-- uxarray/io/_healpix.py | 5 ++++- uxarray/plot/utils.py | 4 +--- uxarray/remap/weights.py | 8 +++++++- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 119c2bb0d..d51c1872b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,35 +22,36 @@ requires-python = ">=3.10" # minimal dependencies start dependencies = [ "antimeridian", - "cartopy", "dask[dataframe]", - "datashader", - "geoviews", - "holoviews", - "matplotlib", - "matplotlib-inline", "netcdf4", "numba", "numpy", "pandas", - "pyarrow", - "requests", "scikit-learn", "scipy", "shapely", - "spatialpandas", - "geopandas", "xarray", - "hvplot", - "healpix", + "hvplot", # TODO: move to viz after PR #1541 gets merged. "polars", - "pyproj" ] # minimal dependencies end [project.optional-dependencies] -complete = ["uxarray[dev]"] -dev = ['pathlib', 'pre_commit', 'pytest', 'pytest-cov', 'ruff', 'asv'] +all = ["uxarray[dev,geo,viz]"] +complete = ["uxarray[dev,geo,viz]"] # alias to "all" +dev = ['pre_commit', 'pytest', 'pytest-cov', 'ruff', 'asv'] +geo = [ + "geopandas", + "healpix", + "pyproj", + "spatialpandas", +] +viz = [ + "cartopy", + "geoviews", + "holoviews", + "matplotlib", +] [project.urls] Documentation = "https://uxarray.readthedocs.io/" diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py index a2b55a796..1150b161f 100644 --- a/uxarray/core/dataarray.py +++ b/uxarray/core/dataarray.py @@ -5,10 +5,8 @@ from typing import TYPE_CHECKING, Any, Hashable, Literal, Mapping, Optional from warnings import warn -import cartopy.crs as ccrs import numpy as np import xarray as xr -from cartopy.mpl.geoaxes import GeoAxes from xarray.core import dtypes from xarray.core.options import OPTIONS from xarray.core.utils import UncachedAccessor @@ -37,6 +35,9 @@ from uxarray.subset import DataArraySubsetAccessor if TYPE_CHECKING: + import cartopy.crs as ccrs + from cartopy.mpl.geoaxes import GeoAxes + from uxarray.core.dataset import UxDataset diff --git a/uxarray/cross_sections/sample.py b/uxarray/cross_sections/sample.py index 2b34b4e44..881e5dca4 100644 --- a/uxarray/cross_sections/sample.py +++ b/uxarray/cross_sections/sample.py @@ -1,6 +1,5 @@ import numpy as np from numba import njit, prange -from pyproj import Geod @njit(parallel=True) @@ -17,6 +16,8 @@ def _fill_numba(flat_orig, face_idx, n_face, n_steps): def sample_geodesic( start: tuple[float, float], end: tuple[float, float], steps: int ) -> tuple[np.ndarray, np.ndarray]: + from pyproj import Geod + lon0, lat0 = start lon1, lat1 = end diff --git a/uxarray/grid/geometry.py b/uxarray/grid/geometry.py index 6d54ec971..f1843096f 100644 --- a/uxarray/grid/geometry.py +++ b/uxarray/grid/geometry.py @@ -397,8 +397,6 @@ def _build_corrected_polygon_shells(polygon_shells): _corrected_shells_to_original_faces : np.ndarray Original indices used to map the corrected polygon shells to their entries in face nodes """ - - # import optional dependencies import antimeridian from shapely import Polygon diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index f4f4d96ff..ad75d9dfa 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import copy import os from html import escape -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence from warnings import warn -import cartopy.crs as ccrs import numpy as np import xarray as xr from xarray.core.options import OPTIONS @@ -99,6 +100,9 @@ from uxarray.plot.accessor import GridPlotAccessor from uxarray.subset import GridSubsetAccessor +if TYPE_CHECKING: + import cartopy.crs as ccrs + class Grid: """Represents a two-dimensional unstructured grid encoded following the diff --git a/uxarray/io/_healpix.py b/uxarray/io/_healpix.py index 21fe87f7c..3e6bdfef7 100644 --- a/uxarray/io/_healpix.py +++ b/uxarray/io/_healpix.py @@ -1,6 +1,5 @@ from typing import Any -import healpix as hp import numpy as np import polars as pl import xarray as xr @@ -68,6 +67,8 @@ def pix2corner_ang( ---- This will be updated when https://github.com/ntessore/healpix/issues/66 is implemented. """ + import healpix as hp + if nest: fu = hp._chp.nest2ang_uv else: @@ -102,6 +103,8 @@ def _pixels_to_ugrid(zoom, nest): A dataset containing pixel longitude and latitude coordinates along with related attributes. """ + import healpix as hp + ds = xr.Dataset() nside = hp.order2nside(zoom) diff --git a/uxarray/plot/utils.py b/uxarray/plot/utils.py index e97919d6c..83b032ce8 100644 --- a/uxarray/plot/utils.py +++ b/uxarray/plot/utils.py @@ -1,6 +1,3 @@ -import holoviews as hv - - class HoloviewsBackend: """Utility class to compare and set a HoloViews plotting backend for visualization.""" @@ -16,6 +13,7 @@ def assign(self, backend: str): backend : str Plotting backend to use, one of 'matplotlib', 'bokeh' """ + import holoviews as hv if self.matplotlib_backend is None: import matplotlib as mpl diff --git a/uxarray/remap/weights.py b/uxarray/remap/weights.py index 9b0c233bf..2c3f045f8 100644 --- a/uxarray/remap/weights.py +++ b/uxarray/remap/weights.py @@ -4,13 +4,17 @@ from dataclasses import dataclass from os import PathLike from pathlib import Path +from typing import TYPE_CHECKING import numpy as np import xarray as xr -from scipy import sparse +from numpy.random.mtrand import f from uxarray.core.utils import _open_dataset_with_fallback +if TYPE_CHECKING: + from scipy import sparse + # LRU-bounded cache for loaded remap operators. _WEIGHTS_CACHE_MAXSIZE = 32 _WEIGHTS_CACHE: "OrderedDict[tuple[str, int, int], RemapWeights]" = OrderedDict() @@ -83,6 +87,8 @@ class RemapWeights: @classmethod def from_file(cls, filename_or_obj: str | PathLike[str] | xr.Dataset): """Load a standard sparse remap-weight file into memory once.""" + from scipy import sparse + if isinstance(filename_or_obj, xr.Dataset): ds = filename_or_obj close_ds = False From 5507e8356158b499439f89fafb47cbe1fc3d933b Mon Sep 17 00:00:00 2001 From: Sam Evans Date: Mon, 6 Jul 2026 14:35:27 -0400 Subject: [PATCH 2/3] fix: forgot to pre-commit run --all-files (copilot suggestions hallucinated a strange import, this commit fixes that.) --- uxarray/remap/weights.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uxarray/remap/weights.py b/uxarray/remap/weights.py index 2c3f045f8..4fdee8cdd 100644 --- a/uxarray/remap/weights.py +++ b/uxarray/remap/weights.py @@ -8,7 +8,6 @@ import numpy as np import xarray as xr -from numpy.random.mtrand import f from uxarray.core.utils import _open_dataset_with_fallback From 3c174b2a9b96b5c6ab6611a0b50ef45a95c53162 Mon Sep 17 00:00:00 2001 From: Sam Evans Date: Mon, 6 Jul 2026 14:54:52 -0400 Subject: [PATCH 3/3] fix missing import GeoAxes; missing pooch Missed an import of GeoAxes, while refactoring to remove required dependencies. Added pooch dependency, which is necessary for test/io/test_structure.py tests to run, and previously was not included anywhere in dependencies. --- pyproject.toml | 9 ++++++++- test/io/test_structured.py | 3 +++ uxarray/core/dataarray.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d51c1872b..737e2d328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,14 @@ dependencies = [ [project.optional-dependencies] all = ["uxarray[dev,geo,viz]"] complete = ["uxarray[dev,geo,viz]"] # alias to "all" -dev = ['pre_commit', 'pytest', 'pytest-cov', 'ruff', 'asv'] +dev = [ + 'asv', + 'pooch', + 'pre_commit', + 'pytest', + 'pytest-cov', + 'ruff', +] geo = [ "geopandas", "healpix", diff --git a/test/io/test_structured.py b/test/io/test_structured.py index f53e3241f..2ecfbc652 100644 --- a/test/io/test_structured.py +++ b/test/io/test_structured.py @@ -2,6 +2,9 @@ import xarray as xr import pytest +# import pooch # not necessary to actually import, but tests would likely fail if this fails. +# (commented here for future reference, since pooch is included in uxarray[dev] +# dependencies, but not imported explicitly anywhere in uxarray.) @pytest.mark.parametrize("ds_name", ["air_temperature", "ersstv5"]) def test_read_structured_grid_from_ds(ds_name): diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py index 1150b161f..177f888bb 100644 --- a/uxarray/core/dataarray.py +++ b/uxarray/core/dataarray.py @@ -463,6 +463,8 @@ def to_raster( >>> ax.imshow(raster, origin="lower", extent=ax.get_xlim() + ax.get_ylim()) """ + from cartopy.mpl.geoaxes import GeoAxes + from uxarray.constants import INT_DTYPE from uxarray.plot.matplotlib import ( _ensure_dimensions,