Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/internals/how-to-add-new-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ it should implement the following attributes and methods:
- the `guess_can_open` method (optional)
- the `description` attribute (optional)
- the `url` attribute (optional).
- the `open_dataarray` method (optional)
- the `open_datatree` method (optional)

This is what a `BackendEntrypoint` subclass should look like:

Expand Down Expand Up @@ -144,6 +146,29 @@ If you don't want to support the lazy loading, then the
{py:class}`~xarray.Dataset` shall contain values as a {py:class}`numpy.ndarray`
and your work is almost done.

(rst-open-dataarray)=

### open_dataarray

The backend `open_dataarray` may shall reading from file, the variables
decoding and it shall instantiate the output Xarray class {py:class}`~xarray.DataArray`.

If `MyBackendEntrypoint.open_dataarray` is not implemented and `xarray.open_dataarray(engine='my_engine')` is called then `MyBackendEntrypoint.open_dataset` is used instead.
If `open_dataset` is used to open a `DataArray`, if the `Dataset` contains a single variable, that is returned. If the `Dataset` contains multiple variables then a `ValueError` is raised.

All other processing and requirements are the same as for {ref}`rst-open_dataset`.

(rst-open-datatree)=

### open_datatree

The backend `open_datatree` may shall reading from file, the variables
decoding and it shall instantiate the output Xarray class {py:class}`~xarray.DataTree`.

If `MyBackendEntrypoint.open_datatree` is not implemented and `xarray.open_datatree(engine='my_engine')` is called a `NotImplementedError` is raised.

All other processing and requirements are the same as for {ref}`rst-open_dataset`.

(rst-open-dataset-parameters)=

### open_dataset_parameters
Expand Down
5 changes: 5 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ New Features
silently being written uncompressed (:issue:`10657`, :pull:`11067`).
By `Mark Harfouche <https://github.com/hmaarrfk>`_.

- ``xarray.BackendEntrypoint`` now supports implementing ``open_dataarray``.
Previously ``open_dataset`` was used when ``xarray.open_dataarray(file, engine='my-engine')`` was called.
Now, if ``BackendEntrypoint.open_dataarray`` is implemented, it will be used. (:issue:`10562`, :pull:`11537`).
By `Duncan McDougall <https://github.com/dncnmcdougall>`_.


Breaking Changes
~~~~~~~~~~~~~~~~
Expand Down
203 changes: 184 additions & 19 deletions xarray/backends/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ def _get_mtime(filename_or_obj):
return mtime


def _protect_dataarray_variables_inplace(dataarray: DataArray, cache: bool) -> None:
data: indexing.ExplicitlyIndexedNDArrayMixin
data = indexing.CopyOnWriteArray(dataarray._data)
if cache:
data = indexing.MemoryCachedArray(data)
dataarray.data = data


def _protect_dataset_variables_inplace(dataset: Dataset, cache: bool) -> None:
for name, variable in dataset.variables.items():
if name not in dataset._indexes:
Expand Down Expand Up @@ -220,6 +228,67 @@ def load_datatree(filename_or_obj: T_PathFileOrDataStore, **kwargs) -> DataTree:
return dt.load()


def _chunk_da(
backend_da,
filename_or_obj,
engine,
chunks,
overwrite_encoded_chunks,
inline_array,
chunked_array_type,
from_array_kwargs,
name=None,
chunkmanager=None,
token=(None,),
name_prefix=None,
**extra_tokens,
):

if chunkmanager is None:
chunkmanager = guess_chunkmanager(chunked_array_type)

# TODO refactor to move this dask-specific logic inside the DaskManager class
is_dask_chunkmanager = isinstance(chunkmanager, DaskManager) or any(
name == "dask" and manager is chunkmanager
for name, manager in list_chunkmanagers().items()
)
if is_dask_chunkmanager:
from dask.base import tokenize

mtime = _get_mtime(filename_or_obj)
token = tokenize(filename_or_obj, mtime, engine, chunks, **extra_tokens)
name_prefix = "open_dataset-"
else:
# not used
token = (None,)
name_prefix = None

if backend_da._in_memory:
return backend_da
var_chunks = _get_chunk(
backend_da._data,
chunks,
chunkmanager,
preferred_chunks=backend_da.encoding.get("preferred_chunks", {}),
dims=backend_da.dims,
)
if name is None and hasattr(backend_da, "name"):
name = backend_da.name

return _maybe_chunk(
name,
backend_da,
var_chunks,
overwrite_encoded_chunks=overwrite_encoded_chunks,
name_prefix=name_prefix,
token=token,
inline_array=inline_array,
chunked_array_type=chunkmanager,
from_array_kwargs=from_array_kwargs.copy(),
just_use_token=True,
)


