Repository: astropy/astropy

Issue:
Cannot build Angle from pyarrow array
### Description

`Angle` constructor fails when used with a float `pyarrow` array.

### Expected behavior

Input pyarrow array is wrapped into a numpy array and everything works correctly, just like with `Quantity`

### How to Reproduce

```python
import pyarrow as pa
from astropy.coordinates import Angle

Angle(pa.array([1.1, 2.2]), 'deg')
```

<details><summary>Error</summary>

```
TypeError                                 Traceback (most recent call last)
Cell In[1], line 4
      1 import pyarrow as pa
      2 from astropy.coordinates import Angle
----> 4 Angle(pa.array([1.1, 2.2]), 'deg')

File ~/.virtualenvs/v/lib/python3.12/site-packages/astropy/coordinates/angles/core.py:186, in Angle.__new__(cls, angle, unit, dtype, copy, **kwargs)
    181             angle = u.Quantity(angle, angle_unit, copy=COPY_IF_NEEDED)
    183     elif isiterable(angle) and not (
    184         isinstance(angle, np.ndarray) and angle.dtype.kind not in "SUVO"
    185     ):
--> 186         angle = [Angle(x, unit, copy=COPY_IF_NEEDED) for x in angle]
    188 return super().__new__(cls, angle, unit, dtype=dtype, copy=copy, **kwargs)

File ~/.virtualenvs/v/lib/python3.12/site-packages/astropy/coordinates/angles/core.py:188, in Angle.__new__(cls, angle, unit, dtype, copy, **kwargs)
    183     elif isiterable(angle) and not (
    184         isinstance(angle, np.ndarray) and angle.dtype.kind not in "SUVO"
    185     ):
    186         angle = [Angle(x, unit, copy=COPY_IF_NEEDED) for x in angle]
--> 188 return super().__new__(cls, angle, unit, dtype=dtype, copy=copy, **kwargs)

File ~/.virtualenvs/v/lib/python3.12/site-packages/astropy/units/quantity.py:557, in Quantity.__new__(cls, value, unit, dtype, copy, order, subok, ndmin)
    553 # check that array contains numbers or long int objects
    554 if value.dtype.kind in "OSU" and not (
    555     value.dtype.kind == "O" and isinstance(value.item(0), numbers.Number)
    556 ):
--> 557     raise TypeError("The value must be a valid Python or Numpy numeric type.")
    559 # by default, cast any integer, boolean, etc., to float
    560 if float_default and value.dtype.kind in "iuO":

TypeError: The value must be a valid Python or Numpy numeric type.
```

</details>


### Versions

```python
import astropy
try:
    astropy.system_info()
except AttributeError:
    import platform; print(platform.platform())
    import sys; print("Python", sys.version)
    import astropy; print("astropy", astropy.__version__)
    import numpy; print("Numpy", numpy.__version__)
    import erfa; print("pyerfa", erfa.__version__)
    try:
        import scipy
        print("Scipy", scipy.__version__)
    except ImportError:
        print("Scipy not installed")
    try:
        import matplotlib
        print("Matplotlib", matplotlib.__version__)
    except ImportError:
        print("Matplotlib not installed")
```
```
macOS-15.0.1-arm64-arm-64bit
Python 3.12.7 (main, Oct  1 2024, 02:05:46) [Clang 16.0.0 (clang-1600.0.26.3)]
astropy 6.1.4
Numpy 2.0.2
pyerfa 2.0.1.4
Scipy 1.13.0
Matplotlib 3.9.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?
pllim: Do we really need to backport this all the way to 6.1.x ? I think 6.x series will be retired when v7.0 is out, no?
pllim: Yes, I *think* there will be a 6.1.5 but it would probably be right before 7.0.0 anyway. I guess does not hurt. 🤷‍♀️ 
eerovaher: Can you clarify if this solution is compatible with the [array API](https://data-apis.org/array-api/latest/index.html)?

The patch here concerns `Angle` and from what I can tell also `Longitude` and `Latitude`. Have you checked if `Distance` or `SpectralQuantity` require analogous changes?
neutrinoceros: > Can you clarify if this solution is compatible with the [array API](https://data-apis.org/array-api/latest/index.html)?

After some research I don't think the `__array__` protocol (required for `np.asarray` to work) is part of the Array API, but it's part of an older set of rules to implement interoperability *with* numpy. I *think* (though I don't have a reference for this), that any array implementation that aims at supporting the Array API would also implement this protocol.
See
https://numpy.org/doc/stable/user/basics.interoperability.html#using-arbitrary-objects-in-numpy

I think one could describe this solution as "compatible with the Array API", though it doesn't avoid conversion.

> The patch here concerns Angle and from what I can tell also Longitude and Latitude. Have you checked if Distance or SpectralQuantity require analogous changes?

Thanks for asking. I'll do it now !

neutrinoceros: `Distance` and `SpectralQuantity` do not choke on pyarrow.array, either on main, or this branch
eerovaher: Why is it that `Angle` needs this compatibility code but `Distance` does not?
neutrinoceros: > Why is it that Angle needs this compatibility code but Distance does not?

That's because of the special logic in `Angle.__new__` that's meant to support instanciating as, e.g., `Angle(["90d", "89d"])`, which is currently where things end up if you pass any iterable (other than a numpy array with dtype.kind in `"S"`, `"U"`, `"V"` or `"O"`).

https://github.com/astropy/astropy/blob/24eb66392b97e9685eafd7f75e917b8782abf13f/astropy/coordinates/angles/core.py#L183-L186
neutrinoceros: updated with all suggestions taken into account !
neutrinoceros: history rewritten as 2 commits (test, fix) as requested by @eerovaher 
neutrinoceros: > I'm not sure this is correct for the general case that something is happy to be converted to ndarray (as indicated by __array__). What if that array minic is not numeric? Should one not go via the iteration in that case?

what do you mean by "minic" ? (I assume that's a typo but I'd like to avoid extrapolation)

> I wonder if tests pass if we did an unconditional conversion to array first, as in:
>
>```python
> array = np.asanyarray(array)
>if array.dtype.kind in "SUVO":
>    angle = [cls(...)...]
>```
> But that may be too drastic. 


Here's my best take on this suggestion
```patch
diff --git a/astropy/coordinates/angles/core.py b/astropy/coordinates/angles/core.py
index 10e40e76af..7bb41c91e8 100644
--- a/astropy/coordinates/angles/core.py
+++ b/astropy/coordinates/angles/core.py
@@ -180,16 +180,12 @@ class Angle(SpecificTypeQuantity):
                     # Possible conversion to `unit` will be done below.
                     angle = u.Quantity(angle, angle_unit, copy=COPY_IF_NEEDED)
 
