From ef4b0f9c6ae052e96eba327353c643a845dd236c Mon Sep 17 00:00:00 2001 From: "Liang, Jingyi" <619631993@qq.com> Date: Fri, 23 Oct 2026 22:14:48 +0800 Subject: [PATCH] Fix query on coordinate-only datasets (GH6449) Include coordinate names in the pandas resolver scope while retaining the Dataset as the first resolver. Extend query coverage to coordinate-only datasets and explicit and virtual dimension coordinates. Co-authored-by: Codex --- doc/whats-new.rst | 3 +++ xarray/core/dataset.py | 7 +++++-- xarray/tests/test_dataset.py | 25 +++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 21f3671188a..bf50d2c4ba2 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -165,6 +165,9 @@ Deprecations Bug Fixes ~~~~~~~~~ +- :py:meth:`Dataset.query` now resolves coordinates in datasets with no data + variables (:issue:`6449`). + By `Liang, Jingyi `_. - Results of the string accessor methods ``strip``, ``pad``, ``center``, ``ljust``, ``rjust``, ``zfill`` and ``replace`` on dask arrays are no longer truncated to a single character, and ``repeat`` no longer fails on dask arrays diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 7976a8b5f62..2e23a5dfaab 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -10021,9 +10021,12 @@ def query( msg = f"expr for dim {dim} must be a string to be evaluated, {type(expr)} given" raise ValueError(msg) - # evaluate the queries to create the indexers + # Include coords so pandas considers the resolvers nonempty even when + # there are no data variables. indexers = { - dim: pd.eval(expr, resolvers=[self], parser=parser, engine=engine) + dim: pd.eval( + expr, resolvers=[self, self.coords], parser=parser, engine=engine + ) for dim, expr in queries.items() } diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index a0887c5b9cb..4daa9a06741 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -37,7 +37,7 @@ from xarray.core.common import duck_array_ops, full_like from xarray.core.coordinates import Coordinates, DatasetCoordinates from xarray.core.indexes import Index, PandasIndex -from xarray.core.types import ArrayLike +from xarray.core.types import ArrayLike, QueryEngineOptions, QueryParserOptions from xarray.core.utils import is_scalar from xarray.groupers import SeasonResampler, TimeResampler from xarray.namedarray.pycompat import array_type, integer_types @@ -7926,11 +7926,18 @@ def test_astype_attrs(self) -> None: assert not data.astype(float, keep_attrs=False).attrs assert not data.astype(float, keep_attrs=False).var1.attrs + @pytest.mark.parametrize("coords_only", [False, True]) @pytest.mark.parametrize("parser", ["pandas", "python"]) @pytest.mark.parametrize( "engine", ["python", None, pytest.param("numexpr", marks=[requires_numexpr])] ) - def test_query(self, use_dask: bool, engine, parser) -> None: + def test_query( + self, + use_dask: bool, + engine: QueryEngineOptions, + parser: QueryParserOptions, + coords_only: bool, + ) -> None: """Test querying a dataset.""" # setup test data np.random.seed(42) @@ -7985,6 +7992,9 @@ def test_query(self, use_dask: bool, engine, parser) -> None: }, ) + if coords_only: + ds = ds.set_coords(list(ds.data_vars)) + # query single dim, single variable with raise_if_dask_computes(): actual = ds.query(x="a2 > 5", engine=engine, parser=parser) @@ -8077,6 +8087,17 @@ def test_query(self, use_dask: bool, engine, parser) -> None: expect = ds.isel(dict(x=(a > 5), y=(c < 0.5), z=(d == "bar"))) assert_identical(expect, actual) + # query a dimension without a coordinate + with raise_if_dask_computes(): + actual = ds.query(x="x > 5", engine=engine, parser=parser) + assert_identical(ds.isel(x=slice(6, None)), actual) + + # query a dimension coordinate + ds = ds.assign_coords(x=a + 10) + with raise_if_dask_computes(): + actual = ds.query(x="x > 15", engine=engine, parser=parser) + assert_identical(ds.isel(x=slice(6, None)), actual) + # test error handling with pytest.raises(ValueError): ds.query("a > 5") # type: ignore[arg-type] # must be dict or kwargs