1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
Description: PR 11020 - bugfix from upstream
Bug-Origin: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1123151
Forwarded: not-needed
Last-Updated: 2026-01-06
Index: python-xarray-2026.01.0/doc/whats-new.rst
===================================================================
--- python-xarray-2026.01.0.orig/doc/whats-new.rst
+++ python-xarray-2026.01.0/doc/whats-new.rst
@@ -140,6 +140,10 @@ New Features
Bug Fixes
~~~~~~~~~
+- :py:meth:`Dataset.map` now merges attrs from the function result and the original
+ using the ``drop_conflicts`` strategy when ``keep_attrs=True``, preserving attrs
+ set by the function (:issue:`11019`, :pull:`11020`).
+ By `Maximilian Roos <https://github.com/max-sixty>`_.
- When assigning an indexed coordinate to a data variable or coordinate, coerce it from
``IndexVariable`` to ``Variable`` (:issue:`9859`, :issue:`10829`, :pull:`10909`).
By `Julia Signell <https://github.com/jsignell>`_.
Index: python-xarray-2026.01.0/xarray/computation/weighted.py
===================================================================
--- python-xarray-2026.01.0.orig/xarray/computation/weighted.py
+++ python-xarray-2026.01.0/xarray/computation/weighted.py
@@ -544,14 +544,28 @@ class DataArrayWeighted(Weighted["DataAr
dataset = self.obj._to_temp_dataset()
dataset = dataset.map(func, dim=dim, **kwargs)
- return self.obj._from_temp_dataset(dataset)
+ result = self.obj._from_temp_dataset(dataset)
+ # Clear attrs when keep_attrs is explicitly False
+ # (weighted operations can propagate attrs from weights through internal computations)
+ if kwargs.get("keep_attrs") is False:
+ result.attrs = {}
+
+ return result
class DatasetWeighted(Weighted["Dataset"]):
def _implementation(self, func, dim, **kwargs) -> Dataset:
self._check_dim(dim)
- return self.obj.map(func, dim=dim, **kwargs)
+ result = self.obj.map(func, dim=dim, **kwargs)
+
+ # Clear attrs when keep_attrs is explicitly False
+ # (weighted operations can propagate attrs from weights through internal computations)
+ if kwargs.get("keep_attrs") is False:
+ result.attrs = {}
+ for var in result.data_vars.values():
+ var.attrs = {}
+ return result
def _inject_docstring(cls, cls_name):
cls.sum_of_weights.__doc__ = _SUM_OF_WEIGHTS_DOCSTRING.format(cls=cls_name)
Index: python-xarray-2026.01.0/xarray/core/dataset.py
===================================================================
--- python-xarray-2026.01.0.orig/xarray/core/dataset.py
+++ python-xarray-2026.01.0/xarray/core/dataset.py
@@ -6914,8 +6914,11 @@ class Dataset(
DataArray.
keep_attrs : bool or None, optional
If True, both the dataset's and variables' attributes (`attrs`) will be
- copied from the original objects to the new ones. If False, the new dataset
- and variables will be returned without copying the attributes.
+ combined from the original objects and the function results using the
+ ``drop_conflicts`` strategy: matching attrs are kept, conflicting attrs
+ are dropped. If False, the new dataset and variables will have only
+ the attributes set by the function.
+
args : iterable, optional
Positional arguments passed on to `func`.
**kwargs : Any
@@ -6964,16 +6967,19 @@ class Dataset(
coords = Coordinates._construct_direct(coords=coord_vars, indexes=indexes)
if keep_attrs:
+ # Merge attrs from function result and original, dropping conflicts
+ from xarray.structure.merge import merge_attrs
+
for k, v in variables.items():
- v._copy_attrs_from(self.data_vars[k])
+ v.attrs = merge_attrs(
+ [v.attrs, self.data_vars[k].attrs], "drop_conflicts"
+ )
for k, v in coords.items():
if k in self.coords:
- v._copy_attrs_from(self.coords[k])
- else:
- for v in variables.values():
- v.attrs = {}
- for v in coords.values():
- v.attrs = {}
+ v.attrs = merge_attrs(
+ [v.attrs, self.coords[k].attrs], "drop_conflicts"
+ )
+ # When keep_attrs=False, leave attrs as the function returned them
attrs = self.attrs if keep_attrs else None
return type(self)(variables, coords=coords, attrs=attrs)
Index: python-xarray-2026.01.0/xarray/core/datatree.py
===================================================================
--- python-xarray-2026.01.0.orig/xarray/core/datatree.py
+++ python-xarray-2026.01.0/xarray/core/datatree.py
@@ -397,8 +397,10 @@ class DatasetView(Dataset):
DataArray.
keep_attrs : bool | None, optional
If True, both the dataset's and variables' attributes (`attrs`) will be
- copied from the original objects to the new ones. If False, the new dataset
- and variables will be returned without copying the attributes.
+ combined from the original objects and the function results using the
+ ``drop_conflicts`` strategy: matching attrs are kept, conflicting attrs
+ are dropped. If False, the new dataset and variables will have only
+ the attributes set by the function.
args : iterable, optional
Positional arguments passed on to `func`.
**kwargs : Any
@@ -438,8 +440,13 @@ class DatasetView(Dataset):
for k, v in self.data_vars.items()
}
if keep_attrs:
+ # Merge attrs from function result and original, dropping conflicts
+ from xarray.structure.merge import merge_attrs
+
for k, v in variables.items():
- v._copy_attrs_from(self.data_vars[k])
+ v.attrs = merge_attrs(
+ [v.attrs, self.data_vars[k].attrs], "drop_conflicts"
+ )
attrs = self.attrs if keep_attrs else None
# return type(self)(variables, attrs=attrs)
return Dataset(variables, attrs=attrs)
Index: python-xarray-2026.01.0/xarray/tests/test_dataset.py
===================================================================
--- python-xarray-2026.01.0.orig/xarray/tests/test_dataset.py
+++ python-xarray-2026.01.0/xarray/tests/test_dataset.py
@@ -6509,6 +6509,36 @@ class TestDataset:
expected = xr.Dataset({"foo": 42, "bar": ("y", [4, 5])})
assert_identical(result, expected)
+ def test_map_preserves_function_attrs(self) -> None:
+ # Regression test for GH11019
+ # Attrs added by function should be preserved in result
+ ds = xr.Dataset({"test": ("x", [1, 2, 3], {"original": "value"})})
+
+ def add_attr(da):
+ return da.assign_attrs(new_attr="foobar")
+
+ # With keep_attrs=True: merge using drop_conflicts (no conflict here)
+ result = ds.map(add_attr, keep_attrs=True)
+ assert result["test"].attrs == {"original": "value", "new_attr": "foobar"}
+
+ # With keep_attrs=False: function's attrs preserved
+ result = ds.map(add_attr, keep_attrs=False)
+ assert result["test"].attrs == {"original": "value", "new_attr": "foobar"}
+
+ # When function modifies existing attr with keep_attrs=True, conflict is dropped
+ def modify_attr(da):
+ return da.assign_attrs(original="modified", extra="added")
+
+ result = ds.map(modify_attr, keep_attrs=True)
+ assert result["test"].attrs == {
+ "extra": "added"
+ } # "original" dropped due to conflict
+
+ # When function modifies existing attr with keep_attrs=False, function wins
+ result = ds.map(modify_attr, keep_attrs=False)
+ assert result["test"].attrs == {"original": "modified", "extra": "added"}
+
+
def test_apply_pending_deprecated_map(self) -> None:
data = create_test_data()
data.attrs["foo"] = "bar"
|