From b54f9d29066446934b99fbe5acd60fdfb03ea1d7 Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:08:31 -0400 Subject: [PATCH 1/4] add _raise_hint_if_optional_deps_missing --- uxarray/core/dataarray.py | 3 ++ uxarray/cross_sections/sample.py | 3 ++ uxarray/errors.py | 6 +++ uxarray/grid/geometry.py | 8 +++ uxarray/grid/grid.py | 3 +- uxarray/io/_geopandas.py | 3 +- uxarray/io/_healpix.py | 3 ++ uxarray/plot/accessor.py | 4 ++ uxarray/plot/matplotlib.py | 2 + uxarray/plot/utils.py | 6 +++ uxarray/utils/imports.py | 90 ++++++++++++++++++++++++++++++++ 11 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 uxarray/utils/imports.py diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py index cf9419775..82db8ddb0 100644 --- a/uxarray/core/dataarray.py +++ b/uxarray/core/dataarray.py @@ -39,6 +39,7 @@ from uxarray.plot.accessor import UxDataArrayPlotAccessor from uxarray.remap.accessor import RemapAccessor from uxarray.subset import DataArraySubsetAccessor +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing if TYPE_CHECKING: import cartopy.crs as ccrs @@ -473,6 +474,7 @@ def to_raster( >>> ax.imshow(raster, origin="lower", extent=ax.get_xlim() + ax.get_ylim()) """ + _raise_hint_if_optional_deps_missing("cartopy") from cartopy.mpl.geoaxes import GeoAxes from uxarray.constants import INT_DTYPE @@ -517,6 +519,7 @@ def _is_default_extent() -> bool: if _is_default_extent(): try: + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs lon_min = float(self.uxgrid.node_lon.min(skipna=True).values) diff --git a/uxarray/cross_sections/sample.py b/uxarray/cross_sections/sample.py index 881e5dca4..37d1bd573 100644 --- a/uxarray/cross_sections/sample.py +++ b/uxarray/cross_sections/sample.py @@ -1,6 +1,8 @@ import numpy as np from numba import njit, prange +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing + @njit(parallel=True) def _fill_numba(flat_orig, face_idx, n_face, n_steps): @@ -16,6 +18,7 @@ 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]: + _raise_hint_if_optional_deps_missing("pyproj") from pyproj import Geod lon0, lat0 = start diff --git a/uxarray/errors.py b/uxarray/errors.py index 9e5cbab23..51b281405 100644 --- a/uxarray/errors.py +++ b/uxarray/errors.py @@ -38,3 +38,9 @@ class GridsMismatchError(ValueError): class YacNotAvailableError(RuntimeError): """Raised when the YAC backend is requested but unavailable.""" + + +# # # ----- Miscellaneous Errors ----- # # # + +class OptionalDependencyNotFoundError(ModuleNotFoundError): + """indicates functionality relies on a not-yet-installed optional dependency.""" diff --git a/uxarray/grid/geometry.py b/uxarray/grid/geometry.py index f1843096f..4c5cdfd10 100644 --- a/uxarray/grid/geometry.py +++ b/uxarray/grid/geometry.py @@ -15,6 +15,7 @@ ) from uxarray.grid.point_in_face import _face_contains_point from uxarray.grid.utils import _get_cartesian_face_edge_nodes +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing POLE_POINTS_XYZ = { "North": np.array([0.0, 0.0, 1.0]), @@ -116,6 +117,7 @@ def _build_polygon_shells( ): """Builds an array of polygon shells, which can be used with Shapely to construct polygons.""" + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs closed_face_nodes = _pad_closed_face_nodes( @@ -145,6 +147,7 @@ def _correct_central_longitude(node_lon, node_lat, projection): """Shifts the central longitude of an unstructured grid, which moves the antimeridian when visualizing, which is used when projections have a central longitude other than 0.0.""" + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs if projection: @@ -169,6 +172,7 @@ def _correct_central_longitude(node_lon, node_lat, projection): def _grid_to_polygon_geodataframe(grid, periodic_elements, projection, project, engine): """Converts the faces of a ``Grid`` into a ``spatialpandas.GeoDataFrame`` or ``geopandas.GeoDataFrame`` with a geometry column of polygons.""" + _raise_hint_if_optional_deps_missing("geopandas", "spatialpandas") import geopandas import shapely import spatialpandas @@ -260,6 +264,7 @@ def _build_geodataframe_without_antimeridian( """Builds a ``spatialpandas.GeoDataFrame`` or ``geopandas.GeoDataFrame``excluding any faces that cross the antimeridian.""" + _raise_hint_if_optional_deps_missing("geopandas", "spatialpandas") import geopandas import shapely import spatialpandas @@ -296,6 +301,7 @@ def _build_geodataframe_with_antimeridian( ): """Builds a ``spatialpandas.GeoDataFrame`` or ``geopandas.GeoDataFrame`` including any faces that cross the antimeridian.""" + _raise_hint_if_optional_deps_missing("geopandas", "spatialpandas") import geopandas import spatialpandas from spatialpandas.geometry import MultiPolygonArray @@ -441,6 +447,7 @@ def _grid_to_matplotlib_polycollection( grid, periodic_elements, projection=None, **kwargs ): """Constructs and returns a ``matplotlib.collections.PolyCollection``""" + _raise_hint_if_optional_deps_missing("cartopy", "matplotlib") import cartopy.crs as ccrs from matplotlib.collections import PolyCollection @@ -647,6 +654,7 @@ def _grid_to_matplotlib_linecollection( grid, periodic_elements, projection=None, **kwargs ): """Constructs and returns a ``matplotlib.collections.LineCollection``""" + _raise_hint_if_optional_deps_missing("cartopy", "matplotlib") import cartopy.crs as ccrs from matplotlib.collections import LineCollection diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 3c86890f2..ec3497468 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -100,6 +100,7 @@ from uxarray.io.utils import _parse_grid_type from uxarray.plot.accessor import GridPlotAccessor from uxarray.subset import GridSubsetAccessor +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing if TYPE_CHECKING: import cartopy.crs as ccrs @@ -2296,7 +2297,7 @@ def to_geodataframe( gdf : spatialpandas.GeoDataFrame or geopandas.GeoDataFrame The output ``GeoDataFrame`` with a filled out "geometry" column of polygons. """ - + _raise_hint_if_optional_deps_missing("spatialpandas") from spatialpandas import GeoDataFrame if engine not in ["spatialpandas", "geopandas"]: diff --git a/uxarray/io/_geopandas.py b/uxarray/io/_geopandas.py index a47606ac2..b2b1a5c73 100644 --- a/uxarray/io/_geopandas.py +++ b/uxarray/io/_geopandas.py @@ -3,6 +3,7 @@ from uxarray.constants import INT_DTYPE, INT_FILL_VALUE, WGS84_CRS from uxarray.conventions import ugrid +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing def _read_geodataframe(filepath, driver=None, **kwargs): @@ -63,7 +64,7 @@ def _gpd_read(filepath, driver=None, **kwargs): int Maximum number of nodes in a polygon/multipolygon. """ - + _raise_hint_if_optional_deps_missing("geopandas") import geopandas as gpd try: diff --git a/uxarray/io/_healpix.py b/uxarray/io/_healpix.py index 3e6bdfef7..38f4d155a 100644 --- a/uxarray/io/_healpix.py +++ b/uxarray/io/_healpix.py @@ -6,6 +6,7 @@ import uxarray.conventions.ugrid as ugrid from uxarray.constants import INT_DTYPE +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing def get_zoom_from_cells(cells): @@ -67,6 +68,7 @@ def pix2corner_ang( ---- This will be updated when https://github.com/ntessore/healpix/issues/66 is implemented. """ + _raise_hint_if_optional_deps_missing("healpix") import healpix as hp if nest: @@ -103,6 +105,7 @@ def _pixels_to_ugrid(zoom, nest): A dataset containing pixel longitude and latitude coordinates along with related attributes. """ + _raise_hint_if_optional_deps_missing("healpix") import healpix as hp ds = xr.Dataset() diff --git a/uxarray/plot/accessor.py b/uxarray/plot/accessor.py index 2f8bb56fd..24456570c 100644 --- a/uxarray/plot/accessor.py +++ b/uxarray/plot/accessor.py @@ -14,6 +14,7 @@ from uxarray.grid import Grid from uxarray.plot.utils import backend as plotting_backend +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing # import speedup trick: # code here uses obj.hvplot, which requires import hvplot.pandas and/or hvplot.xarray. @@ -30,6 +31,7 @@ def _ensure_hvplot_imported() -> None: """ global _IMPORTED_HVPLOT if not _IMPORTED_HVPLOT: + _raise_hint_if_optional_deps_missing("holoviews", "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. @@ -244,6 +246,7 @@ def edges( gdf.hvplot.paths : hvplot.paths A paths plot of the edges of the unstructured grid """ + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs plotting_backend.assign(backend) @@ -445,6 +448,7 @@ def polygons( gdf.hvplot.polygons : hvplot.polygons A shaded polygon plot """ + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs plotting_backend.assign(backend) diff --git a/uxarray/plot/matplotlib.py b/uxarray/plot/matplotlib.py index 186b7d65f..3a823572c 100644 --- a/uxarray/plot/matplotlib.py +++ b/uxarray/plot/matplotlib.py @@ -5,6 +5,7 @@ import numpy as np from uxarray.errors import DimensionError +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing if TYPE_CHECKING: from cartopy.mpl.geoaxes import GeoAxes @@ -126,6 +127,7 @@ def _get_points_from_axis(ax: GeoAxes, *, pixel_ratio: float = 1): ny : int Number of rows (height) in the pixel grid. """ + _raise_hint_if_optional_deps_missing("cartopy") import cartopy.crs as ccrs ax_attrs = _RasterAxAttrs.from_ax(ax, pixel_ratio=pixel_ratio) diff --git a/uxarray/plot/utils.py b/uxarray/plot/utils.py index 17275fe25..d4e9bc6b9 100644 --- a/uxarray/plot/utils.py +++ b/uxarray/plot/utils.py @@ -1,3 +1,6 @@ + +from uxarray.utils.imports import _raise_hint_if_optional_deps_missing + class HoloviewsBackend: """Compare and set the HoloViews plotting backend.""" @@ -6,6 +9,7 @@ def __init__(self): def assign(self, backend: str): """Assign a HoloViews backend, one of 'matplotlib', 'bokeh'.""" + _raise_hint_if_optional_deps_missing("holoviews") import holoviews as hv if backend not in ["bokeh", "matplotlib", None]: @@ -13,6 +17,7 @@ def assign(self, backend: str): f"Unsupported backend. Expected one of ['bokeh', 'matplotlib'], but received {backend}" ) if backend is not None and backend != hv.Store.current_backend: + _raise_hint_if_optional_deps_missing("matplotlib") import matplotlib as mpl # Capture the live backend now (not once at init) so a backend the @@ -63,6 +68,7 @@ def reset_mpl_backend(self): except Exception: pass + _raise_hint_if_optional_deps_missing("matplotlib") import matplotlib as mpl mpl.use(self.matplotlib_backend) diff --git a/uxarray/utils/imports.py b/uxarray/utils/imports.py new file mode 100644 index 000000000..79e887d95 --- /dev/null +++ b/uxarray/utils/imports.py @@ -0,0 +1,90 @@ +""" +Purpose: utils related to imports, e.g. handling optional dependency imports +""" +import importlib + +from uxarray.errors import OptionalDependencyNotFoundError + +# Mapping from optional dependency to corresponding extras, to help improve +# error messages in case of forgetting to install necessary optional deps. +# Hard-coded intentionally to avoid extra overhead of looking up package info, +# and to avoid any confusion if installed from wheels. +# Intentionally excluded "dev" because "dev" is mostly for tests. +_OPTIONAL_DEPS_TO_EXTRAS = { + "cartopy": ("geo", "viz"), + "geopandas": "geo", + "healpix": "geo", + "pyproj": "geo", + "spatialpandas": "geo", + "datashader": "viz", + "matplotlib": "viz", + "geoviews": "viz", + "holoviews": "viz", + "hvplot": "viz", +} + +def _raise_hint_if_optional_deps_missing(*packages: str): + """try to import these optional dependencies; raise helpful hint if any ModuleNotFoundError. + + packages: str + names of packages to try to import. + Must be keys in _OPTIONAL_DEPS_TO_EXTRAS, i.e. one or more of the following: + cartopy, geopandas, healpix, pyproj, spatialpandas, + datashader, matplotlib, geoviews, holoviews, hvplot + """ + # whitelist package names; crash if anything unexpected is provided. + _unknown = [pkg for pkg in packages if pkg not in _OPTIONAL_DEPS_TO_EXTRAS] + if len(_unknown) > 0: + raise ValueError( + f"Unrecognized package names in _raise_hint_if_optional_deps_missing(): {_unknown}. " + f"Recognized names are: {list(_OPTIONAL_DEPS_TO_EXTRAS.keys())}" + ) # (if this error occurs, it is almost certainly a bug in the UXarray codebase itself.) + + # note: want to provide one error covering all missing modules, to improve user experience. + # also note: if cartopy is one of the modules, need to be smart about error message. + missing = [] + last_err = None + for pkg in packages: + try: + importlib.import_module(pkg) + except ModuleNotFoundError as err: + missing.append(pkg) + last_err = err # will raise result from last_err to keep some error traceback info. + + if len(missing) == 0: + pass # nothing to do; all requested packages imported successfully! + else: # raise error with helpful message. + # Trying to be slightly smart with the message here, to be improve user experience: + # (1) if everything would be covered by one extra, suggest it. (E.g. holoviews & cartopy --> [viz]) + # (2) if everything would easily be covered by doing multiple extras, suggest them, + # and also mention [all] as an option. (E.g. healpix & holoviews --> [geo,viz] or [all]) + # (3) if just one missing package, with multiple extras, suggest "or" (E.g. just cartopy --> [geo] or [viz]) + # (4) in any other case, stop trying to be smart; just suggest [all]. + need_extras = set() + or_extras = [] + missing_extras = {pkg: _OPTIONAL_DEPS_TO_EXTRAS[pkg] for pkg in missing} + one_extra = {pkg: extra for pkg, extra in missing_extras.items() if isinstance(extra, str)} + many_extras = {pkg: extras for pkg, extras in missing_extras.items() if not isinstance(extras, str)} + assert all(len(extras) >= 2 for extras in many_extras.values()) # else wrong format in _OPTIONAL_DEPS_TO_EXTRAS. + for pkg, extra in one_extra.items(): + need_extras.add(extra) # definitely need to include all of these + for pkg, extras in many_extras.items(): + if any(extra in need_extras for extra in extras): # still maybe in case (1) or (2). + pass # this package is already covered by other needed extras! + elif len(extras) == 0: # case (3) + need_extras.add(extras[0]) + or_extras.extend(extras[1:]) + else: # case (4) + need_extras = set(["all"]) + break + need_extras_str = ",".join(sorted(need_extras)) # sort is just for style + errmsg = "Failed to import: " + ", ".join(sorted(missing)) + errmsg += f'.\nConsider running ``pip install "uxarray[{need_extras_str}]"``' + if len(need_extras) >= 2: + errmsg += ' or ``pip install "uxarray[all]"``' + elif len(or_extras) == 1: + errmsg += f' or ``pip install "uxarray[{or_extras[0]}]"``' + elif len(or_extras) >= 2: + errmsg += f' or pip install with any of {set(or_extras)} instead' + errmsg += ', then try again.' + raise OptionalDependencyNotFoundError(errmsg) from last_err From b3a58e83aa6ee59b5abebd469d4eef6ff76bfe90 Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:14:45 -0400 Subject: [PATCH 2/4] optional deps test ensure helpful hint gets raised --- test_optional_deps/test_installed_with_geo.py | 2 +- test_optional_deps/test_installed_with_no_opts.py | 4 ++-- test_optional_deps/test_installed_with_viz.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test_optional_deps/test_installed_with_geo.py b/test_optional_deps/test_installed_with_geo.py index 5895dbe26..7f7d01231 100644 --- a/test_optional_deps/test_installed_with_geo.py +++ b/test_optional_deps/test_installed_with_geo.py @@ -20,7 +20,7 @@ def test_check_requires_no_opts(): def test_check_requires_only_viz(): """ensure failure for checks which should require viz optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r"pip install uxarray\[viz\]"): check_requires_only_viz() diff --git a/test_optional_deps/test_installed_with_no_opts.py b/test_optional_deps/test_installed_with_no_opts.py index 4408078f6..0ba7ed5cd 100644 --- a/test_optional_deps/test_installed_with_no_opts.py +++ b/test_optional_deps/test_installed_with_no_opts.py @@ -20,13 +20,13 @@ def test_check_requires_no_opts(): def test_check_requires_only_viz(): """ensure failure for checks which should require viz optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r"pip install uxarray\[viz\]"): check_requires_only_viz() def test_check_requires_only_geo(): """ensure failure for checks which should require geo optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r"pip install uxarray\[geo\]"): check_requires_only_geo() diff --git a/test_optional_deps/test_installed_with_viz.py b/test_optional_deps/test_installed_with_viz.py index fd551169d..94d740ef5 100644 --- a/test_optional_deps/test_installed_with_viz.py +++ b/test_optional_deps/test_installed_with_viz.py @@ -25,7 +25,7 @@ def test_check_requires_only_viz(): def test_check_requires_only_geo(): """ensure failure for checks which should require geo optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r"pip install uxarray\[geo\]"): check_requires_only_geo() From 71036d1ed39b2a6eb642961e58a7b2e975a0cba9 Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:20:26 -0400 Subject: [PATCH 3/4] forgot pre-commit ruff formatting --- uxarray/errors.py | 1 + uxarray/plot/utils.py | 2 +- uxarray/utils/imports.py | 28 +++++++++++++++++++++------- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/uxarray/errors.py b/uxarray/errors.py index 51b281405..c1f3b5131 100644 --- a/uxarray/errors.py +++ b/uxarray/errors.py @@ -42,5 +42,6 @@ class YacNotAvailableError(RuntimeError): # # # ----- Miscellaneous Errors ----- # # # + class OptionalDependencyNotFoundError(ModuleNotFoundError): """indicates functionality relies on a not-yet-installed optional dependency.""" diff --git a/uxarray/plot/utils.py b/uxarray/plot/utils.py index d4e9bc6b9..c19ad5798 100644 --- a/uxarray/plot/utils.py +++ b/uxarray/plot/utils.py @@ -1,6 +1,6 @@ - from uxarray.utils.imports import _raise_hint_if_optional_deps_missing + class HoloviewsBackend: """Compare and set the HoloViews plotting backend.""" diff --git a/uxarray/utils/imports.py b/uxarray/utils/imports.py index 79e887d95..a93825155 100644 --- a/uxarray/utils/imports.py +++ b/uxarray/utils/imports.py @@ -1,6 +1,7 @@ """ Purpose: utils related to imports, e.g. handling optional dependency imports """ + import importlib from uxarray.errors import OptionalDependencyNotFoundError @@ -23,6 +24,7 @@ "hvplot": "viz", } + def _raise_hint_if_optional_deps_missing(*packages: str): """try to import these optional dependencies; raise helpful hint if any ModuleNotFoundError. @@ -53,7 +55,7 @@ def _raise_hint_if_optional_deps_missing(*packages: str): if len(missing) == 0: pass # nothing to do; all requested packages imported successfully! - else: # raise error with helpful message. + else: # raise error with helpful message. # Trying to be slightly smart with the message here, to be improve user experience: # (1) if everything would be covered by one extra, suggest it. (E.g. holoviews & cartopy --> [viz]) # (2) if everything would easily be covered by doing multiple extras, suggest them, @@ -63,13 +65,25 @@ def _raise_hint_if_optional_deps_missing(*packages: str): need_extras = set() or_extras = [] missing_extras = {pkg: _OPTIONAL_DEPS_TO_EXTRAS[pkg] for pkg in missing} - one_extra = {pkg: extra for pkg, extra in missing_extras.items() if isinstance(extra, str)} - many_extras = {pkg: extras for pkg, extras in missing_extras.items() if not isinstance(extras, str)} - assert all(len(extras) >= 2 for extras in many_extras.values()) # else wrong format in _OPTIONAL_DEPS_TO_EXTRAS. + one_extra = { + pkg: extra + for pkg, extra in missing_extras.items() + if isinstance(extra, str) + } + many_extras = { + pkg: extras + for pkg, extras in missing_extras.items() + if not isinstance(extras, str) + } + assert all( + len(extras) >= 2 for extras in many_extras.values() + ) # else wrong format in _OPTIONAL_DEPS_TO_EXTRAS. for pkg, extra in one_extra.items(): need_extras.add(extra) # definitely need to include all of these for pkg, extras in many_extras.items(): - if any(extra in need_extras for extra in extras): # still maybe in case (1) or (2). + if any( + extra in need_extras for extra in extras + ): # still maybe in case (1) or (2). pass # this package is already covered by other needed extras! elif len(extras) == 0: # case (3) need_extras.add(extras[0]) @@ -85,6 +99,6 @@ def _raise_hint_if_optional_deps_missing(*packages: str): elif len(or_extras) == 1: errmsg += f' or ``pip install "uxarray[{or_extras[0]}]"``' elif len(or_extras) >= 2: - errmsg += f' or pip install with any of {set(or_extras)} instead' - errmsg += ', then try again.' + errmsg += f" or pip install with any of {set(or_extras)} instead" + errmsg += ", then try again." raise OptionalDependencyNotFoundError(errmsg) from last_err From 1210ae0d0b364728daca80a88502a6003454aa43 Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:41:09 -0400 Subject: [PATCH 4/4] fix opt deps error hint tests typos also add string matching to test_check_requires_viz_and_geo in both the installed_with_geo and the installed_with_viz cases, and left a comment about why it wasn't added in the installed_with_no_opts case. --- test_optional_deps/test_installed_with_geo.py | 4 ++-- test_optional_deps/test_installed_with_no_opts.py | 6 ++++-- test_optional_deps/test_installed_with_viz.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test_optional_deps/test_installed_with_geo.py b/test_optional_deps/test_installed_with_geo.py index 7f7d01231..dba615af3 100644 --- a/test_optional_deps/test_installed_with_geo.py +++ b/test_optional_deps/test_installed_with_geo.py @@ -20,7 +20,7 @@ def test_check_requires_no_opts(): def test_check_requires_only_viz(): """ensure failure for checks which should require viz optional dependencies""" - with pytest.raises(ImportError, match=r"pip install uxarray\[viz\]"): + with pytest.raises(ImportError, match=r'pip install "uxarray\[viz\]"'): check_requires_only_viz() @@ -31,5 +31,5 @@ def test_check_requires_only_geo(): def test_check_requires_viz_and_geo(): """ensure failure for checks which should require both viz and geo optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r'pip install "uxarray\[viz\]"'): check_requires_viz_and_geo() diff --git a/test_optional_deps/test_installed_with_no_opts.py b/test_optional_deps/test_installed_with_no_opts.py index 0ba7ed5cd..e3cdf6b75 100644 --- a/test_optional_deps/test_installed_with_no_opts.py +++ b/test_optional_deps/test_installed_with_no_opts.py @@ -20,17 +20,19 @@ def test_check_requires_no_opts(): def test_check_requires_only_viz(): """ensure failure for checks which should require viz optional dependencies""" - with pytest.raises(ImportError, match=r"pip install uxarray\[viz\]"): + with pytest.raises(ImportError, match=r'pip install "uxarray\[viz\]"'): check_requires_only_viz() def test_check_requires_only_geo(): """ensure failure for checks which should require geo optional dependencies""" - with pytest.raises(ImportError, match=r"pip install uxarray\[geo\]"): + with pytest.raises(ImportError, match=r'pip install "uxarray\[geo\]"'): check_requires_only_geo() def test_check_requires_viz_and_geo(): """ensure failure for checks which should require both viz and geo optional dependencies""" with pytest.raises(ImportError): + # ^no match "uxarray[geo,viz]" here; might crash in a viz-only or a geo-only method, + # even though the check itself ultimately requires both viz and geo. check_requires_viz_and_geo() diff --git a/test_optional_deps/test_installed_with_viz.py b/test_optional_deps/test_installed_with_viz.py index 94d740ef5..4090f94b1 100644 --- a/test_optional_deps/test_installed_with_viz.py +++ b/test_optional_deps/test_installed_with_viz.py @@ -25,11 +25,11 @@ def test_check_requires_only_viz(): def test_check_requires_only_geo(): """ensure failure for checks which should require geo optional dependencies""" - with pytest.raises(ImportError, match=r"pip install uxarray\[geo\]"): + with pytest.raises(ImportError, match=r'pip install "uxarray\[geo\]"'): check_requires_only_geo() def test_check_requires_viz_and_geo(): """ensure failure for checks which should require both viz and geo optional dependencies""" - with pytest.raises(ImportError): + with pytest.raises(ImportError, match=r'pip install "uxarray\[geo\]"'): check_requires_viz_and_geo()