Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

## [v2607.0.0]
### Fixed
- `decode_cf`, `cache` and `create_default_indexes` were silently ignored
- `backend_kwargs={"decode_cf": False}` disagreed with `decode_cf=False` on GeoTIFFs
- `backend_kwargs` is now flattened before engine-specific sanitizing
- `decode_times` and `decode_timedelta` no longer dropped for rasterio since rioxarray accepts them
### Changed
- `open_posix()`, `open_cloud()` and `PrismBackendEntrypoint.open_dataset()` now return a raw backend dataset

## [v2605.0.0]
## Fixed
- `FileNotFoundError` for missing local files instead of misleading "cannot detect format"
Expand Down
98 changes: 98 additions & 0 deletions src/xarray_prism/_delegate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Delegation to the concrete xarray backend entrypoint.
"""

from __future__ import annotations

from typing import Any, Dict, Optional

# Decoder flags that xarray.open_dataset resolves from decode_cf
DECODER_PARAMETERS = (
"mask_and_scale",
"decode_times",
"decode_timedelta",
"concat_characters",
"use_cftime",
"decode_coords",
)


# Arguments xarray.open_dataset handles itself, after the backend
# has returned
API_ONLY_KWARGS = frozenset(
{
"cache",
"chunks",
"create_default_indexes",
"inline_array",
"chunked_array_type",
"from_array_kwargs",
"engine",
}
)


def _reject_api_only_kwargs(kwargs: Dict[str, Any]) -> None:
misplaced = sorted(API_ONLY_KWARGS & kwargs.keys())
if not misplaced:
return
names = ", ".join(repr(m) for m in misplaced)
example = misplaced[0]
raise TypeError(
f"xarray-prism: {names} must be passed directly to xarray.open_dataset, "
f"not inside backend_kwargs. xarray applies these itself after the "
f"backend returns, so nesting them cannot work with any engine.\n"
f" use: xr.open_dataset(uri, engine='prism', {example}=...)\n"
f" not: xr.open_dataset(uri, engine='prism', "
f"backend_kwargs={{{example!r}: ...}})"
)


def _expand_decode_cf(kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""
Handle a decode_cf that reached us through backend_kwargs
"""
if "decode_cf" not in kwargs:
return kwargs

decode_cf = kwargs.pop("decode_cf")
if decode_cf is False:
for name in DECODER_PARAMETERS:
kwargs[name] = False
return kwargs


def normalize_open_kwargs(
kwargs: Dict[str, Any],
backend_kwargs: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Flatten backend_kwargs and expand decode_cf into decoder flags
"""
merged = dict(kwargs)
if backend_kwargs:
merged.update(backend_kwargs)
_reject_api_only_kwargs(merged)
return _expand_decode_cf(merged)


def open_with_engine(
target: Any,
engine: str,
drop_variables: Optional[Any] = None,
backend_kwargs: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Any:
"""
Open target with engine's backend entrypoint
"""
from xarray.backends.plugins import get_backend

kwargs = normalize_open_kwargs(kwargs, backend_kwargs)

kwargs = {
k: v for k, v in kwargs.items() if not (k in DECODER_PARAMETERS and v is None)
}

backend = get_backend(engine)
return backend.open_dataset(target, drop_variables=drop_variables, **kwargs)
2 changes: 1 addition & 1 deletion src/xarray_prism/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for xarray-prism."""

__version__ = "2605.0.0"
__version__ = "2607.0.0"
11 changes: 5 additions & 6 deletions src/xarray_prism/backends/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ def open_cloud(
**kwargs,
) -> Any:
"""Open remote file with detected engine."""
import xarray as xr

from .._delegate import open_with_engine
from ..utils import gdal_env

storage_options = kwargs.pop("storage_options", None)
Expand All @@ -30,7 +29,7 @@ def open_cloud(
# GRIB / NetCDF3: must download the full file first
if engine in ("cfgrib", "scipy"):
local_path = cache_remote_file(uri, engine, storage_options, show_progress)
return xr.open_dataset(
return open_with_engine(
local_path,
engine=engine,
drop_variables=drop_variables,
Expand All @@ -40,7 +39,7 @@ def open_cloud(

# NetCDF4 (OPeNDAP)
if engine == "netcdf4":
return xr.open_dataset(
return open_with_engine(
uri,
engine=engine,
drop_variables=drop_variables,
Expand All @@ -53,7 +52,7 @@ def open_cloud(
from ..utils import sanitize_rasterio_kwargs

with gdal_env(storage_options):
return xr.open_dataset(
return open_with_engine(
uri,
engine=engine,
drop_variables=drop_variables,
Expand All @@ -62,7 +61,7 @@ def open_cloud(
)

# Zarr, h5netcdf
ds = xr.open_dataset(
ds = open_with_engine(
uri,
engine=engine,
drop_variables=drop_variables,
Expand Down
4 changes: 2 additions & 2 deletions src/xarray_prism/backends/posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def open_posix(
**kwargs,
) -> Any:
"""Open local file with detected engine."""
import xarray as xr
from .._delegate import open_with_engine

# the following posix backends don't accept storage_options
_NO_STORAGE_OPTIONS = frozenset({"cfgrib", "scipy", "netcdf4", "rasterio"})
Expand All @@ -35,7 +35,7 @@ def open_posix(
from ..utils import sanitize_rasterio_kwargs

kwargs = sanitize_rasterio_kwargs(kwargs)
ds = xr.open_dataset(
ds = open_with_engine(
uri,
engine=engine,
drop_variables=drop_variables,
Expand Down
21 changes: 20 additions & 1 deletion src/xarray_prism/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,19 @@ class PrismBackendEntrypoint(BackendEntrypoint):
)

url = "https://github.com/freva-org/xarray-prism"
open_dataset_parameters = ("filename_or_obj", "drop_variables", "storage_options")

# Everything Prism forwards to the underlying engine
open_dataset_parameters = (
"filename_or_obj",
"drop_variables",
"storage_options",
"mask_and_scale",
"decode_times",
"decode_timedelta",
"concat_characters",
"use_cftime",
"decode_coords",
)

ENGINE_MAP: Dict[str, str] = {
"zarr": "zarr",
Expand Down Expand Up @@ -154,6 +166,13 @@ def open_dataset(
kwargs.pop("xarray_engine", None)
backend_kwargs = kwargs.pop("backend_kwargs", None) or {}

# flatten backend_kwargs and expand a decode_cf that arrived through
# it
from ._delegate import normalize_open_kwargs

kwargs = normalize_open_kwargs(kwargs, backend_kwargs)
backend_kwargs = {}

# Check custom registry first (handles custom uri_types too)
custom_handler = registry.get(engine, uri_type)
if custom_handler:
Expand Down
5 changes: 2 additions & 3 deletions src/xarray_prism/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ def gdal_env(storage_options: Optional[Dict[str, Any]] = None) -> Iterator[None]
[
"use_cftime",
"decode_cf",
"decode_times",
"decode_timedelta",
"concat_characters",
"use_default_fill_value",
"cftime_variables",
]
Expand Down Expand Up @@ -197,7 +196,7 @@ def sanitize_rasterio_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
for key, (allowed, reason) in _RASTERIO_RESTRICTED_KWARGS.items():
if key in sanitized and sanitized[key] != allowed:
logger.warning(
"'%s=%r' is not supported — " "%s (using '%s' instead).",
"'%s=%r' is not supported — %s (using '%s' instead).",
key,
sanitized[key],
reason,
Expand Down
Loading
Loading