Repository: astropy/astropy

Issue:
astropy 6.0.1 breaks using quantities as unit exponents
### Description

This works in astropy 6.0.0:

```
u.m**u.Quantity(2)
```

but raises an exception in astropy 6.0.1:

```
u.m**u.Quantity(2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[2], line 1
----> 1 u.m**u.Quantity(2)

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/core.py:831, in UnitBase.__pow__(self, p)
    829 def __pow__(self, p):
    830     p = validate_power(p)
--> 831     return CompositeUnit(1, [self], [p], _error_check=False)

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/core.py:2341, in CompositeUnit.__init__(self, scale, bases, powers, decompose, decompose_bases, _error_check)
   2339         scale *= unit.scale**power
   2340         self._bases = unit.bases
-> 2341         self._powers = [
   2342             sanitize_power(operator.mul(*resolve_fractions(p, power)))
   2343             for p in unit.powers
   2344         ]
   2346     self._scale = sanitize_scale(scale)
   2347 else:
   2348     # Regular case: use inputs as preliminary scale, bases, and powers,
   2349     # then "expand and gather" identical bases, sanitize the scale, &c.

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/core.py:2342, in <listcomp>(.0)
   2339         scale *= unit.scale**power
   2340         self._bases = unit.bases
   2341         self._powers = [
-> 2342             sanitize_power(operator.mul(*resolve_fractions(p, power)))
   2343             for p in unit.powers
   2344         ]
   2346     self._scale = sanitize_scale(scale)
   2347 else:
   2348     # Regular case: use inputs as preliminary scale, bases, and powers,
   2349     # then "expand and gather" identical bases, sanitize the scale, &c.

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/utils.py:291, in sanitize_power(p)
    288 denom = getattr(p, "denominator", None)
    289 if denom is None:
    290     # This returns either a (simple) Fraction or the same float.
--> 291     p = maybe_simple_fraction(p)
    292     # If still a float, nothing more to be done.
    293     if isinstance(p, float):

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/utils.py:217, in maybe_simple_fraction(p, max_denominator)
    215 if p == 0 or p.__class__ is int or p.__class__ is Fraction:
    216     return p
--> 217 n, d = p.as_integer_ratio()
    218 a = n // d
    219 # Normally, start with 0,1 and 1,0; here we have applied first iteration.

File ~/.local/conda/envs/gammapy-dev/lib/python3.11/site-packages/astropy/units/quantity.py:1108, in Quantity.__getattr__(self, attr)
   1103 """
   1104 Quantities are able to directly convert to other units that
   1105 have the same physical type.
   1106 """
   1107 if not self._include_easy_conversion_members:
-> 1108     raise AttributeError(
   1109         f"'{self.__class__.__name__}' object has no '{attr}' member"
   1110     )
   1112 def get_virtual_unit_attribute():
   1113     registry = get_current_unit_registry().registry

AttributeError: 'Quantity' object has no 'as_integer_ratio' member
```



### Expected behavior

Bugfix releases don't introduce breaking changes

### How to Reproduce

```python
import astropy.units as u

u.m**u.Quantity(2)
```


### Versions

astropy 6.0.1


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#astropy-documentation-rules-and-guidelines)?
  - [ ] 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/workflow/development_workflow.html#rebase-if-necessary) and [squash](https://docs.astropy.org/en/latest/development/workflow/development_workflow.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/workflow/development_workflow.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?

Patch:
diff --git a/astropy/coordinates/matching.py b/astropy/coordinates/matching.py
index 585f2cb28f..aa219ab69e 100644
--- a/astropy/coordinates/matching.py
+++ b/astropy/coordinates/matching.py
@@ -455,11 +455,23 @@ def _get_cartesian_kdtree(coord, attrname_or_kdt="kdtree", forceunit=None):
 
     Returns
     -------