-            elif isinstance(angle, np.ndarray):
+            else:
+                angle = np.asanyarray(angle)
                 if angle.dtype.kind in "SUVO":
                     angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
 
-            elif hasattr(angle, "__array__"):
-                angle = np.asarray(angle)
-
-            elif isiterable(angle):
-                angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
-
         return super().__new__(cls, angle, unit, dtype=dtype, copy=copy, **kwargs)
 
     @staticmethod
```
and no, it doesn't survive testing: it'll choke on a list of scalar Quantities with different units (which we actually exercise, so it seems, e.g. `[<Quantity 1. deg>, <Quantity 1. hourangle>]`).

> Alternatively, really that isiterable check is just meant for tuple and list, perhaps we should not try to generalize it...

the following indeed passes tests
```patch
diff --git a/astropy/coordinates/angles/core.py b/astropy/coordinates/angles/core.py
index 10e40e76af..366a6dc5a5 100644
--- a/astropy/coordinates/angles/core.py
+++ b/astropy/coordinates/angles/core.py
@@ -184,12 +184,12 @@ class Angle(SpecificTypeQuantity):
                 if angle.dtype.kind in "SUVO":
                     angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
 
+            elif isinstance(angle, (list, tuple)):
+                angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
+
             elif hasattr(angle, "__array__"):
                 angle = np.asarray(angle)
 
-            elif isiterable(angle):
-                angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
-
         return super().__new__(cls, angle, unit, dtype=dtype, copy=copy, **kwargs)
 
     @staticmethod
```
... but I'm concerned about this subtly breaking APIs for anyone out there who would be instanciating from `dict_values` (possibly without being aware of it)
mhvk: I indeed meant "mimic" - and probably should just have refrained from commenting without enough time. Best to keep the change minimal.

Patch:
diff --git a/astropy/coordinates/angles/core.py b/astropy/coordinates/angles/core.py
index 6d2ff95859..10e40e76af 100644
--- a/astropy/coordinates/angles/core.py
+++ b/astropy/coordinates/angles/core.py
@@ -180,9 +180,14 @@ class Angle(SpecificTypeQuantity):
                     # Possible conversion to `unit` will be done below.
                     angle = u.Quantity(angle, angle_unit, copy=COPY_IF_NEEDED)
 
-            elif isiterable(angle) and not (
-                isinstance(angle, np.ndarray) and angle.dtype.kind not in "SUVO"
-            ):
+            elif isinstance(angle, np.ndarray):
+                if angle.dtype.kind in "SUVO":
+                    angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
+
+            elif hasattr(angle, "__array__"):
+                angle = np.asarray(angle)
+
+            elif isiterable(angle):
                 angle = [cls(x, unit, copy=COPY_IF_NEEDED) for x in angle]
 
         return super().__new__(cls, angle, unit, dtype=dtype, copy=copy, **kwargs)
diff --git a/docs/changes/coordinates/17263.bugfix.rst b/docs/changes/coordinates/17263.bugfix.rst
new file mode 100644
index 0000000000..fb618bdb70
--- /dev/null
+++ b/docs/changes/coordinates/17263.bugfix.rst
@@ -0,0 +1,2 @@
+Fix a crash when instantiating ``Angle`` (or ``Latitude``, or ``Longitude``)
+from a non-numpy array (for instance pyarrow arrays).


Test patch:
diff --git a/astropy/coordinates/tests/test_arrays.py b/astropy/coordinates/tests/test_arrays.py
index c02cc9406d..102b0a4f41 100644
--- a/astropy/coordinates/tests/test_arrays.py
+++ b/astropy/coordinates/tests/test_arrays.py
@@ -55,6 +55,18 @@ def test_angle_arrays():
         Angle(["04:02:02", "03:02:01", "06:02:01"])
 
 
+def test_angle_from_pyarrow():
+    # Creating Angle instances from some array classes (e.g. pyarrow.array) failed
+    # even though creating Quantity instances succeeded.
+    # see https://github.com/astropy/astropy/issues/17255
+    pa = pytest.importorskip("pyarrow")
+
+    input_data = [1.1, 2.2]
+    arr = pa.array(input_data)
+    angle = Angle(arr, "deg")
+    npt.assert_array_equal(angle.value, input_data)
+
+
 def test_dms():
     a1 = Angle([0, 45.5, -45.5], unit=u.degree)
     d, m, s = a1.dms