def _chunk_ds(
backend_ds,
filename_or_obj,
Expand Down Expand Up @@ -251,28 +320,22 @@ def _chunk_ds(

variables = {}
for name, var in backend_ds.variables.items():
if var._in_memory:
variables[name] = var
continue
var_chunks = _get_chunk(
var._data,
chunks,
chunkmanager,
preferred_chunks=var.encoding.get("preferred_chunks", {}),
dims=var.dims,
)
variables[name] = _maybe_chunk(
name,
variables[name] = _chunk_da(
var,
var_chunks,
overwrite_encoded_chunks=overwrite_encoded_chunks,
name_prefix=name_prefix,
filename_or_obj,
engine,
chunks,
overwrite_encoded_chunks,
inline_array,
chunked_array_type,
from_array_kwargs,
name=name,
chunkmanager=chunkmanager,
token=token,
inline_array=inline_array,
chunked_array_type=chunkmanager,
from_array_kwargs=from_array_kwargs.copy(),
just_use_token=True,
name_prefix=name_prefix,
**extra_tokens,
)

return backend_ds._replace(variables)


Expand All @@ -285,6 +348,56 @@ def _maybe_create_default_indexes(ds):
return ds.assign_coords(Coordinates(to_index))


def _dataarray_from_backend_dataarray(
backend_da,
filename_or_obj,
engine,
chunks,
cache,
overwrite_encoded_chunks,
inline_array,
chunked_array_type,
from_array_kwargs,
create_default_indexes,
**extra_tokens,
):
if not isinstance(chunks, int | dict) and chunks not in {None, "auto"}:
raise ValueError(
f"chunks must be an int, dict, 'auto', or None. Instead found {chunks}."
)

_protect_dataarray_variables_inplace(backend_da, cache)

if create_default_indexes:
da = _maybe_create_default_indexes(backend_da)
else:
da = backend_da

if chunks is not None:
da = _chunk_da(
da,
filename_or_obj,
engine,
chunks,
overwrite_encoded_chunks,
inline_array,
chunked_array_type,
from_array_kwargs,
**extra_tokens,
)

da.set_close(backend_da._close)

# Ensure source filename always stored in dataset object
if "source" not in da.encoding:
path = getattr(filename_or_obj, "path", filename_or_obj)

if isinstance(path, str | os.PathLike):
da.encoding["source"] = _normalize_path(path)

return da


def _dataset_from_backend_dataset(
backend_ds,
filename_or_obj,
Expand Down Expand Up @@ -823,6 +936,58 @@ class (a subclass of ``BackendEntrypoint``) can also be used.
open_dataset
"""

try:
if cache is None:
cache = chunks is None

if backend_kwargs is not None:
kwargs.update(backend_kwargs)

if engine is None:
engine = plugins.guess_engine(filename_or_obj)

if from_array_kwargs is None:
from_array_kwargs = {}

backend = plugins.get_backend(engine)

decoders = _resolve_decoders_kwargs(
decode_cf,
open_backend_dataset_parameters=backend.open_dataset_parameters,
mask_and_scale=mask_and_scale,
decode_times=decode_times,
decode_timedelta=decode_timedelta,
concat_characters=concat_characters,
use_cftime=use_cftime,
decode_coords=decode_coords,
)

overwrite_encoded_chunks = kwargs.pop("overwrite_encoded_chunks", None)
backend_da = backend.open_dataarray(
filename_or_obj,
drop_variables=drop_variables,
**decoders,
**kwargs,
)
da = _dataarray_from_backend_dataarray(
backend_da,
filename_or_obj,
engine,
chunks,
cache,
overwrite_encoded_chunks,
inline_array,
chunked_array_type,
from_array_kwargs,
drop_variables=drop_variables,
create_default_indexes=create_default_indexes,
**decoders,
**kwargs,
)
return da
except NotImplementedError:
pass

dataset = open_dataset(
filename_or_obj,
decode_cf=decode_cf,
Expand Down
15 changes: 14 additions & 1 deletion xarray/backends/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from xarray.namedarray.utils import is_duck_dask_array

if TYPE_CHECKING:
from xarray.core.dataarray import DataArray
from xarray.core.dataset import Dataset
from xarray.core.types import NestedSequence

Expand Down Expand Up @@ -775,14 +776,26 @@ def __repr__(self) -> str:
txt += f"\n Learn more at {self.url}"
return txt

def open_dataarray(
self,
filename_or_obj: T_PathFileOrDataStore,
*,
drop_variables: str | Iterable[str] | None = None,
) -> DataArray:
"""
Backend open_dataarray method used by Xarray in :py:func:`~xarray.open_dataarray`.
"""

raise NotImplementedError()

def open_dataset(
self,
filename_or_obj: T_PathFileOrDataStore,
*,
drop_variables: str | Iterable[str] | None = None,
) -> Dataset:
"""
Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`.
Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset` and :py:func:`~xarray.open_dataarray` of open_dataarray si not implemented.
"""

raise NotImplementedError()
Expand Down
Loading
Loading