-    kdt : `~scipy.spatial.KDTree`
+    kdt : `~scipy.spatial.cKDTree` or `~scipy.spatial.KDTree`
         The KD-Tree representing the 3D cartesian representation of the input
         coordinates.
     """
-    from scipy.spatial import KDTree
+    from warnings import warn
+
+    # without scipy this will immediately fail
+    from scipy import spatial
+
+    try:
+        KDTree = spatial.cKDTree
+    except Exception:
+        warn(
+            "C-based KD tree not found, falling back on (much slower) "
+            "python implementation"
+        )
+        KDTree = spatial.KDTree
 
     if attrname_or_kdt is True:  # backwards compatibility for pre v0.4
         attrname_or_kdt = "kdtree"
@@ -491,9 +503,15 @@ def _get_cartesian_kdtree(coord, attrname_or_kdt="kdtree", forceunit=None):
         # There should be no NaNs in the kdtree data.
         if np.isnan(flatxyz.value).any():
             raise ValueError("Catalog coordinates cannot contain NaN entries.")
-        # Not obvious if compact_nodes=False, balanced_tree=False is still needed but
-        # we stay backwards-compatible with previous versions of `astropy` for now.
-        kdt = KDTree(flatxyz.value.T, compact_nodes=False, balanced_tree=False)
+        try:
+            # Set compact_nodes=False, balanced_tree=False to use
+            # "sliding midpoint" rule, which is much faster than standard for
+            # many common use cases
+            kdt = KDTree(flatxyz.value.T, compact_nodes=False, balanced_tree=False)
+        except TypeError:
+            # Python implementation does not take compact_nodes and balanced_tree
+            # as arguments.  However, it uses sliding midpoint rule by default
+            kdt = KDTree(flatxyz.value.T)
 
     if attrname_or_kdt:
         # cache the kdtree in `coord`
diff --git a/astropy/io/votable/tree.py b/astropy/io/votable/tree.py
index bec5411555..0a1c2fdfd4 100644
--- a/astropy/io/votable/tree.py
+++ b/astropy/io/votable/tree.py
@@ -2740,10 +2740,7 @@ class TableElement(
                 try:
                     colnumbers = [names.index(x) for x in columns]
                 except ValueError:
-                    missing_columns = [name for name in columns if name not in names]
-                    raise ValueError(
-                        f"Columns {missing_columns} were not found in fields list"
-                    ) from None
+                    raise ValueError(f"Columns '{columns}' not found in fields list")
             else:
                 raise TypeError("Invalid columns list")
 
diff --git a/astropy/table/operations.py b/astropy/table/operations.py
index 95a9ddb6dd..8da3f6cf4a 100644
--- a/astropy/table/operations.py
+++ b/astropy/table/operations.py
@@ -234,7 +234,7 @@ def join_distance(distance, kdtree_args=None, query_args=None):
     "fuzzy" match can apply to 1-D or 2-D columns, where in the latter case
     the distance is a vector distance.
 
-    The distance cross-matching is done using `scipy.spatial.KDTree`. If
+    The distance cross-matching is done using `scipy.spatial.cKDTree`. If
     necessary you can tweak the default behavior by providing ``dict`` values
     for the ``kdtree_args`` or ``query_args``.
 
@@ -243,9 +243,9 @@ def join_distance(distance, kdtree_args=None, query_args=None):
     distance : float or `~astropy.units.Quantity` ['length']
         Maximum distance between points to be considered a join match
     kdtree_args : dict, None
-        Optional extra args for `~scipy.spatial.KDTree`
+        Optional extra args for `~scipy.spatial.cKDTree`
     query_args : dict, None
-        Optional extra args for `~scipy.spatial.KDTree.query_ball_tree`
+        Optional extra args for `~scipy.spatial.cKDTree.query_ball_tree`
 
     Returns
     -------
@@ -275,7 +275,7 @@ def join_distance(distance, kdtree_args=None, query_args=None):
 
     """
     try:
-        from scipy.spatial import KDTree
+        from scipy.spatial import cKDTree
     except ImportError as exc:
         raise ImportError("scipy is required to use join_distance()") from exc
 
@@ -306,8 +306,8 @@ def join_distance(distance, kdtree_args=None, query_args=None):
             col2.shape = col2.shape + (1,)
 
         # Cross-match col1 and col2 within dist using KDTree
-        kd1 = KDTree(col1, **kdtree_args)
-        kd2 = KDTree(col2, **kdtree_args)
+        kd1 = cKDTree(col1, **kdtree_args)
+        kd2 = cKDTree(col2, **kdtree_args)
         nears = kd1.query_ball_tree(kd2, r=dist, **query_args)
 
         # Output of above is nears which is a list of lists, where the outer
