Skip to content

Commit 098158d

Browse files
mrocklincodex
andauthored
Add map_blocks and some testing support for array query expressions (#11398)
Co-authored-by: OpenAI <noreply@openai.com>
1 parent 71cfb13 commit 098158d

37 files changed

Lines changed: 1063 additions & 350 deletions

conftest.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,35 @@
11
"""Configuration for pytest."""
22

3+
import os
4+
35
import pytest
46

57

8+
def _use_dask_array(config: pytest.Config) -> bool:
9+
env_value = os.environ.get("XR_USE_DASK_ARRAY_WITH_EXPR", "")
10+
return config.getoption("--use-dask-array-with-expr") or env_value.lower() in {
11+
"1",
12+
"true",
13+
"yes",
14+
"on",
15+
}
16+
17+
18+
def _register_dask_array() -> None:
19+
try:
20+
import dask_array.xarray
21+
except ImportError as err:
22+
raise pytest.UsageError(
23+
"--use-dask-array-with-expr requires dask-array to be importable"
24+
) from err
25+
26+
dask_array.xarray.register()
27+
if not dask_array.xarray.isactive():
28+
raise pytest.UsageError(
29+
"--use-dask-array-with-expr registered dask-array, but it is not the active dask chunk manager"
30+
)
31+
32+
633
def pytest_addoption(parser: pytest.Parser):
734
"""Add command-line flags for pytest."""
835
parser.addoption("--run-flaky", action="store_true", help="runs flaky tests")
@@ -12,9 +39,31 @@ def pytest_addoption(parser: pytest.Parser):
1239
help="runs tests requiring a network connection",
1340
)
1441
parser.addoption("--run-mypy", action="store_true", help="runs mypy tests")
42+
parser.addoption(
43+
"--use-dask-array-with-expr",
44+
action="store_true",
45+
help="register dask-array as xarray's dask chunk manager",
46+
)
47+
48+
49+
def pytest_configure(config: pytest.Config):
50+
config.addinivalue_line(
51+
"markers",
52+
"skip_with_dask_array: skip when dask-array is registered as xarray's dask chunk manager",
53+
)
54+
config.addinivalue_line(
55+
"markers",
56+
"xfail_with_dask_array: xfail when dask-array is registered as xarray's dask chunk manager",
57+
)
58+
if not _use_dask_array(config):
59+
return
60+
61+
_register_dask_array()
1562

1663

1764
def pytest_runtest_setup(item):
65+
if _use_dask_array(item.config):
66+
_register_dask_array()
1867
# based on https://stackoverflow.com/questions/47559524
1968
if "flaky" in item.keywords and not item.config.getoption("--run-flaky"):
2069
pytest.skip("set --run-flaky option to run flaky tests")
@@ -39,6 +88,18 @@ def pytest_collection_modifyitems(items):
3988
# marking approach, meaning that each test case must contain "mypy" in the
4089
# name.
4190
item.add_marker(pytest.mark.mypy)
91+
if _use_dask_array(item.config) and "skip_with_dask_array" in item.keywords:
92+
item.add_marker(
93+
pytest.mark.skip(reason="skipped with dask-array chunk manager")
94+
)
95+
if _use_dask_array(item.config) and "xfail_with_dask_array" in item.keywords:
96+
mark = item.get_closest_marker("xfail_with_dask_array")
97+
kwargs = dict(mark.kwargs) if mark is not None else {}
98+
kwargs.setdefault(
99+
"reason", "expected failure with dask-array chunk manager"
100+
)
101+
kwargs.setdefault("strict", True)
102+
item.add_marker(pytest.mark.xfail(**kwargs))
42103

43104

44105
@pytest.fixture(autouse=True)

