Repository: astropy/astropy

Issue:
erfa.p2s does not accept units except for distance
### Description

In https://github.com/liberfa/pyerfa/issues/149, @maxnoe reported the following error, which had him puzzled (transferred here since `astropy.units.quantity_helpers.erfa` is responsible for making `pyerfa` work with units).

```python
from erfa.ufunc import s2p as spherical_to_cartesian
import astropy.units as u
import numpy  as np
 
lon = np.pi / 2
lat = np.pi / 4
 
spherical_to_cartesian(lon, lat, 5 * u.km)
```

results in this relatively unhelpful error:

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 8
      5 lon = np.pi / 2
      6 lat = np.pi / 4
----> 8 spherical_to_cartesian(lon, lat, 5 * u.km)

File ~/.local/conda/envs/cta-dev/lib/python3.11/site-packages/astropy/units/quantity.py:691, in Quantity.__array_ufunc__(self, function, method, *inputs, **kwargs)
    689     return NotImplemented
    690 else:
--> 691     raise e

File ~/.local/conda/envs/cta-dev/lib/python3.11/site-packages/astropy/units/quantity.py:636, in Quantity.__array_ufunc__(self, function, method, *inputs, **kwargs)
    631 # Determine required conversion functions -- to bring the unit of the
    632 # input to that expected (e.g., radian for np.sin), or to get
    633 # consistent units between two inputs (e.g., in np.add) --
    634 # and the unit of the result (or tuple of units for nout > 1).
    635 try:
--> 636     converters, unit = converters_and_unit(function, method, *inputs)
    638     out = kwargs.get("out", None)
    639     # Avoid loop back by turning any Quantity output into array views.

File ~/.local/conda/envs/cta-dev/lib/python3.11/site-packages/astropy/units/quantity_helper/converters.py:181, in converters_and_unit(function, method, *args)
    178 units = [getattr(arg, "unit", None) for arg in args]
    180 # Determine possible conversion functions, and the result unit.
--> 181 converters, result_unit = ufunc_helper(function, *units)
    183 if any(converter is False for converter in converters):
    184     # for multi-argument ufuncs with a quantity and a non-quantity,
    185     # the quantity normally needs to be dimensionless, *except*
   (...)
    188     # can just have the unit of the quantity
    189     # (this allows, e.g., `q > 0.` independent of unit)
    190     try:
    191         # Don't fold this loop in the test above: this rare case
    192         # should not make the common case slower.

File ~/.local/conda/envs/cta-dev/lib/python3.11/site-packages/astropy/units/quantity_helper/erfa.py:70, in helper_s2p(f, unit1, unit2, unit3)
     67 from astropy.units.si import radian
     69 try:
---> 70     return [get_converter(unit1, radian), get_converter(unit2, radian), None], unit3
     71 except UnitsError:
     72     raise UnitTypeError(
     73         f"Can only apply '{f.__name__}' function to quantities with angle units"
     74     )

File ~/.local/conda/envs/cta-dev/lib/python3.11/site-packages/astropy/units/quantity_helper/helpers.py:42, in get_converter(from_unit, to_unit)
     38 def get_converter(from_unit, to_unit):
     39     """Like Unit._get_converter, except returns None if no scaling is needed,
     40     i.e., if the inferred scale is unity.
     41     """
---> 42     converter = from_unit._get_converter(to_unit)
     43     return None if converter is unit_scale_converter else converter

AttributeError: 'NoneType' object has no attribute '_get_converter'
```

### Expected behavior

_No response_

### How to Reproduce

_No response_

### 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")
```
```
# Paste the result here
```


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.

