Skip to content

Commit 9bd51ed

Browse files
authored
Merge branch 'main' into rajeeja/zonal-mean-analytic-band
2 parents f901473 + 460eb23 commit 9bd51ed

File tree

6 files changed

+100
-11
lines changed

6 files changed

+100
-11
lines changed

test/core/test_api.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
import uxarray as ux
33
import numpy as np
44
import pytest
5+
import tempfile
6+
import xarray as xr
7+
from unittest.mock import patch
8+
from uxarray.core.utils import _open_dataset_with_fallback
9+
import os
510

611
def test_open_geoflow_dataset(gridpath, datasetpath):
712
"""Loads a single dataset with its grid topology file using uxarray's
@@ -111,4 +116,46 @@ def test_open_dataset_grid_kwargs(gridpath, datasetpath):
111116
gridpath("ugrid", "outCSne30", "outCSne30.ug"),
112117
datasetpath("ugrid", "outCSne30", "outCSne30_var2.nc"),
113118
grid_kwargs={"drop_variables": "Mesh2_face_nodes"}
114-
)
119+
)
120+
121+
122+
def test_open_dataset_with_fallback():
123+
"""Test that the fallback mechanism works when the default engine fails."""
124+
125+
tmp_path = ""
126+
ds = None
127+
ds_fallback = None
128+
try:
129+
# Create a simple test dataset
130+
with tempfile.NamedTemporaryFile(suffix='.nc', delete=False) as tmp:
131+
data = xr.Dataset({'temp': (['x', 'y'], np.random.rand(5, 5))})
132+
data.to_netcdf(tmp.name)
133+
tmp_path = tmp.name
134+
135+
# Test normal case
136+
ds = _open_dataset_with_fallback(tmp_path)
137+
assert isinstance(ds, xr.Dataset)
138+
assert 'temp' in ds.data_vars
139+
140+
# Test fallback mechanism with mocked failure
141+
original_open = xr.open_dataset
142+
call_count = 0
143+
def mock_open_dataset(*args, **kwargs):
144+
nonlocal call_count
145+
call_count += 1
146+
if call_count == 1 and 'engine' not in kwargs:
147+
raise Exception("Simulated engine failure")
148+
return original_open(*args, **kwargs)
149+
150+
with patch('uxarray.core.utils.xr.open_dataset', side_effect=mock_open_dataset):
151+
ds_fallback = _open_dataset_with_fallback(tmp_path)
152+
assert isinstance(ds_fallback, xr.Dataset)
153+
assert call_count == 2 # First failed, second succeeded
154+
155+
finally:
156+
if ds is not None:
157+
ds.close()
158+
if ds_fallback is not None:
159+
ds_fallback.close()
160+
if os.path.exists(tmp_path):
161+
os.unlink(tmp_path)

test/grid/grid/test_initialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_grid_init_verts_fill_values():
4949
def test_read_shpfile(test_data_dir):
5050
"""Reads a shape file and write ugrid file."""
5151
shp_filename = test_data_dir / "shp" / "grid_fire.shp"
52-
with pytest.raises(ValueError):
52+
with pytest.raises((ValueError, FileNotFoundError, OSError)):
5353
grid_shp = ux.open_grid(shp_filename)
5454

5555

uxarray/core/api.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import numpy as np
88

99
from uxarray.core.dataset import UxDataset
10-
from uxarray.core.utils import _map_dims_to_ugrid, match_chunks_to_ugrid
10+
from uxarray.core.utils import (
11+
_map_dims_to_ugrid,
12+
_open_dataset_with_fallback,
13+
match_chunks_to_ugrid,
14+
)
1115
from uxarray.grid import Grid
1216

1317
if TYPE_CHECKING:
@@ -107,7 +111,9 @@ def open_grid(
107111

108112
else:
109113
# Attempt to use Xarray directly for remaining input types
110-
grid_ds = xr.open_dataset(grid_filename_or_obj, chunks=grid_chunks, **kwargs)
114+
grid_ds = _open_dataset_with_fallback(
115+
grid_filename_or_obj, chunks=grid_chunks, **kwargs
116+
)
111117
grid = Grid.from_dataset(grid_ds, use_dual=use_dual)
112118

113119
# Return the grid (and chunks, if requested) in a consistent manner.
@@ -173,8 +179,6 @@ def open_dataset(
173179
>>> import uxarray as ux
174180
>>> ux_ds = ux.open_dataset("grid_file.nc", "data_file.nc")
175181
"""
176-
import xarray as xr
177-
178182
if grid_kwargs is None:
179183
grid_kwargs = {}
180184

