Skip to content

Commit 1abb2ba

Browse files
committed
Merge branch 'main' into fix/zarr-v3
* main: Add close() method to DataTree and use it to clean-up open files in tests (pydata#9651) Change URL for pydap test (pydata#9655)
2 parents 268e3eb + 863184d commit 1abb2ba

8 files changed

Lines changed: 195 additions & 78 deletions

File tree

xarray/backends/common.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import os
55
import time
66
import traceback
7-
from collections.abc import Iterable
7+
from collections.abc import Iterable, Mapping
88
from glob import glob
99
from typing import TYPE_CHECKING, Any, ClassVar
1010

1111
import numpy as np
1212

1313
from xarray.conventions import cf_encoder
1414
from xarray.core import indexing
15+
from xarray.core.datatree import DataTree
1516
from xarray.core.utils import FrozenDict, NdimSizeLenMixin, is_remote_uri
1617
from xarray.namedarray.parallelcompat import get_chunked_array_type
1718
from xarray.namedarray.pycompat import is_chunked_array
@@ -20,7 +21,6 @@
2021
from io import BufferedIOBase
2122

2223
from xarray.core.dataset import Dataset
23-
from xarray.core.datatree import DataTree
2424
from xarray.core.types import NestedSequence
2525

2626
# Create a logger object, but don't add any handlers. Leave that to user code.
@@ -149,6 +149,19 @@ def find_root_and_group(ds):
149149
return ds, group
150150

151151

152+
def datatree_from_dict_with_io_cleanup(groups_dict: Mapping[str, Dataset]) -> DataTree:
153+
"""DataTree.from_dict with file clean-up."""
154+
try:
155+
tree = DataTree.from_dict(groups_dict)
156+
except Exception:
157+
for ds in groups_dict.values():
158+
ds.close()
159+
raise
160+
for path, ds in groups_dict.items():
161+
tree[path].set_close(ds._close)
162+
return tree
163+
164+
152165
def robust_getitem(array, key, catch=Exception, max_retries=6, initial_delay=500):
153166
"""
154167
Robustly index an array, using retry logic with exponential backoff if any

xarray/backends/h5netcdf_.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
BackendEntrypoint,
1414
WritableCFDataStore,
1515
_normalize_path,
16+
datatree_from_dict_with_io_cleanup,
1617
find_root_and_group,
1718
)
1819
from xarray.backends.file_manager import CachingFileManager, DummyFileManager
@@ -474,8 +475,6 @@ def open_datatree(
474475
driver_kwds=None,
475476
**kwargs,
476477
) -> DataTree:
477-
from xarray.core.datatree import DataTree
478-
479478
groups_dict = self.open_groups_as_dict(
480479
filename_or_obj,
481480
mask_and_scale=mask_and_scale,
@@ -495,8 +494,7 @@ def open_datatree(
495494
driver_kwds=driver_kwds,
496495
**kwargs,
497496
)
498-
499-
return DataTree.from_dict(groups_dict)
497+
return datatree_from_dict_with_io_cleanup(groups_dict)
500498

501499
def open_groups_as_dict(
502500
self,

xarray/backends/netCDF4_.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
BackendEntrypoint,
1717
WritableCFDataStore,
1818
_normalize_path,
19+
datatree_from_dict_with_io_cleanup,
1920
find_root_and_group,
2021
robust_getitem,
2122
)
@@ -710,8 +711,6 @@ def open_datatree(
710711
autoclose=False,
711712
**kwargs,
712713
) -> DataTree:
713-
from xarray.core.datatree import DataTree
714-
715714
groups_dict = self.open_groups_as_dict(
716715
filename_or_obj,
717716
mask_and_scale=mask_and_scale,
@@ -730,8 +729,7 @@ def open_datatree(
730729
autoclose=autoclose,
731730
**kwargs,
732731
)
733-
734-
return DataTree.from_dict(groups_dict)
732+
return datatree_from_dict_with_io_cleanup(groups_dict)
735733

736734
def open_groups_as_dict(
737735
self,

xarray/backends/zarr.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
BackendEntrypoint,
2020
_encode_variable_name,
2121
_normalize_path,
22+
datatree_from_dict_with_io_cleanup,
2223
)
2324
from xarray.backends.store import StoreBackendEntrypoint
2425
from xarray.core import indexing
@@ -1444,8 +1445,6 @@ def open_datatree(
14441445
zarr_format=None,
14451446
**kwargs,
14461447
) -> DataTree:
1447-
from xarray.core.datatree import DataTree
1448-
14491448
filename_or_obj = _normalize_path(filename_or_obj)
14501449
groups_dict = self.open_groups_as_dict(
14511450
filename_or_obj=filename_or_obj,
@@ -1467,8 +1466,7 @@ def open_datatree(
14671466
zarr_format=zarr_format,
14681467
**kwargs,
14691468
)
1470-
1471-
return DataTree.from_dict(groups_dict)
1469+
return datatree_from_dict_with_io_cleanup(groups_dict)
14721470

14731471
def open_groups_as_dict(
14741472
self,

xarray/core/datatree.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ def update(self, other) -> NoReturn:
266266
"use `.copy()` first to get a mutable version of the input dataset."
267267
)
268268

269+
def set_close(self, close: Callable[[], None] | None) -> None:
270+
raise AttributeError("cannot modify a DatasetView()")
271+
272+
def close(self) -> None:
273+
raise AttributeError(
274+
"cannot close a DatasetView(). Close the associated DataTree node "
275+
"instead"
276+
)
277+
269278
# FIXME https://github.com/python/mypy/issues/7328
270279
@overload # type: ignore[override]
271280
def __getitem__(self, key: Mapping) -> Dataset: # type: ignore[overload-overlap]
@@ -633,7 +642,7 @@ def to_dataset(self, inherit: bool = True) -> Dataset:
633642
None if self._attrs is None else dict(self._attrs),
634643
dict(self._indexes if inherit else self._node_indexes),
635644
None if self._encoding is None else dict(self._encoding),
636-
self._close,
645+
None,
637646
)
638647

639648
@property
@@ -796,6 +805,29 @@ def _repr_html_(self):
796805
return f"<pre>{escape(repr(self))}</pre>"
797806
return datatree_repr_html(self)
798807

808+
def __enter__(self) -> Self:
809+
return self
810+
811+
def __exit__(self, exc_type, exc_value, traceback) -> None:
812+
self.close()
813+
814+
# DatasetView does not support close() or set_close(), so we reimplement
815+
# these methods on DataTree.
816+
817+
def _close_node(self) -> None:
818+
if self._close is not None:
819+
self._close()
820+
self._close = None
821+
822+
def close(self) -> None:
823+
"""Close any files associated with this tree."""
824+
for node in self.subtree:
825+
node._close_node()
826+
827+
def set_close(self, close: Callable[[], None] | None) -> None:
828+
"""Set the closer for this node."""
829+
self._close = close
830+
799831
def _replace_node(
800832
self: DataTree,
801833
data: Dataset | Default = _default,

xarray/tests/test_backends.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5185,7 +5185,7 @@ def test_dask(self) -> None:
51855185
class TestPydapOnline(TestPydap):
51865186
@contextlib.contextmanager
51875187
def create_datasets(self, **kwargs):
5188-
url = "http://test.opendap.org/opendap/hyrax/data/nc/bears.nc"
5188+
url = "http://test.opendap.org/opendap/data/nc/bears.nc"
51895189
actual = open_dataset(url, engine="pydap", **kwargs)
51905190
with open_example_dataset("bears.nc") as expected:
51915191
# workaround to restore string which is converted to byte

0 commit comments

Comments
 (0)