Repository: astropy/astropy

Issue:
`numpy.nanvar()` now raises an error if `out` is specified in version 6.1.5
### Description

Apparently a change in #17239 broke the `out` argument in `np.nanvar`, and now a `AttributeError` is raised.

### Expected behavior

For this function to work the same as 6.1.4 and earlier and support the `out` argument

### How to Reproduce

```python
import numpy as np
import astropy.units as u

a = np.random.uniform(0, 1, size=(16, 16)) * u.mm

out = np.zeros(16) * u.mm

np.nanstd(a, axis=1, out=out)
```

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
[<ipython-input-9-807e78ea43b5>](https://localhost:8080/#) in <cell line: 1>()
----> 1 np.nanstd(a, out=out)

3 frames
[/usr/local/lib/python3.10/dist-packages/astropy/units/quantity.py](https://localhost:8080/#) in __array_function__(self, function, types, args, kwargs)
   1863                 return self._not_implemented_or_raise(function, types)
   1864 
-> 1865             result = super().__array_function__(function, types, args, kwargs)
   1866             # Fall through to return section
   1867 

[/usr/local/lib/python3.10/dist-packages/numpy/lib/nanfunctions.py](https://localhost:8080/#) in nanstd(a, axis, dtype, out, ddof, keepdims, where)
   1877 
   1878     """
-> 1879     var = nanvar(a, axis=axis, dtype=dtype, out=out, ddof=ddof,
   1880                  keepdims=keepdims, where=where)
   1881     if isinstance(var, np.ndarray):

[/usr/local/lib/python3.10/dist-packages/astropy/units/quantity.py](https://localhost:8080/#) in __array_function__(self, function, types, args, kwargs)
   1859             function_helper = FUNCTION_HELPERS[function]
   1860             try:
-> 1861                 args, kwargs, unit, out = function_helper(*args, **kwargs)
   1862             except NotImplementedError:
   1863                 return self._not_implemented_or_raise(function, types)

[/usr/local/lib/python3.10/dist-packages/astropy/units/quantity_helper/function_helpers.py](https://localhost:8080/#) in nanvar(a, *args, **kwargs)
    258 @function_helper
    259 def nanvar(a, *args, **kwargs):
--> 260     return (a.view(np.ndarray),) + args, kwargs, a.unit**2, None
    261 
    262 

AttributeError: 'numpy.ndarray' object has no attribute 'unit'
```
Which outputs:

### Versions

```
Linux-6.1.85+-x86_64-with-glibc2.35
Python 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0]
astropy 6.1.5
Numpy 1.26.4
pyerfa 2.0.1.4
Scipy 1.13.1
Matplotlib 3.8.0
```


Discussion:
github-actions[bot]: Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  - [ ] Do the proposed changes actually accomplish desired goals?
  - [ ] Do the proposed changes follow the [Astropy coding guidelines](https://docs.astropy.org/en/latest/development/codeguide.html)?
  - [ ] Are tests added/updated as required? If so, do they follow the [Astropy testing guidelines](https://docs.astropy.org/en/latest/development/testguide.html)?
  - [ ] Are docs added/updated as required? If so, do they follow the [Astropy documentation guidelines](https://docs.astropy.org/en/latest/development/docguide.html)?
  - [ ] Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for [rebase](https://docs.astropy.org/en/latest/development/development_details.html#rebase-if-necessary) and [squash](https://docs.astropy.org/en/latest/development/development_details.html#squash-if-necessary).
  - [ ] Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the [bot](https://docs.astropy.org/en/latest/development/development_details.html#pre-commit).
  - [ ] Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  - [ ] Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  - [ ] At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) *before* merge.
github-actions[bot]: 👋 Thank you for your draft pull request! Do you know that you can use `[ci skip]` or `[skip ci]` in your commit messages to skip running continuous integration tests until you are ready?
mhvk: @neutrinoceros - I had to try myself to see what the problem was and ended up fixing it locally - and felt it was easiest to just force-push your branch, sorry! The tests now also include one for array input with quantity output.

Patch:
diff --git a/astropy/coordinates/angles/core.py b/astropy/coordinates/angles/core.py
index b91543a405..10e40e76af 100644
--- a/astropy/coordinates/angles/core.py
+++ b/astropy/coordinates/angles/core.py
@@ -184,9 +184,7 @@ class Angle(SpecificTypeQuantity):
                 if angle.dtype.kind in "SUVO":
                     angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
 
-            elif hasattr(angle, "__array__") and (
-                not hasattr(angle, "dtype") or angle.dtype.kind not in "SUVO"
-            ):
+            elif hasattr(angle, "__array__"):
                 angle = np.asarray(angle)
 
             elif isiterable(angle):
diff --git a/astropy/units/quantity_helper/function_helpers.py b/astropy/units/quantity_helper/function_helpers.py
index be464c573b..dafbd6009f 100644
--- a/astropy/units/quantity_helper/function_helpers.py
+++ b/astropy/units/quantity_helper/function_helpers.py
@@ -218,7 +218,6 @@ dispatched_function = FunctionAssigner(DISPATCHED_FUNCTIONS)
         np.fft.fft2, np.fft.ifft2, np.fft.rfft2, np.fft.irfft2,
         np.fft.fftn, np.fft.ifftn, np.fft.rfftn, np.fft.irfftn,
         np.fft.hfft, np.fft.ihfft,
-        np.nanstd,  # See comment on nanvar helper.
         np.linalg.eigvals, np.linalg.eigvalsh,
     } | ({np.asfarray} if NUMPY_LT_2_0 else set())  # noqa: NPY201
 )  # fmt: skip
@@ -252,15 +251,45 @@ def like_helper(a, *args, **kwargs):
     return (a.view(np.ndarray),) + args, kwargs, unit, None
 
 
+def _quantity_out_as_array(out):
+    from astropy.units import Quantity
+
+    if isinstance(out, Quantity):
+        return out.view(np.ndarray)
+    else:
+        # TODO: for an ndarray output, one could in principle
+        # try converting the input to dimensionless.
+        raise NotImplementedError
+
+
 # nanvar is safe for Quantity and was previously in SUBCLASS_FUNCTIONS, but it
 # is not safe for Angle, since the resulting unit is inconsistent with being
 # an Angle. By using FUNCTION_HELPERS, the unit gets passed through
 # _result_as_quantity, which will correctly drop to Quantity.
 # A side effect would be that np.nanstd then also produces Quantity; this
-# is avoided by it being helped by invariant_a_helpers above.
+# is avoided by it being helped below.
 @function_helper
-def nanvar(a, *args, **kwargs):
-    return (a.view(np.ndarray),) + args, kwargs, a.unit**2, None
+def nanvar(a, axis=None, dtype=None, out=None, ddof=0, keepdims=np._NoValue, **kwargs):
+    a = _as_quantity(a)
+    out_array = None if out is None else _quantity_out_as_array(out)
+    return (
+        (a.view(np.ndarray), axis, dtype, out_array, ddof, keepdims),
+        kwargs,
+        a.unit**2,
+        out,
+    )
+
+
+@function_helper
+def nanstd(a, axis=None, dtype=None, out=None, ddof=0, keepdims=np._NoValue, **kwargs):
+    a = _as_quantity(a)
+    out_array = None if out is None else _quantity_out_as_array(out)
+    return (
+        (a.view(np.ndarray), axis, dtype, out_array, ddof, keepdims),
+        kwargs,
+        a.unit,
+        out,
+    )
 
 
 @function_helper
@@ -424,15 +453,8 @@ def _quantities2arrays(*args, unit_from_first=False):
 
 def _iterable_helper(*args, out=None, **kwargs):
     """Convert arguments to Quantity, and treat possible 'out'."""
-    from astropy.units import Quantity
-
     if out is not None:
-        if isinstance(out, Quantity):
-            kwargs["out"] = out.view(np.ndarray)
-        else:
-            # TODO: for an ndarray output, we could in principle
-            # try converting all Quantity to dimensionless.
-            raise NotImplementedError
+        kwargs["out"] = _quantity_out_as_array(out)  # raises if not Quantity.
 
     arrays, unit = _quantities2arrays(*args)
     return arrays, kwargs, unit, out
@@ -867,19 +889,13 @@ def cross_like_a_v(a, v, *args, **kwargs):
 
 @function_helper
 def einsum(*operands, out=None, **kwargs):
-    from astropy.units import Quantity
-
     subscripts, *operands = operands
 
     if not isinstance(subscripts, str):
         raise ValueError('only "subscripts" string mode supported for einsum.')
 
     if out is not None:
-        if not isinstance(out, Quantity):
-            raise NotImplementedError
-
-        else:
-            kwargs["out"] = out.view(np.ndarray)
+        kwargs["out"] = _quantity_out_as_array(out)
 
     qs = _as_quantities(*operands)
     unit = functools.reduce(operator.mul, (q.unit for q in qs), dimensionless_unscaled)
diff --git a/docs/changes/coordinates/17358.bugfix.rst b/docs/changes/coordinates/17358.bugfix.rst
deleted file mode 100644
index 6d6076372b..0000000000
--- a/docs/changes/coordinates/17358.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Fixed instantiating ``Angle`` from a ``pandas`` ``Series`` object.
diff --git a/docs/changes/units/17354.bugfix.rst b/docs/changes/units/17354.bugfix.rst
new file mode 100644
index 0000000000..a22e1743ef
--- /dev/null
+++ b/docs/changes/units/17354.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed calling ``np.nanvar`` and ``np.nanstd`` with ``Quantity`` ``out`` argument.


Test patch:
diff --git a/astropy/coordinates/tests/test_arrays.py b/astropy/coordinates/tests/test_arrays.py
index bbb0570c54..102b0a4f41 100644
--- a/astropy/coordinates/tests/test_arrays.py
+++ b/astropy/coordinates/tests/test_arrays.py
@@ -67,17 +67,6 @@ def test_angle_from_pyarrow():
     npt.assert_array_equal(angle.value, input_data)
 
 
-def test_angle_from_pandas():
-    # see https://github.com/astropy/astropy/issues/17357
-    pd = pytest.importorskip("pandas")
-
-    input_data = ["10 0 0", "12 0 0"]
-    df = pd.DataFrame({"angle": input_data})
-    angle = Angle(df["angle"], unit=u.hourangle)
-    expected = Angle(input_data, u.hourangle)
-    npt.assert_array_equal(angle.value, expected.value)
-
-
 def test_dms():
     a1 = Angle([0, 45.5, -45.5], unit=u.degree)
     d, m, s = a1.dms
diff --git a/astropy/units/tests/test_quantity_non_ufuncs.py b/astropy/units/tests/test_quantity_non_ufuncs.py
index 68ef6b3364..40a514926f 100644
--- a/astropy/units/tests/test_quantity_non_ufuncs.py
+++ b/astropy/units/tests/test_quantity_non_ufuncs.py
@@ -1280,11 +1280,53 @@ class TestNanFunctions(InvariantUnitTestSetup):
     def test_nanstd(self):
         self.check(np.nanstd)
 
+    @pytest.mark.parametrize(
+        "out_init",
+        [
+            pytest.param(u.Quantity(-1, "m"), id="out with coorect unit"),
+            # this should work too: out.unit will be overridden
+            pytest.param(u.Quantity(-1), id="out with a different unit"),
+        ],
+    )
+    def test_nanstd_out(self, out_init):
+        out = out_init.copy()
+        o = np.nanstd(self.q, out=out)
+        assert o is out
+        assert o == np.nanstd(self.q)
+
+        # Also check array input, Quantity output.
+        out = out_init.copy()
+        o2 = np.nanstd(self.q.value, out=out)
+        assert o2 is out
+        assert o2.unit == u.dimensionless_unscaled
+        assert o2 == np.nanstd(self.q.value)
+
     def test_nanvar(self):
         out = np.nanvar(self.q)
         expected = np.nanvar(self.q.value) * self.q.unit**2
         assert np.all(out == expected)
 
+    @pytest.mark.parametrize(
+        "out_init",
+        [
+            pytest.param(u.Quantity(-1, "m"), id="out with coorect unit"),
+            # this should work too: out.unit will be overridden
+            pytest.param(u.Quantity(-1), id="out with a different unit"),
+        ],
+    )
+    def test_nanvar_out(self, out_init):
+        out = out_init.copy()
+        o = np.nanvar(self.q, out=out)
+        assert o is out
+        assert o == np.nanvar(self.q)
+
+        # Also check array input, Quantity output.
+        out = out_init.copy()
+        o2 = np.nanvar(self.q.value, out=out)
+        assert o2 is out
+        assert o2.unit == u.dimensionless_unscaled
+        assert o2 == np.nanvar(self.q.value)
+
     def test_nanprod(self):
         with pytest.raises(u.UnitsError):
             np.nanprod(self.q)