diff --git a/astropy/units/utils.py b/astropy/units/utils.py
index cf40f8318a..c62e2f0b90 100644
--- a/astropy/units/utils.py
+++ b/astropy/units/utils.py
@@ -214,7 +214,7 @@ def maybe_simple_fraction(p, max_denominator=100):
     """
     if p == 0 or p.__class__ is int or p.__class__ is Fraction:
         return p
-    n, d = p.as_integer_ratio()
+    n, d = float(p).as_integer_ratio()
     a = n // d
     # Normally, start with 0,1 and 1,0; here we have applied first iteration.
     n0, d0 = 1, 0
diff --git a/docs/changes/io.votable/15956.bugfix.rst b/docs/changes/io.votable/15956.bugfix.rst
deleted file mode 100644
index 87c5f4da14..0000000000
--- a/docs/changes/io.votable/15956.bugfix.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-When reading a VOTable, if some user-requested columns were not present then the
-resulting error message previously listed all the requested column names.
-Now only columns that are actually missing are shown.
diff --git a/docs/changes/units/16261.bugfix.rst b/docs/changes/units/16261.bugfix.rst
new file mode 100644
index 0000000000..ed5186b0cd
--- /dev/null
+++ b/docs/changes/units/16261.bugfix.rst
@@ -0,0 +1,2 @@
+Using a dimensionless ``Quantity`` as an exponent works anew.
+In astropy 6.0.1 an exception was erroneously raised.
diff --git a/docs/table/operations.rst b/docs/table/operations.rst
index 85e9a4835a..5e494569f5 100644
--- a/docs/table/operations.rst
+++ b/docs/table/operations.rst
@@ -1186,8 +1186,8 @@ a generic distance between column values using the
 :func:`~astropy.table.join_distance` join function. This can apply to 1D or 2D
 (vector) columns. This will look very similar to the coordinates example, but
 here there is a bit more flexibility. The matching is done using
-:class:`scipy.spatial.KDTree` and
-:meth:`scipy.spatial.KDTree.query_ball_tree`, and the behavior of these can be
+:class:`scipy.spatial.cKDTree` and
+:meth:`scipy.spatial.cKDTree.query_ball_tree`, and the behavior of these can be
 controlled via the ``kdtree_args`` and ``query_args`` arguments, respectively.
 
 .. _unique-rows:


Test patch:
diff --git a/astropy/coordinates/tests/test_matching.py b/astropy/coordinates/tests/test_matching.py
index 91b6731a6c..b642cac437 100644
--- a/astropy/coordinates/tests/test_matching.py
+++ b/astropy/coordinates/tests/test_matching.py
@@ -119,6 +119,20 @@ def test_kdtree_storage(functocheck, args, defaultkdtname, bothsaved):
     assert "KD" in e.value.args[0]
 
 
+@pytest.mark.skipif(not HAS_SCIPY, reason="Requires scipy.")
+def test_python_kdtree(monkeypatch):
+    from astropy.coordinates import ICRS
+
+    cmatch = ICRS([4, 2.1] * u.degree, [0, 0] * u.degree, distance=[1, 2] * u.kpc)
+    ccatalog = ICRS(
+        [1, 2, 3, 4] * u.degree, [0, 0, 0, 0] * u.degree, distance=[1, 2, 3, 4] * u.kpc
+    )
+
+    monkeypatch.delattr("scipy.spatial.cKDTree")
+    with pytest.warns(UserWarning, match=r"C-based KD tree not found"):
+        matching.match_coordinates_sky(cmatch, ccatalog)
+
+
 @pytest.mark.skipif(not HAS_SCIPY, reason="Requires scipy.")
 def test_matching_method():
     from astropy.coordinates import ICRS, SkyCoord
diff --git a/astropy/io/votable/tests/test_vo.py b/astropy/io/votable/tests/test_vo.py
index 2fbde75df1..fae7ac66d2 100644
--- a/astropy/io/votable/tests/test_vo.py
+++ b/astropy/io/votable/tests/test_vo.py
@@ -8,7 +8,6 @@ import difflib
 import gzip
 import io
 import pathlib
-import re
 import sys
 from unittest import mock
 
@@ -253,27 +252,6 @@ class TestReferences:
         assert len(list(self.votable.iter_coosys())) == 1
 
 
-@pytest.mark.parametrize(
-    "columns, expected_missing",
-    [
-        # a single non-existent column
-        pytest.param(["c1"], ["c1"], id="basic"),
-        # multiple missing columns (checking that order is preserved)
-        pytest.param(["c1", "c2", "c3"], ["c1", "c2", "c3"], id="check-ordering"),
-        # mixing existing with missing columns
-        pytest.param(["c1", "string_test", "c2"], ["c1", "c2"], id="list-only-missing"),
-    ],
-)
-def test_select_missing_columns_error_message(columns, expected_missing):
-    # see https://github.com/astropy/astropy/pull/15956
-    filename = get_pkg_data_filename("data/regression.xml")
-    with pytest.raises(
-        ValueError,
-        match=re.escape(f"Columns {expected_missing!r} were not found in fields list"),
-    ):
-        parse_single_table(filename, columns=columns)
-
-
 def test_select_columns_by_index():
     columns = [0, 5, 13]
     table = parse(
diff --git a/astropy/units/tests/test_quantity.py b/astropy/units/tests/test_quantity.py
index 1b1e571e9a..7463425121 100644
--- a/astropy/units/tests/test_quantity.py
+++ b/astropy/units/tests/test_quantity.py
@@ -16,6 +16,7 @@ from astropy.units.quantity import _UNIT_NOT_INITIALISED
 from astropy.utils import isiterable, minversion
 from astropy.utils.compat import COPY_IF_NEEDED
 from astropy.utils.exceptions import AstropyWarning
+from astropy.utils.masked import Masked
 
 """ The Quantity class will represent a number + unit + uncertainty """
 
@@ -468,6 +469,16 @@ class TestQuantityOperations:
         assert_array_almost_equal(new_quantity.value, 1489.355288, decimal=7)
         assert new_quantity.unit == u.Unit("m^3")
 
+    @pytest.mark.parametrize(
+        "exponent_type",
+        [int, float, np.uint64, np.int32, np.float32, u.Quantity, Masked],
+    )
+    def test_quantity_as_power(self, exponent_type):
+        # raise unit to a dimensionless Quantity power
+        # regression test for https://github.com/astropy/astropy/issues/16260
+        q = u.m ** exponent_type(2)
+        assert q == u.m**2
+
     def test_matrix_multiplication(self):
         a = np.eye(3)
         q = a * u.m