Patch:
diff --git a/astropy/units/quantity_helper/helpers.py b/astropy/units/quantity_helper/helpers.py
index 4c05dafc5f..d47d28e5a0 100644
--- a/astropy/units/quantity_helper/helpers.py
+++ b/astropy/units/quantity_helper/helpers.py
@@ -7,6 +7,7 @@ In particular, this implements the logic that determines scaling and result
 units for a given ufunc, given input units.
 """
 
+import sys
 from fractions import Fraction
 
 import numpy as np
@@ -34,7 +35,28 @@ def get_converter(from_unit, to_unit):
     """Like Unit.get_converter, except returns None if no scaling is needed,
     i.e., if the inferred scale is unity.
     """
-    converter = from_unit.get_converter(to_unit)
+    try:
+        converter = from_unit.get_converter(to_unit)
+    except AttributeError as e:
+        # Check for lack of unit only now, to avoid delay for cases where a unit
+        # unit was present. Note that cases where dimensionless is expected are
+        # already short-circuited; here, we cover just the case where, e.g., the
+        # user has done u.add_enabled_equivalencies(u.dimensionless_angles()).
+        if from_unit is not None:  # pragma: no cover
+            raise
+        try:
+            converter = dimensionless_unscaled.get_converter(to_unit)
+        except UnitsError:
+            note = (
+                "Input without a 'unit' attribute? Such input is treated "
+                f"as dimensionless and cannot be converted to {to_unit}."
+            )
+            if sys.version_info >= (3, 11):
+                e.add_note(note)
+                raise e
+            else:
+                raise AttributeError("\n".join([e.args[0], note])) from None
+
     return None if converter is unit_scale_converter else converter
 
 


Test patch:
diff --git a/astropy/units/tests/test_quantity_erfa_ufuncs.py b/astropy/units/tests/test_quantity_erfa_ufuncs.py
index 56461372f4..fcb6088cd2 100644
--- a/astropy/units/tests/test_quantity_erfa_ufuncs.py
+++ b/astropy/units/tests/test_quantity_erfa_ufuncs.py
@@ -123,6 +123,20 @@ class TestPVUfuncs:
         assert_quantity_allclose(pv["p"], self.pv["p"], atol=1 * u.m, rtol=0)
         assert_quantity_allclose(pv["v"], self.pv["v"], atol=1 * u.mm / u.s, rtol=0)
 
+    def test_s2p_not_all_quantity(self):
+        # Test for a useful error message - see gh-16873.
+        # Non-quantity input should be treated as dimensionless and thus cannot
+        # be converted to radians.
+        with pytest.raises(AttributeError, match="is treated as dimensionless"):
+            erfa_ufunc.s2p(0.5, 0.5, 4 * u.km)
+
+        # Except if we have the right equivalency in place.
+        with u.add_enabled_equivalencies(u.dimensionless_angles()):
+            result = erfa_ufunc.s2p(0.5, 0.5, 4 * u.km)
+
+        expected = erfa_ufunc.s2p(0.5 * u.radian, 0.5 * u.radian, 4 * u.km)
+        assert_array_equal(result, expected)
+
     def test_pvstar(self):
         ra, dec, pmr, pmd, px, rv, stat = erfa_ufunc.pvstar(self.pv)
         assert_array_equal(stat, np.zeros(self.pv.shape, dtype="i4"))
@@ -528,7 +542,7 @@ class TestGeodetic:
     def test_unit_errors(self):
         """Test unit errors when dimensionless parameters are used"""
 
-        msg = "'NoneType' object has no attribute 'get_converter'"
+        msg = "'NoneType'.*no attribute 'get_converter'.*\n.*treated as dimensionless"
         with pytest.raises(AttributeError, match=msg):
             erfa_ufunc.gc2gde(self.equatorial_radius_value, self.flattening, self.xyz)
         with pytest.raises(AttributeError, match=msg):
diff --git a/astropy/units/tests/test_quantity_ufuncs.py b/astropy/units/tests/test_quantity_ufuncs.py
index 3514afd476..2e35e52831 100644
--- a/astropy/units/tests/test_quantity_ufuncs.py
+++ b/astropy/units/tests/test_quantity_ufuncs.py
@@ -323,6 +323,21 @@ class TestQuantityTrigonometricFuncs:
     def test_testwarns(self, tw):
         return test_testwarn(tw)
 
+    def test_sin_with_quantity_out(self):
+        # Test for a useful error message - see gh-16873.
+        # Non-quantity input should be treated as dimensionless and thus cannot
+        # be converted to radians.
+        out = u.Quantity(0)
+        with pytest.raises(AttributeError, match="is treated as dimensionless"):
+            np.sin(0.5, out=out)
+
+        # Except if we have the right equivalency in place.
+        with u.add_enabled_equivalencies(u.dimensionless_angles()):
+            result = np.sin(0.5, out=out)
+
+        assert result is out
+        assert result == np.sin(0.5) * u.dimensionless_unscaled
+
 
 class TestQuantityMathFuncs:
     """