xarray/backends/api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from xarray.core.types import ReadBuffer
3636
from xarray.core.utils import emit_user_level_warning, is_remote_uri
3737
from xarray.namedarray.daskmanager import DaskManager
38-
from xarray.namedarray.parallelcompat import guess_chunkmanager
38+
from xarray.namedarray.parallelcompat import guess_chunkmanager, list_chunkmanagers
3939
from xarray.namedarray.utils import _get_chunk
4040
from xarray.structure.chunks import _maybe_chunk
4141
from xarray.structure.combine import (
@@ -234,7 +234,11 @@ def _chunk_ds(
234234
chunkmanager = guess_chunkmanager(chunked_array_type)
235235

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

240244
mtime = _get_mtime(filename_or_obj)

xarray/compat/dask_array_compat.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any
22

33
from xarray.namedarray.parallelcompat import get_chunked_array_type
4+
from xarray.namedarray.pycompat import is_chunked_array
45
from xarray.namedarray.utils import module_available
56

67

@@ -12,6 +13,8 @@ def reshape_blockwise(
1213
try:
1314
array_api = get_chunked_array_type(x).array_api
1415
except TypeError:
16+
if is_chunked_array(x):
17+
raise
1518
array_api = None
1619

1720
if array_api is not None and hasattr(array_api, "reshape_blockwise"):
@@ -29,6 +32,23 @@ def sliding_window_view(
2932
):
3033
# Backcompat for handling `automatic_rechunk`, delete when dask>=2024.11.0
3134
# Note that subok, writeable are unsupported by dask, so we ignore those in kwargs
35+
try:
36+
array_api = get_chunked_array_type(x).array_api
37+
except TypeError:
38+
if is_chunked_array(x):
39+
raise
40+
array_api = None
41+
42+
if array_api is not None:
43+
array_sliding_window_view = getattr(array_api, "sliding_window_view", None)
44+
if array_sliding_window_view is not None:
45+
return array_sliding_window_view(
46+
x,
47+
window_shape=window_shape,
48+
axis=axis,
49+
automatic_rechunk=automatic_rechunk,
50+
)
51+
3252
from dask.array.lib.stride_tricks import sliding_window_view
3353

3454
if module_available("dask", "2024.11.0"):

xarray/core/accessor_dt.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import warnings
4-
from typing import TYPE_CHECKING, Generic
4+
from typing import TYPE_CHECKING, Generic, cast
55

66
import numpy as np
77
import pandas as pd
@@ -17,6 +17,7 @@
1717
)
1818
from xarray.core.types import T_DataArray
1919
from xarray.core.variable import IndexVariable, Variable
20+
from xarray.namedarray.parallelcompat import get_chunked_array_type
2021
from xarray.namedarray.utils import is_duck_dask_array
2122

2223
if TYPE_CHECKING:
@@ -129,15 +130,14 @@ def _get_date_field(values, name, dtype):
129130
access_method = _access_through_cftimeindex
130131

131132
if is_duck_dask_array(values):
132-
from dask.array import map_blocks
133-
133+
chunkmanager = get_chunked_array_type(values)
134134
new_axis = chunks = None
135135
# isocalendar adds an axis
136136
if name == "isocalendar":
137-
chunks = (3,) + values.chunksize
137+
chunks = cast(tuple[int, ...], (3,) + values.chunksize)
138138
new_axis = 0
139139

140-
return map_blocks(
140+
return chunkmanager.map_blocks(
141141
access_method, values, name, dtype=dtype, new_axis=new_axis, chunks=chunks
142142
)
143143
else:
@@ -187,10 +187,13 @@ def _round_field(values, name, freq):
187187
188188
"""
189189
if is_duck_dask_array(values):
190-
from dask.array import map_blocks
191-
192-
dtype = np.datetime64 if is_np_datetime_like(values.dtype) else np.dtype("O")
193-
return map_blocks(
190+
chunkmanager = get_chunked_array_type(values)
191+
dtype = (
192+
np.dtype(values.dtype)
193+
if is_np_datetime_like(values.dtype)
194+
else np.dtype("O")
195+
)
196+
return chunkmanager.map_blocks(
194197
_round_through_series_or_index, values, name, freq=freq, dtype=dtype
195198
)
196199
else:
@@ -224,9 +227,8 @@ def _strftime(values, date_format):
224227
else:
225228
access_method = _strftime_through_cftimeindex
226229
if is_duck_dask_array(values):
227-
from dask.array import map_blocks
228-
229-
return map_blocks(access_method, values, date_format)
230+
chunkmanager = get_chunked_array_type(values)
231+
return chunkmanager.map_blocks(access_method, values, date_format)
230232
else:
231233
return access_method(values, date_format)
232234

0 commit comments

Comments
 (0)