Repository: pydata/xarray

Issue:
DataArray().drop_attrs(deep=False) doesn't do anything
[Edit] I didn't see any issue templates because I took the route via opening an issue linked to a line of code, which seems to have failed anyway.

I tried to drop DataArray attributes without dropping the coordinate attributes. For this, I set `deep=False` in the dataarray's method `.drop_attrs()`. This did not work.

## Environment

```
INSTALLED VERSIONS
------------------
commit: None
python: 3.12.3 (main, Jan 17 2025, 18:03:48) [GCC 13.3.0]
python-bits: 64
OS: Linux
OS-release: 6.8.0-52-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: ('en_US', 'UTF-8')
libhdf5: 1.14.2
libnetcdf: 4.9.4-development

xarray: 2025.1.1
pandas: 2.2.3
numpy: 2.2.2
scipy: 1.15.1
netCDF4: 1.7.2
pydap: None
h5netcdf: None
h5py: None
zarr: 3.0.1
cftime: 1.6.4.post1
nc_time_axis: None
iris: None
bottleneck: None
dask: 2025.1.0
distributed: None
matplotlib: 3.10.0
cartopy: 0.24.1
seaborn: None
numbagg: None
fsspec: 2024.12.0
cupy: None
pint: 0.24.4
sparse: None
flox: None
numpy_groupies: None
setuptools: 75.8.0
pip: 25.0
conda: None
pytest: 8.3.4
mypy: None
IPython: 8.31.0
sphinx: None
```

## Minimal example to reproduce

```python
import xarray as xr
a = xr.DataArray(0).assign_attrs({"b": "c"})
a = a.drop_attrs(deep=False)
print(a)
```
results in
```
<xarray.DataArray ()> Size: 8B
array(0)
Attributes:
    b:        c
```
The attributes should have been dropped but they are still present.

--------------
*my initial guess*

[Edit] removed, was wrong, see below

Discussion:
welcome[bot]: Thank you for opening this pull request! It may take us a few days to respond here, so thank you for being patient.
If you have questions, some answers may be found in our [contributing guidelines](https://docs.xarray.dev/en/stable/contributing.html).

max-sixty: A welcome follow-up would be adding `attrs` to `_replace`, like we have in Dataset. I'm not sure why it's missing tbh. But no obligation.

Could we add a whatsnew and then merge?
j-haacker: I added attrs to `_replace` by replacing the attrs of the variable because attrs cannot be passed to the `DataArray` constructor by themselves.

I also updated the `whats-new.rst`.
max-sixty: @headtr1ck  you need to approve for us to merge
j-haacker: I fixed the issue @headtr1ck  raised, that replacing a variable and attributes simultaneously would "skip" the variable.

I added tests for `_replace`. However:

1. I noticed the methods let by one "_" don't have tests. Are they tested somewhere else or not meant to be tested?
2. I'm not sure about the desired replacement logic if a variable is replaced that has attributes.

(2) I made the assumption that if a variable and attributes are replaced simultaneously, the passed attributes override the variable attributes. And further, if the replacement variable contains attributes they should replace any original attributes. I made both explicit in the test. I'm happy to change the behavior or add a warning.
max-sixty: Great — we do try and test private methods, but sometimes assume they're implicitly tested. When making a change to them, adding a test like you most recently did makes a lot of sense.

Thanks a lot @j-haacker ! Appreciate you making the underlying code better as well as fixing the immediate problem!

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index fe63a923..8b74d3cd 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -42,6 +42,9 @@ Bug fixes
 - Use mean of min/max years as offset in calculation of datetime64 mean
   (:issue:`10019`, :pull:`10035`).
   By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
+- Fix DataArray().drop_attrs(deep=False) and add support for attrs to
+  DataArray()._replace(). (:issue:`10027`, :pull:`10030`). By `Jan
+  Haacker <https://github.com/j-haacker>`_.
 
 
 Documentation
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 6c4c17e7..ff2d5198 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import copy
 import datetime
 import warnings
 from collections.abc import (
@@ -522,6 +523,7 @@ class DataArray(
         variable: Variable | None = None,
         coords=None,
         name: Hashable | None | Default = _default,
+        attrs=_default,
         indexes=None,
     ) -> Self:
         if variable is None:
@@ -532,6 +534,11 @@ class DataArray(
             indexes = self._indexes
         if name is _default:
             name = self.name
+        if attrs is _default:
+            attrs = copy.copy(self.attrs)
+        else:
+            variable = variable.copy()
+            variable.attrs = attrs
         return type(self)(variable, coords, name=name, indexes=indexes, fastpath=True)
 
     def _replace_maybe_drop_dims(
@@ -7575,6 +7582,11 @@ class DataArray(
         -------
         DataArray
         """
-        return (
-            self._to_temp_dataset().drop_attrs(deep=deep).pipe(self._from_temp_dataset)
-        )
+        if not deep:
+            return self._replace(attrs={})
+        else:
+            return (
+                self._to_temp_dataset()
+                .drop_attrs(deep=deep)
+                .pipe(self._from_temp_dataset)
+            )


Test patch:
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index 7d1017f1..75d6d919 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -1908,6 +1908,21 @@ class TestDataArray:
             warnings.simplefilter("error")
             da.rename(x="x")
 
+    def test_replace(self) -> None:
+        # Tests the `attrs` replacement and whether it interferes with a
+        # `variable` replacement
+        da = self.mda
+        attrs1 = {"a1": "val1", "a2": 161}
+        x = np.ones((10, 20))
+        v = Variable(["x", "y"], x)
+        assert da._replace(variable=v, attrs=attrs1).attrs == attrs1
+        attrs2 = {"b1": "val2", "b2": 1312}
+        va = Variable(["x", "y"], x, attrs2)
+        # assuming passed `attrs` should prevail
+        assert da._replace(variable=va, attrs=attrs1).attrs == attrs1
+        # assuming `va.attrs` should be adopted
+        assert da._replace(variable=va).attrs == attrs2
+
     def test_init_value(self) -> None:
         expected = DataArray(
             np.full((3, 4), 3), dims=["x", "y"], coords=[range(3), range(4)]
@@ -2991,8 +3006,11 @@ class TestDataArray:
 
     def test_drop_attrs(self) -> None:
         # Mostly tested in test_dataset.py, but adding a very small test here
-        da = DataArray([], attrs=dict(a=1, b=2))
+        coord_ = DataArray([], attrs=dict(d=3, e=4))
+        da = DataArray([], attrs=dict(a=1, b=2)).assign_coords(dict(coord_=coord_))
         assert da.drop_attrs().attrs == {}
+        assert da.drop_attrs().coord_.attrs == {}
+        assert da.drop_attrs(deep=False).coord_.attrs == dict(d=3, e=4)
 
     @pytest.mark.parametrize(
         "func", [lambda x: x.clip(0, 1), lambda x: np.float64(1.0) * x, np.abs, abs]