diff --git a/doc/api.rst b/doc/api.rst index 6ed8d513934..66cd66db28a 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1107,6 +1107,10 @@ See the :ref:`documentation page on testing ` for a guide on testing.strategies.dimension_sizes testing.strategies.attrs testing.strategies.variables + testing.strategies.coordinate_variables + testing.strategies.dataarrays + testing.strategies.data_variables + testing.strategies.datasets testing.strategies.unique_subset_of Exceptions diff --git a/doc/conf.py b/doc/conf.py index 93a0e459a33..a00e2e7ec37 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -329,6 +329,8 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "cftime": ("https://unidata.github.io/cftime", None), + "sparse": ("https://sparse.pydata.org/en/latest/", None), + "hypothesis": ("https://hypothesis.readthedocs.io/en/latest/", None), "cubed": ("https://cubed-dev.github.io/cubed/", None), "dask": ("https://docs.dask.org/en/latest", None), "datatree": ("https://xarray-datatree.readthedocs.io/en/latest/", None), diff --git a/doc/user-guide/testing.rst b/doc/user-guide/testing.rst index d82d9d7d7d9..4e686722b21 100644 --- a/doc/user-guide/testing.rst +++ b/doc/user-guide/testing.rst @@ -51,6 +51,10 @@ These strategies are accessible in the :py:mod:`xarray.testing.strategies` modul testing.strategies.dimension_sizes testing.strategies.attrs testing.strategies.variables + testing.strategies.coordinate_variables + testing.strategies.dataarrays + testing.strategies.data_variables + testing.strategies.datasets testing.strategies.unique_subset_of These build upon the numpy and array API strategies offered in :py:mod:`hypothesis.extra.numpy` and :py:mod:`hypothesis.extra.array_api`: @@ -89,7 +93,6 @@ In your tests however you should not use ``.example()`` - instead you should par def test_function_that_acts_on_variables(var): assert func(var) == ... - Chaining Strategies ~~~~~~~~~~~~~~~~~~~ @@ -145,6 +148,7 @@ objects your chained strategy will generate. fixed_x_variable_y_maybe_z = st.fixed_dictionaries( {"x": st.just(2), "y": st.integers(3, 4)}, optional={"z": st.just(2)} ) + fixed_x_variable_y_maybe_z.example() special_variables = xrst.variables(dims=fixed_x_variable_y_maybe_z) @@ -156,6 +160,7 @@ Here we have used one of hypothesis' built-in strategies :py:func:`hypothesis.st strategy which generates mappings of dimension names to lengths (i.e. the ``size`` of the xarray object we want). This particular strategy will always generate an ``x`` dimension of length 2, and a ``y`` dimension of length either 3 or 4, and will sometimes also generate a ``z`` dimension of length 2. + By feeding this strategy for dictionaries into the ``dims`` argument of xarray's :py:func:`~st.variables` strategy, we can generate arbitrary :py:class:`~xarray.Variable` objects whose dimensions will always match these specifications. diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 3c6b7bfb58d..d7bd3a98f68 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -26,6 +26,10 @@ New Features By `Tom White `_. - Allow data variable specific ``constant_values`` in the dataset ``pad`` function (:pull:`9353``). By `Tiago Sanona `_. +- Added a suite of hypothesis strategies for generating xarray objects containing arbitrary data, useful for testing. + Accessible under :py:func:`testing.strategies`, and documented in a new page on testing in the User Guide. + (:issue:`6911`, :pull:`6908`) + By `Tom Nicholas `_. Breaking changes ~~~~~~~~~~~~~~~~ diff --git a/xarray/testing/strategies.py b/xarray/testing/strategies.py index b76733d113f..1f7b3e7a819 100644 --- a/xarray/testing/strategies.py +++ b/xarray/testing/strategies.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from collections.abc import Hashable, Iterable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Protocol, overload +from typing import TYPE_CHECKING, Any, Optional, Protocol, Union, overload try: import hypothesis.strategies as st @@ -27,6 +29,10 @@ "dimension_sizes", "attrs", "variables", + "coordinate_variables", + "dataarrays", + "data_variables", + "datasets", "unique_subset_of", ] @@ -35,8 +41,8 @@ class ArrayStrategyFn(Protocol[T_DuckArray]): def __call__( self, *, - shape: "_ShapeLike", - dtype: "_DTypeLikeNested", + shape: _ShapeLike, + dtype: _DTypeLikeNested, ) -> st.SearchStrategy[T_DuckArray]: ... @@ -467,3 +473,359 @@ def unique_subset_of( return ( {k: objs[k] for k in subset_keys} if isinstance(objs, Mapping) else subset_keys ) + + +@st.composite +def _alignable_variables( + draw: st.DrawFn, + *, + var_names: st.SearchStrategy[str], + dim_sizes: Mapping[Hashable, int], +) -> Mapping[Hashable, xr.Variable]: + """ + Generates dicts of names mapping to variables with compatible (i.e. alignable) dimensions and sizes. + """ + + alignable_dim_sizes = draw(unique_subset_of(dim_sizes)) if dim_sizes else {} + + vars = variables(dims=st.just(alignable_dim_sizes)) + # TODO don't hard code max number of variables + return draw(st.dictionaries(var_names, vars, max_size=3)) + + +@st.composite +def coordinate_variables( + draw: st.DrawFn, + *, + dim_sizes: Mapping[Hashable, int], + coord_names: st.SearchStrategy[Hashable] = names(), +) -> Mapping[Hashable, xr.Variable]: + """ + Generates dicts of alignable Variable objects for use as coordinates. + + Differs from data_variables strategy in that it deliberately creates dimension coordinates + (i.e. 1D variables with the same name as a dimension) as well as non-dimension coordinates. + + Requires the hypothesis package to be installed. + + Parameters + ---------- + dim_sizes: Mapping of str to int + Sizes of dimensions to use for coordinates. + coord_names: Strategy generating strings, optional + Allowed names for non-dimension coordinates. Defaults to `names` strategy. + """ + + all_coords = {} + + # Allow for no coordinate variables - explicit possibility not to helps with shrinking + if draw(st.booleans()): + dim_names = list(dim_sizes.keys()) + + # Possibly generate 1D "dimension coordinates" - explicit possibility not to helps with shrinking + if len(dim_names) > 0 and draw(st.booleans()): + # first generate subset of dimension names - these set which dimension coords will be included + dim_coord_names_and_lengths = draw(unique_subset_of(dim_sizes)) + + # then generate 1D variables for each name + dim_coords = { + n: draw(variables(dims=st.just({n: length}))) + for n, length in dim_coord_names_and_lengths.items() + } + all_coords.update(dim_coords) + + # Possibly generate ND "non-dimension coordinates" - explicit possibility not to helps with shrinking + if draw(st.booleans()): + # can't have same name as a dimension + valid_non_dim_coord_names = coord_names.filter(lambda n: n not in dim_names) + non_dim_coords = draw( + _alignable_variables( + var_names=valid_non_dim_coord_names, dim_sizes=dim_sizes + ) + ) + all_coords.update(non_dim_coords) + + return all_coords + + +def _sizes_from_dim_names( + dims: Sequence[Hashable], +) -> st.SearchStrategy[dict[Hashable, int]]: + size_along_dim = st.integers(min_value=1, max_value=6) + return st.fixed_dictionaries({d: size_along_dim for d in dims}) + + +@st.composite +def dataarrays( + draw: st.DrawFn, + *, + data: Optional[st.SearchStrategy[T_DuckArray]] = None, + dims: Optional[ + st.SearchStrategy[Union[Sequence[Hashable], Mapping[Hashable, int]]] + ] = None, + name: st.SearchStrategy[Union[Hashable, None]] = names(), + attrs: st.SearchStrategy[Mapping] = attrs(), +) -> xr.DataArray: + """ + Generates arbitrary xarray.DataArray objects. + + Follows the basic signature of the xarray.DataArray constructor, but you can also pass alternative strategies to + generate either numpy-like array data, dimensions, or coordinates. + + Passing nothing will generate a completely arbitrary DataArray (backed by a numpy array). + + Requires the hypothesis package to be installed. + + Parameters + ---------- + data: Strategy generating array-likes, optional + Default is to generate numpy data of arbitrary shape, values and dtypes. + dims: Strategy for generating the dimensions, optional + Can either be a strategy for generating a sequence of string dimension names, + or a strategy for generating a mapping of string dimension names to integer lengths along each dimension. + If provided in the former form the lengths of the returned Variable will either be determined from the + data argument if given or arbitrarily generated if not. + Default is to generate arbitrary dimension sizes, or arbitrary dimension names for each axis in data. + name: Strategy for generating a string name, optional + Default is to use the `names` strategy, or to create an unnamed DataArray. + attrs: Strategy which generates dicts, optional + + Raises + ------ + hypothesis.errors.InvalidArgument + If custom strategies passed try to draw examples which together cannot create a valid DataArray. + """ + + np_arrays = npst.arrays(dtype=supported_dtypes()) + + _name = draw(st.none() | name) + + # TODO add a coords argument? + + if data is not None and dims is None: + # no dims -> generate dims to match data + _data = draw(data) + + dim_names = draw(dimension_names(min_dims=_data.ndim, max_dims=_data.ndim)) + dim_sizes: Mapping[Hashable, int] = { + n: length for n, length in zip(dim_names, _data.shape) + } + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + + elif data is None and dims is not None: + # no data -> generate data to match dims + _dims = draw(dims) + if isinstance(_dims, Sequence): + dim_sizes = draw(_sizes_from_dim_names(_dims)) + elif isinstance(_dims, Mapping): + # should be a mapping of form {dim_names: lengths} + dim_sizes = _dims + else: + raise ValueError(f"Invalid type for dims argument - got type {type(_dims)}") + + dim_names, shape = list(dim_sizes.keys()), tuple(dim_sizes.values()) + _data = draw(np_arrays(shape=shape)) + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + + elif data is not None and dims is not None: + # both data and dims provided -> check drawn examples are compatible + _dims = draw(dims) + _data = draw(data) + if isinstance(_dims, Sequence): + dim_names = list(_dims) + if _data.ndim != len(_dims): + raise InvalidArgument( + f"Strategy attempting to generate data with {_data.ndim} dims but {len(_dims)} " + "unique dimension names. Please only pass strategies which are guaranteed to " + "draw compatible examples for data and dims." + ) + dim_sizes = {n: length for n, length in zip(_dims, _data.shape)} + elif isinstance(_dims, Mapping): + # should be a mapping of form {dim_names: lengths} + dim_sizes = _dims + dim_names, shape = list(dim_sizes.keys()), tuple(dim_sizes.values()) + if _data.shape != shape: + raise InvalidArgument( + f"Strategy attempting to generate data with shape {_data.shape} dims but dimension " + f"sizes implying shape {shape}. Please only pass strategies which are guaranteed to " + "draw compatible examples for data and dims." + ) + else: + raise ValueError(f"Invalid type for dims argument - got type {type(_dims)}") + + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + + else: + # nothing provided, so generate everything consistently by drawing dims to match data, and coords to match both + _data = draw(np_arrays()) + dim_names = draw(dimension_names(min_dims=_data.ndim, max_dims=_data.ndim)) + dim_sizes = {n: length for n, length in zip(dim_names, _data.shape)} + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + + return xr.DataArray( + data=_data, + coords=coords, + name=_name, + dims=dim_names, + attrs=draw(attrs), + ) + + +@st.composite +def data_variables( + draw: st.DrawFn, + *, + dim_sizes: Mapping[Hashable, int], + var_names: st.SearchStrategy[Hashable] = names(), +) -> Mapping[Hashable, xr.Variable]: + """ + Generates dicts of alignable Variable objects for use as Dataset data variables. + + Requires the hypothesis package to be installed. + + Parameters + ---------- + dim_sizes: Mapping of str to int + Sizes of dimensions to use for variables. + var_names: Strategy generating strings + Allowed names for data variables. Needed to avoid conflict with names of coordinate variables & dimensions. + """ + # Allow for no coordinate variables - explicit possibility not to helps with shrinking + if draw(st.booleans()): + dim_names = list(dim_sizes.keys()) + + # can't have same name as a dimension + # TODO this is also used in coordinate_variables so refactor it out into separate function + valid_var_names = var_names.filter(lambda n: n not in dim_names) + data_vars = draw( + _alignable_variables(var_names=valid_var_names, dim_sizes=dim_sizes) + ) + else: + data_vars = {} + + return data_vars + + +@st.composite +def datasets( + draw: st.DrawFn, + *, + data_vars: Optional[st.SearchStrategy[Mapping[Hashable, xr.Variable]]] = None, + dims: Optional[ + st.SearchStrategy[Union[Sequence[Hashable], Mapping[Hashable, int]]] + ] = None, + attrs: st.SearchStrategy[Mapping] = attrs(), +) -> xr.Dataset: + """ + Generates arbitrary xarray.Dataset objects. + + Follows the basic signature of the xarray.Dataset constructor, but you can also pass alternative strategies to + generate either numpy-like array data variables or dimensions. + + Passing nothing will generate a completely arbitrary Dataset (backed by numpy arrays). + + Requires the hypothesis package to be installed. + + Parameters + ---------- + data_vars: Strategy generating mappings from variable names to xr.Variable objects, optional + Default is to generate an arbitrary combination of compatible variables with sizes matching dims, + but arbitrary names, dtypes, and values. + dims: Strategy for generating the dimensions, optional + Can either be a strategy for generating a sequence of string dimension names, + or a strategy for generating a mapping of string dimension names to integer lengths along each dimension. + If provided in the former form the lengths of the returned Variable will either be determined from the + data argument if given or arbitrarily generated if not. + Default is to generate arbitrary dimension sizes. + attrs: Strategy which generates dicts, optional + + Raises + ------ + hypothesis.errors.InvalidArgument + If custom strategies passed try to draw examples which together cannot create a valid DataArray. + """ + + # TODO add a coords argument? + + if data_vars is not None and dims is None: + # no dims -> generate dims to match data + _data_vars = draw(data_vars) + dim_sizes = _find_overall_sizes(_data_vars) + # only draw coordinate variables whose names don't conflict with data variables + allowed_coord_names = names().filter(lambda n: n not in list(_data_vars.keys())) + coords = draw( + coordinate_variables(coord_names=allowed_coord_names, dim_sizes=dim_sizes) + ) + + elif data_vars is None and dims is not None: + # no data -> generate data to match dims + _dims = draw(dims) + if isinstance(_dims, Sequence): + dim_sizes = draw(_sizes_from_dim_names(_dims)) + elif isinstance(_dims, Mapping): + # should be a mapping of form {dim_names: lengths} + dim_sizes = _dims + else: + raise ValueError(f"Invalid type for dims argument - got type {type(_dims)}") + + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + coord_names = list(coords.keys()) + allowed_data_var_names = names().filter(lambda n: n not in coord_names) + _data_vars = draw( + data_variables(dim_sizes=dim_sizes, var_names=allowed_data_var_names) + ) + + elif data_vars is not None and dims is not None: + # both data and dims provided -> check drawn examples are compatible + _dims = draw(dims) + if isinstance(_dims, Sequence): + # TODO support dims as list too? + raise NotImplementedError() + elif isinstance(_dims, Mapping): + # should be a mapping of form {dim_names: lengths} + dim_sizes = _dims + _data_vars = draw(data_vars) + _check_compatible_sizes(_data_vars, dim_sizes) + else: + raise ValueError(f"Invalid type for dims argument - got type {type(_dims)}") + + # only draw coordinate variables whose names don't conflict with data variables + allowed_coord_names = names().filter(lambda n: n not in list(_data_vars.keys())) + coords = draw( + coordinate_variables(coord_names=allowed_coord_names, dim_sizes=dim_sizes) + ) + + else: + # nothing provided, so generate everything consistently by drawing data to match dims, and coords to match both + dim_sizes = draw(dimension_sizes()) + coords = draw(coordinate_variables(dim_sizes=dim_sizes)) + allowed_data_var_names = names().filter(lambda n: n not in list(coords.keys())) + _data_vars = draw( + data_variables(dim_sizes=dim_sizes, var_names=allowed_data_var_names) + ) + + return xr.Dataset(data_vars=_data_vars, coords=coords, attrs=draw(attrs)) + + +def _find_overall_sizes(vars: Mapping[Hashable, xr.Variable]) -> Mapping[Hashable, int]: + """Given a set of variables, find their common sizes.""" + # TODO raise an error if inconsistent (i.e. if different values appear under same key) + # TODO narrow type by checking if values are not ints + sizes_dicts = [v.sizes for v in vars.values()] + dim_sizes = {d: s for dim_sizes in sizes_dicts for d, s in dim_sizes.items()} + return dim_sizes + + +def _check_compatible_sizes( + vars: Mapping[Hashable, xr.Variable], dim_sizes: Mapping[Hashable, int] +): + """Check set of variables have sizes compatible with given dim_sizes. If not raise InvalidArgument error.""" + + for name, v in vars.items(): + if not set(v.sizes.items()).issubset(set(dim_sizes.items())): + raise InvalidArgument( + f"Strategy attempting to generate object with dimension sizes {dim_sizes} but drawn " + f"variable {name} has sizes {v.sizes}, which is incompatible." + "Please only pass strategies which are guaranteed to draw compatible examples for data " + "and dims." + ) diff --git a/xarray/tests/test_strategies.py b/xarray/tests/test_strategies.py index 79ae4769005..66ea8f2f664 100644 --- a/xarray/tests/test_strategies.py +++ b/xarray/tests/test_strategies.py @@ -1,3 +1,4 @@ +import contextlib import warnings import numpy as np @@ -10,12 +11,18 @@ import hypothesis.extra.numpy as npst import hypothesis.strategies as st -from hypothesis import given +from hypothesis import Phase, given, settings +from hypothesis.errors import InvalidArgument from hypothesis.extra.array_api import make_strategies_namespace +from xarray import DataArray, Dataset from xarray.core.variable import Variable from xarray.testing.strategies import ( attrs, + coordinate_variables, + data_variables, + dataarrays, + datasets, dimension_names, dimension_sizes, supported_dtypes, @@ -23,6 +30,8 @@ variables, ) +np_arrays = npst.arrays(dtype=supported_dtypes()) + ALLOWED_ATTRS_VALUES_TYPES = (int, bool, str, np.ndarray) @@ -52,7 +61,6 @@ def test_types(self, dims): for d, n in dims.items(): assert isinstance(d, str) assert len(d) >= 1 - assert isinstance(n, int) assert n >= 0 @@ -278,3 +286,179 @@ def test_mean(self, data, var): # assert property is always satisfied result = var.mean(dim=reduction_dims).data npt.assert_equal(expected, result) + + +class TestCoordinateVariablesStrategy: + @given(coordinate_variables(dim_sizes={"x": 2, "y": 3})) + def test_alignable(self, coord_vars): + # TODO there must be a better way of checking align-ability than this + for v in coord_vars.values(): + if "x" in v.dims: + assert v.sizes["x"] == 2 + if "y" in v.dims: + assert v.sizes["y"] == 3 + if not set(v.dims).issubset({"x", "y"}): + assert False, v + + @given(st.data()) + def test_valid_set_of_coords(self, data): + coord_vars = data.draw(coordinate_variables(dim_sizes={"x": 2, "y": 3})) + + arr = data.draw(np_arrays(shape=(2, 3))) + da = DataArray(data=arr, coords=coord_vars, dims=["x", "y"]) + assert isinstance(da, DataArray) + + def test_sometimes_generates_1d_dim_coords(self): + found_one = False + + @given(st.data()) + @settings(phases=[Phase.generate]) + def inner(data): + coord_vars = data.draw(coordinate_variables(dim_sizes={"x": 2, "y": 3})) + for name, var in coord_vars.items(): + if var.ndim == 1 and name == var.dims[0]: + nonlocal found_one + found_one = True + raise AssertionError # early stopping - test is correct but slower without this + + with contextlib.suppress(AssertionError): + inner() + + assert found_one + + def test_sometimes_generates_non_dim_coords(self): + found_one = False + + @given(st.data()) + @settings(phases=[Phase.generate]) + def inner(data): + coord_vars = data.draw(coordinate_variables(dim_sizes={"x": 2, "y": 3})) + for name, var in coord_vars.items(): + if var.ndim != 1 or (var.ndim == 1 and name != var.dims[0]): + nonlocal found_one + found_one = True + raise AssertionError # early stopping - test is correct but slower without this + + with contextlib.suppress(AssertionError): + inner() + + assert found_one + + @given(st.data()) + def test_restrict_names(self, data): + capitalized_names = st.text(st.characters(), min_size=1).map(str.upper) + coord_vars = data.draw( + coordinate_variables( + dim_sizes={"x": 2, "y": 3}, coord_names=capitalized_names + ) + ) + for name in coord_vars.keys(): + if name not in ["x", "y"]: + assert name.upper() == name + + +class TestDataArraysStrategy: + @given(dataarrays()) + def test_given_nothing(self, da): + assert isinstance(da, DataArray) + + @given(st.data()) + def test_given_dims(self, data): + da = data.draw(dataarrays(dims=st.just(["x", "y"]))) + assert da.dims == ("x", "y") + + da = data.draw(dataarrays(dims=st.just({"x": 2, "y": 3}))) + assert da.sizes == {"x": 2, "y": 3} + + @given(st.data()) + def test_given_data(self, data): + shape = (2, 3) + arrs = np_arrays(shape=shape) + da = data.draw(dataarrays(data=arrs)) + + assert da.shape == shape + + @given(st.data()) + def test_given_data_and_dims(self, data): + arrs = np_arrays(shape=(2, 3)) + dims = dimension_names(min_dims=2, max_dims=2) + da = data.draw(dataarrays(data=arrs, dims=dims)) + assert da.shape == (2, 3) + + dims = dimension_names(min_dims=3, max_dims=3) + with pytest.raises(InvalidArgument): + data.draw(dataarrays(data=arrs, dims=dims)) + + arrs = np_arrays(shape=(3, 4)) + dims = st.just({"x": 3, "y": 4}) + da = data.draw(dataarrays(data=arrs, dims=dims)) + assert da.sizes == {"x": 3, "y": 4} + + +class TestDataVariablesStrategy: + @given(st.data()) + def test_given_only_sizes(self, data): + dim_sizes = {"x": 2, "y": 3} + data_vars = data.draw(data_variables(dim_sizes=dim_sizes)) + for k, v in data_vars.items(): + assert isinstance(v, Variable) + assert set(v.sizes.items()).issubset(set(dim_sizes.items())) + + @given(st.data()) + def test_restrict_names(self, data): + capitalized_names = st.text(st.characters(), min_size=1).map(str.upper) + data_vars = data.draw( + data_variables(dim_sizes={"x": 2, "y": 3}, var_names=capitalized_names) + ) + for name in data_vars.keys(): + assert name.upper() == name + + +class TestDatasetsStrategy: + @given(datasets()) + def test_given_nothing(self, ds): + assert isinstance(ds, Dataset) + + @given(st.data()) + def test_given_data(self, data): + dim_sizes = {"x": 3, "y": 4} + data_vars = data.draw(data_variables(dim_sizes=dim_sizes)) + ds = data.draw(datasets(data_vars=st.just(data_vars))) + assert set(ds.sizes.items()).issubset(set(dim_sizes.items())) + + @given(st.data()) + def test_given_dims(self, data): + dims = ["x", "y"] + ds = data.draw(datasets(dims=st.just(dims))) + assert set(ds.dims).issubset(set(dims)) + + dim_sizes = {"x": 3, "y": 4} + ds = data.draw(datasets(dims=st.just(dim_sizes))) + assert set(ds.sizes.items()).issubset(set(dim_sizes.items())) + + @given(st.data()) + def test_given_data_and_dims(self, data): + # pass dims as mapping + dim_sizes = {"x": 3, "y": 4} + data_vars = data.draw(data_variables(dim_sizes=dim_sizes)) + ds = data.draw(datasets(data_vars=st.just(data_vars), dims=st.just(dim_sizes))) + assert set(ds.sizes.items()).issubset(set(dim_sizes.items())) + + incompatible_dim_sizes = {"x": 1, "y": 4} + data_vars = {"foo": Variable(data=[0, 1, 2], dims="x")} + with pytest.raises(InvalidArgument, match="drawn variable"): + data.draw( + datasets( + data_vars=st.just(data_vars), dims=st.just(incompatible_dim_sizes) + ) + ) + + @pytest.mark.xfail(reason="not implemented") + @given(st.data()) + def test_given_data_and_dims_as_sequence(self, data): + # pass dims as sequence + dim_sizes = {"x": 3, "y": 4} + dims = list(dim_sizes.keys()) + data_vars = data.draw(data_variables(dim_sizes=dim_sizes)) + ds = data.draw(datasets(data_vars=st.just(data_vars), dims=st.just(dims))) + assert set(ds.sizes.items()).issubset(set(dim_sizes.items()))