@@ -184,7 +188,7 @@ def open_dataset(
184188
)
185189

186190
# Load the data as a Xarray Dataset
187-
ds = xr.open_dataset(filename_or_obj, chunks=corrected_chunks, **kwargs)
191+
ds = _open_dataset_with_fallback(filename_or_obj, chunks=corrected_chunks, **kwargs)
188192

189193
# Map original dimensions to the UGRID conventions
190194
ds = _map_dims_to_ugrid(ds, uxgrid._source_dims_dict, uxgrid)

uxarray/core/dataset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import uxarray
1616
from uxarray.core.dataarray import UxDataArray
17-
from uxarray.core.utils import _map_dims_to_ugrid
17+
from uxarray.core.utils import _map_dims_to_ugrid, _open_dataset_with_fallback
1818
from uxarray.formatting_html import dataset_repr
1919
from uxarray.grid import Grid
2020
from uxarray.grid.dual import construct_dual
@@ -326,7 +326,7 @@ def from_healpix(
326326
"""
327327

328328
if not isinstance(ds, xr.Dataset):
329-
ds = xr.open_dataset(ds, **kwargs)
329+
ds = _open_dataset_with_fallback(ds, **kwargs)
330330

331331
if face_dim not in ds.dims:
332332
raise ValueError(

uxarray/core/utils.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,41 @@
33
from uxarray.io.utils import _get_source_dims_dict, _parse_grid_type
44

55

6+
def _open_dataset_with_fallback(filename_or_obj, chunks=None, **kwargs):
7+
"""Internal utility function to open datasets with fallback to netcdf4 engine.
8+
9+
Attempts to use Xarray's default read engine first, which may be "h5netcdf"
10+
or "scipy" after v2025.09.0. If that fails (typically for h5-incompatible files),
11+
falls back to using the "netcdf4" engine.
12+
13+
Parameters
14+
----------
15+
filename_or_obj : str, Path, file-like or DataStore
16+
Strings and Path objects are interpreted as a path to a netCDF file
17+
or an OpenDAP URL and opened with python-netCDF4, unless the filename
18+
ends with .gz, in which case the file is gunzipped and opened with
19+
scipy.io.netcdf (only netCDF3 supported).
20+
chunks : int, dict, 'auto' or None, optional
21+
If chunks is provided, it is used to load the new dataset into dask
22+
arrays.
23+
**kwargs
24+
Additional keyword arguments passed to xr.open_dataset
25+
26+
Returns
27+
-------
28+
xr.Dataset
29+
The opened dataset
30+
"""
31+
try:
32+
# Try opening with xarray's default read engine
33+
return xr.open_dataset(filename_or_obj, chunks=chunks, **kwargs)
34+
except Exception:
35+
# If it fails, use the "netcdf4" engine as backup
36+
# Extract engine from kwargs to prevent duplicate parameter error
37+
engine = kwargs.pop("engine", "netcdf4")
38+
return xr.open_dataset(filename_or_obj, engine=engine, chunks=chunks, **kwargs)
39+
40+
641
def _map_dims_to_ugrid(
742
ds,
843
_source_dims_dict,
@@ -69,7 +104,7 @@ def match_chunks_to_ugrid(grid_filename_or_obj, chunks):
69104
# No need to rename
70105
return chunks
71106

72-
ds = xr.open_dataset(grid_filename_or_obj, chunks=chunks)
107+
ds = _open_dataset_with_fallback(grid_filename_or_obj, chunks=chunks)
73108
grid_spec, _, _ = _parse_grid_type(ds)
74109

75110
source_dims_dict = _get_source_dims_dict(ds, grid_spec)

uxarray/grid/grid.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919
from uxarray.constants import INT_FILL_VALUE
2020
from uxarray.conventions import ugrid
21+
22+
# Import the utility function for opening datasets with fallback
23+
from uxarray.core.utils import _open_dataset_with_fallback
2124
from uxarray.cross_sections import GridCrossSectionAccessor
2225
from uxarray.formatting_html import grid_repr
2326
from uxarray.grid.area import get_all_face_area_from_coords
@@ -348,7 +351,7 @@ def from_file(
348351
grid_ds, source_dims_dict = _read_geodataframe(filename)
349352

350353
elif backend == "xarray":
351-
dataset = xr.open_dataset(filename, **kwargs)
354+
dataset = _open_dataset_with_fallback(filename, **kwargs)
352355
return cls.from_dataset(dataset)
353356

354357
else:

0 commit comments

Comments
 (0)