Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2fd3b8b

Browse files
committedMay 3, 2024··
Merge remote-tracking branch 'origin/main' into backend-indexing
* origin/main: call `np.cross` with 3D vectors only (#8993) Mark `test_use_cftime_false_standard_calendar_in_range` as an expected failure (#8996) Migration of datatree/ops.py -> datatree_ops.py (#8976) avoid a couple of warnings in `polyfit` (#8939)
2 parents 245c3db + aaa778c commit 2fd3b8b

13 files changed

+175
-43
lines changed
 

‎doc/whats-new.rst

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ Internal Changes
7373
``xarray/testing/assertions`` for ``DataTree``. (:pull:`8967`)
7474
By `Owen Littlejohns <https://github.com/owenlittlejohns>`_ and
7575
`Tom Nicholas <https://github.com/TomNicholas>`_.
76+
- Migrates ``ops.py`` functionality into ``xarray/core/datatree_ops.py`` (:pull:`8976`)
77+
By `Matt Savoie <https://github.com/flamingbear>`_ and `Tom Nicholas <https://github.com/TomNicholas>`_.
7678
- ``transpose``, ``set_dims``, ``stack`` & ``unstack`` now use a ``dim`` kwarg
7779
rather than ``dims`` or ``dimensions``. This is the final change to make xarray methods
7880
consistent with their use of ``dim``. Using the existing kwarg will raise a

‎pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ filterwarnings = [
330330
"default:Using a non-tuple sequence for multidimensional indexing is deprecated:FutureWarning",
331331
"default:Duplicate dimension names present:UserWarning:xarray.namedarray.core",
332332
"default:::xarray.tests.test_strategies",
333+
# TODO: remove once we know how to deal with a changed signature in protocols
334+
"ignore:__array__ implementation doesn't accept a copy keyword, so passing copy=False failed.",
333335
]
334336

335337
log_cli_level = "INFO"

‎xarray/core/dataarray.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5269,7 +5269,7 @@ def differentiate(
52695269
edge_order: Literal[1, 2] = 1,
52705270
datetime_unit: DatetimeUnitOptions = None,
52715271
) -> Self:
5272-
""" Differentiate the array with the second order accurate central
5272+
"""Differentiate the array with the second order accurate central
52735273
differences.
52745274
52755275
.. note::

‎xarray/core/dataset.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1524,7 +1524,7 @@ def __iter__(self) -> Iterator[Hashable]:
15241524

15251525
else:
15261526

1527-
def __array__(self, dtype=None):
1527+
def __array__(self, dtype=None, copy=None):
15281528
raise TypeError(
15291529
"cannot directly convert an xarray.Dataset into a "
15301530
"numpy array. Instead, create an xarray.DataArray "
@@ -8354,7 +8354,7 @@ def differentiate(
83548354
edge_order: Literal[1, 2] = 1,
83558355
datetime_unit: DatetimeUnitOptions | None = None,
83568356
) -> Self:
8357-
""" Differentiate with the second order accurate central
8357+
"""Differentiate with the second order accurate central
83588358
differences.
83598359
83608360
.. note::
@@ -8937,9 +8937,7 @@ def polyfit(
89378937
lhs = np.vander(x, order)
89388938

89398939
if rcond is None:
8940-
rcond = (
8941-
x.shape[0] * np.core.finfo(x.dtype).eps # type: ignore[attr-defined]
8942-
)
8940+
rcond = x.shape[0] * np.finfo(x.dtype).eps
89438941

89448942
# Weights:
89458943
if w is not None:

‎xarray/core/datatree.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
check_isomorphic,
2424
map_over_subtree,
2525
)
26+
from xarray.core.datatree_ops import (
27+
DataTreeArithmeticMixin,
28+
MappedDatasetMethodsMixin,
29+
MappedDataWithCoords,
30+
)
2631
from xarray.core.datatree_render import RenderDataTree
2732
from xarray.core.formatting import datatree_repr
2833
from xarray.core.formatting_html import (
@@ -42,11 +47,6 @@
4247
)
4348
from xarray.core.variable import Variable
4449
from xarray.datatree_.datatree.common import TreeAttrAccessMixin
45-
from xarray.datatree_.datatree.ops import (
46-
DataTreeArithmeticMixin,
47-
MappedDatasetMethodsMixin,
48-
MappedDataWithCoords,
49-
)
5050

5151
try:
5252
from xarray.core.variable import calculate_dimensions
@@ -624,7 +624,7 @@ def __bool__(self) -> bool:
624624
def __iter__(self) -> Iterator[Hashable]:
625625
return itertools.chain(self.ds.data_vars, self.children)
626626

627-
def __array__(self, dtype=None):
627+
def __array__(self, dtype=None, copy=None):
628628
raise TypeError(
629629
"cannot directly convert a DataTree into a "
630630
"numpy array. Instead, create an xarray.DataArray "

‎xarray/core/datatree_mapping.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,10 @@ def map_over_subtree(func: Callable) -> Callable:
9898
Function will not be applied to any nodes without datasets.
9999
*args : tuple, optional
100100
Positional arguments passed on to `func`. If DataTrees any data-containing nodes will be converted to Datasets
101-
via .ds .
101+
via `.ds`.
102102
**kwargs : Any
103103
Keyword arguments passed on to `func`. If DataTrees any data-containing nodes will be converted to Datasets
104-
via .ds .
104+
via `.ds`.
105105
106106
Returns
107107
-------

‎xarray/datatree_/datatree/ops.py renamed to ‎xarray/core/datatree_ops.py

+62-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from __future__ import annotations
2+
3+
import re
14
import textwrap
25

36
from xarray.core.dataset import Dataset
4-
57
from xarray.core.datatree_mapping import map_over_subtree
68

79
"""
@@ -12,11 +14,10 @@
1214
"""
1315

1416

15-
_MAPPED_DOCSTRING_ADDENDUM = textwrap.fill(
17+
_MAPPED_DOCSTRING_ADDENDUM = (
1618
"This method was copied from xarray.Dataset, but has been altered to "
1719
"call the method on the Datasets stored in every node of the subtree. "
18-
"See the `map_over_subtree` function for more details.",
19-
width=117,
20+
"See the `map_over_subtree` function for more details."
2021
)
2122

2223
# TODO equals, broadcast_equals etc.
@@ -173,7 +174,7 @@ def _wrap_then_attach_to_cls(
173174
target_cls_dict, source_cls, methods_to_set, wrap_func=None
174175
):
175176
"""
176-
Attach given methods on a class, and optionally wrap each method first. (i.e. with map_over_subtree)
177+
Attach given methods on a class, and optionally wrap each method first. (i.e. with map_over_subtree).
177178
178179
Result is like having written this in the classes' definition:
179180
```
@@ -208,16 +209,62 @@ def method_name(self, *args, **kwargs):
208209
if wrap_func is map_over_subtree:
209210
# Add a paragraph to the method's docstring explaining how it's been mapped
210211
orig_method_docstring = orig_method.__doc__
211-
# if orig_method_docstring is not None:
212-
# if "\n" in orig_method_docstring:
213-
# new_method_docstring = orig_method_docstring.replace(
214-
# "\n", _MAPPED_DOCSTRING_ADDENDUM, 1
215-
# )
216-
# else:
217-
# new_method_docstring = (
218-
# orig_method_docstring + f"\n\n{_MAPPED_DOCSTRING_ADDENDUM}"
219-
# )
220-
setattr(target_cls_dict[method_name], "__doc__", orig_method_docstring)
212+
213+
if orig_method_docstring is not None:
214+
new_method_docstring = insert_doc_addendum(
215+
orig_method_docstring, _MAPPED_DOCSTRING_ADDENDUM
216+
)
217+
setattr(target_cls_dict[method_name], "__doc__", new_method_docstring)
218+
219+
220+
def insert_doc_addendum(docstring: str | None, addendum: str) -> str | None:
221+
"""Insert addendum after first paragraph or at the end of the docstring.
222+
223+
There are a number of Dataset's functions that are wrapped. These come from
224+
Dataset directly as well as the mixins: DataWithCoords, DatasetAggregations, and DatasetOpsMixin.
225+
226+
The majority of the docstrings fall into a parseable pattern. Those that
227+
don't, just have the addendum appeneded after. None values are returned.
228+
229+
"""
230+
if docstring is None:
231+
return None
232+
233+
pattern = re.compile(
234+
r"^(?P<start>(\S+)?(.*?))(?P<paragraph_break>\n\s*\n)(?P<whitespace>[ ]*)(?P<rest>.*)",
235+
re.DOTALL,
236+
)
237+
capture = re.match(pattern, docstring)
238+
if capture is None:
239+
### single line docstring.
240+
return (
241+
docstring
242+
+ "\n\n"
243+
+ textwrap.fill(
244+
addendum,
245+
subsequent_indent=" ",
246+
width=79,
247+
)
248+
)
249+
250+
if len(capture.groups()) == 6:
251+
return (
252+
capture["start"]
253+
+ capture["paragraph_break"]
254+
+ capture["whitespace"]
255+
+ ".. note::\n"
256+
+ textwrap.fill(
257+
addendum,
258+
initial_indent=capture["whitespace"] + " ",
259+
subsequent_indent=capture["whitespace"] + " ",
260+
width=79,
261+
)
262+
+ capture["paragraph_break"]
263+
+ capture["whitespace"]
264+
+ capture["rest"]
265+
)
266+
else:
267+
return docstring
221268

222269

223270
class MappedDatasetMethodsMixin:

‎xarray/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def _importorskip(
142142
not has_scipy_or_netCDF4, reason="requires scipy or netCDF4"
143143
)
144144
has_numpy_array_api, requires_numpy_array_api = _importorskip("numpy", "1.26.0")
145+
has_numpy_2, requires_numpy_2 = _importorskip("numpy", "2.0.0")
145146

146147

147148
def _importorskip_h5netcdf_ros3():

‎xarray/tests/test_assertions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def dims(self):
149149
warnings.warn("warning in test")
150150
return super().dims
151151

152-
def __array__(self):
152+
def __array__(self, dtype=None, copy=None):
153153
warnings.warn("warning in test")
154154
return super().__array__()
155155

‎xarray/tests/test_backends.py

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
assert_no_warnings,
6464
has_dask,
6565
has_netCDF4,
66+
has_numpy_2,
6667
has_scipy,
6768
mock,
6869
network,
@@ -5088,6 +5089,9 @@ def test_use_cftime_true(calendar, units_year) -> None:
50885089

50895090
@requires_scipy_or_netCDF4
50905091
@pytest.mark.parametrize("calendar", _STANDARD_CALENDARS)
5092+
@pytest.mark.xfail(
5093+
has_numpy_2, reason="https://github.com/pandas-dev/pandas/issues/56996"
5094+
)
50915095
def test_use_cftime_false_standard_calendar_in_range(calendar) -> None:
50925096
x = [0, 1]
50935097
time = [0, 720]

‎xarray/tests/test_computation.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -2500,32 +2500,32 @@ def test_polyfit_polyval_integration(
25002500
[
25012501
xr.DataArray([1, 2, 3]),
25022502
xr.DataArray([4, 5, 6]),
2503-
[1, 2, 3],
2504-
[4, 5, 6],
2503+
np.array([1, 2, 3]),
2504+
np.array([4, 5, 6]),
25052505
"dim_0",
25062506
-1,
25072507
],
25082508
[
25092509
xr.DataArray([1, 2]),
25102510
xr.DataArray([4, 5, 6]),
2511-
[1, 2],
2512-
[4, 5, 6],
2511+
np.array([1, 2, 0]),
2512+
np.array([4, 5, 6]),
25132513
"dim_0",
25142514
-1,
25152515
],
25162516
[
25172517
xr.Variable(dims=["dim_0"], data=[1, 2, 3]),
25182518
xr.Variable(dims=["dim_0"], data=[4, 5, 6]),
2519-
[1, 2, 3],
2520-
[4, 5, 6],
2519+
np.array([1, 2, 3]),
2520+
np.array([4, 5, 6]),
25212521
"dim_0",
25222522
-1,
25232523
],
25242524
[
25252525
xr.Variable(dims=["dim_0"], data=[1, 2]),
25262526
xr.Variable(dims=["dim_0"], data=[4, 5, 6]),
2527-
[1, 2],
2528-
[4, 5, 6],
2527+
np.array([1, 2, 0]),
2528+
np.array([4, 5, 6]),
25292529
"dim_0",
25302530
-1,
25312531
],
@@ -2564,8 +2564,8 @@ def test_polyfit_polyval_integration(
25642564
dims=["cartesian"],
25652565
coords=dict(cartesian=(["cartesian"], ["x", "y", "z"])),
25662566
),
2567-
[0, 0, 1],
2568-
[4, 5, 6],
2567+
np.array([0, 0, 1]),
2568+
np.array([4, 5, 6]),
25692569
"cartesian",
25702570
-1,
25712571
],
@@ -2580,8 +2580,8 @@ def test_polyfit_polyval_integration(
25802580
dims=["cartesian"],
25812581
coords=dict(cartesian=(["cartesian"], ["x", "y", "z"])),
25822582
),
2583-
[1, 0, 2],
2584-
[4, 5, 6],
2583+
np.array([1, 0, 2]),
2584+
np.array([4, 5, 6]),
25852585
"cartesian",
25862586
-1,
25872587
],

‎xarray/tests/test_datatree.py

+78
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from copy import copy, deepcopy
2+
from textwrap import dedent
23

34
import numpy as np
45
import pytest
56

67
import xarray as xr
78
from xarray.core.datatree import DataTree
9+
from xarray.core.datatree_ops import _MAPPED_DOCSTRING_ADDENDUM, insert_doc_addendum
810
from xarray.core.treenode import NotFoundInTreeError
911
from xarray.testing import assert_equal, assert_identical
1012
from xarray.tests import create_test_data, source_ndarray
@@ -824,3 +826,79 @@ def test_tree(self, create_test_datatree):
824826
expected = create_test_datatree(modify=lambda ds: np.sin(ds))
825827
result_tree = np.sin(dt)
826828
assert_equal(result_tree, expected)
829+
830+
831+
class TestDocInsertion:
832+
"""Tests map_over_subtree docstring injection."""
833+
834+
def test_standard_doc(self):
835+
836+
dataset_doc = dedent(
837+
"""\
838+
Manually trigger loading and/or computation of this dataset's data
839+
from disk or a remote source into memory and return this dataset.
840+
Unlike compute, the original dataset is modified and returned.
841+
842+
Normally, it should not be necessary to call this method in user code,
843+
because all xarray functions should either work on deferred data or
844+
load data automatically. However, this method can be necessary when
845+
working with many file objects on disk.
846+
847+
Parameters
848+
----------
849+
**kwargs : dict
850+
Additional keyword arguments passed on to ``dask.compute``.
851+
852+
See Also
853+
--------
854+
dask.compute"""
855+
)
856+
857+
expected_doc = dedent(
858+
"""\
859+
Manually trigger loading and/or computation of this dataset's data
860+
from disk or a remote source into memory and return this dataset.
861+
Unlike compute, the original dataset is modified and returned.
862+
863+
.. note::
864+
This method was copied from xarray.Dataset, but has been altered to
865+
call the method on the Datasets stored in every node of the
866+
subtree. See the `map_over_subtree` function for more details.
867+
868+
Normally, it should not be necessary to call this method in user code,
869+
because all xarray functions should either work on deferred data or
870+
load data automatically. However, this method can be necessary when
871+
working with many file objects on disk.
872+
873+
Parameters
874+
----------
875+
**kwargs : dict
876+
Additional keyword arguments passed on to ``dask.compute``.
877+
878+
See Also
879+
--------
880+
dask.compute"""
881+
)
882+
883+
wrapped_doc = insert_doc_addendum(dataset_doc, _MAPPED_DOCSTRING_ADDENDUM)
884+
885+
assert expected_doc == wrapped_doc
886+
887+
def test_one_liner(self):
888+
mixin_doc = "Same as abs(a)."
889+
890+
expected_doc = dedent(
891+
"""\
892+
Same as abs(a).
893+
894+
This method was copied from xarray.Dataset, but has been altered to call the
895+
method on the Datasets stored in every node of the subtree. See the
896+
`map_over_subtree` function for more details."""
897+
)
898+
899+
actual_doc = insert_doc_addendum(mixin_doc, _MAPPED_DOCSTRING_ADDENDUM)
900+
assert expected_doc == actual_doc
901+
902+
def test_none(self):
903+
actual_doc = insert_doc_addendum(None, _MAPPED_DOCSTRING_ADDENDUM)
904+
assert actual_doc is None

‎xarray/tests/test_formatting.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ def test_lazy_array_wont_compute() -> None:
877877
from xarray.core.indexing import LazilyIndexedArray
878878

879879
class LazilyIndexedArrayNotComputable(LazilyIndexedArray):
880-
def __array__(self, dtype=None):
880+
def __array__(self, dtype=None, copy=None):
881881
raise NotImplementedError("Computing this array is not possible.")
882882

883883
arr = LazilyIndexedArrayNotComputable(np.array([1, 2]))

0 commit comments

Comments
 (0)
Please sign in to comment.