From 21696bfdfc1292b39e16491cea35ef2ac1fda6ac Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 00:39:17 +0100 Subject: [PATCH 001/140] add a initial, tiny draft of the automatic duckarray test machinery --- xarray/duckarray.py | 79 ++++++++++++++++++++++++++ xarray/tests/test_duckarray_testing.py | 23 ++++++++ 2 files changed, 102 insertions(+) create mode 100644 xarray/duckarray.py create mode 100644 xarray/tests/test_duckarray_testing.py diff --git a/xarray/duckarray.py b/xarray/duckarray.py new file mode 100644 index 00000000000..6b467e0a3c9 --- /dev/null +++ b/xarray/duckarray.py @@ -0,0 +1,79 @@ +import re + +import numpy as np + +import xarray as xr + +identifier_re = r"[a-zA-Z_][a-zA-Z0-9_]*" +variant_re = re.compile( + rf"^(?P{identifier_re}(?:\.{identifier_re})*)(?:\[(?P[^]]+)\])?$" +) + + +def apply_marks(module, name, marks): + def get_test(module, components): + *parent_names, name = components + + parent = module + for parent_name in parent_names: + parent = getattr(parent, parent_name) + + test = getattr(parent, name) + + return parent, test, name + + match = variant_re.match(name) + if match is not None: + groups = match.groupdict() + variant = groups["variant"] + name = groups["name"] + else: + raise ValueError(f"invalid test name: {name!r}") + + components = name.split(".") + if variant is not None: + raise ValueError("variants are not supported, yet") + else: + parent, test, test_name = get_test(module, components) + for mark in marks: + test = mark(test) + setattr(parent, test_name, test) + + +def duckarray_module(name, create, global_marks=None, marks=None): + import pytest + + class TestModule: + pytestmarks = global_marks + + class TestDataset: + @pytest.mark.parametrize( + "method", + ( + "mean", + "median", + "prod", + "sum", + "std", + "var", + ), + ) + def test_reduce(self, method): + a = create(np.linspace(0, 1, 10), method) + b = create(np.arange(10), method) + + reduced_a = getattr(np, method)(a) + reduced_b = getattr(np, method)(b) + + ds = xr.Dataset({"a": ("x", a), "b": ("x", b)}) + expected = xr.Dataset({"a": reduced_a, "b": reduced_b}) + actual = getattr(ds, method)() + xr.testing.assert_identical(actual, expected) + + for name, marks_ in marks.items(): + apply_marks(TestModule, name, marks_) + + TestModule.__name__ = f"Test{name.title()}" + TestModule.__qualname__ = f"Test{name.title()}" + + return TestModule diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py new file mode 100644 index 00000000000..d852494edde --- /dev/null +++ b/xarray/tests/test_duckarray_testing.py @@ -0,0 +1,23 @@ +import pint +import pytest + +from xarray.duckarray import duckarray_module + +ureg = pint.UnitRegistry(force_ndarray_like=True) + + +def create(data, method): + if method in ("prod"): + units = "dimensionless" + else: + units = "m" + return ureg.Quantity(data, units) + + +TestPint = duckarray_module( + "pint", + create, + marks={ + "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented yet")], + }, +) From f14ba290c6ba9cc59e30777fe574c087a3d05de3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 00:51:20 +0100 Subject: [PATCH 002/140] add missing comma --- xarray/tests/test_duckarray_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index d852494edde..f14d1f35014 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -7,7 +7,7 @@ def create(data, method): - if method in ("prod"): + if method in ("prod",): units = "dimensionless" else: units = "m" From 90f9c41118c3f2edbb6d897fbcf01a8dc087db4f Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 00:54:49 +0100 Subject: [PATCH 003/140] fix the global marks --- xarray/duckarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index 6b467e0a3c9..e1fddee7e55 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -44,7 +44,7 @@ def duckarray_module(name, create, global_marks=None, marks=None): import pytest class TestModule: - pytestmarks = global_marks + pytestmark = global_marks class TestDataset: @pytest.mark.parametrize( From aa4a45746a421f2103a2a8e0b6e2034108b094eb Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 00:54:59 +0100 Subject: [PATCH 004/140] don't try to apply marks if marks is None --- xarray/duckarray.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index e1fddee7e55..404654a329b 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -70,8 +70,9 @@ def test_reduce(self, method): actual = getattr(ds, method)() xr.testing.assert_identical(actual, expected) - for name, marks_ in marks.items(): - apply_marks(TestModule, name, marks_) + if marks is not None: + for name, marks_ in marks.items(): + apply_marks(TestModule, name, marks_) TestModule.__name__ = f"Test{name.title()}" TestModule.__qualname__ = f"Test{name.title()}" From 9fa2eca456a9cd5cb4eca2f93823335c8a5d4cd5 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 01:18:17 +0100 Subject: [PATCH 005/140] only set pytestmark if the value is not None --- xarray/duckarray.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index 404654a329b..9a296ce783d 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -44,8 +44,6 @@ def duckarray_module(name, create, global_marks=None, marks=None): import pytest class TestModule: - pytestmark = global_marks - class TestDataset: @pytest.mark.parametrize( "method", @@ -70,6 +68,9 @@ def test_reduce(self, method): actual = getattr(ds, method)() xr.testing.assert_identical(actual, expected) + if global_marks is not None: + TestModule.pytestmark = global_marks + if marks is not None: for name, marks_ in marks.items(): apply_marks(TestModule, name, marks_) From 7994bad83152ba69c396f16822bf776cbb24950a Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 02:57:31 +0100 Subject: [PATCH 006/140] skip the module if pint is not installed --- xarray/tests/test_duckarray_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index f14d1f35014..138a51aa603 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,8 +1,8 @@ -import pint import pytest from xarray.duckarray import duckarray_module +pint = pytest.importorskip("pint") ureg = pint.UnitRegistry(force_ndarray_like=True) From c4a35f05bffe5a088337b1f8b1a198d2a5e731af Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 02:58:13 +0100 Subject: [PATCH 007/140] filter UnitStrippedWarnings --- xarray/tests/test_duckarray_testing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 138a51aa603..e6ec1a0d1b0 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -20,4 +20,7 @@ def create(data, method): marks={ "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented yet")], }, + global_marks=[ + pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), + ], ) From 0efbbbb9db61e8ab38aee9cb721078d762fd6e5b Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 02:58:45 +0100 Subject: [PATCH 008/140] also test sparse --- xarray/tests/test_duckarray_testing.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index e6ec1a0d1b0..0c99b3e2eed 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -3,10 +3,11 @@ from xarray.duckarray import duckarray_module pint = pytest.importorskip("pint") +sparse = pytest.importorskip("sparse") ureg = pint.UnitRegistry(force_ndarray_like=True) -def create(data, method): +def create_pint(data, method): if method in ("prod",): units = "dimensionless" else: @@ -14,9 +15,13 @@ def create(data, method): return ureg.Quantity(data, units) +def create_sparse(data, method): + return sparse.COO.from_numpy(data) + + TestPint = duckarray_module( "pint", - create, + create_pint, marks={ "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented yet")], }, @@ -24,3 +29,8 @@ def create(data, method): pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), ], ) + +TestSparse = duckarray_module( + "sparse", + create_sparse, +) From 73499b5aca0f6c7505471e82328f22b1ce173b1e Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 02:01:41 +0100 Subject: [PATCH 009/140] add a test for the test extractor --- xarray/duckarray.py | 17 ++++++------ xarray/tests/test_duckarray_testing.py | 36 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index 9a296ce783d..48fddec0e3f 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -10,18 +10,19 @@ ) -def apply_marks(module, name, marks): - def get_test(module, components): - *parent_names, name = components +def get_test(module, components): + *parent_names, name = components + + parent = module + for parent_name in parent_names: + parent = getattr(parent, parent_name) - parent = module - for parent_name in parent_names: - parent = getattr(parent, parent_name) + test = getattr(parent, name) - test = getattr(parent, name) + return parent, test, name - return parent, test, name +def apply_marks(module, name, marks): match = variant_re.match(name) if match is not None: groups = match.groupdict() diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 0c99b3e2eed..b6d0bcac5d0 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,5 +1,6 @@ import pytest +from xarray import duckarray from xarray.duckarray import duckarray_module pint = pytest.importorskip("pint") @@ -7,6 +8,41 @@ ureg = pint.UnitRegistry(force_ndarray_like=True) +class Module: + def module_test1(self): + pass + + def module_test2(self): + pass + + @pytest.mark.parametrize("param1", ("a", "b", "c")) + def parametrized_test(self, param1): + pass + + class Submodule: + def submodule_test(self): + pass + + +class TestUtils: + @pytest.mark.parametrize( + ["components", "expected"], + ( + (["module_test1"], (Module, Module.module_test1, "module_test1")), + ( + ["Submodule", "submodule_test"], + (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), + ), + ), + ) + def test_get_test(self, components, expected): + module = Module + actual = duckarray.get_test(module, components) + print(actual) + print(expected) + assert actual == expected + + def create_pint(data, method): if method in ("prod",): units = "dimensionless" From 532f2132053a7350bd43631ba279583d76cd9fd8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 15:39:18 +0100 Subject: [PATCH 010/140] move the selector parsing code to a new function --- xarray/duckarray.py | 9 +++++++-- xarray/tests/test_duckarray_testing.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index 48fddec0e3f..34ff998b104 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -22,8 +22,8 @@ def get_test(module, components): return parent, test, name -def apply_marks(module, name, marks): - match = variant_re.match(name) +def parse_selector(selector): + match = variant_re.match(selector) if match is not None: groups = match.groupdict() variant = groups["variant"] @@ -32,6 +32,11 @@ def apply_marks(module, name, marks): raise ValueError(f"invalid test name: {name!r}") components = name.split(".") + return components, variant + + +def apply_marks(module, name, marks): + components, variant = parse_selector(name) if variant is not None: raise ValueError("variants are not supported, yet") else: diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index b6d0bcac5d0..8389392b499 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -25,6 +25,25 @@ def submodule_test(self): class TestUtils: + @pytest.mark.parametrize( + ["selector", "expected"], + ( + ("test_function", (["test_function"], None)), + ( + "TestGroup.TestSubgroup.test_function", + (["TestGroup", "TestSubgroup", "test_function"], None), + ), + ("test_function[variant]", (["test_function"], "variant")), + ( + "TestGroup.test_function[variant]", + (["TestGroup", "test_function"], "variant"), + ), + ), + ) + def test_parse_selector(self, selector, expected): + actual = duckarray.parse_selector(selector) + assert actual == expected + @pytest.mark.parametrize( ["components", "expected"], ( From f44aafaf33a2f2353f0799c1d4ba6d8e168fba2a Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 15:41:49 +0100 Subject: [PATCH 011/140] also skip the sparse tests --- xarray/tests/test_duckarray_testing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 8389392b499..20f20a7350d 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -88,4 +88,7 @@ def create_sparse(data, method): TestSparse = duckarray_module( "sparse", create_sparse, + marks={ + "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented, yet")], + }, ) From d65143823b5100c3792cb5c382e35c09889982d9 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 15:53:52 +0100 Subject: [PATCH 012/140] move the utils tests into a different file --- xarray/tests/test_duckarray_testing.py | 55 -------------------- xarray/tests/test_duckarray_testing_utils.py | 55 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 xarray/tests/test_duckarray_testing_utils.py diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 20f20a7350d..2a3a0d2aa4d 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,6 +1,5 @@ import pytest -from xarray import duckarray from xarray.duckarray import duckarray_module pint = pytest.importorskip("pint") @@ -8,60 +7,6 @@ ureg = pint.UnitRegistry(force_ndarray_like=True) -class Module: - def module_test1(self): - pass - - def module_test2(self): - pass - - @pytest.mark.parametrize("param1", ("a", "b", "c")) - def parametrized_test(self, param1): - pass - - class Submodule: - def submodule_test(self): - pass - - -class TestUtils: - @pytest.mark.parametrize( - ["selector", "expected"], - ( - ("test_function", (["test_function"], None)), - ( - "TestGroup.TestSubgroup.test_function", - (["TestGroup", "TestSubgroup", "test_function"], None), - ), - ("test_function[variant]", (["test_function"], "variant")), - ( - "TestGroup.test_function[variant]", - (["TestGroup", "test_function"], "variant"), - ), - ), - ) - def test_parse_selector(self, selector, expected): - actual = duckarray.parse_selector(selector) - assert actual == expected - - @pytest.mark.parametrize( - ["components", "expected"], - ( - (["module_test1"], (Module, Module.module_test1, "module_test1")), - ( - ["Submodule", "submodule_test"], - (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), - ), - ), - ) - def test_get_test(self, components, expected): - module = Module - actual = duckarray.get_test(module, components) - print(actual) - print(expected) - assert actual == expected - - def create_pint(data, method): if method in ("prod",): units = "dimensionless" diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py new file mode 100644 index 00000000000..0234f0496cd --- /dev/null +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -0,0 +1,55 @@ +import pytest + +from xarray import duckarray + + +class Module: + def module_test1(self): + pass + + def module_test2(self): + pass + + @pytest.mark.parametrize("param1", ("a", "b", "c")) + def parametrized_test(self, param1): + pass + + class Submodule: + def submodule_test(self): + pass + + +class TestUtils: + @pytest.mark.parametrize( + ["selector", "expected"], + ( + ("test_function", (["test_function"], None)), + ( + "TestGroup.TestSubgroup.test_function", + (["TestGroup", "TestSubgroup", "test_function"], None), + ), + ("test_function[variant]", (["test_function"], "variant")), + ( + "TestGroup.test_function[variant]", + (["TestGroup", "test_function"], "variant"), + ), + ), + ) + def test_parse_selector(self, selector, expected): + actual = duckarray.parse_selector(selector) + assert actual == expected + + @pytest.mark.parametrize( + ["components", "expected"], + ( + (["module_test1"], (Module, Module.module_test1, "module_test1")), + ( + ["Submodule", "submodule_test"], + (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), + ), + ), + ) + def test_get_test(self, components, expected): + module = Module + actual = duckarray.get_test(module, components) + assert actual == expected From f84894a52fd88b5b3f1a1a6afc668fcfee0433dd Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 17:42:14 +0100 Subject: [PATCH 013/140] don't keep the utils tests in a test group --- xarray/tests/test_duckarray_testing_utils.py | 58 ++++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 0234f0496cd..0fb48e7de67 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -19,37 +19,37 @@ def submodule_test(self): pass -class TestUtils: - @pytest.mark.parametrize( - ["selector", "expected"], +@pytest.mark.parametrize( + ["selector", "expected"], + ( + ("test_function", (["test_function"], None)), ( - ("test_function", (["test_function"], None)), - ( - "TestGroup.TestSubgroup.test_function", - (["TestGroup", "TestSubgroup", "test_function"], None), - ), - ("test_function[variant]", (["test_function"], "variant")), - ( - "TestGroup.test_function[variant]", - (["TestGroup", "test_function"], "variant"), - ), + "TestGroup.TestSubgroup.test_function", + (["TestGroup", "TestSubgroup", "test_function"], None), ), - ) - def test_parse_selector(self, selector, expected): - actual = duckarray.parse_selector(selector) - assert actual == expected + ("test_function[variant]", (["test_function"], "variant")), + ( + "TestGroup.test_function[variant]", + (["TestGroup", "test_function"], "variant"), + ), + ), +) +def test_parse_selector(selector, expected): + actual = duckarray.parse_selector(selector) + assert actual == expected + - @pytest.mark.parametrize( - ["components", "expected"], +@pytest.mark.parametrize( + ["components", "expected"], + ( + (["module_test1"], (Module, Module.module_test1, "module_test1")), ( - (["module_test1"], (Module, Module.module_test1, "module_test1")), - ( - ["Submodule", "submodule_test"], - (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), - ), + ["Submodule", "submodule_test"], + (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), ), - ) - def test_get_test(self, components, expected): - module = Module - actual = duckarray.get_test(module, components) - assert actual == expected + ), +) +def test_get_test(components, expected): + module = Module + actual = duckarray.get_test(module, components) + assert actual == expected From 0090db5a0f6874f48214a3204e502600220bd82c Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 1 Mar 2021 17:43:15 +0100 Subject: [PATCH 014/140] split apply_marks into two separate functions --- xarray/duckarray.py | 20 ++++++++++++---- xarray/tests/test_duckarray_testing_utils.py | 25 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/xarray/duckarray.py b/xarray/duckarray.py index 34ff998b104..8ab766a09ca 100644 --- a/xarray/duckarray.py +++ b/xarray/duckarray.py @@ -35,15 +35,25 @@ def parse_selector(selector): return components, variant +def apply_marks_normal(test, marks): + for mark in marks: + test = mark(test) + return test + + +def apply_marks_variant(test, variant, marks): + raise NotImplementedError("variants are not supported, yet") + + def apply_marks(module, name, marks): components, variant = parse_selector(name) + parent, test, test_name = get_test(module, components) if variant is not None: - raise ValueError("variants are not supported, yet") + marked_test = apply_marks_variant(test, variant, marks) else: - parent, test, test_name = get_test(module, components) - for mark in marks: - test = mark(test) - setattr(parent, test_name, test) + marked_test = apply_marks_normal(test, marks) + + setattr(parent, test_name, marked_test) def duckarray_module(name, create, global_marks=None, marks=None): diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 0fb48e7de67..e375e24dff3 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -53,3 +53,28 @@ def test_get_test(components, expected): module = Module actual = duckarray.get_test(module, components) assert actual == expected + + +@pytest.mark.parametrize( + "marks", + ( + pytest.param([pytest.mark.skip(reason="arbitrary")], id="single mark"), + pytest.param( + [ + pytest.mark.filterwarnings("error"), + pytest.mark.parametrize("a", (0, 1, 2)), + ], + id="multiple marks", + ), + ), +) +def test_apply_marks_normal(marks): + def func(): + pass + + expected = [m.mark for m in marks] + + marked = duckarray.apply_marks_normal(func, marks) + actual = marked.pytestmark + + assert actual == expected From ef05c7d9061a26ddfbd68c3bd87b777fa3ccdefb Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:16:29 +0100 Subject: [PATCH 015/140] add a mark which attaches marks to test variants --- xarray/tests/conftest.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 xarray/tests/conftest.py diff --git a/xarray/tests/conftest.py b/xarray/tests/conftest.py new file mode 100644 index 00000000000..78cbaa3fa32 --- /dev/null +++ b/xarray/tests/conftest.py @@ -0,0 +1,26 @@ +def pytest_configure(config): + config.addinivalue_line( + "markers", + "attach_marks(marks): function to attach marks to tests and test variants", + ) + + +def pytest_collection_modifyitems(session, config, items): + for item in items: + mark = item.get_closest_marker("attach_marks") + if mark is None: + continue + index = item.own_markers.index(mark) + del item.own_markers[index] + + marks = mark.args[0] + if not isinstance(marks, dict): + continue + + variant = item.name[len(item.originalname) :] + to_attach = marks.get(variant) + if to_attach is None: + continue + + for mark in to_attach: + item.add_marker(mark) From 20334d9e8019f720e376cd4c57c6780ad9eb70d0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:19:56 +0100 Subject: [PATCH 016/140] move the duckarray testing module to tests --- xarray/{ => tests}/duckarray.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename xarray/{ => tests}/duckarray.py (100%) diff --git a/xarray/duckarray.py b/xarray/tests/duckarray.py similarity index 100% rename from xarray/duckarray.py rename to xarray/tests/duckarray.py From f7acc0f02f4f92f9ce62e5749a1caee596d5e30c Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:25:40 +0100 Subject: [PATCH 017/140] move the utils to a separate module --- xarray/tests/duckarray.py | 58 ++----------------- xarray/tests/duckarray_testing_utils.py | 76 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 xarray/tests/duckarray_testing_utils.py diff --git a/xarray/tests/duckarray.py b/xarray/tests/duckarray.py index 8ab766a09ca..b450dbab114 100644 --- a/xarray/tests/duckarray.py +++ b/xarray/tests/duckarray.py @@ -1,59 +1,8 @@ -import re - import numpy as np import xarray as xr -identifier_re = r"[a-zA-Z_][a-zA-Z0-9_]*" -variant_re = re.compile( - rf"^(?P{identifier_re}(?:\.{identifier_re})*)(?:\[(?P[^]]+)\])?$" -) - - -def get_test(module, components): - *parent_names, name = components - - parent = module - for parent_name in parent_names: - parent = getattr(parent, parent_name) - - test = getattr(parent, name) - - return parent, test, name - - -def parse_selector(selector): - match = variant_re.match(selector) - if match is not None: - groups = match.groupdict() - variant = groups["variant"] - name = groups["name"] - else: - raise ValueError(f"invalid test name: {name!r}") - - components = name.split(".") - return components, variant - - -def apply_marks_normal(test, marks): - for mark in marks: - test = mark(test) - return test - - -def apply_marks_variant(test, variant, marks): - raise NotImplementedError("variants are not supported, yet") - - -def apply_marks(module, name, marks): - components, variant = parse_selector(name) - parent, test, test_name = get_test(module, components) - if variant is not None: - marked_test = apply_marks_variant(test, variant, marks) - else: - marked_test = apply_marks_normal(test, marks) - - setattr(parent, test_name, marked_test) +from .duckarray_testing_utils import apply_marks, preprocess_marks def duckarray_module(name, create, global_marks=None, marks=None): @@ -88,8 +37,9 @@ def test_reduce(self, method): TestModule.pytestmark = global_marks if marks is not None: - for name, marks_ in marks.items(): - apply_marks(TestModule, name, marks_) + processed = preprocess_marks(marks) + for components, marks_ in processed: + apply_marks(TestModule, components, marks_) TestModule.__name__ = f"Test{name.title()}" TestModule.__qualname__ = f"Test{name.title()}" diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py new file mode 100644 index 00000000000..c4c00aa0d26 --- /dev/null +++ b/xarray/tests/duckarray_testing_utils.py @@ -0,0 +1,76 @@ +import itertools +import re + +import pytest + +identifier_re = r"[a-zA-Z_][a-zA-Z0-9_]*" +variant_re = re.compile( + rf"^(?P{identifier_re}(?:(?:\.|::){identifier_re})*)(?:\[(?P[^]]+)\])?$" +) + + +def is_variant(k): + return k.startswith("[") and k.endswith("]") + + +def process_spec(name, value): + components, variant = parse_selector(name) + + if variant is not None and not isinstance(value, list): + raise ValueError(f"invalid spec: {name} → {value}") + elif isinstance(value, list): + if variant is not None: + value = {f"[{variant}]": value} + + yield components, value + elif isinstance(value, dict) and all(is_variant(k) for k in value.keys()): + yield components, value + else: + yield from itertools.chain.from_iterable( + process_spec(name, value) for name, value in value.items() + ) + + +def preprocess_marks(marks): + return list( + itertools.chain.from_iterable( + process_spec(name, value) for name, value in marks.items() + ) + ) + + +def parse_selector(selector): + match = variant_re.match(selector) + if match is not None: + groups = match.groupdict() + variant = groups["variant"] + name = groups["name"] + else: + raise ValueError(f"invalid test name: {name!r}") + + components = name.split(".") + return components, variant + + +def get_test(module, components): + *parent_names, name = components + + parent = module + for parent_name in parent_names: + parent = getattr(parent, parent_name) + + test = getattr(parent, name) + + return parent, test, name + + +def apply_marks(module, components, marks): + parent, test, test_name = get_test(module, components) + if isinstance(marks, list): + # mark the whole test + marked_test = test + for mark in marks: + marked_test = mark(marked_test) + else: + marked_test = pytest.mark.attach_marks(marks)(test) + setattr(parent, test_name, marked_test) From e41a15b60d85aeff7049f23b0ad9ab31d65fa42c Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:34:14 +0100 Subject: [PATCH 018/140] fix the existing tests --- xarray/tests/test_duckarray_testing_utils.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index e375e24dff3..152d90ffc68 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -1,6 +1,6 @@ import pytest -from xarray import duckarray +from . import duckarray_testing_utils class Module: @@ -35,7 +35,7 @@ def submodule_test(self): ), ) def test_parse_selector(selector, expected): - actual = duckarray.parse_selector(selector) + actual = duckarray_testing_utils.parse_selector(selector) assert actual == expected @@ -51,7 +51,7 @@ def test_parse_selector(selector, expected): ) def test_get_test(components, expected): module = Module - actual = duckarray.get_test(module, components) + actual = duckarray_testing_utils.get_test(module, components) assert actual == expected @@ -69,12 +69,15 @@ def test_get_test(components, expected): ), ) def test_apply_marks_normal(marks): - def func(): - pass + if hasattr(Module.module_test1, "pytestmark"): + del Module.module_test1.pytestmark - expected = [m.mark for m in marks] + module = Module + components = ["module_test1"] - marked = duckarray.apply_marks_normal(func, marks) + duckarray_testing_utils.apply_marks(module, components, marks) + marked = Module.module_test1 actual = marked.pytestmark + expected = [m.mark for m in marks] assert actual == expected From 1f095a1d0d97599848167dd6baa6e6869fb596ee Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:40:14 +0100 Subject: [PATCH 019/140] completely isolate the apply_marks tests --- xarray/tests/test_duckarray_testing_utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 152d90ffc68..beca507ef2f 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -69,14 +69,15 @@ def test_get_test(components, expected): ), ) def test_apply_marks_normal(marks): - if hasattr(Module.module_test1, "pytestmark"): - del Module.module_test1.pytestmark + class Module: + def module_test(self): + pass module = Module - components = ["module_test1"] + components = ["module_test"] duckarray_testing_utils.apply_marks(module, components, marks) - marked = Module.module_test1 + marked = Module.module_test actual = marked.pytestmark expected = [m.mark for m in marks] From 2503af744caa687d6ed3b3f1f24f99ccb0238ae1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:40:43 +0100 Subject: [PATCH 020/140] add a test for applying marks to test variants --- xarray/tests/test_duckarray_testing_utils.py | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index beca507ef2f..7e4952c3270 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -82,3 +82,33 @@ def module_test(self): expected = [m.mark for m in marks] assert actual == expected + + +@pytest.mark.parametrize( + "marks", + ( + pytest.param([pytest.mark.skip(reason="arbitrary")], id="single mark"), + pytest.param( + [ + pytest.mark.filterwarnings("error"), + pytest.mark.parametrize("a", (0, 1, 2)), + ], + id="multiple marks", + ), + ), +) +@pytest.mark.parametrize("variant", ("[a]", "[b]", "[c]")) +def test_apply_marks_variant(marks, variant): + class Module: + @pytest.mark.parametrize("param1", ("a", "b", "c")) + def func(param1): + pass + + module = Module + components = ["func"] + + duckarray_testing_utils.apply_marks(module, components, {variant: marks}) + marked = Module.func + actual = marked.pytestmark + + assert len(actual) > 1 and any(mark.name == "attach_marks" for mark in actual) From b229645f137468acc8af08be0d81d3cb424900e5 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:41:51 +0100 Subject: [PATCH 021/140] skip failing test variants --- xarray/tests/test_duckarray_testing.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 2a3a0d2aa4d..4031a43319e 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -23,7 +23,9 @@ def create_sparse(data, method): "pint", create_pint, marks={ - "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented yet")], + "TestDataset.test_reduce[prod]": [ + pytest.mark.skip(reason="not implemented yet") + ], }, global_marks=[ pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), @@ -34,6 +36,10 @@ def create_sparse(data, method): "sparse", create_sparse, marks={ - "TestDataset.test_reduce": [pytest.mark.skip(reason="not implemented, yet")], + "TestDataset.test_reduce": { + "[median]": [pytest.mark.skip(reason="not implemented, yet")], + "[std]": [pytest.mark.skip(reason="not implemented, yet")], + "[var]": [pytest.mark.skip(reason="not implemented, yet")], + }, }, ) From 07234188ab6033d4be41fba9de534ef82a1afd13 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 6 Mar 2021 22:43:10 +0100 Subject: [PATCH 022/140] fix the import path --- xarray/tests/test_duckarray_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 4031a43319e..86f36531b59 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,6 +1,6 @@ import pytest -from xarray.duckarray import duckarray_module +from .duckarray import duckarray_module pint = pytest.importorskip("pint") sparse = pytest.importorskip("sparse") From 6c4ccb0e520feb24a5529c48f89aad48f7777b7a Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 15:11:04 +0100 Subject: [PATCH 023/140] rename the duckarray testing module --- xarray/tests/{duckarray.py => duckarray_testing.py} | 0 xarray/tests/test_duckarray_testing.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename xarray/tests/{duckarray.py => duckarray_testing.py} (100%) diff --git a/xarray/tests/duckarray.py b/xarray/tests/duckarray_testing.py similarity index 100% rename from xarray/tests/duckarray.py rename to xarray/tests/duckarray_testing.py diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 86f36531b59..c1949fc9a3f 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,6 +1,6 @@ import pytest -from .duckarray import duckarray_module +from .duckarray_testing import duckarray_module pint = pytest.importorskip("pint") sparse = pytest.importorskip("sparse") From c4aa05a7384da9f7cf2bf17344dd0ef25f843016 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 15:48:23 +0100 Subject: [PATCH 024/140] use Variable as example --- xarray/tests/duckarray_testing.py | 33 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index b450dbab114..9be5fb635fe 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -9,29 +9,42 @@ def duckarray_module(name, create, global_marks=None, marks=None): import pytest class TestModule: - class TestDataset: + class TestVariable: @pytest.mark.parametrize( "method", ( + "all", + "any", + "argmax", + "argmin", + "argsort", + "cumprod", + "cumsum", + "max", "mean", "median", + "min", "prod", - "sum", "std", + "sum", "var", ), ) def test_reduce(self, method): - a = create(np.linspace(0, 1, 10), method) - b = create(np.arange(10), method) + data = create(np.linspace(0, 1, 10), method) + + reduced = getattr(np, method)(data, axis=0) + + var = xr.Variable("x", data) + + expected_dims = ( + () if method not in ("argsort", "cumsum", "cumprod") else "x" + ) + expected = xr.Variable(expected_dims, reduced) - reduced_a = getattr(np, method)(a) - reduced_b = getattr(np, method)(b) + actual = getattr(var, method)(dim="x") - ds = xr.Dataset({"a": ("x", a), "b": ("x", b)}) - expected = xr.Dataset({"a": reduced_a, "b": reduced_b}) - actual = getattr(ds, method)() - xr.testing.assert_identical(actual, expected) + xr.testing.assert_allclose(actual, expected) if global_marks is not None: TestModule.pytestmark = global_marks From fc97e90361de24f3f6bdff7004df8a5e19db6e89 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 15:51:09 +0100 Subject: [PATCH 025/140] fix the skips --- xarray/tests/test_duckarray_testing.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index c1949fc9a3f..4a784bd5ff6 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -23,9 +23,12 @@ def create_sparse(data, method): "pint", create_pint, marks={ - "TestDataset.test_reduce[prod]": [ - pytest.mark.skip(reason="not implemented yet") - ], + "TestVariable.test_reduce": { + "[argsort]": [ + pytest.mark.skip(reason="xarray.Variable.argsort does not support dim") + ], + "[prod]": [pytest.mark.skip(reason="nanprod drops units")], + }, }, global_marks=[ pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), @@ -36,10 +39,15 @@ def create_sparse(data, method): "sparse", create_sparse, marks={ - "TestDataset.test_reduce": { - "[median]": [pytest.mark.skip(reason="not implemented, yet")], - "[std]": [pytest.mark.skip(reason="not implemented, yet")], - "[var]": [pytest.mark.skip(reason="not implemented, yet")], + "TestVariable.test_reduce": { + "[argmax]": [pytest.mark.skip(reason="not implemented by sparse")], + "[argmin]": [pytest.mark.skip(reason="not implemented by sparse")], + "[argsort]": [pytest.mark.skip(reason="not implemented by sparse")], + "[cumprod]": [pytest.mark.skip(reason="not implemented by sparse")], + "[cumsum]": [pytest.mark.skip(reason="not implemented by sparse")], + "[median]": [pytest.mark.skip(reason="not implemented by sparse")], + "[std]": [pytest.mark.skip(reason="nanstd not implemented, yet")], + "[var]": [pytest.mark.skip(reason="nanvar not implemented, yet")], }, }, ) From 31e577a77ea28cb625dd325f0cc4e9f43e672b91 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 15:51:19 +0100 Subject: [PATCH 026/140] only use dimensionless for cumprod --- xarray/tests/test_duckarray_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 4a784bd5ff6..1440f080ec9 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -8,7 +8,7 @@ def create_pint(data, method): - if method in ("prod",): + if method in ("cumprod",): units = "dimensionless" else: units = "m" From 8d8021282f8e49b2514696fbff24dfe7477a87f5 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 15:51:56 +0100 Subject: [PATCH 027/140] also test dask wrapped by pint --- xarray/tests/test_duckarray_testing.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 1440f080ec9..14c3f47cab8 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -2,6 +2,7 @@ from .duckarray_testing import duckarray_module +da = pytest.importorskip("dask.array") pint = pytest.importorskip("pint") sparse = pytest.importorskip("sparse") ureg = pint.UnitRegistry(force_ndarray_like=True) @@ -15,6 +16,11 @@ def create_pint(data, method): return ureg.Quantity(data, units) +def create_pint_dask(data, method): + data = da.from_array(data, chunks=(2,)) + return create_pint(data, method) + + def create_sparse(data, method): return sparse.COO.from_numpy(data) @@ -35,6 +41,24 @@ def create_sparse(data, method): ], ) +TestPintDask = duckarray_module( + "pint_dask", + create_pint_dask, + marks={ + "TestVariable.test_reduce": { + "[argsort]": [ + pytest.mark.skip(reason="xarray.Variable.argsort does not support dim") + ], + "[cumsum]": [pytest.mark.skip(reason="nancumsum drops the units")], + "[median]": [pytest.mark.skip(reason="median does not support dim")], + "[prod]": [pytest.mark.skip(reason="prod drops the units")], + }, + }, + global_marks=[ + pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), + ], +) + TestSparse = duckarray_module( "sparse", create_sparse, From 7c43e91611d2f153d9ca1028e813b62cb714f09c Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 16:33:10 +0100 Subject: [PATCH 028/140] add a function to concatenate mappings --- xarray/tests/duckarray_testing_utils.py | 20 +++++++ xarray/tests/test_duckarray_testing_utils.py | 57 ++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py index c4c00aa0d26..63edeb088fb 100644 --- a/xarray/tests/duckarray_testing_utils.py +++ b/xarray/tests/duckarray_testing_utils.py @@ -9,6 +9,26 @@ ) +def concat_mappings(mapping, *others, duplicates="error"): + if not isinstance(mapping, dict): + if others: + raise ValueError("cannot pass both a iterable and multiple values") + + mapping, *others = mapping + + if duplicates == "error": + all_keys = [m.keys() for m in [mapping] + others] + if len(set(itertools.chain.from_iterable(all_keys))) != len(all_keys): + duplicate_keys = [] + raise ValueError(f"duplicate keys found: {duplicate_keys!r}") + + result = mapping.copy() + for m in others: + result.update(m) + + return result + + def is_variant(k): return k.startswith("[") and k.endswith("]") diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 7e4952c3270..25ddd9a6295 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -84,6 +84,63 @@ def module_test(self): assert actual == expected +@pytest.mark.parametrize( + ["mappings", "use_others", "duplicates", "expected", "error", "message"], + ( + pytest.param( + [{"a": 1}, {"b": 2}], + False, + False, + {"a": 1, "b": 2}, + False, + None, + id="iterable", + ), + pytest.param( + [{"a": 1}, {"b": 2}], + True, + False, + {"a": 1, "b": 2}, + False, + None, + id="use others", + ), + pytest.param( + [[{"a": 1}], {"b": 2}], + True, + False, + None, + True, + "cannot pass both a iterable and multiple values", + id="iterable and args", + ), + pytest.param( + [{"a": 1}, {"a": 2}], + False, + "error", + None, + True, + "duplicate keys found: ['a']", + id="raise on duplicates", + ), + ), +) +def test_concat_mappings(mappings, use_others, duplicates, expected, error, message): + func = duckarray_testing_utils.concat_mappings + call = ( + lambda m: func(*m, duplicates=duplicates) + if use_others + else func(m, duplicates=duplicates) + ) + if error: + with pytest.raises(ValueError): + call(mappings) + else: + actual = call(mappings) + + assert actual == expected + + @pytest.mark.parametrize( "marks", ( From b6a90dfeb46c517dc4a769c59983d6b5030c5c0e Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 16:33:58 +0100 Subject: [PATCH 029/140] add tests for preprocess_marks --- xarray/tests/test_duckarray_testing_utils.py | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 25ddd9a6295..6ed38cbd90a 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -141,6 +141,75 @@ def test_concat_mappings(mappings, use_others, duplicates, expected, error, mess assert actual == expected +@pytest.mark.parametrize( + ["marks", "expected", "error"], + ( + pytest.param( + {"test_func": [1, 2]}, + [(["test_func"], [1, 2])], + False, + id="single test", + ), + pytest.param( + {"test_func1": [1, 2], "test_func2": [3, 4]}, + [(["test_func1"], [1, 2]), (["test_func2"], [3, 4])], + False, + id="multiple tests", + ), + pytest.param( + {"TestGroup": {"test_func1": [1, 2], "test_func2": [3, 4]}}, + [ + (["TestGroup", "test_func1"], [1, 2]), + (["TestGroup", "test_func2"], [3, 4]), + ], + False, + id="test group dict", + ), + pytest.param( + {"test_func[variant]": [1, 2]}, + [(["test_func"], {"[variant]": [1, 2]})], + False, + id="single variant", + ), + pytest.param( + {"test_func[variant]": {"key": [1, 2]}}, + None, + True, + id="invalid variant", + ), + pytest.param( + {"test_func": {"[v1]": [1, 2], "[v2]": [3, 4]}}, + [(["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]})], + False, + id="multiple variants combined", + ), + pytest.param( + {"test_func[v1]": [1, 2], "test_func[v2]": [3, 4]}, + [(["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]})], + False, + id="multiple variants separate", + ), + pytest.param( + {"test_func[v1]": [1, 2], "test_func[v2]": [3, 4], "test_func2": [4, 5]}, + [ + (["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]}), + (["test_func2"], [4, 5]), + ], + False, + id="multiple variants multiple tests separate", + ), + ), +) +def test_preprocess_marks(marks, expected, error): + if error: + with pytest.raises(ValueError): + duckarray_testing_utils.preprocess_marks(marks) + else: + actual = duckarray_testing_utils.preprocess_marks(marks) + + assert actual == expected + + @pytest.mark.parametrize( "marks", ( From a95b5c4f483f7dc7fac1f3f10f838923e530ebb4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 16:34:41 +0100 Subject: [PATCH 030/140] fix the tests --- xarray/tests/duckarray_testing_utils.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py index 63edeb088fb..c8d5d454471 100644 --- a/xarray/tests/duckarray_testing_utils.py +++ b/xarray/tests/duckarray_testing_utils.py @@ -46,17 +46,26 @@ def process_spec(name, value): elif isinstance(value, dict) and all(is_variant(k) for k in value.keys()): yield components, value else: - yield from itertools.chain.from_iterable( + processed = itertools.chain.from_iterable( process_spec(name, value) for name, value in value.items() ) + yield from ( + (components + new_components, value) for new_components, value in processed + ) + def preprocess_marks(marks): - return list( - itertools.chain.from_iterable( - process_spec(name, value) for name, value in marks.items() - ) + flattened = itertools.chain.from_iterable( + process_spec(name, value) for name, value in marks.items() ) + key = lambda x: x[0] + grouped = itertools.groupby(sorted(flattened, key=key), key=key) + result = [ + (components, concat_mappings(v for _, v in group)) + for components, group in grouped + ] + return result def parse_selector(selector): From aa7caaa69fe66e15b0e2c1266bc34a1727062f6e Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 16:47:51 +0100 Subject: [PATCH 031/140] show the duplicates in the error message --- xarray/tests/duckarray_testing.py | 1 - xarray/tests/duckarray_testing_utils.py | 15 +++++++++++---- xarray/tests/test_duckarray_testing_utils.py | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 9be5fb635fe..4ce45d191dc 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -43,7 +43,6 @@ def test_reduce(self, method): expected = xr.Variable(expected_dims, reduced) actual = getattr(var, method)(dim="x") - xr.testing.assert_allclose(actual, expected) if global_marks is not None: diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py index c8d5d454471..edea7f71716 100644 --- a/xarray/tests/duckarray_testing_utils.py +++ b/xarray/tests/duckarray_testing_utils.py @@ -1,3 +1,4 @@ +import collections import itertools import re @@ -17,10 +18,16 @@ def concat_mappings(mapping, *others, duplicates="error"): mapping, *others = mapping if duplicates == "error": - all_keys = [m.keys() for m in [mapping] + others] - if len(set(itertools.chain.from_iterable(all_keys))) != len(all_keys): - duplicate_keys = [] - raise ValueError(f"duplicate keys found: {duplicate_keys!r}") + all_keys = list( + itertools.chain.from_iterable(m.keys() for m in [mapping] + others) + ) + duplicates = { + key: value + for key, value in collections.Counter(all_keys).items() + if value > 1 + } + if duplicates: + raise ValueError(f"duplicate keys found: {list(duplicates.keys())!r}") result = mapping.copy() for m in others: diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py index 6ed38cbd90a..4b55bf9b0c3 100644 --- a/xarray/tests/test_duckarray_testing_utils.py +++ b/xarray/tests/test_duckarray_testing_utils.py @@ -120,7 +120,7 @@ def module_test(self): "error", None, True, - "duplicate keys found: ['a']", + "duplicate keys found", id="raise on duplicates", ), ), @@ -133,7 +133,7 @@ def test_concat_mappings(mappings, use_others, duplicates, expected, error, mess else func(m, duplicates=duplicates) ) if error: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=message): call(mappings) else: actual = call(mappings) From 6415be88b0c5fab63f41657c713832dd896add0d Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 16:55:10 +0100 Subject: [PATCH 032/140] add back support for test marks --- xarray/tests/duckarray_testing_utils.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py index edea7f71716..539d16408fe 100644 --- a/xarray/tests/duckarray_testing_utils.py +++ b/xarray/tests/duckarray_testing_utils.py @@ -63,13 +63,24 @@ def process_spec(name, value): def preprocess_marks(marks): + def concat_values(values): + if len({type(v) for v in values}) != 1: + raise ValueError("mixed types are not supported") + + if len(values) == 1: + return values[0] + elif isinstance(values[0], list): + raise ValueError("cannot have multiple mark lists per test") + else: + return concat_mappings(values) + flattened = itertools.chain.from_iterable( process_spec(name, value) for name, value in marks.items() ) key = lambda x: x[0] grouped = itertools.groupby(sorted(flattened, key=key), key=key) result = [ - (components, concat_mappings(v for _, v in group)) + (components, concat_values([v for _, v in group])) for components, group in grouped ] return result From de255949f4a3a2e7861a1bd45844901078a7ca91 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 18:18:43 +0100 Subject: [PATCH 033/140] allow passing a list of addition assert functions --- xarray/tests/duckarray_testing.py | 17 +++++++++++++---- xarray/tests/test_duckarray_testing.py | 7 +++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 4ce45d191dc..3da6573e63c 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -5,9 +5,18 @@ from .duckarray_testing_utils import apply_marks, preprocess_marks -def duckarray_module(name, create, global_marks=None, marks=None): +def duckarray_module(name, create, extra_asserts=None, global_marks=None, marks=None): import pytest + def extra_assert(a, b): + if extra_asserts is None: + return + + funcs = [extra_asserts] if callable(extra_asserts) else extra_asserts + + for func in funcs: + func(a, b) + class TestModule: class TestVariable: @pytest.mark.parametrize( @@ -34,15 +43,15 @@ def test_reduce(self, method): data = create(np.linspace(0, 1, 10), method) reduced = getattr(np, method)(data, axis=0) - - var = xr.Variable("x", data) - expected_dims = ( () if method not in ("argsort", "cumsum", "cumprod") else "x" ) expected = xr.Variable(expected_dims, reduced) + var = xr.Variable("x", data) actual = getattr(var, method)(dim="x") + + extra_assert(actual, expected) xr.testing.assert_allclose(actual, expected) if global_marks is not None: diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 14c3f47cab8..10f2c26f667 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,6 +1,7 @@ import pytest from .duckarray_testing import duckarray_module +from .test_units import assert_units_equal da = pytest.importorskip("dask.array") pint = pytest.importorskip("pint") @@ -28,6 +29,7 @@ def create_sparse(data, method): TestPint = duckarray_module( "pint", create_pint, + extra_asserts=assert_units_equal, marks={ "TestVariable.test_reduce": { "[argsort]": [ @@ -44,6 +46,7 @@ def create_sparse(data, method): TestPintDask = duckarray_module( "pint_dask", create_pint_dask, + extra_asserts=assert_units_equal, marks={ "TestVariable.test_reduce": { "[argsort]": [ @@ -52,6 +55,10 @@ def create_sparse(data, method): "[cumsum]": [pytest.mark.skip(reason="nancumsum drops the units")], "[median]": [pytest.mark.skip(reason="median does not support dim")], "[prod]": [pytest.mark.skip(reason="prod drops the units")], + "[cumprod]": [pytest.mark.skip(reason="cumprod drops the units")], + "[std]": [pytest.mark.skip(reason="nanstd drops the units")], + "[sum]": [pytest.mark.skip(reason="sum drops the units")], + "[var]": [pytest.mark.skip(reason="var drops the units")], }, }, global_marks=[ From 1b0f372990bd7edba5b4ef11ebfc735047a1bcdc Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 22:42:16 +0100 Subject: [PATCH 034/140] add some notes about the test suite --- xarray/tests/duckarray_testing.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 3da6573e63c..69bcd745470 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -17,6 +17,18 @@ def extra_assert(a, b): for func in funcs: func(a, b) + # TODO: + # - find a way to add create args as parametrizations + # - add a optional type parameter to the create func spec + # - how do we construct the expected values? + # - should we check multiple dtypes? + # - should we check multiple fill values? + # - should we allow duckarray libraries to expect errors (pytest.raises / pytest.warns)? + # - low priority: how do we redistribute the apply_marks mechanism? + + # convention: method specs for parametrize: one of + # - method name + # - tuple of method name, args, kwargs class TestModule: class TestVariable: @pytest.mark.parametrize( From 706ee545973de8940832706f0e91c4fd2fd96697 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Mar 2021 22:45:51 +0100 Subject: [PATCH 035/140] simplify the extra_assert function --- xarray/tests/duckarray_testing.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 69bcd745470..881269c6ba0 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -5,6 +5,22 @@ from .duckarray_testing_utils import apply_marks, preprocess_marks +def is_iterable(x): + try: + iter(x) + except TypeError: + return False + + return True + + +def always_iterable(x): + if is_iterable(x) and not isinstance(x, (str, bytes)): + return x + else: + return [x] + + def duckarray_module(name, create, extra_asserts=None, global_marks=None, marks=None): import pytest @@ -12,9 +28,7 @@ def extra_assert(a, b): if extra_asserts is None: return - funcs = [extra_asserts] if callable(extra_asserts) else extra_asserts - - for func in funcs: + for func in always_iterable(extra_asserts): func(a, b) # TODO: From cd5aa708fc1ab419319c86622006b5f7c6b6dfa3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 24 Mar 2021 01:13:09 +0100 Subject: [PATCH 036/140] convert to hypothesis --- xarray/tests/duckarray_testing.py | 28 ++++++++++++++---- xarray/tests/test_duckarray_testing.py | 40 ++++++++++++++++++++------ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 881269c6ba0..a96cb21f751 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -21,16 +21,26 @@ def always_iterable(x): return [x] -def duckarray_module(name, create, extra_asserts=None, global_marks=None, marks=None): +def duckarray_module( + name, create_data, extra_asserts=None, global_marks=None, marks=None +): + # create_data: partial builds target + import hypothesis.extra.numpy as npst + import hypothesis.strategies as st import pytest + from hypothesis import given def extra_assert(a, b): + __tracebackhide__ = True if extra_asserts is None: return for func in always_iterable(extra_asserts): func(a, b) + def numpy_data(shape, dtype): + return npst.arrays(shape=shape, dtype=dtype) + # TODO: # - find a way to add create args as parametrizations # - add a optional type parameter to the create func spec @@ -45,6 +55,7 @@ def extra_assert(a, b): # - tuple of method name, args, kwargs class TestModule: class TestVariable: + @given(st.data(), npst.floating_dtypes() | npst.integer_dtypes()) @pytest.mark.parametrize( "method", ( @@ -65,16 +76,21 @@ class TestVariable: "var", ), ) - def test_reduce(self, method): - data = create(np.linspace(0, 1, 10), method) - - reduced = getattr(np, method)(data, axis=0) + def test_reduce(self, method, data, dtype): + shape = (10,) + x = data.draw(create_data(numpy_data(shape, dtype), method)) + + func = getattr(np, method) + if x.dtype.kind in "cf": + # nan values possible + func = getattr(np, f"nan{method}", func) + reduced = func(x, axis=0) expected_dims = ( () if method not in ("argsort", "cumsum", "cumprod") else "x" ) expected = xr.Variable(expected_dims, reduced) - var = xr.Variable("x", data) + var = xr.Variable("x", x) actual = getattr(var, method)(dim="x") extra_assert(actual, expected) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py index 10f2c26f667..3d56d7abf42 100644 --- a/xarray/tests/test_duckarray_testing.py +++ b/xarray/tests/test_duckarray_testing.py @@ -1,3 +1,4 @@ +import hypothesis.strategies as st import pytest from .duckarray_testing import duckarray_module @@ -9,21 +10,44 @@ ureg = pint.UnitRegistry(force_ndarray_like=True) -def create_pint(data, method): +def units(): + # preserve the order so these will be reduced to + units = ["m", "cm", "s", "ms"] + return st.sampled_from(units) + + +@st.composite +def create_pint(draw, data, method): if method in ("cumprod",): - units = "dimensionless" + units_ = st.just("dimensionless") + else: + units_ = units() + x = draw(data) + u = draw(units_) + + if u is not None: + q = ureg.Quantity(x, u) else: - units = "m" - return ureg.Quantity(data, units) + q = x + + return q + + +@st.composite +def create_dask(draw, data, method): + x = draw(data) + return da.from_array(x, chunks=(2,)) def create_pint_dask(data, method): - data = da.from_array(data, chunks=(2,)) - return create_pint(data, method) + x = create_dask(data, method) + return create_pint(x, method) -def create_sparse(data, method): - return sparse.COO.from_numpy(data) +@st.composite +def create_sparse(draw, data, method): + x = draw(data) + return sparse.COO.from_numpy(x) TestPint = duckarray_module( From 08d72ed0f8f316180cba338f5fee6d44adc148a4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:15:17 +0100 Subject: [PATCH 037/140] add a marker to convert label-space parameters --- xarray/tests/duckarray_testing.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index a96cb21f751..a1114108065 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -21,6 +21,25 @@ def always_iterable(x): return [x] +class Label: + """mark a parameter as in coordinate space""" + + def __init__(self, value): + self.value = value + + +def convert_labels(draw, create, args, kwargs): + def convert(value): + if not isinstance(value, Label): + return value + else: + return draw(create(value)) + + args = [convert(value) for value in args] + kwargs = {key: convert(value) for key, value in kwargs.items()} + return args, kwargs + + def duckarray_module( name, create_data, extra_asserts=None, global_marks=None, marks=None ): From 0649d599067a0aaaef13de0eb2e35932c1ef78a3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:16:25 +0100 Subject: [PATCH 038/140] add a dummy expect_error function --- xarray/tests/duckarray_testing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index a1114108065..880bd3126a4 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -40,6 +40,10 @@ def convert(value): return args, kwargs +def default_expect_error(method, data, *args, **kwargs): + return None, None + + def duckarray_module( name, create_data, extra_asserts=None, global_marks=None, marks=None ): From c32cb5a5e135afc2c65cf9edd3f0a3bc46780940 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:18:56 +0100 Subject: [PATCH 039/140] compute actual before expected --- xarray/tests/duckarray_testing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 880bd3126a4..abba02ea777 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -103,6 +103,9 @@ def test_reduce(self, method, data, dtype): shape = (10,) x = data.draw(create_data(numpy_data(shape, dtype), method)) + var = xr.Variable("x", x) + actual = getattr(var, method)(dim="x") + func = getattr(np, method) if x.dtype.kind in "cf": # nan values possible @@ -113,9 +116,6 @@ def test_reduce(self, method, data, dtype): ) expected = xr.Variable(expected_dims, reduced) - var = xr.Variable("x", x) - actual = getattr(var, method)(dim="x") - extra_assert(actual, expected) xr.testing.assert_allclose(actual, expected) From 440e0bdfc61878cb9a74658cb2de9863f9faf2b0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:22:04 +0100 Subject: [PATCH 040/140] pass a strategy instead of a single dtype --- xarray/tests/duckarray_testing.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index abba02ea777..caac026b3a7 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -45,14 +45,23 @@ def default_expect_error(method, data, *args, **kwargs): def duckarray_module( - name, create_data, extra_asserts=None, global_marks=None, marks=None + name, + create, + *, + expect_error=None, + extra_asserts=None, + global_marks=None, + marks=None, ): - # create_data: partial builds target import hypothesis.extra.numpy as npst import hypothesis.strategies as st import pytest from hypothesis import given + dtypes = ( + npst.floating_dtypes() | npst.integer_dtypes() | npst.complex_number_dtypes() + ) + def extra_assert(a, b): __tracebackhide__ = True if extra_asserts is None: @@ -61,8 +70,8 @@ def extra_assert(a, b): for func in always_iterable(extra_asserts): func(a, b) - def numpy_data(shape, dtype): - return npst.arrays(shape=shape, dtype=dtype) + def numpy_data(shape): + return npst.arrays(shape=shape, dtype=dtypes) # TODO: # - find a way to add create args as parametrizations @@ -78,7 +87,7 @@ def numpy_data(shape, dtype): # - tuple of method name, args, kwargs class TestModule: class TestVariable: - @given(st.data(), npst.floating_dtypes() | npst.integer_dtypes()) + @given(st.data()) @pytest.mark.parametrize( "method", ( @@ -99,9 +108,9 @@ class TestVariable: "var", ), ) - def test_reduce(self, method, data, dtype): + def test_reduce(self, method, data): shape = (10,) - x = data.draw(create_data(numpy_data(shape, dtype), method)) + x = data.draw(create(numpy_data(shape), method)) var = xr.Variable("x", x) actual = getattr(var, method)(dim="x") From f74a29c1bbd6981823f934bd0b16c3f6954ff691 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:22:55 +0100 Subject: [PATCH 041/140] set a default for expect_error --- xarray/tests/duckarray_testing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index caac026b3a7..0492ce90c72 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -58,6 +58,8 @@ def duckarray_module( import pytest from hypothesis import given + if expect_error is None: + expect_error = default_expect_error dtypes = ( npst.floating_dtypes() | npst.integer_dtypes() | npst.complex_number_dtypes() ) From d9346f82fdff97bbbdc5931d4b435adcdf3286fd Mon Sep 17 00:00:00 2001 From: Keewis Date: Sat, 27 Mar 2021 01:23:34 +0100 Subject: [PATCH 042/140] add a test for clip --- xarray/tests/duckarray_testing.py | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index 0492ce90c72..e44620aaa5b 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -60,6 +60,7 @@ def duckarray_module( if expect_error is None: expect_error = default_expect_error + values = st.just(None) | st.integers() | st.floats() | st.complex_numbers() dtypes = ( npst.floating_dtypes() | npst.integer_dtypes() | npst.complex_number_dtypes() ) @@ -130,6 +131,45 @@ def test_reduce(self, method, data): extra_assert(actual, expected) xr.testing.assert_allclose(actual, expected) + @given(st.data()) + @pytest.mark.parametrize( + ["method", "args", "kwargs"], + ( + pytest.param( + "clip", + [], + {"min": Label(values), "max": Label(values)}, + id="clip", + ), + ), + ) + def test_numpy_methods(self, method, args, kwargs, data): + # 1. create both numpy and duckarray data and put them into a variable + # 2. compute for both + # 3. convert the numpy data to duckarray data + # 4. compare + shape = (10,) + x = data.draw(create(numpy_data(shape), method)) + + args, kwargs = convert_labels(data.draw, create, args, kwargs) + + var = xr.Variable("x", x) + + error, match = expect_error(method, x, args, kwargs) + if error is not None: + with pytest.raises(error, match=match): + getattr(var, method)(*args, dim="x", **kwargs) + + return + else: + actual = getattr(var, method)(*args, dim="x", **kwargs) + expected = xr.Variable( + "x", getattr(np, method)(x, *args, axis=0, **kwargs) + ) + + extra_assert(actual, expected) + xr.testing.assert_allclose(actual, expected) + if global_marks is not None: TestModule.pytestmark = global_marks From 75f584ab954b8a815de74ce4813a3f5aecf4e52f Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 31 Mar 2021 20:17:18 +0200 Subject: [PATCH 043/140] allow passing a separate "create_label" function --- xarray/tests/duckarray_testing.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py index e44620aaa5b..688bdc9a35b 100644 --- a/xarray/tests/duckarray_testing.py +++ b/xarray/tests/duckarray_testing.py @@ -48,6 +48,7 @@ def duckarray_module( name, create, *, + create_label=None, expect_error=None, extra_asserts=None, global_marks=None, @@ -60,6 +61,10 @@ def duckarray_module( if expect_error is None: expect_error = default_expect_error + + if create_label is None: + create_label = create + values = st.just(None) | st.integers() | st.floats() | st.complex_numbers() dtypes = ( npst.floating_dtypes() | npst.integer_dtypes() | npst.complex_number_dtypes() @@ -77,17 +82,10 @@ def numpy_data(shape): return npst.arrays(shape=shape, dtype=dtypes) # TODO: - # - find a way to add create args as parametrizations - # - add a optional type parameter to the create func spec - # - how do we construct the expected values? - # - should we check multiple dtypes? - # - should we check multiple fill values? - # - should we allow duckarray libraries to expect errors (pytest.raises / pytest.warns)? - # - low priority: how do we redistribute the apply_marks mechanism? - - # convention: method specs for parametrize: one of - # - method name - # - tuple of method name, args, kwargs + # - add a "create_label" kwarg, which defaults to a thin wrapper around "create" + # - formalize "expect_error" + # - figure out which tests need a separation of data, dims, and coords + class TestModule: class TestVariable: @given(st.data()) @@ -151,7 +149,7 @@ def test_numpy_methods(self, method, args, kwargs, data): shape = (10,) x = data.draw(create(numpy_data(shape), method)) - args, kwargs = convert_labels(data.draw, create, args, kwargs) + args, kwargs = convert_labels(data.draw, create_label, args, kwargs) var = xr.Variable("x", x) From b94c84d0e2988c887be13e869196b5342017822a Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 11 Apr 2021 00:20:14 +0200 Subject: [PATCH 044/140] draft the base class hierarchy tailored after pandas' extension array tests --- xarray/tests/duckarrays/__init__.py | 0 xarray/tests/duckarrays/base/__init__.py | 5 +++ xarray/tests/duckarrays/base/reduce.py | 47 ++++++++++++++++++++++++ xarray/tests/duckarrays/base/utils.py | 31 ++++++++++++++++ xarray/tests/duckarrays/test_sparse.py | 16 ++++++++ 5 files changed, 99 insertions(+) create mode 100644 xarray/tests/duckarrays/__init__.py create mode 100644 xarray/tests/duckarrays/base/__init__.py create mode 100644 xarray/tests/duckarrays/base/reduce.py create mode 100644 xarray/tests/duckarrays/base/utils.py create mode 100644 xarray/tests/duckarrays/test_sparse.py diff --git a/xarray/tests/duckarrays/__init__.py b/xarray/tests/duckarrays/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/tests/duckarrays/base/__init__.py new file mode 100644 index 00000000000..c6a3a49829d --- /dev/null +++ b/xarray/tests/duckarrays/base/__init__.py @@ -0,0 +1,5 @@ +from .reduce import ReduceMethodTests + +__all__ = [ + "ReduceMethodTests", +] diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py new file mode 100644 index 00000000000..fc1d6ba6f59 --- /dev/null +++ b/xarray/tests/duckarrays/base/reduce.py @@ -0,0 +1,47 @@ +import hypothesis.strategies as st +import pytest +from hypothesis import given + +import xarray as xr + +from .utils import create_dimension_names, numpy_array, valid_axes, valid_dims_from_axes + + +class ReduceMethodTests: + @staticmethod + def create(op): + return numpy_array + + @pytest.mark.parametrize( + "method", + ( + "all", + "any", + "argmax", + "argmin", + "argsort", + "cumprod", + "cumsum", + "max", + "mean", + "median", + "min", + "prod", + "std", + "sum", + "var", + ), + ) + @given(st.data()) + def test_variable_reduce(self, method, data): + raw = data.draw(self.create(method)) + dims = create_dimension_names(raw.ndim) + var = xr.Variable(dims, raw) + + reduce_axes = data.draw(valid_axes(raw.ndim)) + reduce_dims = valid_dims_from_axes(dims, reduce_axes) + + actual = getattr(var, method)(dim=reduce_dims) + print(actual) + + assert False diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py new file mode 100644 index 00000000000..5df29c53c11 --- /dev/null +++ b/xarray/tests/duckarrays/base/utils.py @@ -0,0 +1,31 @@ +import hypothesis.extra.numpy as npst +import hypothesis.strategies as st + +shapes = npst.array_shapes() +dtypes = ( + npst.floating_dtypes() + | npst.integer_dtypes() + | npst.unsigned_integer_dtypes() + | npst.complex_number_dtypes() +) + + +numpy_array = npst.arrays(dtype=dtypes, shape=shapes) + + +def create_dimension_names(ndim): + return [f"dim_{n}" for n in range(ndim)] + + +def valid_axes(ndim): + return st.none() | st.integers(-ndim, ndim - 1) | npst.valid_tuple_axes(ndim) + + +def valid_dims_from_axes(dims, axes): + if axes is None: + return None + + if isinstance(axes, int): + return dims[axes] + + return type(axes)(dims[axis] for axis in axes) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py new file mode 100644 index 00000000000..57747ff0559 --- /dev/null +++ b/xarray/tests/duckarrays/test_sparse.py @@ -0,0 +1,16 @@ +import pytest + +from . import base +from .base import utils + +sparse = pytest.importorskip("sparse") + + +def create(op): + return utils.numpy_array.map(sparse.COO.from_numpy) + + +class TestReduceMethods(base.ReduceMethodTests): + @staticmethod + def create(op): + return create(op) From 7a150f8ad21f93e9c34d04643ed8f4af1d7fb299 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 14:34:42 +0200 Subject: [PATCH 045/140] make sure multiple dims are passed as a list --- xarray/tests/duckarrays/base/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index 5df29c53c11..b19be11f5bd 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -28,4 +28,4 @@ def valid_dims_from_axes(dims, axes): if isinstance(axes, int): return dims[axes] - return type(axes)(dims[axis] for axis in axes) + return [dims[axis] for axis in axes] From a6eecb8f3ebbbb59d60f60b245093452f8517ab8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 14:36:26 +0200 Subject: [PATCH 046/140] sort the dtypes differently --- xarray/tests/duckarrays/base/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index b19be11f5bd..7f2187c459e 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -3,9 +3,9 @@ shapes = npst.array_shapes() dtypes = ( - npst.floating_dtypes() - | npst.integer_dtypes() + npst.integer_dtypes() | npst.unsigned_integer_dtypes() + | npst.floating_dtypes() | npst.complex_number_dtypes() ) From dcb9fc066e0dc65eaf6b5c49a30f976c9c6b5af8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 14:36:45 +0200 Subject: [PATCH 047/140] add a strategy to generate a single axis --- xarray/tests/duckarrays/base/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index 7f2187c459e..510fd1e7139 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -17,8 +17,12 @@ def create_dimension_names(ndim): return [f"dim_{n}" for n in range(ndim)] +def valid_axis(ndim): + return st.none() | st.integers(-ndim, ndim - 1) + + def valid_axes(ndim): - return st.none() | st.integers(-ndim, ndim - 1) | npst.valid_tuple_axes(ndim) + return valid_axis(ndim) | npst.valid_tuple_axes(ndim) def valid_dims_from_axes(dims, axes): From 0ee096dad48bbc5dc6ddd514d82d00f12536160e Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 14:37:35 +0200 Subject: [PATCH 048/140] add a function to compute the axes from the dims --- xarray/tests/duckarrays/base/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index 510fd1e7139..98113c909dc 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -33,3 +33,12 @@ def valid_dims_from_axes(dims, axes): return dims[axes] return [dims[axis] for axis in axes] + + +def valid_axes_from_dims(all_dims, dims): + if dims is None: + return None + elif isinstance(dims, list): + return [all_dims.index(dim) for dim in dims] + else: + return all_dims.index(dims) From 53debb262211b43d2bcfeb9e6768e88790851029 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 14:39:43 +0200 Subject: [PATCH 049/140] move the call of the operation to a hook which means that expecting failures, constructing expected values and comparing are customizable --- xarray/tests/duckarrays/base/__init__.py | 4 ++-- xarray/tests/duckarrays/base/reduce.py | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/tests/duckarrays/base/__init__.py index c6a3a49829d..14a4ce0ed89 100644 --- a/xarray/tests/duckarrays/base/__init__.py +++ b/xarray/tests/duckarrays/base/__init__.py @@ -1,5 +1,5 @@ -from .reduce import ReduceMethodTests +from .reduce import VariableReduceTests __all__ = [ - "ReduceMethodTests", + "VariableReduceTests", ] diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index fc1d6ba6f59..d5e040a9622 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -1,13 +1,26 @@ import hypothesis.strategies as st +import numpy as np import pytest -from hypothesis import given +from hypothesis import given, note import xarray as xr +from ... import assert_identical from .utils import create_dimension_names, numpy_array, valid_axes, valid_dims_from_axes -class ReduceMethodTests: +class VariableReduceTests: + def check_reduce(self, obj, op, *args, **kwargs): + actual = getattr(obj, op)(*args, **kwargs) + + data = np.asarray(obj.data) + expected = getattr(obj.copy(data=data), op)(*args, **kwargs) + + note(f"actual:\n{actual}") + note(f"expected:\n{expected}") + + assert_identical(actual, expected) + @staticmethod def create(op): return numpy_array @@ -41,7 +54,4 @@ def test_variable_reduce(self, method, data): reduce_axes = data.draw(valid_axes(raw.ndim)) reduce_dims = valid_dims_from_axes(dims, reduce_axes) - actual = getattr(var, method)(dim=reduce_dims) - print(actual) - - assert False + self.check_reduce(var, method, dim=reduce_dims) From 9b2c0a381a1a7507718aa94bdba072a7f1f87987 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 15:33:57 +0200 Subject: [PATCH 050/140] remove the arg* methods since they are not reducing anything --- xarray/tests/duckarrays/base/reduce.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index d5e040a9622..9826ad291d6 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -30,9 +30,6 @@ def create(op): ( "all", "any", - "argmax", - "argmin", - "argsort", "cumprod", "cumsum", "max", From a6efbe193dd38b743f67dadcc3c9097e9f0f2da7 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 15:34:36 +0200 Subject: [PATCH 051/140] add a context manager to suppress specific warnings --- xarray/tests/duckarrays/base/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index 98113c909dc..bb8e16340e4 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -1,3 +1,6 @@ +import warnings +from contextlib import contextmanager + import hypothesis.extra.numpy as npst import hypothesis.strategies as st @@ -10,6 +13,14 @@ ) +@contextmanager +def suppress_warning(category, message=""): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=category, message=message) + + yield + + numpy_array = npst.arrays(dtype=dtypes, shape=shapes) From 5d679bf852a52942dc2d67eb34389108f0de26de Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 15:35:37 +0200 Subject: [PATCH 052/140] don't try to reduce along multiple dimensions numpy doesn't seem to support that for all reduce functions --- xarray/tests/duckarrays/base/reduce.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 9826ad291d6..8ed2e3915bf 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -6,7 +6,7 @@ import xarray as xr from ... import assert_identical -from .utils import create_dimension_names, numpy_array, valid_axes, valid_dims_from_axes +from .utils import create_dimension_names, numpy_array, valid_axis, valid_dims_from_axes class VariableReduceTests: @@ -48,7 +48,7 @@ def test_variable_reduce(self, method, data): dims = create_dimension_names(raw.ndim) var = xr.Variable(dims, raw) - reduce_axes = data.draw(valid_axes(raw.ndim)) + reduce_axes = data.draw(valid_axis(raw.ndim)) reduce_dims = valid_dims_from_axes(dims, reduce_axes) self.check_reduce(var, method, dim=reduce_dims) From 50db3c369c09c02a0e9a4a223e1792f0215f84be Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 15:37:20 +0200 Subject: [PATCH 053/140] demonstrate the new pattern using pint --- xarray/tests/duckarrays/test_units.py | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 xarray/tests/duckarrays/test_units.py diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py new file mode 100644 index 00000000000..422b274080d --- /dev/null +++ b/xarray/tests/duckarrays/test_units.py @@ -0,0 +1,63 @@ +import hypothesis.strategies as st +import numpy as np +import pytest +from hypothesis import note + +from .. import assert_identical +from ..test_units import assert_units_equal +from . import base +from .base import utils + +pint = pytest.importorskip("pint") +unit_registry = pint.UnitRegistry(force_ndarray_like=True) +Quantity = unit_registry.Quantity + +pytestmark = [pytest.mark.filterwarnings("error")] + + +all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) + + +class TestVariableReduceMethods(base.VariableReduceTests): + @st.composite + @staticmethod + def create(draw, op): + if op in ("cumprod",): + units = st.just("dimensionless") + else: + units = all_units + + return Quantity(draw(utils.numpy_array), draw(units)) + + def check_reduce(self, obj, op, *args, **kwargs): + if ( + op in ("cumprod",) + and obj.data.size > 1 + and obj.data.units != unit_registry.dimensionless + ): + with pytest.raises(pint.DimensionalityError): + getattr(obj, op)(*args, **kwargs) + else: + actual = getattr(obj, op)(*args, **kwargs) + + note(f"actual:\n{actual}") + + without_units = obj.copy(data=obj.data.magnitude) + expected = getattr(without_units, op)(*args, **kwargs) + + func_name = f"nan{op}" if obj.dtype.kind in "fc" else op + func = getattr(np, func_name, getattr(np, op)) + func_kwargs = kwargs.copy() + dim = func_kwargs.pop("dim", None) + axis = utils.valid_axes_from_dims(obj.dims, dim) + func_kwargs["axis"] = axis + with utils.suppress_warning(RuntimeWarning): + result = func(obj.data, *args, **func_kwargs) + units = getattr(result, "units", None) + if units is not None: + expected = expected.copy(data=Quantity(expected.data, units)) + + note(f"expected:\n{expected}") + + assert_units_equal(actual, expected) + assert_identical(actual, expected) From 8114d2af59afe126d61fa94c605e8db660c0e1ec Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 12 Apr 2021 15:59:39 +0200 Subject: [PATCH 054/140] fix the sparse tests --- xarray/tests/duckarrays/test_sparse.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 57747ff0559..a08ed7baf0d 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,5 +1,6 @@ import pytest +from .. import assert_allclose from . import base from .base import utils @@ -7,10 +8,27 @@ def create(op): - return utils.numpy_array.map(sparse.COO.from_numpy) + def convert(arr): + if arr.ndim == 0: + return arr + return sparse.COO.from_numpy(arr) -class TestReduceMethods(base.ReduceMethodTests): + return utils.numpy_array.map(convert) + + +class TestVariableReduceMethods(base.VariableReduceTests): @staticmethod def create(op): return create(op) + + def check_reduce(self, obj, op, *args, **kwargs): + actual = getattr(obj, op)(*args, **kwargs) + + if isinstance(actual.data, sparse.COO): + actual = actual.copy(data=actual.data.todense()) + + dense = obj.copy(data=obj.data.todense()) + expected = getattr(dense, op)(*args, **kwargs) + + assert_allclose(actual, expected) From 14349f26e0d79d4bf816298d779e6ab78c721c41 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 20 Apr 2021 23:41:29 +0200 Subject: [PATCH 055/140] back to only raising for UnitStrippedWarning --- xarray/tests/duckarrays/test_units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 422b274080d..1b9d65f357d 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -12,7 +12,7 @@ unit_registry = pint.UnitRegistry(force_ndarray_like=True) Quantity = unit_registry.Quantity -pytestmark = [pytest.mark.filterwarnings("error")] +pytestmark = [pytest.mark.filterwarnings("error::pint.UnitStrippedWarning")] all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) From 6a658efa57ec3827c04ff00bd7dba5bd34648a35 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 17:22:47 +0200 Subject: [PATCH 056/140] remove the old duckarray testing module --- xarray/tests/duckarray_testing.py | 182 -------------- xarray/tests/duckarray_testing_utils.py | 123 ---------- xarray/tests/test_duckarray_testing.py | 108 --------- xarray/tests/test_duckarray_testing_utils.py | 240 ------------------- 4 files changed, 653 deletions(-) delete mode 100644 xarray/tests/duckarray_testing.py delete mode 100644 xarray/tests/duckarray_testing_utils.py delete mode 100644 xarray/tests/test_duckarray_testing.py delete mode 100644 xarray/tests/test_duckarray_testing_utils.py diff --git a/xarray/tests/duckarray_testing.py b/xarray/tests/duckarray_testing.py deleted file mode 100644 index 688bdc9a35b..00000000000 --- a/xarray/tests/duckarray_testing.py +++ /dev/null @@ -1,182 +0,0 @@ -import numpy as np - -import xarray as xr - -from .duckarray_testing_utils import apply_marks, preprocess_marks - - -def is_iterable(x): - try: - iter(x) - except TypeError: - return False - - return True - - -def always_iterable(x): - if is_iterable(x) and not isinstance(x, (str, bytes)): - return x - else: - return [x] - - -class Label: - """mark a parameter as in coordinate space""" - - def __init__(self, value): - self.value = value - - -def convert_labels(draw, create, args, kwargs): - def convert(value): - if not isinstance(value, Label): - return value - else: - return draw(create(value)) - - args = [convert(value) for value in args] - kwargs = {key: convert(value) for key, value in kwargs.items()} - return args, kwargs - - -def default_expect_error(method, data, *args, **kwargs): - return None, None - - -def duckarray_module( - name, - create, - *, - create_label=None, - expect_error=None, - extra_asserts=None, - global_marks=None, - marks=None, -): - import hypothesis.extra.numpy as npst - import hypothesis.strategies as st - import pytest - from hypothesis import given - - if expect_error is None: - expect_error = default_expect_error - - if create_label is None: - create_label = create - - values = st.just(None) | st.integers() | st.floats() | st.complex_numbers() - dtypes = ( - npst.floating_dtypes() | npst.integer_dtypes() | npst.complex_number_dtypes() - ) - - def extra_assert(a, b): - __tracebackhide__ = True - if extra_asserts is None: - return - - for func in always_iterable(extra_asserts): - func(a, b) - - def numpy_data(shape): - return npst.arrays(shape=shape, dtype=dtypes) - - # TODO: - # - add a "create_label" kwarg, which defaults to a thin wrapper around "create" - # - formalize "expect_error" - # - figure out which tests need a separation of data, dims, and coords - - class TestModule: - class TestVariable: - @given(st.data()) - @pytest.mark.parametrize( - "method", - ( - "all", - "any", - "argmax", - "argmin", - "argsort", - "cumprod", - "cumsum", - "max", - "mean", - "median", - "min", - "prod", - "std", - "sum", - "var", - ), - ) - def test_reduce(self, method, data): - shape = (10,) - x = data.draw(create(numpy_data(shape), method)) - - var = xr.Variable("x", x) - actual = getattr(var, method)(dim="x") - - func = getattr(np, method) - if x.dtype.kind in "cf": - # nan values possible - func = getattr(np, f"nan{method}", func) - reduced = func(x, axis=0) - expected_dims = ( - () if method not in ("argsort", "cumsum", "cumprod") else "x" - ) - expected = xr.Variable(expected_dims, reduced) - - extra_assert(actual, expected) - xr.testing.assert_allclose(actual, expected) - - @given(st.data()) - @pytest.mark.parametrize( - ["method", "args", "kwargs"], - ( - pytest.param( - "clip", - [], - {"min": Label(values), "max": Label(values)}, - id="clip", - ), - ), - ) - def test_numpy_methods(self, method, args, kwargs, data): - # 1. create both numpy and duckarray data and put them into a variable - # 2. compute for both - # 3. convert the numpy data to duckarray data - # 4. compare - shape = (10,) - x = data.draw(create(numpy_data(shape), method)) - - args, kwargs = convert_labels(data.draw, create_label, args, kwargs) - - var = xr.Variable("x", x) - - error, match = expect_error(method, x, args, kwargs) - if error is not None: - with pytest.raises(error, match=match): - getattr(var, method)(*args, dim="x", **kwargs) - - return - else: - actual = getattr(var, method)(*args, dim="x", **kwargs) - expected = xr.Variable( - "x", getattr(np, method)(x, *args, axis=0, **kwargs) - ) - - extra_assert(actual, expected) - xr.testing.assert_allclose(actual, expected) - - if global_marks is not None: - TestModule.pytestmark = global_marks - - if marks is not None: - processed = preprocess_marks(marks) - for components, marks_ in processed: - apply_marks(TestModule, components, marks_) - - TestModule.__name__ = f"Test{name.title()}" - TestModule.__qualname__ = f"Test{name.title()}" - - return TestModule diff --git a/xarray/tests/duckarray_testing_utils.py b/xarray/tests/duckarray_testing_utils.py deleted file mode 100644 index 539d16408fe..00000000000 --- a/xarray/tests/duckarray_testing_utils.py +++ /dev/null @@ -1,123 +0,0 @@ -import collections -import itertools -import re - -import pytest - -identifier_re = r"[a-zA-Z_][a-zA-Z0-9_]*" -variant_re = re.compile( - rf"^(?P{identifier_re}(?:(?:\.|::){identifier_re})*)(?:\[(?P[^]]+)\])?$" -) - - -def concat_mappings(mapping, *others, duplicates="error"): - if not isinstance(mapping, dict): - if others: - raise ValueError("cannot pass both a iterable and multiple values") - - mapping, *others = mapping - - if duplicates == "error": - all_keys = list( - itertools.chain.from_iterable(m.keys() for m in [mapping] + others) - ) - duplicates = { - key: value - for key, value in collections.Counter(all_keys).items() - if value > 1 - } - if duplicates: - raise ValueError(f"duplicate keys found: {list(duplicates.keys())!r}") - - result = mapping.copy() - for m in others: - result.update(m) - - return result - - -def is_variant(k): - return k.startswith("[") and k.endswith("]") - - -def process_spec(name, value): - components, variant = parse_selector(name) - - if variant is not None and not isinstance(value, list): - raise ValueError(f"invalid spec: {name} → {value}") - elif isinstance(value, list): - if variant is not None: - value = {f"[{variant}]": value} - - yield components, value - elif isinstance(value, dict) and all(is_variant(k) for k in value.keys()): - yield components, value - else: - processed = itertools.chain.from_iterable( - process_spec(name, value) for name, value in value.items() - ) - - yield from ( - (components + new_components, value) for new_components, value in processed - ) - - -def preprocess_marks(marks): - def concat_values(values): - if len({type(v) for v in values}) != 1: - raise ValueError("mixed types are not supported") - - if len(values) == 1: - return values[0] - elif isinstance(values[0], list): - raise ValueError("cannot have multiple mark lists per test") - else: - return concat_mappings(values) - - flattened = itertools.chain.from_iterable( - process_spec(name, value) for name, value in marks.items() - ) - key = lambda x: x[0] - grouped = itertools.groupby(sorted(flattened, key=key), key=key) - result = [ - (components, concat_values([v for _, v in group])) - for components, group in grouped - ] - return result - - -def parse_selector(selector): - match = variant_re.match(selector) - if match is not None: - groups = match.groupdict() - variant = groups["variant"] - name = groups["name"] - else: - raise ValueError(f"invalid test name: {name!r}") - - components = name.split(".") - return components, variant - - -def get_test(module, components): - *parent_names, name = components - - parent = module - for parent_name in parent_names: - parent = getattr(parent, parent_name) - - test = getattr(parent, name) - - return parent, test, name - - -def apply_marks(module, components, marks): - parent, test, test_name = get_test(module, components) - if isinstance(marks, list): - # mark the whole test - marked_test = test - for mark in marks: - marked_test = mark(marked_test) - else: - marked_test = pytest.mark.attach_marks(marks)(test) - setattr(parent, test_name, marked_test) diff --git a/xarray/tests/test_duckarray_testing.py b/xarray/tests/test_duckarray_testing.py deleted file mode 100644 index 3d56d7abf42..00000000000 --- a/xarray/tests/test_duckarray_testing.py +++ /dev/null @@ -1,108 +0,0 @@ -import hypothesis.strategies as st -import pytest - -from .duckarray_testing import duckarray_module -from .test_units import assert_units_equal - -da = pytest.importorskip("dask.array") -pint = pytest.importorskip("pint") -sparse = pytest.importorskip("sparse") -ureg = pint.UnitRegistry(force_ndarray_like=True) - - -def units(): - # preserve the order so these will be reduced to - units = ["m", "cm", "s", "ms"] - return st.sampled_from(units) - - -@st.composite -def create_pint(draw, data, method): - if method in ("cumprod",): - units_ = st.just("dimensionless") - else: - units_ = units() - x = draw(data) - u = draw(units_) - - if u is not None: - q = ureg.Quantity(x, u) - else: - q = x - - return q - - -@st.composite -def create_dask(draw, data, method): - x = draw(data) - return da.from_array(x, chunks=(2,)) - - -def create_pint_dask(data, method): - x = create_dask(data, method) - return create_pint(x, method) - - -@st.composite -def create_sparse(draw, data, method): - x = draw(data) - return sparse.COO.from_numpy(x) - - -TestPint = duckarray_module( - "pint", - create_pint, - extra_asserts=assert_units_equal, - marks={ - "TestVariable.test_reduce": { - "[argsort]": [ - pytest.mark.skip(reason="xarray.Variable.argsort does not support dim") - ], - "[prod]": [pytest.mark.skip(reason="nanprod drops units")], - }, - }, - global_marks=[ - pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), - ], -) - -TestPintDask = duckarray_module( - "pint_dask", - create_pint_dask, - extra_asserts=assert_units_equal, - marks={ - "TestVariable.test_reduce": { - "[argsort]": [ - pytest.mark.skip(reason="xarray.Variable.argsort does not support dim") - ], - "[cumsum]": [pytest.mark.skip(reason="nancumsum drops the units")], - "[median]": [pytest.mark.skip(reason="median does not support dim")], - "[prod]": [pytest.mark.skip(reason="prod drops the units")], - "[cumprod]": [pytest.mark.skip(reason="cumprod drops the units")], - "[std]": [pytest.mark.skip(reason="nanstd drops the units")], - "[sum]": [pytest.mark.skip(reason="sum drops the units")], - "[var]": [pytest.mark.skip(reason="var drops the units")], - }, - }, - global_marks=[ - pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), - ], -) - -TestSparse = duckarray_module( - "sparse", - create_sparse, - marks={ - "TestVariable.test_reduce": { - "[argmax]": [pytest.mark.skip(reason="not implemented by sparse")], - "[argmin]": [pytest.mark.skip(reason="not implemented by sparse")], - "[argsort]": [pytest.mark.skip(reason="not implemented by sparse")], - "[cumprod]": [pytest.mark.skip(reason="not implemented by sparse")], - "[cumsum]": [pytest.mark.skip(reason="not implemented by sparse")], - "[median]": [pytest.mark.skip(reason="not implemented by sparse")], - "[std]": [pytest.mark.skip(reason="nanstd not implemented, yet")], - "[var]": [pytest.mark.skip(reason="nanvar not implemented, yet")], - }, - }, -) diff --git a/xarray/tests/test_duckarray_testing_utils.py b/xarray/tests/test_duckarray_testing_utils.py deleted file mode 100644 index 4b55bf9b0c3..00000000000 --- a/xarray/tests/test_duckarray_testing_utils.py +++ /dev/null @@ -1,240 +0,0 @@ -import pytest - -from . import duckarray_testing_utils - - -class Module: - def module_test1(self): - pass - - def module_test2(self): - pass - - @pytest.mark.parametrize("param1", ("a", "b", "c")) - def parametrized_test(self, param1): - pass - - class Submodule: - def submodule_test(self): - pass - - -@pytest.mark.parametrize( - ["selector", "expected"], - ( - ("test_function", (["test_function"], None)), - ( - "TestGroup.TestSubgroup.test_function", - (["TestGroup", "TestSubgroup", "test_function"], None), - ), - ("test_function[variant]", (["test_function"], "variant")), - ( - "TestGroup.test_function[variant]", - (["TestGroup", "test_function"], "variant"), - ), - ), -) -def test_parse_selector(selector, expected): - actual = duckarray_testing_utils.parse_selector(selector) - assert actual == expected - - -@pytest.mark.parametrize( - ["components", "expected"], - ( - (["module_test1"], (Module, Module.module_test1, "module_test1")), - ( - ["Submodule", "submodule_test"], - (Module.Submodule, Module.Submodule.submodule_test, "submodule_test"), - ), - ), -) -def test_get_test(components, expected): - module = Module - actual = duckarray_testing_utils.get_test(module, components) - assert actual == expected - - -@pytest.mark.parametrize( - "marks", - ( - pytest.param([pytest.mark.skip(reason="arbitrary")], id="single mark"), - pytest.param( - [ - pytest.mark.filterwarnings("error"), - pytest.mark.parametrize("a", (0, 1, 2)), - ], - id="multiple marks", - ), - ), -) -def test_apply_marks_normal(marks): - class Module: - def module_test(self): - pass - - module = Module - components = ["module_test"] - - duckarray_testing_utils.apply_marks(module, components, marks) - marked = Module.module_test - actual = marked.pytestmark - expected = [m.mark for m in marks] - - assert actual == expected - - -@pytest.mark.parametrize( - ["mappings", "use_others", "duplicates", "expected", "error", "message"], - ( - pytest.param( - [{"a": 1}, {"b": 2}], - False, - False, - {"a": 1, "b": 2}, - False, - None, - id="iterable", - ), - pytest.param( - [{"a": 1}, {"b": 2}], - True, - False, - {"a": 1, "b": 2}, - False, - None, - id="use others", - ), - pytest.param( - [[{"a": 1}], {"b": 2}], - True, - False, - None, - True, - "cannot pass both a iterable and multiple values", - id="iterable and args", - ), - pytest.param( - [{"a": 1}, {"a": 2}], - False, - "error", - None, - True, - "duplicate keys found", - id="raise on duplicates", - ), - ), -) -def test_concat_mappings(mappings, use_others, duplicates, expected, error, message): - func = duckarray_testing_utils.concat_mappings - call = ( - lambda m: func(*m, duplicates=duplicates) - if use_others - else func(m, duplicates=duplicates) - ) - if error: - with pytest.raises(ValueError, match=message): - call(mappings) - else: - actual = call(mappings) - - assert actual == expected - - -@pytest.mark.parametrize( - ["marks", "expected", "error"], - ( - pytest.param( - {"test_func": [1, 2]}, - [(["test_func"], [1, 2])], - False, - id="single test", - ), - pytest.param( - {"test_func1": [1, 2], "test_func2": [3, 4]}, - [(["test_func1"], [1, 2]), (["test_func2"], [3, 4])], - False, - id="multiple tests", - ), - pytest.param( - {"TestGroup": {"test_func1": [1, 2], "test_func2": [3, 4]}}, - [ - (["TestGroup", "test_func1"], [1, 2]), - (["TestGroup", "test_func2"], [3, 4]), - ], - False, - id="test group dict", - ), - pytest.param( - {"test_func[variant]": [1, 2]}, - [(["test_func"], {"[variant]": [1, 2]})], - False, - id="single variant", - ), - pytest.param( - {"test_func[variant]": {"key": [1, 2]}}, - None, - True, - id="invalid variant", - ), - pytest.param( - {"test_func": {"[v1]": [1, 2], "[v2]": [3, 4]}}, - [(["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]})], - False, - id="multiple variants combined", - ), - pytest.param( - {"test_func[v1]": [1, 2], "test_func[v2]": [3, 4]}, - [(["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]})], - False, - id="multiple variants separate", - ), - pytest.param( - {"test_func[v1]": [1, 2], "test_func[v2]": [3, 4], "test_func2": [4, 5]}, - [ - (["test_func"], {"[v1]": [1, 2], "[v2]": [3, 4]}), - (["test_func2"], [4, 5]), - ], - False, - id="multiple variants multiple tests separate", - ), - ), -) -def test_preprocess_marks(marks, expected, error): - if error: - with pytest.raises(ValueError): - duckarray_testing_utils.preprocess_marks(marks) - else: - actual = duckarray_testing_utils.preprocess_marks(marks) - - assert actual == expected - - -@pytest.mark.parametrize( - "marks", - ( - pytest.param([pytest.mark.skip(reason="arbitrary")], id="single mark"), - pytest.param( - [ - pytest.mark.filterwarnings("error"), - pytest.mark.parametrize("a", (0, 1, 2)), - ], - id="multiple marks", - ), - ), -) -@pytest.mark.parametrize("variant", ("[a]", "[b]", "[c]")) -def test_apply_marks_variant(marks, variant): - class Module: - @pytest.mark.parametrize("param1", ("a", "b", "c")) - def func(param1): - pass - - module = Module - components = ["func"] - - duckarray_testing_utils.apply_marks(module, components, {variant: marks}) - marked = Module.func - actual = marked.pytestmark - - assert len(actual) > 1 and any(mark.name == "attach_marks" for mark in actual) From d595cd66b891443fc8b0c96d293d87e1003e2ffb Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 17:29:21 +0200 Subject: [PATCH 057/140] rename the tests --- xarray/tests/duckarrays/base/reduce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 8ed2e3915bf..d00ac12b877 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -43,7 +43,7 @@ def create(op): ), ) @given(st.data()) - def test_variable_reduce(self, method, data): + def test_reduce(self, method, data): raw = data.draw(self.create(method)) dims = create_dimension_names(raw.ndim) var = xr.Variable(dims, raw) From 2b0dcba1c6769ce1f2c0bc8d2cdc9ce3bd187da5 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 17:58:12 +0200 Subject: [PATCH 058/140] add a mark to skip individual test nodes --- xarray/tests/conftest.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/xarray/tests/conftest.py b/xarray/tests/conftest.py index 78cbaa3fa32..a74e8ee46d9 100644 --- a/xarray/tests/conftest.py +++ b/xarray/tests/conftest.py @@ -1,26 +1,40 @@ def pytest_configure(config): config.addinivalue_line( "markers", - "attach_marks(marks): function to attach marks to tests and test variants", + "apply_marks(marks): function to attach marks to tests and test variants", ) +def always_sequence(obj): + if not isinstance(obj, (list, tuple)): + obj = [obj] + + return obj + + def pytest_collection_modifyitems(session, config, items): for item in items: - mark = item.get_closest_marker("attach_marks") + mark = item.get_closest_marker("apply_marks") if mark is None: continue - index = item.own_markers.index(mark) - del item.own_markers[index] marks = mark.args[0] if not isinstance(marks, dict): continue + possible_marks = marks.get(item.originalname) + if possible_marks is None: + continue + + if not isinstance(possible_marks, dict): + for mark in always_sequence(possible_marks): + item.add_marker(mark) + continue + variant = item.name[len(item.originalname) :] - to_attach = marks.get(variant) + to_attach = possible_marks.get(variant) if to_attach is None: continue - for mark in to_attach: + for mark in always_sequence(to_attach): item.add_marker(mark) From 6b61900a4ec9890669e1c37981ba7f6234266f6e Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 17:58:54 +0200 Subject: [PATCH 059/140] skip the prod and std tests --- xarray/tests/duckarrays/test_units.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 1b9d65f357d..d094aa39cf5 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -18,6 +18,16 @@ all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) +@pytest.mark.apply_marks( + { + "test_reduce": { + "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), + "[std]": pytest.mark.skip( + reason="bottleneck's implementation of std is incorrect for float32" + ), + } + } +) class TestVariableReduceMethods(base.VariableReduceTests): @st.composite @staticmethod From b9535a1a4d040458926d7f1ab7fcc119acae8f0e Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 18:00:50 +0200 Subject: [PATCH 060/140] skip all sparse tests for now --- xarray/tests/duckarrays/test_sparse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index a08ed7baf0d..a03d143d745 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -17,6 +17,9 @@ def convert(arr): return utils.numpy_array.map(convert) +@pytest.mark.apply_marks( + {"test_reduce": pytest.mark.skip(reason="sparse times out on the first call")} +) class TestVariableReduceMethods(base.VariableReduceTests): @staticmethod def create(op): From c675f8d51de087eec76be65b872be46faec62bfc Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 23:40:03 +0200 Subject: [PATCH 061/140] also skip var --- xarray/tests/duckarrays/test_units.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index d094aa39cf5..085c0500ccd 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -25,6 +25,9 @@ "[std]": pytest.mark.skip( reason="bottleneck's implementation of std is incorrect for float32" ), + "[var]": pytest.mark.skip( + reason="bottleneck's implementation of var is incorrect for float32" + ), } } ) From 6e7c5387899a0e7605dd91f80d53cce3f64dea9e Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 22 Apr 2021 23:40:24 +0200 Subject: [PATCH 062/140] add a duckarray base class --- xarray/tests/duckarrays/base/__init__.py | 3 +- xarray/tests/duckarrays/base/reduce.py | 45 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/tests/duckarrays/base/__init__.py index 14a4ce0ed89..b794b29d301 100644 --- a/xarray/tests/duckarrays/base/__init__.py +++ b/xarray/tests/duckarrays/base/__init__.py @@ -1,5 +1,6 @@ -from .reduce import VariableReduceTests +from .reduce import DataArrayReduceTests, VariableReduceTests __all__ = [ "VariableReduceTests", + "DataArrayReduceTests", ] diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index d00ac12b877..0dccd089652 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -52,3 +52,48 @@ def test_reduce(self, method, data): reduce_dims = valid_dims_from_axes(dims, reduce_axes) self.check_reduce(var, method, dim=reduce_dims) + + +class DataArrayReduceTests: + def check_reduce(self, obj, op, *args, **kwargs): + actual = getattr(obj, op)(*args, **kwargs) + + data = np.asarray(obj.data) + expected = getattr(obj.copy(data=data), op)(*args, **kwargs) + + note(f"actual:\n{actual}") + note(f"expected:\n{expected}") + + assert_identical(actual, expected) + + @staticmethod + def create(op): + return numpy_array + + @pytest.mark.parametrize( + "method", + ( + "all", + "any", + "cumprod", + "cumsum", + "max", + "mean", + "median", + "min", + "prod", + "std", + "sum", + "var", + ), + ) + @given(st.data()) + def test_reduce(self, method, data): + raw = data.draw(self.create(method)) + dims = create_dimension_names(raw.ndim) + var = xr.DataArray(dims=dims, data=raw) + + reduce_axes = data.draw(valid_axis(raw.ndim)) + reduce_dims = valid_dims_from_axes(dims, reduce_axes) + + self.check_reduce(var, method, dim=reduce_dims) From e0ee7a683e0e005ed52e093114a058ab07056a86 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 01:18:39 +0200 Subject: [PATCH 063/140] move the strategies to a separate file and add a variable strategy --- xarray/tests/duckarrays/base/reduce.py | 27 ++++++------ xarray/tests/duckarrays/base/strategies.py | 49 ++++++++++++++++++++++ xarray/tests/duckarrays/base/utils.py | 22 ---------- xarray/tests/duckarrays/test_units.py | 6 +-- 4 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 xarray/tests/duckarrays/base/strategies.py diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 0dccd089652..e2716cb58a1 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -6,7 +6,8 @@ import xarray as xr from ... import assert_identical -from .utils import create_dimension_names, numpy_array, valid_axis, valid_dims_from_axes +from . import strategies +from .utils import valid_dims_from_axes class VariableReduceTests: @@ -22,8 +23,8 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op): - return numpy_array + def create(op, shape): + return strategies.numpy_array(shape) @pytest.mark.parametrize( "method", @@ -44,12 +45,10 @@ def create(op): ) @given(st.data()) def test_reduce(self, method, data): - raw = data.draw(self.create(method)) - dims = create_dimension_names(raw.ndim) - var = xr.Variable(dims, raw) + var = data.draw(strategies.variable(lambda shape: self.create(method, shape))) - reduce_axes = data.draw(valid_axis(raw.ndim)) - reduce_dims = valid_dims_from_axes(dims, reduce_axes) + reduce_axes = data.draw(strategies.valid_axis(var.ndim)) + reduce_dims = valid_dims_from_axes(var.dims, reduce_axes) self.check_reduce(var, method, dim=reduce_dims) @@ -67,8 +66,8 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op): - return numpy_array + def create(op, shape): + return strategies.numpy_array(shape) @pytest.mark.parametrize( "method", @@ -90,10 +89,10 @@ def create(op): @given(st.data()) def test_reduce(self, method, data): raw = data.draw(self.create(method)) - dims = create_dimension_names(raw.ndim) - var = xr.DataArray(dims=dims, data=raw) + dims = strategies.create_dimension_names(raw.ndim) + arr = xr.DataArray(dims=dims, data=raw) - reduce_axes = data.draw(valid_axis(raw.ndim)) + reduce_axes = data.draw(strategies.valid_axis(raw.ndim)) reduce_dims = valid_dims_from_axes(dims, reduce_axes) - self.check_reduce(var, method, dim=reduce_dims) + self.check_reduce(arr, method, dim=reduce_dims) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py new file mode 100644 index 00000000000..daa39c1745b --- /dev/null +++ b/xarray/tests/duckarrays/base/strategies.py @@ -0,0 +1,49 @@ +import hypothesis.extra.numpy as npst +import hypothesis.strategies as st + +import xarray as xr + + +def shapes(ndim=None): + return npst.array_shapes() + + +dtypes = ( + npst.integer_dtypes() + | npst.unsigned_integer_dtypes() + | npst.floating_dtypes() + | npst.complex_number_dtypes() +) + + +def numpy_array(shape=None): + if shape is None: + shape = npst.array_shapes() + return npst.arrays(dtype=dtypes, shape=shape) + + +def create_dimension_names(ndim): + return [f"dim_{n}" for n in range(ndim)] + + +@st.composite +def variable(draw, create_data, dims=None, shape=None, sizes=None): + if sizes is not None: + dims, sizes = zip(*draw(sizes).items()) + else: + if shape is None: + shape = draw(shapes()) + if dims is None: + dims = create_dimension_names(len(shape)) + + data = create_data(shape) + + return xr.Variable(dims, draw(data)) + + +def valid_axis(ndim): + return st.none() | st.integers(-ndim, ndim - 1) + + +def valid_axes(ndim): + return valid_axis(ndim) | npst.valid_tuple_axes(ndim) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index bb8e16340e4..abe2749ada3 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -1,17 +1,6 @@ import warnings from contextlib import contextmanager -import hypothesis.extra.numpy as npst -import hypothesis.strategies as st - -shapes = npst.array_shapes() -dtypes = ( - npst.integer_dtypes() - | npst.unsigned_integer_dtypes() - | npst.floating_dtypes() - | npst.complex_number_dtypes() -) - @contextmanager def suppress_warning(category, message=""): @@ -21,21 +10,10 @@ def suppress_warning(category, message=""): yield -numpy_array = npst.arrays(dtype=dtypes, shape=shapes) - - def create_dimension_names(ndim): return [f"dim_{n}" for n in range(ndim)] -def valid_axis(ndim): - return st.none() | st.integers(-ndim, ndim - 1) - - -def valid_axes(ndim): - return valid_axis(ndim) | npst.valid_tuple_axes(ndim) - - def valid_dims_from_axes(dims, axes): if axes is None: return None diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 085c0500ccd..9718b851e32 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -6,7 +6,7 @@ from .. import assert_identical from ..test_units import assert_units_equal from . import base -from .base import utils +from .base import strategies, utils pint = pytest.importorskip("pint") unit_registry = pint.UnitRegistry(force_ndarray_like=True) @@ -34,13 +34,13 @@ class TestVariableReduceMethods(base.VariableReduceTests): @st.composite @staticmethod - def create(draw, op): + def create(draw, op, shape): if op in ("cumprod",): units = st.just("dimensionless") else: units = all_units - return Quantity(draw(utils.numpy_array), draw(units)) + return Quantity(draw(strategies.numpy_array(shape)), draw(units)) def check_reduce(self, obj, op, *args, **kwargs): if ( From 3feef1c089716890e1eb86fd72f830af87c819e0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 01:23:17 +0200 Subject: [PATCH 064/140] add a simple DataArray strategy and use it in the DataArray tests --- xarray/tests/duckarrays/base/reduce.py | 10 +++------- xarray/tests/duckarrays/base/strategies.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index e2716cb58a1..15a52ce14d9 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -3,8 +3,6 @@ import pytest from hypothesis import given, note -import xarray as xr - from ... import assert_identical from . import strategies from .utils import valid_dims_from_axes @@ -88,11 +86,9 @@ def create(op, shape): ) @given(st.data()) def test_reduce(self, method, data): - raw = data.draw(self.create(method)) - dims = strategies.create_dimension_names(raw.ndim) - arr = xr.DataArray(dims=dims, data=raw) + arr = data.draw(strategies.data_array(lambda shape: self.create(method, shape))) - reduce_axes = data.draw(strategies.valid_axis(raw.ndim)) - reduce_dims = valid_dims_from_axes(dims, reduce_axes) + reduce_axes = data.draw(strategies.valid_axis(arr.ndim)) + reduce_dims = valid_dims_from_axes(arr.dims, reduce_axes) self.check_reduce(arr, method, dim=reduce_dims) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index daa39c1745b..16f9f64818c 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -41,6 +41,22 @@ def variable(draw, create_data, dims=None, shape=None, sizes=None): return xr.Variable(dims, draw(data)) +@st.composite +def data_array(draw, create_data): + name = draw(st.none() | st.text(min_size=1)) + + shape = draw(shapes()) + + dims = create_dimension_names(len(shape)) + data = draw(create_data(shape)) + + return xr.DataArray( + data=data, + name=name, + dims=dims, + ) + + def valid_axis(ndim): return st.none() | st.integers(-ndim, ndim - 1) From 2a70c380bb972ef9c3700ac79dcae0c664c92011 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 01:24:13 +0200 Subject: [PATCH 065/140] use the DataArray reduce tests with pint --- xarray/tests/duckarrays/test_units.py | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 9718b851e32..923a9987d21 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -74,3 +74,58 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_units_equal(actual, expected) assert_identical(actual, expected) + + +@pytest.mark.apply_marks( + { + "test_reduce": { + "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), + "[std]": pytest.mark.skip( + reason="bottleneck's implementation of std is incorrect for float32" + ), + } + } +) +class TestDataArrayReduceMethods(base.DataArrayReduceTests): + @st.composite + @staticmethod + def create(draw, op, shape): + if op in ("cumprod",): + units = st.just("dimensionless") + else: + units = all_units + + return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + + def check_reduce(self, obj, op, *args, **kwargs): + if ( + op in ("cumprod",) + and obj.data.size > 1 + and obj.data.units != unit_registry.dimensionless + ): + with pytest.raises(pint.DimensionalityError): + getattr(obj, op)(*args, **kwargs) + else: + actual = getattr(obj, op)(*args, **kwargs) + + note(f"actual:\n{actual}") + + without_units = obj.copy(data=obj.data.magnitude) + expected = getattr(without_units, op)(*args, **kwargs) + + func_name = f"nan{op}" if obj.dtype.kind in "fc" else op + func = getattr(np, func_name, getattr(np, op)) + func_kwargs = kwargs.copy() + dim = func_kwargs.pop("dim", None) + axis = utils.valid_axes_from_dims(obj.dims, dim) + func_kwargs["axis"] = axis + with utils.suppress_warning(RuntimeWarning): + result = func(obj.data, *args, **func_kwargs) + units = getattr(result, "units", None) + if units is not None: + expected = expected.copy(data=Quantity(expected.data, units)) + + note(f"expected:\n{expected}") + + assert_units_equal(actual, expected) + assert_identical(actual, expected) From 6a18acfa3fcceee6b05352412e26babbb0a32880 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 01:26:14 +0200 Subject: [PATCH 066/140] add a simple strategy to create Dataset objects --- xarray/tests/duckarrays/base/strategies.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 16f9f64818c..5e6ed23273e 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -57,6 +57,35 @@ def data_array(draw, create_data): ) +def dimension_sizes(sizes): + sizes_ = list(sizes.items()) + return st.lists( + elements=st.sampled_from(sizes_), min_size=1, max_size=len(sizes_) + ).map(dict) + + +@st.composite +def dataset(draw, create_data): + names = st.text(min_size=1) + sizes = draw( + st.dictionaries( + keys=names, + values=st.integers(min_value=2, max_value=20), + min_size=1, + max_size=5, + ) + ) + + data_vars = st.dictionaries( + keys=names, + values=variable(create_data, sizes=dimension_sizes(sizes)), + min_size=1, + max_size=20, + ) + + return xr.Dataset(data_vars=draw(data_vars)) + + def valid_axis(ndim): return st.none() | st.integers(-ndim, ndim - 1) From 835930c769ee3b1771aa7cfe19dbd00bad44cdff Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 17:46:35 +0200 Subject: [PATCH 067/140] fix the variable strategy --- xarray/tests/duckarrays/base/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 5e6ed23273e..88ccd4dd906 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -29,7 +29,7 @@ def create_dimension_names(ndim): @st.composite def variable(draw, create_data, dims=None, shape=None, sizes=None): if sizes is not None: - dims, sizes = zip(*draw(sizes).items()) + dims, shape = zip(*draw(sizes).items()) else: if shape is None: shape = draw(shapes()) From 0a5c487c93070a33f00ed508ffc4d338628a52b7 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 17:47:15 +0200 Subject: [PATCH 068/140] adjust the dataset strategy --- xarray/tests/duckarrays/base/strategies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 88ccd4dd906..8589d7f2d66 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -70,9 +70,9 @@ def dataset(draw, create_data): sizes = draw( st.dictionaries( keys=names, - values=st.integers(min_value=2, max_value=20), + values=st.integers(min_value=2, max_value=10), min_size=1, - max_size=5, + max_size=4, ) ) @@ -80,7 +80,7 @@ def dataset(draw, create_data): keys=names, values=variable(create_data, sizes=dimension_sizes(sizes)), min_size=1, - max_size=20, + max_size=10, ) return xr.Dataset(data_vars=draw(data_vars)) From d1184a4abe3db3f3714359e3a15072d2cfcf7696 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 17:58:34 +0200 Subject: [PATCH 069/140] parametrize the dataset strategy --- xarray/tests/duckarrays/base/strategies.py | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 8589d7f2d66..3e4f72afeb8 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -65,22 +65,34 @@ def dimension_sizes(sizes): @st.composite -def dataset(draw, create_data): +def dataset( + draw, + create_data, + *, + min_dims=1, + max_dims=4, + min_size=2, + max_size=10, + min_vars=1, + max_vars=10, +): names = st.text(min_size=1) sizes = draw( st.dictionaries( keys=names, - values=st.integers(min_value=2, max_value=10), - min_size=1, - max_size=4, + values=st.integers(min_value=min_size, max_value=max_size), + min_size=min_dims, + max_size=max_dims, ) ) + variable_names = names.filter(lambda n: n not in sizes) + data_vars = st.dictionaries( - keys=names, + keys=variable_names, values=variable(create_data, sizes=dimension_sizes(sizes)), - min_size=1, - max_size=10, + min_size=min_vars, + max_size=max_vars, ) return xr.Dataset(data_vars=draw(data_vars)) From 12b552733ea0cc1d945725f7e001b72f87a63820 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:30:28 +0200 Subject: [PATCH 070/140] fix some of the pint testing utils --- xarray/tests/test_units.py | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/xarray/tests/test_units.py b/xarray/tests/test_units.py index cc3c1a292ec..df18547c0a7 100644 --- a/xarray/tests/test_units.py +++ b/xarray/tests/test_units.py @@ -76,11 +76,14 @@ def array_strip_units(array): def array_attach_units(data, unit): + if unit is None or (isinstance(unit, int) and unit == 1): + return data + if isinstance(data, Quantity): raise ValueError(f"cannot attach unit {unit} to quantity {data}") try: - quantity = data * unit + quantity = unit._REGISTRY.Quantity(data, unit) except np.core._exceptions.UFuncTypeError: if isinstance(unit, unit_registry.Unit): raise @@ -165,37 +168,34 @@ def attach_units(obj, units): return array_attach_units(obj, units) if isinstance(obj, xr.Dataset): - data_vars = { - name: attach_units(value, units) for name, value in obj.data_vars.items() + variables = { + name: attach_units(value, {None: units.get(name)}) + for name, value in obj.variables.items() } - coords = { - name: attach_units(value, units) for name, value in obj.coords.items() + name: var for name, var in variables.items() if name in obj._coord_names + } + data_vars = { + name: var for name, var in variables.items() if name not in obj._coord_names } - new_obj = xr.Dataset(data_vars=data_vars, coords=coords, attrs=obj.attrs) elif isinstance(obj, xr.DataArray): # try the array name, "data" and None, then fall back to dimensionless - data_units = units.get(obj.name, None) or units.get(None, None) or 1 - - data = array_attach_units(obj.data, data_units) - - coords = { - name: ( - (value.dims, array_attach_units(value.data, units.get(name) or 1)) - if name in units - # to preserve multiindexes - else value - ) - for name, value in obj.coords.items() - } - dims = obj.dims - attrs = obj.attrs - - new_obj = xr.DataArray( - name=obj.name, data=data, coords=coords, attrs=attrs, dims=dims - ) + units = units.copy() + THIS_ARRAY = xr.core.dataarray._THIS_ARRAY + if obj.name in units: + name = obj.name + elif None in units: + name = None + units[THIS_ARRAY] = units.pop(name) + ds = obj._to_temp_dataset() + attached = attach_units(ds, units) + new_obj = obj._from_temp_dataset(attached, name=obj.name) else: + if isinstance(obj, xr.IndexVariable): + # no units for index variables + return obj + data_units = units.get("data", None) or units.get(None, None) or 1 data = array_attach_units(obj.data, data_units) From 1f9531840766e3620dead8ad52d85157662613a6 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:31:10 +0200 Subject: [PATCH 071/140] use flatmap to define the data_vars strategy --- xarray/tests/duckarrays/base/strategies.py | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 3e4f72afeb8..cd0f4ff2ed7 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -77,22 +77,20 @@ def dataset( max_vars=10, ): names = st.text(min_size=1) - sizes = draw( - st.dictionaries( - keys=names, - values=st.integers(min_value=min_size, max_value=max_size), - min_size=min_dims, - max_size=max_dims, - ) + sizes = st.dictionaries( + keys=names, + values=st.integers(min_value=min_size, max_value=max_size), + min_size=min_dims, + max_size=max_dims, ) - variable_names = names.filter(lambda n: n not in sizes) - - data_vars = st.dictionaries( - keys=variable_names, - values=variable(create_data, sizes=dimension_sizes(sizes)), - min_size=min_vars, - max_size=max_vars, + data_vars = sizes.flatmap( + lambda s: st.dictionaries( + keys=names.filter(lambda n: n not in s), + values=variable(create_data, sizes=dimension_sizes(s)), + min_size=min_vars, + max_size=max_vars, + ) ) return xr.Dataset(data_vars=draw(data_vars)) From 9800db514b3f1b448018e993e6ddc85203701290 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:33:12 +0200 Subject: [PATCH 072/140] add tests for dataset reduce --- xarray/tests/duckarrays/base/__init__.py | 3 +- xarray/tests/duckarrays/base/reduce.py | 46 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/tests/duckarrays/base/__init__.py index b794b29d301..5437e73b515 100644 --- a/xarray/tests/duckarrays/base/__init__.py +++ b/xarray/tests/duckarrays/base/__init__.py @@ -1,6 +1,7 @@ -from .reduce import DataArrayReduceTests, VariableReduceTests +from .reduce import DataArrayReduceTests, DatasetReduceTests, VariableReduceTests __all__ = [ "VariableReduceTests", "DataArrayReduceTests", + "DatasetReduceTests", ] diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 15a52ce14d9..7346d9e3ce0 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -92,3 +92,49 @@ def test_reduce(self, method, data): reduce_dims = valid_dims_from_axes(arr.dims, reduce_axes) self.check_reduce(arr, method, dim=reduce_dims) + + +class DatasetReduceTests: + def check_reduce(self, obj, op, *args, **kwargs): + actual = getattr(obj, op)(*args, **kwargs) + + data = {name: np.asarray(obj.data) for name, obj in obj.variables.items()} + expected = getattr(obj.copy(data=data), op)(*args, **kwargs) + + note(f"actual:\n{actual}") + note(f"expected:\n{expected}") + + assert_identical(actual, expected) + + @staticmethod + def create(op, shape): + return strategies.numpy_array(shape) + + @pytest.mark.parametrize( + "method", + ( + "all", + "any", + "cumprod", + "cumsum", + "max", + "mean", + "median", + "min", + "prod", + "std", + "sum", + "var", + ), + ) + @given(st.data()) + def test_reduce(self, method, data): + ds = data.draw( + strategies.dataset(lambda shape: self.create(method, shape), max_size=5) + ) + + reduce_dims = data.draw(st.sampled_from(list(ds.dims))) + # reduce_axes = data.draw(strategies.valid_axis(len(ds.dims))) + # reduce_dims = valid_dims_from_axes(ds.dims, reduce_axes) + + self.check_reduce(ds, method, dim=reduce_dims) From c43f35e76cdeabbd5f89d224a688f8c77bb12c1b Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:34:36 +0200 Subject: [PATCH 073/140] demonstrate the use of the dataset reduce tests using pint --- xarray/tests/duckarrays/test_units.py | 67 ++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 923a9987d21..ccb7a1188fb 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -4,7 +4,7 @@ from hypothesis import note from .. import assert_identical -from ..test_units import assert_units_equal +from ..test_units import assert_units_equal, attach_units, strip_units from . import base from .base import strategies, utils @@ -129,3 +129,68 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_units_equal(actual, expected) assert_identical(actual, expected) + + +@pytest.mark.apply_marks( + { + "test_reduce": { + "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), + } + } +) +class TestDatasetReduceMethods(base.DatasetReduceTests): + @st.composite + @staticmethod + def create(draw, op, shape): + if op in ("cumprod",): + units = st.just("dimensionless") + else: + units = all_units + + return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + + def compute_expected(self, obj, op, *args, **kwargs): + def apply_func(op, var, *args, **kwargs): + dim = kwargs.pop("dim", None) + if dim in var.dims: + axis = utils.valid_axes_from_dims(var.dims, dim) + else: + axis = None + kwargs["axis"] = axis + + arr = var.data + func_name = f"nan{op}" if arr.dtype.kind in "fc" else op + func = getattr(np, func_name, getattr(np, op)) + with utils.suppress_warning(RuntimeWarning): + result = func(arr, *args, **kwargs) + + return getattr(result, "units", None) + + without_units = strip_units(obj) + result_without_units = getattr(without_units, op)(*args, **kwargs) + units = { + name: apply_func(op, var, *args, **kwargs) + for name, var in obj.variables.items() + } + attached = attach_units(result_without_units, units) + return attached + + def check_reduce(self, obj, op, *args, **kwargs): + if op in ("cumprod",) and any( + var.size > 1 + and getattr(var.data, "units", None) != unit_registry.dimensionless + for var in obj.variables.values() + ): + with pytest.raises(pint.DimensionalityError): + getattr(obj, op)(*args, **kwargs) + else: + actual = getattr(obj, op)(*args, **kwargs) + + note(f"actual:\n{actual}") + + expected = self.compute_expected(obj, op, *args, **kwargs) + + note(f"expected:\n{expected}") + + assert_units_equal(actual, expected) + assert_identical(actual, expected) From d1b541e091c56fcf1bafabf7ea6e3b32ae5f04c0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:50:13 +0200 Subject: [PATCH 074/140] simplify check_reduce --- xarray/tests/duckarrays/test_units.py | 77 ++++++++++++--------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index ccb7a1188fb..c4ff62c23b8 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -18,6 +18,23 @@ all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) +def apply_func(op, var, *args, **kwargs): + dim = kwargs.pop("dim", None) + if dim in var.dims: + axis = utils.valid_axes_from_dims(var.dims, dim) + else: + axis = None + kwargs["axis"] = axis + + arr = var.data + func_name = f"nan{op}" if arr.dtype.kind in "fc" else op + func = getattr(np, func_name, getattr(np, op)) + with utils.suppress_warning(RuntimeWarning): + result = func(arr, *args, **kwargs) + + return getattr(result, "units", None) + + @pytest.mark.apply_marks( { "test_reduce": { @@ -42,6 +59,13 @@ def create(draw, op, shape): return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + def compute_expected(self, obj, op, *args, **kwargs): + without_units = strip_units(obj) + expected = getattr(without_units, op)(*args, **kwargs) + + units = apply_func(op, obj, *args, **kwargs) + return attach_units(expected, {None: units}) + def check_reduce(self, obj, op, *args, **kwargs): if ( op in ("cumprod",) @@ -55,20 +79,7 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"actual:\n{actual}") - without_units = obj.copy(data=obj.data.magnitude) - expected = getattr(without_units, op)(*args, **kwargs) - - func_name = f"nan{op}" if obj.dtype.kind in "fc" else op - func = getattr(np, func_name, getattr(np, op)) - func_kwargs = kwargs.copy() - dim = func_kwargs.pop("dim", None) - axis = utils.valid_axes_from_dims(obj.dims, dim) - func_kwargs["axis"] = axis - with utils.suppress_warning(RuntimeWarning): - result = func(obj.data, *args, **func_kwargs) - units = getattr(result, "units", None) - if units is not None: - expected = expected.copy(data=Quantity(expected.data, units)) + expected = self.compute_expected(obj, op, *args, **kwargs) note(f"expected:\n{expected}") @@ -97,6 +108,13 @@ def create(draw, op, shape): return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + def compute_expected(self, obj, op, *args, **kwargs): + without_units = strip_units(obj) + expected = getattr(without_units, op)(*args, **kwargs) + units = apply_func(op, obj.variable, *args, **kwargs) + + return attach_units(expected, {obj.name: units}) + def check_reduce(self, obj, op, *args, **kwargs): if ( op in ("cumprod",) @@ -110,20 +128,7 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"actual:\n{actual}") - without_units = obj.copy(data=obj.data.magnitude) - expected = getattr(without_units, op)(*args, **kwargs) - - func_name = f"nan{op}" if obj.dtype.kind in "fc" else op - func = getattr(np, func_name, getattr(np, op)) - func_kwargs = kwargs.copy() - dim = func_kwargs.pop("dim", None) - axis = utils.valid_axes_from_dims(obj.dims, dim) - func_kwargs["axis"] = axis - with utils.suppress_warning(RuntimeWarning): - result = func(obj.data, *args, **func_kwargs) - units = getattr(result, "units", None) - if units is not None: - expected = expected.copy(data=Quantity(expected.data, units)) + expected = self.compute_expected(obj, op, *args, **kwargs) note(f"expected:\n{expected}") @@ -150,22 +155,6 @@ def create(draw, op, shape): return Quantity(draw(strategies.numpy_array(shape)), draw(units)) def compute_expected(self, obj, op, *args, **kwargs): - def apply_func(op, var, *args, **kwargs): - dim = kwargs.pop("dim", None) - if dim in var.dims: - axis = utils.valid_axes_from_dims(var.dims, dim) - else: - axis = None - kwargs["axis"] = axis - - arr = var.data - func_name = f"nan{op}" if arr.dtype.kind in "fc" else op - func = getattr(np, func_name, getattr(np, op)) - with utils.suppress_warning(RuntimeWarning): - result = func(arr, *args, **kwargs) - - return getattr(result, "units", None) - without_units = strip_units(obj) result_without_units = getattr(without_units, op)(*args, **kwargs) units = { From 19d9d96baa2e5ff42c658bc274b138ffc09b7f84 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:50:41 +0200 Subject: [PATCH 075/140] remove apparently unnecessary skips --- xarray/tests/duckarrays/test_units.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index c4ff62c23b8..4bb1d75defe 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -39,12 +39,6 @@ def apply_func(op, var, *args, **kwargs): { "test_reduce": { "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), - "[std]": pytest.mark.skip( - reason="bottleneck's implementation of std is incorrect for float32" - ), - "[var]": pytest.mark.skip( - reason="bottleneck's implementation of var is incorrect for float32" - ), } } ) @@ -91,9 +85,6 @@ def check_reduce(self, obj, op, *args, **kwargs): { "test_reduce": { "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), - "[std]": pytest.mark.skip( - reason="bottleneck's implementation of std is incorrect for float32" - ), } } ) From 69e06242d7a38d189cd646ee35ee8fb9bf0c22bc Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 18:57:36 +0200 Subject: [PATCH 076/140] skip the tests if hypothesis is missing --- xarray/tests/duckarrays/test_sparse.py | 2 ++ xarray/tests/duckarrays/test_units.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index a03d143d745..d580f1b550f 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,5 +1,7 @@ import pytest +pytest.importorskip("hypothesis") + from .. import assert_allclose from . import base from .base import utils diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 4bb1d75defe..ba655c523a7 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -1,6 +1,9 @@ +import pytest + +pytest.importorskip("hypothesis") + import hypothesis.strategies as st import numpy as np -import pytest from hypothesis import note from .. import assert_identical From c7f667761e984cf59fae5616bd0d30fc7d1f4801 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 19:04:52 +0200 Subject: [PATCH 077/140] update the sparse tests --- xarray/tests/duckarrays/test_sparse.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index d580f1b550f..6074d437267 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -4,28 +4,36 @@ from .. import assert_allclose from . import base -from .base import utils +from .base import strategies sparse = pytest.importorskip("sparse") -def create(op): +def create(op, shape): def convert(arr): if arr.ndim == 0: return arr return sparse.COO.from_numpy(arr) - return utils.numpy_array.map(convert) + return strategies.numpy_array(shape).map(convert) @pytest.mark.apply_marks( - {"test_reduce": pytest.mark.skip(reason="sparse times out on the first call")} + { + "test_reduce": { + "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), + "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), + "[median]": pytest.mark.skip(reason="median not implemented by sparse"), + "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), + "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), + } + } ) class TestVariableReduceMethods(base.VariableReduceTests): @staticmethod - def create(op): - return create(op) + def create(op, shape): + return create(op, shape) def check_reduce(self, obj, op, *args, **kwargs): actual = getattr(obj, op)(*args, **kwargs) From 396c2ba8d78f0439b0e9f0e107d1809dbf46862d Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 19:16:51 +0200 Subject: [PATCH 078/140] add DataArray and Dataset tests for sparse --- xarray/tests/duckarrays/test_sparse.py | 76 ++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 6074d437267..4ca4cc96027 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -2,6 +2,8 @@ pytest.importorskip("hypothesis") +from xarray import DataArray, Dataset, Variable + from .. import assert_allclose from . import base from .base import strategies @@ -19,6 +21,29 @@ def convert(arr): return strategies.numpy_array(shape).map(convert) +def as_dense(obj): + if isinstance(obj, Variable) and isinstance(obj.data, sparse.COO): + new_obj = obj.copy(data=obj.data.todense()) + elif isinstance(obj, DataArray): + ds = obj._to_temp_dataset() + dense = as_dense(ds) + new_obj = obj._from_temp_dataset(dense) + elif isinstance(obj, Dataset): + variables = {name: as_dense(var) for name, var in obj.variables.items()} + coords = { + name: var for name, var in variables.items() if name in obj._coord_names + } + data_vars = { + name: var for name, var in variables.items() if name not in obj._coord_names + } + + new_obj = Dataset(coords=coords, data_vars=data_vars, attrs=obj.attrs) + else: + new_obj = obj + + return new_obj + + @pytest.mark.apply_marks( { "test_reduce": { @@ -36,12 +61,53 @@ def create(op, shape): return create(op, shape) def check_reduce(self, obj, op, *args, **kwargs): - actual = getattr(obj, op)(*args, **kwargs) + actual = as_dense(getattr(obj, op)(*args, **kwargs)) + expected = getattr(as_dense(obj), op)(*args, **kwargs) + + assert_allclose(actual, expected) - if isinstance(actual.data, sparse.COO): - actual = actual.copy(data=actual.data.todense()) - dense = obj.copy(data=obj.data.todense()) - expected = getattr(dense, op)(*args, **kwargs) +@pytest.mark.apply_marks( + { + "test_reduce": { + "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), + "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), + "[median]": pytest.mark.skip(reason="median not implemented by sparse"), + "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), + "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), + } + } +) +class TestDataArrayReduceMethods(base.DataArrayReduceTests): + @staticmethod + def create(op, shape): + return create(op, shape) + + def check_reduce(self, obj, op, *args, **kwargs): + actual = as_dense(getattr(obj, op)(*args, **kwargs)) + expected = getattr(as_dense(obj), op)(*args, **kwargs) + + assert_allclose(actual, expected) + + +@pytest.mark.apply_marks( + { + "test_reduce": { + "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), + "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), + "[median]": pytest.mark.skip(reason="median not implemented by sparse"), + "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), + "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), + } + } +) +class TestDatasetReduceMethods(base.DatasetReduceTests): + @staticmethod + def create(op, shape): + return create(op, shape) + + def check_reduce(self, obj, op, *args, **kwargs): + actual = as_dense(getattr(obj, op)(*args, **kwargs)) + expected = getattr(as_dense(obj), op)(*args, **kwargs) assert_allclose(actual, expected) From ead706e03df52f6e86abaedc334a014aef8f5f9b Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 19:22:10 +0200 Subject: [PATCH 079/140] fix attach_units --- xarray/tests/test_units.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_units.py b/xarray/tests/test_units.py index df18547c0a7..9489cb8ef90 100644 --- a/xarray/tests/test_units.py +++ b/xarray/tests/test_units.py @@ -183,11 +183,17 @@ def attach_units(obj, units): # try the array name, "data" and None, then fall back to dimensionless units = units.copy() THIS_ARRAY = xr.core.dataarray._THIS_ARRAY + unset = object() if obj.name in units: name = obj.name elif None in units: name = None - units[THIS_ARRAY] = units.pop(name) + else: + name = unset + + if name is not unset: + units[THIS_ARRAY] = units.pop(name) + ds = obj._to_temp_dataset() attached = attach_units(ds, units) new_obj = obj._from_temp_dataset(attached, name=obj.name) From 3cf952311296eee254d47afada61082a091152ab Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 19:27:05 +0200 Subject: [PATCH 080/140] rename the test classes --- xarray/tests/duckarrays/test_sparse.py | 6 +++--- xarray/tests/duckarrays/test_units.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 4ca4cc96027..4492f1a7f20 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -55,7 +55,7 @@ def as_dense(obj): } } ) -class TestVariableReduceMethods(base.VariableReduceTests): +class TestSparseVariableReduceMethods(base.VariableReduceTests): @staticmethod def create(op, shape): return create(op, shape) @@ -78,7 +78,7 @@ def check_reduce(self, obj, op, *args, **kwargs): } } ) -class TestDataArrayReduceMethods(base.DataArrayReduceTests): +class TestSparseDataArrayReduceMethods(base.DataArrayReduceTests): @staticmethod def create(op, shape): return create(op, shape) @@ -101,7 +101,7 @@ def check_reduce(self, obj, op, *args, **kwargs): } } ) -class TestDatasetReduceMethods(base.DatasetReduceTests): +class TestSparseDatasetReduceMethods(base.DatasetReduceTests): @staticmethod def create(op, shape): return create(op, shape) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index ba655c523a7..59023d20f11 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -45,7 +45,7 @@ def apply_func(op, var, *args, **kwargs): } } ) -class TestVariableReduceMethods(base.VariableReduceTests): +class TestPintVariableReduceMethods(base.VariableReduceTests): @st.composite @staticmethod def create(draw, op, shape): @@ -91,7 +91,7 @@ def check_reduce(self, obj, op, *args, **kwargs): } } ) -class TestDataArrayReduceMethods(base.DataArrayReduceTests): +class TestPintDataArrayReduceMethods(base.DataArrayReduceTests): @st.composite @staticmethod def create(draw, op, shape): @@ -137,7 +137,7 @@ def check_reduce(self, obj, op, *args, **kwargs): } } ) -class TestDatasetReduceMethods(base.DatasetReduceTests): +class TestPintDatasetReduceMethods(base.DatasetReduceTests): @st.composite @staticmethod def create(draw, op, shape): From cd132c6631e554d0765a781bfa76ca1819b4de46 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 19:38:43 +0200 Subject: [PATCH 081/140] update a few strategies --- xarray/tests/duckarrays/base/strategies.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index cd0f4ff2ed7..f17ffa14786 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -27,14 +27,12 @@ def create_dimension_names(ndim): @st.composite -def variable(draw, create_data, dims=None, shape=None, sizes=None): +def variable(draw, create_data, *, sizes=None): if sizes is not None: dims, shape = zip(*draw(sizes).items()) else: - if shape is None: - shape = draw(shapes()) - if dims is None: - dims = create_dimension_names(len(shape)) + dims = draw(st.lists(st.text(min_size=1), max_size=4)) + shape = draw(shapes(len(dims))) data = create_data(shape) @@ -45,9 +43,9 @@ def variable(draw, create_data, dims=None, shape=None, sizes=None): def data_array(draw, create_data): name = draw(st.none() | st.text(min_size=1)) - shape = draw(shapes()) + dims = draw(st.lists(elements=st.text(min_size=1), max_size=4)) + shape = draw(shapes(len(dims))) - dims = create_dimension_names(len(shape)) data = draw(create_data(shape)) return xr.DataArray( From 1c310b030e2ab7dca8b15a875ac6428879316452 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 20:06:37 +0200 Subject: [PATCH 082/140] fix the strategies and utils --- xarray/tests/duckarrays/base/strategies.py | 24 ++++++++++++---------- xarray/tests/duckarrays/base/utils.py | 3 +++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index f17ffa14786..afb16742a43 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -4,8 +4,8 @@ import xarray as xr -def shapes(ndim=None): - return npst.array_shapes() +def shapes(ndim): + return npst.array_shapes(min_dims=ndim, max_dims=ndim) dtypes = ( @@ -18,7 +18,7 @@ def shapes(ndim=None): def numpy_array(shape=None): if shape is None: - shape = npst.array_shapes() + shape = shapes() return npst.arrays(dtype=dtypes, shape=shape) @@ -31,19 +31,19 @@ def variable(draw, create_data, *, sizes=None): if sizes is not None: dims, shape = zip(*draw(sizes).items()) else: - dims = draw(st.lists(st.text(min_size=1), max_size=4)) - shape = draw(shapes(len(dims))) + dims = draw(st.lists(st.text(min_size=1), max_size=4, unique=True).map(tuple)) + ndim = len(dims) + shape = draw(shapes(ndim)) - data = create_data(shape) - - return xr.Variable(dims, draw(data)) + data = draw(create_data(shape)) + return xr.Variable(dims, data) @st.composite def data_array(draw, create_data): name = draw(st.none() | st.text(min_size=1)) - dims = draw(st.lists(elements=st.text(min_size=1), max_size=4)) + dims = draw(st.lists(elements=st.text(min_size=1), max_size=4, unique=True)) shape = draw(shapes(len(dims))) data = draw(create_data(shape)) @@ -70,9 +70,9 @@ def dataset( min_dims=1, max_dims=4, min_size=2, - max_size=10, + max_size=5, min_vars=1, - max_vars=10, + max_vars=5, ): names = st.text(min_size=1) sizes = st.dictionaries( @@ -95,6 +95,8 @@ def dataset( def valid_axis(ndim): + if ndim == 0: + return st.none() | st.just(0) return st.none() | st.integers(-ndim, ndim - 1) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index abe2749ada3..2bd353e2116 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -18,6 +18,9 @@ def valid_dims_from_axes(dims, axes): if axes is None: return None + if axes == 0 and len(dims) == 0: + return None + if isinstance(axes, int): return dims[axes] From 7f879b06e27c6880d48d118d8d4ebeeb0b383b26 Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 23 Apr 2021 20:10:10 +0200 Subject: [PATCH 083/140] use allclose instead of identical to compare --- xarray/tests/duckarrays/test_units.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 59023d20f11..d4f97fe250d 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -6,7 +6,7 @@ import numpy as np from hypothesis import note -from .. import assert_identical +from .. import assert_allclose from ..test_units import assert_units_equal, attach_units, strip_units from . import base from .base import strategies, utils @@ -81,7 +81,7 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"expected:\n{expected}") assert_units_equal(actual, expected) - assert_identical(actual, expected) + assert_allclose(actual, expected) @pytest.mark.apply_marks( @@ -127,7 +127,7 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"expected:\n{expected}") assert_units_equal(actual, expected) - assert_identical(actual, expected) + assert_allclose(actual, expected) @pytest.mark.apply_marks( @@ -176,4 +176,4 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"expected:\n{expected}") assert_units_equal(actual, expected) - assert_identical(actual, expected) + assert_allclose(actual, expected) From ff91be8dfdaeca74c0408b7d038b31f73c9bb6c0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 25 Apr 2021 16:35:45 +0200 Subject: [PATCH 084/140] don't provide a default for shape --- xarray/tests/duckarrays/base/strategies.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index afb16742a43..adb0cf54a3a 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -16,9 +16,7 @@ def shapes(ndim): ) -def numpy_array(shape=None): - if shape is None: - shape = shapes() +def numpy_array(shape): return npst.arrays(dtype=dtypes, shape=shape) From cb286ef558f71651e60ed7794371f721df82a830 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 25 Apr 2021 16:38:07 +0200 Subject: [PATCH 085/140] remove the function to generate dimension names --- xarray/tests/duckarrays/base/strategies.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index adb0cf54a3a..50d6745b6fa 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -20,10 +20,6 @@ def numpy_array(shape): return npst.arrays(dtype=dtypes, shape=shape) -def create_dimension_names(ndim): - return [f"dim_{n}" for n in range(ndim)] - - @st.composite def variable(draw, create_data, *, sizes=None): if sizes is not None: From 438f8a5b42e6f096e56cfd90f5a3f43113fdd43b Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 25 Apr 2021 17:24:41 +0200 Subject: [PATCH 086/140] simplify the generation of the dimension sizes --- xarray/tests/duckarrays/base/strategies.py | 68 ++++++++++++---------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 50d6745b6fa..81d433204e0 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -3,11 +3,6 @@ import xarray as xr - -def shapes(ndim): - return npst.array_shapes(min_dims=ndim, max_dims=ndim) - - dtypes = ( npst.integer_dtypes() | npst.unsigned_integer_dtypes() @@ -20,27 +15,50 @@ def numpy_array(shape): return npst.arrays(dtype=dtypes, shape=shape) +def dimension_sizes(min_dims, max_dims, min_size, max_size): + sizes = st.lists( + elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), + min_size=min_dims, + max_size=max_dims, + unique_by=lambda x: x[0], + ) + return sizes + + @st.composite -def variable(draw, create_data, *, sizes=None): - if sizes is not None: - dims, shape = zip(*draw(sizes).items()) +def variable( + draw, create_data, *, sizes=None, min_size=1, max_size=5, min_dims=0, max_dims=5 +): + if sizes is None: + sizes = dimension_sizes( + min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims + ) + + drawn_sizes = draw(sizes) + if not drawn_sizes: + dims = () + shape = () else: - dims = draw(st.lists(st.text(min_size=1), max_size=4, unique=True).map(tuple)) - ndim = len(dims) - shape = draw(shapes(ndim)) + dims, shape = zip(*drawn_sizes) + data = create_data(shape) - data = draw(create_data(shape)) - return xr.Variable(dims, data) + return xr.Variable(dims, draw(data)) @st.composite -def data_array(draw, create_data): +def data_array(draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_size=5): name = draw(st.none() | st.text(min_size=1)) - dims = draw(st.lists(elements=st.text(min_size=1), max_size=4, unique=True)) - shape = draw(shapes(len(dims))) + sizes = st.lists( + elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), + min_size=min_dims, + max_size=max_dims, + unique_by=lambda x: x[0], + ) + drawn_sizes = draw(sizes) + dims, shape = zip(*drawn_sizes) - data = draw(create_data(shape)) + data = create_data(shape) return xr.DataArray( data=data, @@ -49,13 +67,6 @@ def data_array(draw, create_data): ) -def dimension_sizes(sizes): - sizes_ = list(sizes.items()) - return st.lists( - elements=st.sampled_from(sizes_), min_size=1, max_size=len(sizes_) - ).map(dict) - - @st.composite def dataset( draw, @@ -69,17 +80,14 @@ def dataset( max_vars=5, ): names = st.text(min_size=1) - sizes = st.dictionaries( - keys=names, - values=st.integers(min_value=min_size, max_value=max_size), - min_size=min_dims, - max_size=max_dims, + sizes = dimension_sizes( + min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims ) data_vars = sizes.flatmap( lambda s: st.dictionaries( keys=names.filter(lambda n: n not in s), - values=variable(create_data, sizes=dimension_sizes(s)), + values=variable(create_data, sizes=s), min_size=min_vars, max_size=max_vars, ) From 01814ffee516300137359fc7c2e158d4705fb4a3 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 26 Apr 2021 22:25:16 +0200 Subject: [PATCH 087/140] immediately draw the computed dimension sizes --- xarray/tests/duckarrays/base/strategies.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 81d433204e0..a34225e439e 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -30,16 +30,20 @@ def variable( draw, create_data, *, sizes=None, min_size=1, max_size=5, min_dims=0, max_dims=5 ): if sizes is None: - sizes = dimension_sizes( - min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims + sizes = draw( + dimension_sizes( + min_size=min_size, + max_size=max_size, + min_dims=min_dims, + max_dims=max_dims, + ) ) - drawn_sizes = draw(sizes) - if not drawn_sizes: + if not sizes: dims = () shape = () else: - dims, shape = zip(*drawn_sizes) + dims, shape = zip(*sizes) data = create_data(shape) return xr.Variable(dims, draw(data)) From 0f1222eb32fb8796171ef0c3e595efb11d9fea11 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 26 Apr 2021 22:25:46 +0200 Subject: [PATCH 088/140] convert the sizes to a dict when making sure data vars are not dims --- xarray/tests/duckarrays/base/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index a34225e439e..091704d9f37 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -90,7 +90,7 @@ def dataset( data_vars = sizes.flatmap( lambda s: st.dictionaries( - keys=names.filter(lambda n: n not in s), + keys=names.filter(lambda n: n not in dict(s)), values=variable(create_data, sizes=s), min_size=min_vars, max_size=max_vars, From a38a307c5f904456474677bb465c319c3f13dbc1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 26 Apr 2021 22:30:39 +0200 Subject: [PATCH 089/140] align the default maximum number of dimensions --- xarray/tests/duckarrays/base/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 091704d9f37..3e462b33d38 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -27,7 +27,7 @@ def dimension_sizes(min_dims, max_dims, min_size, max_size): @st.composite def variable( - draw, create_data, *, sizes=None, min_size=1, max_size=5, min_dims=0, max_dims=5 + draw, create_data, *, sizes=None, min_size=1, max_size=5, min_dims=1, max_dims=4 ): if sizes is None: sizes = draw( From ea3d015b4464a61a2580a3a28a27c52bd7321fe2 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 26 Apr 2021 22:31:00 +0200 Subject: [PATCH 090/140] draw the data before passing it to DataArray --- xarray/tests/duckarrays/base/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 3e462b33d38..a00ea6570cc 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -62,7 +62,7 @@ def data_array(draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_siz drawn_sizes = draw(sizes) dims, shape = zip(*drawn_sizes) - data = create_data(shape) + data = draw(create_data(shape)) return xr.DataArray( data=data, From afa33ac10e86fdf1435b9b96c34921a927aa91d0 Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 28 Apr 2021 16:44:14 +0200 Subject: [PATCH 091/140] directly generate the reduce dimensions --- xarray/tests/duckarrays/base/reduce.py | 11 +++------- xarray/tests/duckarrays/base/strategies.py | 25 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 7346d9e3ce0..57a866ae5b6 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -5,7 +5,6 @@ from ... import assert_identical from . import strategies -from .utils import valid_dims_from_axes class VariableReduceTests: @@ -45,8 +44,7 @@ def create(op, shape): def test_reduce(self, method, data): var = data.draw(strategies.variable(lambda shape: self.create(method, shape))) - reduce_axes = data.draw(strategies.valid_axis(var.ndim)) - reduce_dims = valid_dims_from_axes(var.dims, reduce_axes) + reduce_dims = data.draw(strategies.valid_dims(var.dims)) self.check_reduce(var, method, dim=reduce_dims) @@ -88,8 +86,7 @@ def create(op, shape): def test_reduce(self, method, data): arr = data.draw(strategies.data_array(lambda shape: self.create(method, shape))) - reduce_axes = data.draw(strategies.valid_axis(arr.ndim)) - reduce_dims = valid_dims_from_axes(arr.dims, reduce_axes) + reduce_dims = data.draw(strategies.valid_dims(arr.dims)) self.check_reduce(arr, method, dim=reduce_dims) @@ -133,8 +130,6 @@ def test_reduce(self, method, data): strategies.dataset(lambda shape: self.create(method, shape), max_size=5) ) - reduce_dims = data.draw(st.sampled_from(list(ds.dims))) - # reduce_axes = data.draw(strategies.valid_axis(len(ds.dims))) - # reduce_dims = valid_dims_from_axes(ds.dims, reduce_axes) + reduce_dims = data.draw(strategies.valid_dims(ds.dims)) self.check_reduce(ds, method, dim=reduce_dims) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index a00ea6570cc..ec664a31c0a 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -2,6 +2,9 @@ import hypothesis.strategies as st import xarray as xr +from xarray.core.utils import is_dict_like + +from . import utils dtypes = ( npst.integer_dtypes() @@ -108,3 +111,25 @@ def valid_axis(ndim): def valid_axes(ndim): return valid_axis(ndim) | npst.valid_tuple_axes(ndim) + + +def valid_dim(dims): + if not isinstance(dims, list): + dims = [dims] + + ndim = len(dims) + axis = valid_axis(ndim) + return axis.map(lambda axes: utils.valid_dims_from_axes(dims, axes)) + + +def valid_dims(dims): + if is_dict_like(dims): + dims = list(dims.keys()) + elif isinstance(dims, tuple): + dims = list(dims) + elif not isinstance(dims, list): + dims = [dims] + + ndim = len(dims) + axes = valid_axes(ndim) + return axes.map(lambda axes: utils.valid_dims_from_axes(dims, axes)) From 2e0c6bffbc0100cb9bd87e7ae364f0cdd27eeffd Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 11 May 2021 15:06:25 +0200 Subject: [PATCH 092/140] disable dim=[] / axis=() because that's not supported by all duckarrays --- xarray/tests/duckarrays/base/strategies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index ec664a31c0a..94f85f00d45 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -110,7 +110,7 @@ def valid_axis(ndim): def valid_axes(ndim): - return valid_axis(ndim) | npst.valid_tuple_axes(ndim) + return valid_axis(ndim) | npst.valid_tuple_axes(ndim, min_size=1) def valid_dim(dims): From 01599b7262adb62f856d25b24c8b85f041c5f8d1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 11 May 2021 15:09:43 +0200 Subject: [PATCH 093/140] skip the sparse tests --- xarray/tests/duckarrays/test_sparse.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 4492f1a7f20..50558068d18 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -10,6 +10,15 @@ sparse = pytest.importorskip("sparse") +pytestmarks = [ + pytest.mark.skip( + reason=( + "timing issues due to the JIT compiler of numba" + " and precision differences between sparse and numpy / bottleneck" + ) + ), +] + def create(op, shape): def convert(arr): From 259e1d53fda5640124677b02f2e4153635de650c Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 11 May 2021 20:01:56 +0200 Subject: [PATCH 094/140] typo --- xarray/tests/duckarrays/test_sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 50558068d18..1a77480322b 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -10,7 +10,7 @@ sparse = pytest.importorskip("sparse") -pytestmarks = [ +pytestmark = [ pytest.mark.skip( reason=( "timing issues due to the JIT compiler of numba" From 527b17c3d253039fe24f40f77a3d85361ca016f1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 1 Jul 2021 00:24:04 +0200 Subject: [PATCH 095/140] use a single dtype for all variables of a dataset --- xarray/tests/duckarrays/base/reduce.py | 26 ++++++++---- xarray/tests/duckarrays/base/strategies.py | 47 ++++++++++++++++------ xarray/tests/duckarrays/test_units.py | 12 +++--- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 57a866ae5b6..b7e1918a638 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -20,7 +20,7 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape): + def create(op, shape, dtypes): return strategies.numpy_array(shape) @pytest.mark.parametrize( @@ -42,7 +42,11 @@ def create(op, shape): ) @given(st.data()) def test_reduce(self, method, data): - var = data.draw(strategies.variable(lambda shape: self.create(method, shape))) + var = data.draw( + strategies.variable( + lambda shape, dtypes: self.create(method, shape, dtypes) + ) + ) reduce_dims = data.draw(strategies.valid_dims(var.dims)) @@ -62,8 +66,8 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape): - return strategies.numpy_array(shape) + def create(op, shape, dtypes): + return strategies.numpy_array(shape, dtypes) @pytest.mark.parametrize( "method", @@ -84,7 +88,11 @@ def create(op, shape): ) @given(st.data()) def test_reduce(self, method, data): - arr = data.draw(strategies.data_array(lambda shape: self.create(method, shape))) + arr = data.draw( + strategies.data_array( + lambda shape, dtypes: self.create(method, shape, dtypes) + ) + ) reduce_dims = data.draw(strategies.valid_dims(arr.dims)) @@ -104,8 +112,8 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape): - return strategies.numpy_array(shape) + def create(op, shape, dtypes): + return strategies.numpy_array(shape, dtypes) @pytest.mark.parametrize( "method", @@ -127,7 +135,9 @@ def create(op, shape): @given(st.data()) def test_reduce(self, method, data): ds = data.draw( - strategies.dataset(lambda shape: self.create(method, shape), max_size=5) + strategies.dataset( + lambda shape, dtypes: self.create(method, shape, dtypes), max_size=5 + ) ) reduce_dims = data.draw(strategies.valid_dims(ds.dims)) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 94f85f00d45..5d7d5e9d756 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -6,15 +6,25 @@ from . import utils -dtypes = ( - npst.integer_dtypes() - | npst.unsigned_integer_dtypes() - | npst.floating_dtypes() - | npst.complex_number_dtypes() -) + +@st.composite +def all_dtypes(draw, single_dtype=False): + dtypes = ( + npst.integer_dtypes() + | npst.unsigned_integer_dtypes() + | npst.floating_dtypes() + | npst.complex_number_dtypes() + ) + + if single_dtype: + return st.just(draw(dtypes)) + else: + return dtypes -def numpy_array(shape): +def numpy_array(shape, dtypes=None): + if dtypes is None: + dtypes = all_dtypes() return npst.arrays(dtype=dtypes, shape=shape) @@ -30,7 +40,15 @@ def dimension_sizes(min_dims, max_dims, min_size, max_size): @st.composite def variable( - draw, create_data, *, sizes=None, min_size=1, max_size=5, min_dims=1, max_dims=4 + draw, + create_data, + *, + sizes=None, + min_size=1, + max_size=5, + min_dims=1, + max_dims=4, + dtypes=None, ): if sizes is None: sizes = draw( @@ -47,14 +65,18 @@ def variable( shape = () else: dims, shape = zip(*sizes) - data = create_data(shape) + data = create_data(shape, dtypes) return xr.Variable(dims, draw(data)) @st.composite -def data_array(draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_size=5): +def data_array( + draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_size=5, dtypes=None +): name = draw(st.none() | st.text(min_size=1)) + if dtypes is None: + dtypes = all_dtypes() sizes = st.lists( elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), @@ -65,7 +87,7 @@ def data_array(draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_siz drawn_sizes = draw(sizes) dims, shape = zip(*drawn_sizes) - data = draw(create_data(shape)) + data = draw(create_data(shape, dtypes)) return xr.DataArray( data=data, @@ -86,6 +108,7 @@ def dataset( min_vars=1, max_vars=5, ): + dtypes = all_dtypes(single_dtype=True) names = st.text(min_size=1) sizes = dimension_sizes( min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims @@ -94,7 +117,7 @@ def dataset( data_vars = sizes.flatmap( lambda s: st.dictionaries( keys=names.filter(lambda n: n not in dict(s)), - values=variable(create_data, sizes=s), + values=variable(create_data, sizes=s, dtypes=dtypes), min_size=min_vars, max_size=max_vars, ) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index d4f97fe250d..a44e54f9b51 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -48,13 +48,13 @@ def apply_func(op, var, *args, **kwargs): class TestPintVariableReduceMethods(base.VariableReduceTests): @st.composite @staticmethod - def create(draw, op, shape): + def create(draw, op, shape, dtypes): if op in ("cumprod",): units = st.just("dimensionless") else: units = all_units - return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) @@ -94,13 +94,13 @@ def check_reduce(self, obj, op, *args, **kwargs): class TestPintDataArrayReduceMethods(base.DataArrayReduceTests): @st.composite @staticmethod - def create(draw, op, shape): + def create(draw, op, shape, dtypes): if op in ("cumprod",): units = st.just("dimensionless") else: units = all_units - return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) @@ -140,13 +140,13 @@ def check_reduce(self, obj, op, *args, **kwargs): class TestPintDatasetReduceMethods(base.DatasetReduceTests): @st.composite @staticmethod - def create(draw, op, shape): + def create(draw, op, shape, dtypes): if op in ("cumprod",): units = st.just("dimensionless") else: units = all_units - return Quantity(draw(strategies.numpy_array(shape)), draw(units)) + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) From 3437c3d3c73fa04d332b565ad5dd6ca6ce4cb3d4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 1 Jul 2021 00:24:48 +0200 Subject: [PATCH 096/140] specify tolerances per dtype --- xarray/tests/duckarrays/test_units.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index a44e54f9b51..e9043b601e2 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -20,6 +20,14 @@ all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) +tolerances = { + np.float64: 1e-8, + np.float32: 1e-4, + np.float16: 1e-2, + np.complex128: 1e-8, + np.complex64: 1e-4, +} + def apply_func(op, var, *args, **kwargs): dim = kwargs.pop("dim", None) From 4866801d100e43fd08f7bd5c566388a1d9d49666 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 1 Jul 2021 00:53:01 +0200 Subject: [PATCH 097/140] abandon the notion of single_dtype=True --- xarray/tests/duckarrays/base/strategies.py | 26 ++++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 5d7d5e9d756..cf13e5de86f 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -6,25 +6,17 @@ from . import utils - -@st.composite -def all_dtypes(draw, single_dtype=False): - dtypes = ( - npst.integer_dtypes() - | npst.unsigned_integer_dtypes() - | npst.floating_dtypes() - | npst.complex_number_dtypes() - ) - - if single_dtype: - return st.just(draw(dtypes)) - else: - return dtypes +all_dtypes = ( + npst.integer_dtypes() + | npst.unsigned_integer_dtypes() + | npst.floating_dtypes() + | npst.complex_number_dtypes() +) def numpy_array(shape, dtypes=None): if dtypes is None: - dtypes = all_dtypes() + dtypes = all_dtypes return npst.arrays(dtype=dtypes, shape=shape) @@ -76,7 +68,7 @@ def data_array( ): name = draw(st.none() | st.text(min_size=1)) if dtypes is None: - dtypes = all_dtypes() + dtypes = all_dtypes sizes = st.lists( elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), @@ -108,7 +100,7 @@ def dataset( min_vars=1, max_vars=5, ): - dtypes = all_dtypes(single_dtype=True) + dtypes = st.just(draw(all_dtypes)) names = st.text(min_size=1) sizes = dimension_sizes( min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims From 8019a20761f592e5f2636766621e8ed243a6a19f Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 1 Jul 2021 00:54:05 +0200 Subject: [PATCH 098/140] limit the values and add dtype specific tolerances --- xarray/tests/duckarrays/base/strategies.py | 13 ++++++++++++- xarray/tests/duckarrays/test_units.py | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index cf13e5de86f..86be9e6af02 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -17,7 +17,18 @@ def numpy_array(shape, dtypes=None): if dtypes is None: dtypes = all_dtypes - return npst.arrays(dtype=dtypes, shape=shape) + + def elements(dtype): + max_value = 100 + min_value = 0 if dtype.kind == "u" else -max_value + + return npst.from_dtype( + dtype, allow_infinity=False, min_value=min_value, max_value=max_value + ) + + return dtypes.flatmap( + lambda dtype: npst.arrays(dtype=dtype, shape=shape, elements=elements(dtype)) + ) def dimension_sizes(min_dims, max_dims, min_size, max_size): diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index e9043b601e2..fcde159c3fd 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -135,7 +135,8 @@ def check_reduce(self, obj, op, *args, **kwargs): note(f"expected:\n{expected}") assert_units_equal(actual, expected) - assert_allclose(actual, expected) + tol = tolerances.get(obj.dtype.name, 1e-8) + assert_allclose(actual, expected, atol=tol) @pytest.mark.apply_marks( From b0e94f18c8e9b7c297b8d780e15f2de188eb833e Mon Sep 17 00:00:00 2001 From: Keewis Date: Fri, 13 Aug 2021 16:22:28 +0200 Subject: [PATCH 099/140] disable bottleneck --- xarray/tests/duckarrays/test_units.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index fcde159c3fd..0c133e22fee 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -18,6 +18,14 @@ pytestmark = [pytest.mark.filterwarnings("error::pint.UnitStrippedWarning")] +@pytest.fixture(autouse=True) +def disable_bottleneck(): + from xarray import set_options + + with set_options(use_bottleneck=False): + yield + + all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) tolerances = { From 33f63a7646c67a74f3590485b90ebd4b43f171fa Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 15 Aug 2021 13:44:31 +0200 Subject: [PATCH 100/140] reduce the maximum number of dims, dim sizes, and variables --- xarray/tests/duckarrays/base/strategies.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 86be9e6af02..42eee29b554 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -48,9 +48,9 @@ def variable( *, sizes=None, min_size=1, - max_size=5, + max_size=3, min_dims=1, - max_dims=4, + max_dims=3, dtypes=None, ): if sizes is None: @@ -75,7 +75,7 @@ def variable( @st.composite def data_array( - draw, create_data, *, min_dims=1, max_dims=4, min_size=1, max_size=5, dtypes=None + draw, create_data, *, min_dims=1, max_dims=3, min_size=1, max_size=3, dtypes=None ): name = draw(st.none() | st.text(min_size=1)) if dtypes is None: @@ -105,11 +105,11 @@ def dataset( create_data, *, min_dims=1, - max_dims=4, - min_size=2, - max_size=5, + max_dims=3, + min_size=1, + max_size=3, min_vars=1, - max_vars=5, + max_vars=3, ): dtypes = st.just(draw(all_dtypes)) names = st.text(min_size=1) From 11d41e3d98d9a2f34390c2b641cd1ec1eb758929 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 15 Aug 2021 14:29:59 +0200 Subject: [PATCH 101/140] disable bottleneck for the sparse tests --- xarray/tests/duckarrays/test_sparse.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 1a77480322b..b553a622576 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -20,6 +20,14 @@ ] +@pytest.fixture(autouse=True) +def disable_bottleneck(): + from xarray import set_options + + with set_options(use_bottleneck=False): + yield + + def create(op, shape): def convert(arr): if arr.ndim == 0: From 71a37ba5daf914d6a9aa85e0713fa91845a35c53 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 15 Aug 2021 14:30:40 +0200 Subject: [PATCH 102/140] try activating the sparse tests --- xarray/tests/duckarrays/test_sparse.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index b553a622576..3be8c5ab125 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -10,14 +10,14 @@ sparse = pytest.importorskip("sparse") -pytestmark = [ - pytest.mark.skip( - reason=( - "timing issues due to the JIT compiler of numba" - " and precision differences between sparse and numpy / bottleneck" - ) - ), -] +# pytestmark = [ +# pytest.mark.skip( +# reason=( +# "timing issues due to the JIT compiler of numba" +# " and precision differences between sparse and numpy / bottleneck" +# ) +# ), +# ] @pytest.fixture(autouse=True) From 1d98fec13c41b3b07f30904e563412d91a348aa4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 15 Aug 2021 14:47:40 +0200 Subject: [PATCH 103/140] propagate the dtypes --- xarray/tests/duckarrays/test_sparse.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 3be8c5ab125..be30329b2f1 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -28,14 +28,14 @@ def disable_bottleneck(): yield -def create(op, shape): +def create(op, shape, dtypes): def convert(arr): if arr.ndim == 0: return arr return sparse.COO.from_numpy(arr) - return strategies.numpy_array(shape).map(convert) + return strategies.numpy_array(shape, dtypes).map(convert) def as_dense(obj): @@ -74,8 +74,8 @@ def as_dense(obj): ) class TestSparseVariableReduceMethods(base.VariableReduceTests): @staticmethod - def create(op, shape): - return create(op, shape) + def create(op, shape, dtypes): + return create(op, shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) @@ -97,8 +97,8 @@ def check_reduce(self, obj, op, *args, **kwargs): ) class TestSparseDataArrayReduceMethods(base.DataArrayReduceTests): @staticmethod - def create(op, shape): - return create(op, shape) + def create(op, shape, dtypes): + return create(op, shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) @@ -120,8 +120,8 @@ def check_reduce(self, obj, op, *args, **kwargs): ) class TestSparseDatasetReduceMethods(base.DatasetReduceTests): @staticmethod - def create(op, shape): - return create(op, shape) + def create(op, shape, dtypes): + return create(op, shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) From f2cd8a1084a2495e3a6512640611bd3cde6d381f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Nov 2021 02:40:12 +0000 Subject: [PATCH 104/140] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/tests/duckarrays/test_sparse.py | 5 +++-- xarray/tests/duckarrays/test_units.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index be30329b2f1..25964a37b17 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,13 +1,14 @@ import pytest -pytest.importorskip("hypothesis") - from xarray import DataArray, Dataset, Variable from .. import assert_allclose from . import base from .base import strategies +pytest.importorskip("hypothesis") + + sparse = pytest.importorskip("sparse") # pytestmark = [ diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 0c133e22fee..3d7d7efe094 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -1,9 +1,6 @@ -import pytest - -pytest.importorskip("hypothesis") - import hypothesis.strategies as st import numpy as np +import pytest from hypothesis import note from .. import assert_allclose @@ -11,6 +8,9 @@ from . import base from .base import strategies, utils +pytest.importorskip("hypothesis") + + pint = pytest.importorskip("pint") unit_registry = pint.UnitRegistry(force_ndarray_like=True) Quantity = unit_registry.Quantity From c747733752b691a37a7e8fe8b77efc67c422885e Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 22 Jul 2022 17:00:04 -0600 Subject: [PATCH 105/140] Turn off deadlines --- xarray/tests/duckarrays/base/reduce.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index b7e1918a638..a041422518c 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -1,7 +1,7 @@ import hypothesis.strategies as st import numpy as np import pytest -from hypothesis import given, note +from hypothesis import given, note, settings from ... import assert_identical from . import strategies @@ -41,6 +41,7 @@ def create(op, shape, dtypes): ), ) @given(st.data()) + @settings(deadline=None) def test_reduce(self, method, data): var = data.draw( strategies.variable( @@ -87,6 +88,7 @@ def create(op, shape, dtypes): ), ) @given(st.data()) + @settings(deadline=None) def test_reduce(self, method, data): arr = data.draw( strategies.data_array( @@ -133,6 +135,7 @@ def create(op, shape, dtypes): ), ) @given(st.data()) + @settings(deadline=None) def test_reduce(self, method, data): ds = data.draw( strategies.dataset( From 3f819950a7c27e37db2edd20a8be3853e2675aad Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 22 Jul 2022 17:00:25 -0600 Subject: [PATCH 106/140] Disable float16 tests. --- xarray/tests/duckarrays/test_sparse.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 25964a37b17..3e50f0705f2 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from xarray import DataArray, Dataset, Variable @@ -7,19 +8,8 @@ from .base import strategies pytest.importorskip("hypothesis") - - sparse = pytest.importorskip("sparse") -# pytestmark = [ -# pytest.mark.skip( -# reason=( -# "timing issues due to the JIT compiler of numba" -# " and precision differences between sparse and numpy / bottleneck" -# ) -# ), -# ] - @pytest.fixture(autouse=True) def disable_bottleneck(): @@ -33,6 +23,9 @@ def create(op, shape, dtypes): def convert(arr): if arr.ndim == 0: return arr + # sparse doesn't support float16 + if np.issubdtype(arr.dtype, np.float16): + return arr return sparse.COO.from_numpy(arr) From a282686ee84a0a8ecff11a27379217082dc29b3c Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 22 Jul 2022 17:06:25 -0600 Subject: [PATCH 107/140] Use as_numpy in as_dense --- xarray/tests/duckarrays/test_sparse.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 3e50f0705f2..e755c2ff225 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -33,22 +33,8 @@ def convert(arr): def as_dense(obj): - if isinstance(obj, Variable) and isinstance(obj.data, sparse.COO): - new_obj = obj.copy(data=obj.data.todense()) - elif isinstance(obj, DataArray): - ds = obj._to_temp_dataset() - dense = as_dense(ds) - new_obj = obj._from_temp_dataset(dense) - elif isinstance(obj, Dataset): - variables = {name: as_dense(var) for name, var in obj.variables.items()} - coords = { - name: var for name, var in variables.items() if name in obj._coord_names - } - data_vars = { - name: var for name, var in variables.items() if name not in obj._coord_names - } - - new_obj = Dataset(coords=coords, data_vars=data_vars, attrs=obj.attrs) + if isinstance(obj, (Variable, DataArray, Dataset)): + new_obj = obj.as_numpy() else: new_obj = obj From f5b9bdc841771b8eb637baf309ecfb6222c21fca Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Aug 2022 11:14:32 +0200 Subject: [PATCH 108/140] move the hypothesis importorskip to before the strategy definitions --- xarray/tests/duckarrays/test_sparse.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index e755c2ff225..d6106dcffc2 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -3,11 +3,15 @@ from xarray import DataArray, Dataset, Variable +# isort: off +# needs to stay here to avoid ImportError for the strategy imports +pytest.importorskip("hypothesis") +# isort: on + from .. import assert_allclose from . import base from .base import strategies -pytest.importorskip("hypothesis") sparse = pytest.importorskip("sparse") From ed68dc2db712b99b0305361881397b0b4737260d Mon Sep 17 00:00:00 2001 From: Keewis Date: Wed, 3 Aug 2022 11:20:22 +0200 Subject: [PATCH 109/140] properly filter out float16 dtypes for sparse --- xarray/tests/duckarrays/test_sparse.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index d6106dcffc2..1f23a926d27 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -27,13 +27,13 @@ def create(op, shape, dtypes): def convert(arr): if arr.ndim == 0: return arr - # sparse doesn't support float16 - if np.issubdtype(arr.dtype, np.float16): - return arr return sparse.COO.from_numpy(arr) - return strategies.numpy_array(shape, dtypes).map(convert) + if dtypes is None: + dtypes = strategies.all_dtypes + sparse_dtypes = dtypes.filter(lambda dtype: not np.issubdtype(dtype, np.float16)) + return strategies.numpy_array(shape, sparse_dtypes).map(convert) def as_dense(obj): From 1e4f18e4768bf21361a08d38b058d12202162cee Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 8 Aug 2022 13:16:29 +0200 Subject: [PATCH 110/140] also filter out complex64 because there seems to be a bug in sparse --- xarray/tests/duckarrays/test_sparse.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 1f23a926d27..1a8b940b389 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -32,7 +32,14 @@ def convert(arr): if dtypes is None: dtypes = strategies.all_dtypes - sparse_dtypes = dtypes.filter(lambda dtype: not np.issubdtype(dtype, np.float16)) + + # sparse does not support float16, and there's a bug with complex64 (pydata/sparse#553) + sparse_dtypes = dtypes.filter( + lambda dtype: ( + not np.issubdtype(dtype, np.float16) + and not np.issubdtype(dtype, np.complex64) + ) + ) return strategies.numpy_array(shape, sparse_dtypes).map(convert) From 86377e612f693377f485bbb6a5cc9d9e258e3e62 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Mon, 8 Aug 2022 09:32:21 -0600 Subject: [PATCH 111/140] Update xarray/tests/duckarrays/test_sparse.py --- xarray/tests/duckarrays/test_sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 1a8b940b389..9caf4ba3155 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -37,7 +37,7 @@ def convert(arr): sparse_dtypes = dtypes.filter( lambda dtype: ( not np.issubdtype(dtype, np.float16) - and not np.issubdtype(dtype, np.complex64) + and not np.issubdtype(dtype, np.complex_) ) ) return strategies.numpy_array(shape, sparse_dtypes).map(convert) From 5af49d875f326552e1520a6254cef39897e03ba3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 8 Aug 2022 18:13:21 +0200 Subject: [PATCH 112/140] use the proper base to check the dtypes --- xarray/tests/duckarrays/test_sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 9caf4ba3155..6b4f292f284 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -37,7 +37,7 @@ def convert(arr): sparse_dtypes = dtypes.filter( lambda dtype: ( not np.issubdtype(dtype, np.float16) - and not np.issubdtype(dtype, np.complex_) + and not np.issubdtype(dtype, np.complexfloating) ) ) return strategies.numpy_array(shape, sparse_dtypes).map(convert) From 50151a45c2f2c8d74176a3516f20728d340faa44 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 8 Aug 2022 13:21:58 +0200 Subject: [PATCH 113/140] make sure the importorskip call is before any hypothesis imports --- xarray/tests/duckarrays/test_units.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 3d7d7efe094..7a7f4954152 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -1,6 +1,12 @@ -import hypothesis.strategies as st import numpy as np import pytest + +# isort: off +# needs to stay here to avoid ImportError for the hypothesis imports +pytest.importorskip("hypothesis") +# isort: on + +import hypothesis.strategies as st from hypothesis import note from .. import assert_allclose @@ -8,9 +14,6 @@ from . import base from .base import strategies, utils -pytest.importorskip("hypothesis") - - pint = pytest.importorskip("pint") unit_registry = pint.UnitRegistry(force_ndarray_like=True) Quantity = unit_registry.Quantity From 707aecb62f7bff4433aff904d4924b7eeb104765 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 9 Aug 2022 11:50:51 +0200 Subject: [PATCH 114/140] remove the op parameter to create --- xarray/tests/duckarrays/base/reduce.py | 14 ++++------ xarray/tests/duckarrays/test_sparse.py | 14 +++++----- xarray/tests/duckarrays/test_units.py | 38 +++++++------------------- 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index a041422518c..4e4a7409a85 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -20,7 +20,7 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape, dtypes): + def create(shape, dtypes): return strategies.numpy_array(shape) @pytest.mark.parametrize( @@ -44,9 +44,7 @@ def create(op, shape, dtypes): @settings(deadline=None) def test_reduce(self, method, data): var = data.draw( - strategies.variable( - lambda shape, dtypes: self.create(method, shape, dtypes) - ) + strategies.variable(lambda shape, dtypes: self.create(shape, dtypes)) ) reduce_dims = data.draw(strategies.valid_dims(var.dims)) @@ -91,9 +89,7 @@ def create(op, shape, dtypes): @settings(deadline=None) def test_reduce(self, method, data): arr = data.draw( - strategies.data_array( - lambda shape, dtypes: self.create(method, shape, dtypes) - ) + strategies.data_array(lambda shape, dtypes: self.create(shape, dtypes)) ) reduce_dims = data.draw(strategies.valid_dims(arr.dims)) @@ -114,7 +110,7 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape, dtypes): + def create(shape, dtypes): return strategies.numpy_array(shape, dtypes) @pytest.mark.parametrize( @@ -139,7 +135,7 @@ def create(op, shape, dtypes): def test_reduce(self, method, data): ds = data.draw( strategies.dataset( - lambda shape, dtypes: self.create(method, shape, dtypes), max_size=5 + lambda shape, dtypes: self.create(shape, dtypes), max_size=5 ) ) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 6b4f292f284..86f8a20e3e1 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -23,7 +23,7 @@ def disable_bottleneck(): yield -def create(op, shape, dtypes): +def create(shape, dtypes): def convert(arr): if arr.ndim == 0: return arr @@ -65,8 +65,8 @@ def as_dense(obj): ) class TestSparseVariableReduceMethods(base.VariableReduceTests): @staticmethod - def create(op, shape, dtypes): - return create(op, shape, dtypes) + def create(shape, dtypes): + return create(shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) @@ -88,8 +88,8 @@ def check_reduce(self, obj, op, *args, **kwargs): ) class TestSparseDataArrayReduceMethods(base.DataArrayReduceTests): @staticmethod - def create(op, shape, dtypes): - return create(op, shape, dtypes) + def create(shape, dtypes): + return create(shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) @@ -111,8 +111,8 @@ def check_reduce(self, obj, op, *args, **kwargs): ) class TestSparseDatasetReduceMethods(base.DatasetReduceTests): @staticmethod - def create(op, shape, dtypes): - return create(op, shape, dtypes) + def create(shape, dtypes): + return create(shape, dtypes) def check_reduce(self, obj, op, *args, **kwargs): actual = as_dense(getattr(obj, op)(*args, **kwargs)) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 7a7f4954152..67e2a29535b 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -67,13 +67,8 @@ def apply_func(op, var, *args, **kwargs): class TestPintVariableReduceMethods(base.VariableReduceTests): @st.composite @staticmethod - def create(draw, op, shape, dtypes): - if op in ("cumprod",): - units = st.just("dimensionless") - else: - units = all_units - - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) + def create(draw, shape, dtypes): + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) @@ -85,8 +80,7 @@ def compute_expected(self, obj, op, *args, **kwargs): def check_reduce(self, obj, op, *args, **kwargs): if ( op in ("cumprod",) - and obj.data.size > 1 - and obj.data.units != unit_registry.dimensionless + and getattr(obj.data, "units", None) != unit_registry.dimensionless ): with pytest.raises(pint.DimensionalityError): getattr(obj, op)(*args, **kwargs) @@ -113,13 +107,8 @@ def check_reduce(self, obj, op, *args, **kwargs): class TestPintDataArrayReduceMethods(base.DataArrayReduceTests): @st.composite @staticmethod - def create(draw, op, shape, dtypes): - if op in ("cumprod",): - units = st.just("dimensionless") - else: - units = all_units - - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) + def create(draw, shape, dtypes): + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) @@ -131,8 +120,7 @@ def compute_expected(self, obj, op, *args, **kwargs): def check_reduce(self, obj, op, *args, **kwargs): if ( op in ("cumprod",) - and obj.data.size > 1 - and obj.data.units != unit_registry.dimensionless + and getattr(obj.data, "units", None) != unit_registry.dimensionless ): with pytest.raises(pint.DimensionalityError): getattr(obj, op)(*args, **kwargs) @@ -160,13 +148,8 @@ def check_reduce(self, obj, op, *args, **kwargs): class TestPintDatasetReduceMethods(base.DatasetReduceTests): @st.composite @staticmethod - def create(draw, op, shape, dtypes): - if op in ("cumprod",): - units = st.just("dimensionless") - else: - units = all_units - - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(units)) + def create(draw, shape, dtypes): + return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) def compute_expected(self, obj, op, *args, **kwargs): without_units = strip_units(obj) @@ -180,9 +163,8 @@ def compute_expected(self, obj, op, *args, **kwargs): def check_reduce(self, obj, op, *args, **kwargs): if op in ("cumprod",) and any( - var.size > 1 - and getattr(var.data, "units", None) != unit_registry.dimensionless - for var in obj.variables.values() + getattr(var.data, "units", None) != unit_registry.dimensionless + for var in obj.data_vars.values() ): with pytest.raises(pint.DimensionalityError): getattr(obj, op)(*args, **kwargs) From aa0f9c312b565c132c668df5cee50b44d0e80139 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 12:06:25 -0400 Subject: [PATCH 115/140] merge removal of ops arg --- xarray/tests/duckarrays/base/__init__.py | 8 ++++++++ xarray/tests/duckarrays/base/reduce.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/tests/duckarrays/base/__init__.py index 5437e73b515..8aa06b320f9 100644 --- a/xarray/tests/duckarrays/base/__init__.py +++ b/xarray/tests/duckarrays/base/__init__.py @@ -1,6 +1,14 @@ +from .constructors import ( + DataArrayConstructorTests, + DatasetConstructorTests, + VariableConstructorTests, +) from .reduce import DataArrayReduceTests, DatasetReduceTests, VariableReduceTests __all__ = [ + "VariableConstructorTests", + "DataArrayConstructorTests", + "DatasetConstructorTests", "VariableReduceTests", "DataArrayReduceTests", "DatasetReduceTests", diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/tests/duckarrays/base/reduce.py index 4e4a7409a85..818b9efc229 100644 --- a/xarray/tests/duckarrays/base/reduce.py +++ b/xarray/tests/duckarrays/base/reduce.py @@ -65,7 +65,7 @@ def check_reduce(self, obj, op, *args, **kwargs): assert_identical(actual, expected) @staticmethod - def create(op, shape, dtypes): + def create(shape, dtypes, op): return strategies.numpy_array(shape, dtypes) @pytest.mark.parametrize( From 4b51ce45ea07751afe184bdc55dba0aa613d5b8d Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 14:19:09 -0400 Subject: [PATCH 116/140] strategy for creating duck array objects --- xarray/tests/duckarrays/base/strategies.py | 44 ++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/tests/duckarrays/base/strategies.py index 42eee29b554..e2b806f0a57 100644 --- a/xarray/tests/duckarrays/base/strategies.py +++ b/xarray/tests/duckarrays/base/strategies.py @@ -1,3 +1,5 @@ +from typing import Any + import hypothesis.extra.numpy as npst import hypothesis.strategies as st @@ -41,6 +43,42 @@ def dimension_sizes(min_dims, max_dims, min_size, max_size): return sizes +# Is there a way to do this in general? +# Could make a Protocol... +T_DuckArray = Any + + +@st.composite +def duckarray( + draw, + create_data, + *, + sizes=None, + min_size=1, + max_size=3, + min_dims=1, + max_dims=3, + dtypes=None, +) -> st.SearchStrategy[T_DuckArray]: + if sizes is None: + sizes = draw( + dimension_sizes( + min_size=min_size, + max_size=max_size, + min_dims=min_dims, + max_dims=max_dims, + ) + ) + + if not sizes: + shape = () + else: + _, shape = zip(*sizes) + data = create_data(shape, dtypes) + + return draw(data) + + @st.composite def variable( draw, @@ -52,7 +90,7 @@ def variable( min_dims=1, max_dims=3, dtypes=None, -): +) -> st.SearchStrategy[xr.Variable]: if sizes is None: sizes = draw( dimension_sizes( @@ -76,7 +114,7 @@ def variable( @st.composite def data_array( draw, create_data, *, min_dims=1, max_dims=3, min_size=1, max_size=3, dtypes=None -): +) -> st.SearchStrategy[xr.DataArray]: name = draw(st.none() | st.text(min_size=1)) if dtypes is None: dtypes = all_dtypes @@ -110,7 +148,7 @@ def dataset( max_size=3, min_vars=1, max_vars=3, -): +) -> st.SearchStrategy[xr.Dataset]: dtypes = st.just(draw(all_dtypes)) names = st.text(min_size=1) sizes = dimension_sizes( From a9847186e4483592780f91892d04d481912407d9 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 14:23:38 -0400 Subject: [PATCH 117/140] base class for testing creation of Variable object --- xarray/tests/duckarrays/base/constructors.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 xarray/tests/duckarrays/base/constructors.py diff --git a/xarray/tests/duckarrays/base/constructors.py b/xarray/tests/duckarrays/base/constructors.py new file mode 100644 index 00000000000..de8bb920374 --- /dev/null +++ b/xarray/tests/duckarrays/base/constructors.py @@ -0,0 +1,58 @@ +import hypothesis.strategies as st +import numpy as np +import numpy.testing as npt +from hypothesis import given, note, settings + + +from . import strategies + +import xarray as xr + + +class VariableConstructorTests: + def check(self, var, arr): + self.check_types(var, arr) + self.check_values(var, arr) + self.check_attributes(var, arr) + + def check_types(self, var, arr): + # test type of wrapped array + assert isinstance(var.data, type(arr)), f"found {type(var.data)}, expected {type(arr)}" + + def check_attributes(self, var, arr): + # test ndarry attributes are exposed correctly + assert var.ndim == arr.ndim + assert var.shape == arr.shape + assert var.dtype == arr.dtype + assert var.size == arr.size + assert var.nbytes == arr.nbytes + + def check_values(self, var, arr): + # test coersion to numpy + npt.assert_equal(var.to_numpy(), np.asarray(arr)) + + @staticmethod + def create(shape, dtypes): + return strategies.numpy_array(shape, dtypes) + + @given(st.data()) + @settings(deadline=None) + def test_construct(self, data): + arr = data.draw( + strategies.duckarray(lambda shape, dtypes: self.create(shape, dtypes)) + ) + + # TODO generalize to N dimensions + dims = ["dim_0", "dim_1", "dim_2"] + + var = xr.Variable(dims=dims[0:arr.ndim], data=arr) + + self.check(var, arr) + + +class DataArrayConstructorTests: + ... + + +class DatasetConstructorTests: + ... From 8711d4698890521317eb48634ad2c924a366976d Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 14:24:08 -0400 Subject: [PATCH 118/140] proof-of-principle for testing creation of Variables wrapping sparse arrays --- xarray/tests/duckarrays/test_sparse.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 86f8a20e3e1..c26b1aaa78e 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,4 +1,5 @@ import numpy as np +import numpy.testing as npt import pytest from xarray import DataArray, Dataset, Variable @@ -52,6 +53,15 @@ def as_dense(obj): return new_obj +class TestVariableConstructors(base.VariableConstructorTests): + @staticmethod + def create(shape, dtypes): + return create(shape, dtypes) + + def check_values(self, var, arr): + npt.assert_equal(var.to_numpy(), arr.todense()) + + @pytest.mark.apply_marks( { "test_reduce": { From f40879f78bd50750ad74af4d393ad356d408a5de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:38:41 +0000 Subject: [PATCH 119/140] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/tests/duckarrays/base/constructors.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xarray/tests/duckarrays/base/constructors.py b/xarray/tests/duckarrays/base/constructors.py index de8bb920374..9117338d625 100644 --- a/xarray/tests/duckarrays/base/constructors.py +++ b/xarray/tests/duckarrays/base/constructors.py @@ -3,11 +3,10 @@ import numpy.testing as npt from hypothesis import given, note, settings +import xarray as xr from . import strategies -import xarray as xr - class VariableConstructorTests: def check(self, var, arr): @@ -17,7 +16,9 @@ def check(self, var, arr): def check_types(self, var, arr): # test type of wrapped array - assert isinstance(var.data, type(arr)), f"found {type(var.data)}, expected {type(arr)}" + assert isinstance( + var.data, type(arr) + ), f"found {type(var.data)}, expected {type(arr)}" def check_attributes(self, var, arr): # test ndarry attributes are exposed correctly @@ -45,7 +46,7 @@ def test_construct(self, data): # TODO generalize to N dimensions dims = ["dim_0", "dim_1", "dim_2"] - var = xr.Variable(dims=dims[0:arr.ndim], data=arr) + var = xr.Variable(dims=dims[0 : arr.ndim], data=arr) self.check(var, arr) From 214084c4b59160b6c96c0b6ee38181dbf7946c55 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 15:19:36 -0400 Subject: [PATCH 120/140] generalised generation of dimension names to nd --- xarray/tests/duckarrays/base/constructors.py | 19 +++++++++---------- xarray/tests/duckarrays/base/utils.py | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/xarray/tests/duckarrays/base/constructors.py b/xarray/tests/duckarrays/base/constructors.py index de8bb920374..64b2574da8f 100644 --- a/xarray/tests/duckarrays/base/constructors.py +++ b/xarray/tests/duckarrays/base/constructors.py @@ -1,12 +1,12 @@ import hypothesis.strategies as st import numpy as np import numpy.testing as npt -from hypothesis import given, note, settings +from hypothesis import given, settings +import xarray as xr from . import strategies - -import xarray as xr +from .utils import create_dimension_names class VariableConstructorTests: @@ -17,10 +17,12 @@ def check(self, var, arr): def check_types(self, var, arr): # test type of wrapped array - assert isinstance(var.data, type(arr)), f"found {type(var.data)}, expected {type(arr)}" + assert isinstance( + var.data, type(arr) + ), f"found {type(var.data)}, expected {type(arr)}" def check_attributes(self, var, arr): - # test ndarry attributes are exposed correctly + # test ndarray attributes are exposed correctly assert var.ndim == arr.ndim assert var.shape == arr.shape assert var.dtype == arr.dtype @@ -28,7 +30,7 @@ def check_attributes(self, var, arr): assert var.nbytes == arr.nbytes def check_values(self, var, arr): - # test coersion to numpy + # test coercion to numpy npt.assert_equal(var.to_numpy(), np.asarray(arr)) @staticmethod @@ -42,10 +44,7 @@ def test_construct(self, data): strategies.duckarray(lambda shape, dtypes: self.create(shape, dtypes)) ) - # TODO generalize to N dimensions - dims = ["dim_0", "dim_1", "dim_2"] - - var = xr.Variable(dims=dims[0:arr.ndim], data=arr) + var = xr.Variable(dims=create_dimension_names(arr.ndim), data=arr) self.check(var, arr) diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/tests/duckarrays/base/utils.py index 2bd353e2116..40b7e495e6b 100644 --- a/xarray/tests/duckarrays/base/utils.py +++ b/xarray/tests/duckarrays/base/utils.py @@ -1,5 +1,6 @@ import warnings from contextlib import contextmanager +from typing import List @contextmanager @@ -10,7 +11,7 @@ def suppress_warning(category, message=""): yield -def create_dimension_names(ndim): +def create_dimension_names(ndim: int) -> List[str]: return [f"dim_{n}" for n in range(ndim)] From e0dd10ddd7b3a1ec01e3eb07f8e50bb6a53f9ced Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 9 Aug 2022 16:04:35 -0400 Subject: [PATCH 121/140] test DataArray constructor with sparse arrays --- xarray/tests/duckarrays/base/constructors.py | 28 +++++++++++++++++--- xarray/tests/duckarrays/test_sparse.py | 9 +++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/xarray/tests/duckarrays/base/constructors.py b/xarray/tests/duckarrays/base/constructors.py index 64b2574da8f..b065abbc69c 100644 --- a/xarray/tests/duckarrays/base/constructors.py +++ b/xarray/tests/duckarrays/base/constructors.py @@ -9,7 +9,9 @@ from .utils import create_dimension_names -class VariableConstructorTests: +class ArrayConstructorChecks: + """Mixin for checking results of Variable/DataArray constructors.""" + def check(self, var, arr): self.check_types(var, arr) self.check_values(var, arr) @@ -33,6 +35,8 @@ def check_values(self, var, arr): # test coercion to numpy npt.assert_equal(var.to_numpy(), np.asarray(arr)) + +class VariableConstructorTests(ArrayConstructorChecks): @staticmethod def create(shape, dtypes): return strategies.numpy_array(shape, dtypes) @@ -49,9 +53,27 @@ def test_construct(self, data): self.check(var, arr) -class DataArrayConstructorTests: - ... +class DataArrayConstructorTests(ArrayConstructorChecks): + @staticmethod + def create(shape, dtypes): + return strategies.numpy_array(shape, dtypes) + + @given(st.data()) + @settings(deadline=None) + def test_construct(self, data): + arr = data.draw( + strategies.duckarray(lambda shape, dtypes: self.create(shape, dtypes)) + ) + + da = xr.DataArray( + name=data.draw(st.none() | st.text(min_size=1)), + dims=create_dimension_names(arr.ndim), + data=arr, + ) + + self.check(da, arr) class DatasetConstructorTests: + # TODO can we re-use the strategy for building datasets? ... diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index c26b1aaa78e..378ea9e33a7 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -62,6 +62,15 @@ def check_values(self, var, arr): npt.assert_equal(var.to_numpy(), arr.todense()) +class TestDataArrayConstructors(base.DataArrayConstructorTests): + @staticmethod + def create(shape, dtypes): + return create(shape, dtypes) + + def check_values(self, da, arr): + npt.assert_equal(da.to_numpy(), arr.todense()) + + @pytest.mark.apply_marks( { "test_reduce": { From 2aba7bc97c16408c7c0a7273a2abd2bd4b2d3894 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 17:47:55 -0500 Subject: [PATCH 122/140] move testing framework to testing module --- xarray/{tests/duckarrays/base => testing/duckarrays}/__init__.py | 0 .../{tests/duckarrays/base => testing/duckarrays}/constructors.py | 0 xarray/{tests/duckarrays/base => testing/duckarrays}/reduce.py | 0 .../{tests/duckarrays/base => testing/duckarrays}/strategies.py | 0 xarray/{tests/duckarrays/base => testing/duckarrays}/utils.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename xarray/{tests/duckarrays/base => testing/duckarrays}/__init__.py (100%) rename xarray/{tests/duckarrays/base => testing/duckarrays}/constructors.py (100%) rename xarray/{tests/duckarrays/base => testing/duckarrays}/reduce.py (100%) rename xarray/{tests/duckarrays/base => testing/duckarrays}/strategies.py (100%) rename xarray/{tests/duckarrays/base => testing/duckarrays}/utils.py (100%) diff --git a/xarray/tests/duckarrays/base/__init__.py b/xarray/testing/duckarrays/__init__.py similarity index 100% rename from xarray/tests/duckarrays/base/__init__.py rename to xarray/testing/duckarrays/__init__.py diff --git a/xarray/tests/duckarrays/base/constructors.py b/xarray/testing/duckarrays/constructors.py similarity index 100% rename from xarray/tests/duckarrays/base/constructors.py rename to xarray/testing/duckarrays/constructors.py diff --git a/xarray/tests/duckarrays/base/reduce.py b/xarray/testing/duckarrays/reduce.py similarity index 100% rename from xarray/tests/duckarrays/base/reduce.py rename to xarray/testing/duckarrays/reduce.py diff --git a/xarray/tests/duckarrays/base/strategies.py b/xarray/testing/duckarrays/strategies.py similarity index 100% rename from xarray/tests/duckarrays/base/strategies.py rename to xarray/testing/duckarrays/strategies.py diff --git a/xarray/tests/duckarrays/base/utils.py b/xarray/testing/duckarrays/utils.py similarity index 100% rename from xarray/tests/duckarrays/base/utils.py rename to xarray/testing/duckarrays/utils.py From 9c38519b6c0b12fc2961e22b35a63c37bb8cbdd2 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 17:50:47 -0500 Subject: [PATCH 123/140] absolute imports --- xarray/testing/duckarrays/__init__.py | 8 ++++++-- xarray/testing/duckarrays/constructors.py | 5 ++--- xarray/testing/duckarrays/reduce.py | 4 ++-- xarray/testing/duckarrays/strategies.py | 3 +-- xarray/testing/duckarrays/utils.py | 3 +-- xarray/tests/duckarrays/test_sparse.py | 6 +++--- xarray/tests/duckarrays/test_units.py | 8 ++++---- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/xarray/testing/duckarrays/__init__.py b/xarray/testing/duckarrays/__init__.py index 8aa06b320f9..ff5951d1b90 100644 --- a/xarray/testing/duckarrays/__init__.py +++ b/xarray/testing/duckarrays/__init__.py @@ -1,9 +1,13 @@ -from .constructors import ( +from xarray.testing.duckarrays.constructors import ( DataArrayConstructorTests, DatasetConstructorTests, VariableConstructorTests, ) -from .reduce import DataArrayReduceTests, DatasetReduceTests, VariableReduceTests +from xarray.testing.duckarrays.reduce import ( + DataArrayReduceTests, + DatasetReduceTests, + VariableReduceTests, +) __all__ = [ "VariableConstructorTests", diff --git a/xarray/testing/duckarrays/constructors.py b/xarray/testing/duckarrays/constructors.py index b065abbc69c..fbdff8903d3 100644 --- a/xarray/testing/duckarrays/constructors.py +++ b/xarray/testing/duckarrays/constructors.py @@ -4,9 +4,8 @@ from hypothesis import given, settings import xarray as xr - -from . import strategies -from .utils import create_dimension_names +from xarray.testing.duckarrays import strategies +from xarray.testing.duckarrays.utils import create_dimension_names class ArrayConstructorChecks: diff --git a/xarray/testing/duckarrays/reduce.py b/xarray/testing/duckarrays/reduce.py index 818b9efc229..898d45c6fa3 100644 --- a/xarray/testing/duckarrays/reduce.py +++ b/xarray/testing/duckarrays/reduce.py @@ -3,8 +3,8 @@ import pytest from hypothesis import given, note, settings -from ... import assert_identical -from . import strategies +from xarray import assert_identical +from xarray.testing.duckarrays import strategies class VariableReduceTests: diff --git a/xarray/testing/duckarrays/strategies.py b/xarray/testing/duckarrays/strategies.py index e2b806f0a57..fe28a0b7c66 100644 --- a/xarray/testing/duckarrays/strategies.py +++ b/xarray/testing/duckarrays/strategies.py @@ -5,8 +5,7 @@ import xarray as xr from xarray.core.utils import is_dict_like - -from . import utils +from xarray.testing.duckarrays import utils all_dtypes = ( npst.integer_dtypes() diff --git a/xarray/testing/duckarrays/utils.py b/xarray/testing/duckarrays/utils.py index 40b7e495e6b..913206acb6f 100644 --- a/xarray/testing/duckarrays/utils.py +++ b/xarray/testing/duckarrays/utils.py @@ -1,6 +1,5 @@ import warnings from contextlib import contextmanager -from typing import List @contextmanager @@ -11,7 +10,7 @@ def suppress_warning(category, message=""): yield -def create_dimension_names(ndim: int) -> List[str]: +def create_dimension_names(ndim: int) -> list[str]: return [f"dim_{n}" for n in range(ndim)] diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 378ea9e33a7..9f6467a6790 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -9,9 +9,9 @@ pytest.importorskip("hypothesis") # isort: on -from .. import assert_allclose -from . import base -from .base import strategies +from xarray.tests import assert_allclose +from xarray.testing.duckarrays import base +from xarray.testing.duckarrays.base import strategies sparse = pytest.importorskip("sparse") diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 67e2a29535b..fd5c806ff02 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -9,10 +9,10 @@ import hypothesis.strategies as st from hypothesis import note -from .. import assert_allclose -from ..test_units import assert_units_equal, attach_units, strip_units -from . import base -from .base import strategies, utils +from xarray.tests import assert_allclose +from xarray.testing.duckarrays import base +from xarray.testing.duckarrays.base import strategies, utils +from xarray.tests.test_units import assert_units_equal, attach_units, strip_units pint = pytest.importorskip("pint") unit_registry = pint.UnitRegistry(force_ndarray_like=True) From 8b899119380bb65d6fea2855db6199c922d56fc5 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 17:51:52 -0500 Subject: [PATCH 124/140] test_units -> test_pint --- xarray/tests/duckarrays/{test_units.py => test_pint.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename xarray/tests/duckarrays/{test_units.py => test_pint.py} (100%) diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_pint.py similarity index 100% rename from xarray/tests/duckarrays/test_units.py rename to xarray/tests/duckarrays/test_pint.py From ab64e5e9174a00a268fdb6043391c860f160eac2 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 20:24:02 -0500 Subject: [PATCH 125/140] first variable constructor tests pass for numpy.array_api --- xarray/testing/duckarrays/__init__.py | 12 ---- xarray/testing/duckarrays/constructors.py | 65 +++++++------------ .../tests/duckarrays/test_numpy_array_api.py | 32 +++++++++ 3 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 xarray/tests/duckarrays/test_numpy_array_api.py diff --git a/xarray/testing/duckarrays/__init__.py b/xarray/testing/duckarrays/__init__.py index ff5951d1b90..9b480fb6789 100644 --- a/xarray/testing/duckarrays/__init__.py +++ b/xarray/testing/duckarrays/__init__.py @@ -1,19 +1,7 @@ from xarray.testing.duckarrays.constructors import ( - DataArrayConstructorTests, - DatasetConstructorTests, VariableConstructorTests, ) -from xarray.testing.duckarrays.reduce import ( - DataArrayReduceTests, - DatasetReduceTests, - VariableReduceTests, -) __all__ = [ "VariableConstructorTests", - "DataArrayConstructorTests", - "DatasetConstructorTests", - "VariableReduceTests", - "DataArrayReduceTests", - "DatasetReduceTests", ] diff --git a/xarray/testing/duckarrays/constructors.py b/xarray/testing/duckarrays/constructors.py index fbdff8903d3..c8b633d9c09 100644 --- a/xarray/testing/duckarrays/constructors.py +++ b/xarray/testing/duckarrays/constructors.py @@ -1,14 +1,19 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING + import hypothesis.strategies as st import numpy as np import numpy.testing as npt -from hypothesis import given, settings +from hypothesis import given + +import xarray.testing.strategies as xrst +from xarray.core.types import T_DuckArray -import xarray as xr -from xarray.testing.duckarrays import strategies -from xarray.testing.duckarrays.utils import create_dimension_names +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike -class ArrayConstructorChecks: +class ArrayConstructorChecksMixin: """Mixin for checking results of Variable/DataArray constructors.""" def check(self, var, arr): @@ -28,51 +33,29 @@ def check_attributes(self, var, arr): assert var.shape == arr.shape assert var.dtype == arr.dtype assert var.size == arr.size - assert var.nbytes == arr.nbytes def check_values(self, var, arr): # test coercion to numpy npt.assert_equal(var.to_numpy(), np.asarray(arr)) -class VariableConstructorTests(ArrayConstructorChecks): - @staticmethod - def create(shape, dtypes): - return strategies.numpy_array(shape, dtypes) - - @given(st.data()) - @settings(deadline=None) - def test_construct(self, data): - arr = data.draw( - strategies.duckarray(lambda shape, dtypes: self.create(shape, dtypes)) - ) - - var = xr.Variable(dims=create_dimension_names(arr.ndim), data=arr) +class VariableConstructorTests(ArrayConstructorChecksMixin): + dtypes = xrst.supported_dtypes() - self.check(var, arr) - - -class DataArrayConstructorTests(ArrayConstructorChecks): @staticmethod - def create(shape, dtypes): - return strategies.numpy_array(shape, dtypes) + @abstractmethod + def array_strategy_fn( + *, shape: "_ShapeLike", dtype: "_DTypeLikeNested" + ) -> st.SearchStrategy[T_DuckArray]: + ... @given(st.data()) - @settings(deadline=None) - def test_construct(self, data): - arr = data.draw( - strategies.duckarray(lambda shape, dtypes: self.create(shape, dtypes)) - ) - - da = xr.DataArray( - name=data.draw(st.none() | st.text(min_size=1)), - dims=create_dimension_names(arr.ndim), - data=arr, + def test_construct(self, data) -> None: + var = data.draw( + xrst.variables( + array_strategy_fn=self.array_strategy_fn, + dtype=self.dtypes, + ) ) - self.check(da, arr) - - -class DatasetConstructorTests: - # TODO can we re-use the strategy for building datasets? - ... + self.check(var, var.data) diff --git a/xarray/tests/duckarrays/test_numpy_array_api.py b/xarray/tests/duckarrays/test_numpy_array_api.py new file mode 100644 index 00000000000..af28ed28ba6 --- /dev/null +++ b/xarray/tests/duckarrays/test_numpy_array_api.py @@ -0,0 +1,32 @@ +import warnings +from typing import TYPE_CHECKING + +import hypothesis.strategies as st +from hypothesis.extra.array_api import make_strategies_namespace + +from xarray.core.types import T_DuckArray +from xarray.testing import duckarrays +from xarray.tests import _importorskip + +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike + + +with warnings.catch_warnings(): + # ignore the warning that the array_api is experimental raised by numpy + warnings.simplefilter("ignore") + _importorskip("numpy", "1.26.0") + import numpy.array_api as nxp + +nxps = make_strategies_namespace(nxp) + + +class TestVariableConstructors(duckarrays.VariableConstructorTests): + dtypes = nxps.scalar_dtypes() + + @staticmethod + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + return nxps.arrays(shape=shape, dtype=dtype) From f4dd2502b3d06fba1308b35eccac76516334ef4c Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 22:41:44 -0500 Subject: [PATCH 126/140] constructor tests now don't use xarray strategies --- xarray/testing/duckarrays/constructors.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/xarray/testing/duckarrays/constructors.py b/xarray/testing/duckarrays/constructors.py index c8b633d9c09..e9acd1168bb 100644 --- a/xarray/testing/duckarrays/constructors.py +++ b/xarray/testing/duckarrays/constructors.py @@ -1,11 +1,13 @@ from abc import abstractmethod from typing import TYPE_CHECKING +import hypothesis.extra.numpy as npst import hypothesis.strategies as st import numpy as np import numpy.testing as npt from hypothesis import given +import xarray as xr import xarray.testing.strategies as xrst from xarray.core.types import T_DuckArray @@ -40,6 +42,7 @@ def check_values(self, var, arr): class VariableConstructorTests(ArrayConstructorChecksMixin): + shapes = npst.array_shapes() dtypes = xrst.supported_dtypes() @staticmethod @@ -47,15 +50,18 @@ class VariableConstructorTests(ArrayConstructorChecksMixin): def array_strategy_fn( *, shape: "_ShapeLike", dtype: "_DTypeLikeNested" ) -> st.SearchStrategy[T_DuckArray]: + # TODO can we just make this an attribute? ... @given(st.data()) def test_construct(self, data) -> None: - var = data.draw( - xrst.variables( - array_strategy_fn=self.array_strategy_fn, - dtype=self.dtypes, - ) + shape = data.draw(self.shapes) + dtype = data.draw(self.dtypes) + arr = data.draw(self.array_strategy_fn(shape=shape, dtype=dtype)) + + dim_names = data.draw( + xrst.dimension_names(min_dims=len(shape), max_dims=len(shape)) ) + var = xr.Variable(data=arr, dims=dim_names) - self.check(var, var.data) + self.check(var, arr) From 626efdf6888404dc89d09fb8e13c576ee749160f Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 22:42:05 -0500 Subject: [PATCH 127/140] constructor tests for sparse --- xarray/tests/duckarrays/test_sparse.py | 150 +++++++------------------ 1 file changed, 39 insertions(+), 111 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 9f6467a6790..401dd03bcdb 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -1,19 +1,21 @@ +from typing import TYPE_CHECKING + +import hypothesis.extra.numpy as npst +import hypothesis.strategies as st import numpy as np import numpy.testing as npt import pytest -from xarray import DataArray, Dataset, Variable +import xarray.testing.strategies as xrst +from xarray.testing import duckarrays +from xarray.tests import _importorskip -# isort: off -# needs to stay here to avoid ImportError for the strategy imports -pytest.importorskip("hypothesis") -# isort: on +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike -from xarray.tests import assert_allclose -from xarray.testing.duckarrays import base -from xarray.testing.duckarrays.base import strategies -sparse = pytest.importorskip("sparse") +_importorskip("sparse") +import sparse @pytest.fixture(autouse=True) @@ -24,117 +26,43 @@ def disable_bottleneck(): yield -def create(shape, dtypes): - def convert(arr): - if arr.ndim == 0: - return arr +# sparse does not support float16 +sparse_dtypes = xrst.supported_dtypes().filter( + lambda dtype: ( + not np.issubdtype(dtype, np.float16) + ) +) - return sparse.COO.from_numpy(arr) - if dtypes is None: - dtypes = strategies.all_dtypes +@st.composite +def sparse_arrays_fn( + draw: st.DrawFn, + *, + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", +) -> sparse.COO: + """When called generates an arbitrary sparse.COO array of the given shape and dtype.""" + np_arr = draw(npst.arrays(dtype, shape)) - # sparse does not support float16, and there's a bug with complex64 (pydata/sparse#553) - sparse_dtypes = dtypes.filter( - lambda dtype: ( - not np.issubdtype(dtype, np.float16) - and not np.issubdtype(dtype, np.complexfloating) - ) - ) - return strategies.numpy_array(shape, sparse_dtypes).map(convert) + def to_sparse(arr: np.ndarray) -> sparse.COO: + if arr.ndim == 0: + return arr + return sparse.COO.from_numpy(arr) -def as_dense(obj): - if isinstance(obj, (Variable, DataArray, Dataset)): - new_obj = obj.as_numpy() - else: - new_obj = obj + return to_sparse(np_arr) - return new_obj +class TestVariableConstructors(duckarrays.VariableConstructorTests): + # dtypes = nxps.scalar_dtypes() + array_strategy_fn = sparse_arrays_fn -class TestVariableConstructors(base.VariableConstructorTests): @staticmethod - def create(shape, dtypes): - return create(shape, dtypes) + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[sparse.COO]: + return sparse_arrays_fn def check_values(self, var, arr): npt.assert_equal(var.to_numpy(), arr.todense()) - - -class TestDataArrayConstructors(base.DataArrayConstructorTests): - @staticmethod - def create(shape, dtypes): - return create(shape, dtypes) - - def check_values(self, da, arr): - npt.assert_equal(da.to_numpy(), arr.todense()) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), - "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), - "[median]": pytest.mark.skip(reason="median not implemented by sparse"), - "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), - "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), - } - } -) -class TestSparseVariableReduceMethods(base.VariableReduceTests): - @staticmethod - def create(shape, dtypes): - return create(shape, dtypes) - - def check_reduce(self, obj, op, *args, **kwargs): - actual = as_dense(getattr(obj, op)(*args, **kwargs)) - expected = getattr(as_dense(obj), op)(*args, **kwargs) - - assert_allclose(actual, expected) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), - "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), - "[median]": pytest.mark.skip(reason="median not implemented by sparse"), - "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), - "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), - } - } -) -class TestSparseDataArrayReduceMethods(base.DataArrayReduceTests): - @staticmethod - def create(shape, dtypes): - return create(shape, dtypes) - - def check_reduce(self, obj, op, *args, **kwargs): - actual = as_dense(getattr(obj, op)(*args, **kwargs)) - expected = getattr(as_dense(obj), op)(*args, **kwargs) - - assert_allclose(actual, expected) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[cumprod]": pytest.mark.skip(reason="cumprod not implemented by sparse"), - "[cumsum]": pytest.mark.skip(reason="cumsum not implemented by sparse"), - "[median]": pytest.mark.skip(reason="median not implemented by sparse"), - "[std]": pytest.mark.skip(reason="nanstd not implemented by sparse"), - "[var]": pytest.mark.skip(reason="nanvar not implemented by sparse"), - } - } -) -class TestSparseDatasetReduceMethods(base.DatasetReduceTests): - @staticmethod - def create(shape, dtypes): - return create(shape, dtypes) - - def check_reduce(self, obj, op, *args, **kwargs): - actual = as_dense(getattr(obj, op)(*args, **kwargs)) - expected = getattr(as_dense(obj), op)(*args, **kwargs) - - assert_allclose(actual, expected) From ff08473b141093e4a739a9487fd7ac54fd737bf9 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:04:15 -0500 Subject: [PATCH 128/140] use new strategies in reduce test and remove old code --- xarray/testing/duckarrays.py | 140 ++++++ xarray/testing/duckarrays/__init__.py | 7 - xarray/testing/duckarrays/constructors.py | 67 --- xarray/testing/duckarrays/reduce.py | 144 ------ xarray/testing/duckarrays/strategies.py | 198 -------- xarray/testing/strategies.py | 447 ------------------ xarray/testing/{duckarrays => }/utils.py | 0 .../tests/duckarrays/test_numpy_array_api.py | 11 + 8 files changed, 151 insertions(+), 863 deletions(-) create mode 100644 xarray/testing/duckarrays.py delete mode 100644 xarray/testing/duckarrays/__init__.py delete mode 100644 xarray/testing/duckarrays/constructors.py delete mode 100644 xarray/testing/duckarrays/reduce.py delete mode 100644 xarray/testing/duckarrays/strategies.py delete mode 100644 xarray/testing/strategies.py rename xarray/testing/{duckarrays => }/utils.py (100%) diff --git a/xarray/testing/duckarrays.py b/xarray/testing/duckarrays.py new file mode 100644 index 00000000000..4aaacbf5a59 --- /dev/null +++ b/xarray/testing/duckarrays.py @@ -0,0 +1,140 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING + +import hypothesis.extra.numpy as npst +import hypothesis.strategies as st +import numpy as np +import numpy.testing as npt +import pytest +from hypothesis import given, note + +import xarray as xr +import xarray.testing.strategies as xrst +from xarray import assert_identical +from xarray.core.types import T_DuckArray + +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike + + +__all__ = [ + "VariableConstructorTests", + "VariableReduceTests", +] + + +class ArrayConstructorChecksMixin: + """Mixin for checking results of Variable/DataArray constructors.""" + + def check(self, var, arr): + self.check_types(var, arr) + self.check_values(var, arr) + self.check_attributes(var, arr) + + def check_types(self, var, arr): + # test type of wrapped array + assert isinstance( + var.data, type(arr) + ), f"found {type(var.data)}, expected {type(arr)}" + + def check_attributes(self, var, arr): + # test ndarray attributes are exposed correctly + assert var.ndim == arr.ndim + assert var.shape == arr.shape + assert var.dtype == arr.dtype + assert var.size == arr.size + + def check_values(self, var, arr): + # test coercion to numpy + npt.assert_equal(var.to_numpy(), np.asarray(arr)) + + +class VariableConstructorTests(ArrayConstructorChecksMixin): + shapes = npst.array_shapes() + dtypes = xrst.supported_dtypes() + + @staticmethod + @abstractmethod + def array_strategy_fn( + *, shape: "_ShapeLike", dtype: "_DTypeLikeNested" + ) -> st.SearchStrategy[T_DuckArray]: + # TODO can we just make this an attribute? + ... + + @given(st.data()) + def test_construct(self, data) -> None: + shape = data.draw(self.shapes) + dtype = data.draw(self.dtypes) + arr = data.draw(self.array_strategy_fn(shape=shape, dtype=dtype)) + + dim_names = data.draw( + xrst.dimension_names(min_dims=len(shape), max_dims=len(shape)) + ) + var = xr.Variable(data=arr, dims=dim_names) + + self.check(var, arr) + + +class VariableReduceTests: + dtypes = xrst.supported_dtypes() + + @staticmethod + @abstractmethod + def array_strategy_fn( + *, shape: "_ShapeLike", dtype: "_DTypeLikeNested" + ) -> st.SearchStrategy[T_DuckArray]: + # TODO can we just make this an attribute? + ... + + def check_reduce(self, var, op, dim, *args, **kwargs): + actual = getattr(var, op)(*args, **kwargs) + + data = np.asarray(var.data) + expected = getattr(var.copy(data=data), op)(*args, **kwargs) + + # create expected result (using nanmean because arrays with Nans will be generated) + reduce_axes = tuple(var.get_axis_num(d) for d in dim) + data = np.asarray(var.data) + expected = getattr(var.copy(data=data), op)(*args, axis=reduce_axes, **kwargs) + + note(f"actual:\n{actual}") + note(f"expected:\n{expected}") + + assert_identical(actual, expected) + + @pytest.mark.parametrize( + "method", + ( + "all", + "any", + "cumprod", + "cumsum", + "max", + "mean", + "median", + "min", + "prod", + "std", + "sum", + "var", + ), + ) + @given(st.data()) + def test_reduce(self, method, data): + """ + Test that the reduction applied to an xarray Variable is always equal + to the same reduction applied to the underlying array. + """ + + var = data.draw( + xrst.variables( + array_strategy_fn=self.array_strategy_fn, + dims=xrst.dimension_names(min_dims=1), + dtype=self.dtypes, + ) + ) + + # specify arbitrary reduction along at least one dimension + reduce_dims = data.draw(xrst.unique_subset_of(var.dims, min_size=1)) + + self.check_reduce(var, method, dim=reduce_dims) diff --git a/xarray/testing/duckarrays/__init__.py b/xarray/testing/duckarrays/__init__.py deleted file mode 100644 index 9b480fb6789..00000000000 --- a/xarray/testing/duckarrays/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from xarray.testing.duckarrays.constructors import ( - VariableConstructorTests, -) - -__all__ = [ - "VariableConstructorTests", -] diff --git a/xarray/testing/duckarrays/constructors.py b/xarray/testing/duckarrays/constructors.py deleted file mode 100644 index e9acd1168bb..00000000000 --- a/xarray/testing/duckarrays/constructors.py +++ /dev/null @@ -1,67 +0,0 @@ -from abc import abstractmethod -from typing import TYPE_CHECKING - -import hypothesis.extra.numpy as npst -import hypothesis.strategies as st -import numpy as np -import numpy.testing as npt -from hypothesis import given - -import xarray as xr -import xarray.testing.strategies as xrst -from xarray.core.types import T_DuckArray - -if TYPE_CHECKING: - from xarray.core.types import _DTypeLikeNested, _ShapeLike - - -class ArrayConstructorChecksMixin: - """Mixin for checking results of Variable/DataArray constructors.""" - - def check(self, var, arr): - self.check_types(var, arr) - self.check_values(var, arr) - self.check_attributes(var, arr) - - def check_types(self, var, arr): - # test type of wrapped array - assert isinstance( - var.data, type(arr) - ), f"found {type(var.data)}, expected {type(arr)}" - - def check_attributes(self, var, arr): - # test ndarray attributes are exposed correctly - assert var.ndim == arr.ndim - assert var.shape == arr.shape - assert var.dtype == arr.dtype - assert var.size == arr.size - - def check_values(self, var, arr): - # test coercion to numpy - npt.assert_equal(var.to_numpy(), np.asarray(arr)) - - -class VariableConstructorTests(ArrayConstructorChecksMixin): - shapes = npst.array_shapes() - dtypes = xrst.supported_dtypes() - - @staticmethod - @abstractmethod - def array_strategy_fn( - *, shape: "_ShapeLike", dtype: "_DTypeLikeNested" - ) -> st.SearchStrategy[T_DuckArray]: - # TODO can we just make this an attribute? - ... - - @given(st.data()) - def test_construct(self, data) -> None: - shape = data.draw(self.shapes) - dtype = data.draw(self.dtypes) - arr = data.draw(self.array_strategy_fn(shape=shape, dtype=dtype)) - - dim_names = data.draw( - xrst.dimension_names(min_dims=len(shape), max_dims=len(shape)) - ) - var = xr.Variable(data=arr, dims=dim_names) - - self.check(var, arr) diff --git a/xarray/testing/duckarrays/reduce.py b/xarray/testing/duckarrays/reduce.py deleted file mode 100644 index 898d45c6fa3..00000000000 --- a/xarray/testing/duckarrays/reduce.py +++ /dev/null @@ -1,144 +0,0 @@ -import hypothesis.strategies as st -import numpy as np -import pytest -from hypothesis import given, note, settings - -from xarray import assert_identical -from xarray.testing.duckarrays import strategies - - -class VariableReduceTests: - def check_reduce(self, obj, op, *args, **kwargs): - actual = getattr(obj, op)(*args, **kwargs) - - data = np.asarray(obj.data) - expected = getattr(obj.copy(data=data), op)(*args, **kwargs) - - note(f"actual:\n{actual}") - note(f"expected:\n{expected}") - - assert_identical(actual, expected) - - @staticmethod - def create(shape, dtypes): - return strategies.numpy_array(shape) - - @pytest.mark.parametrize( - "method", - ( - "all", - "any", - "cumprod", - "cumsum", - "max", - "mean", - "median", - "min", - "prod", - "std", - "sum", - "var", - ), - ) - @given(st.data()) - @settings(deadline=None) - def test_reduce(self, method, data): - var = data.draw( - strategies.variable(lambda shape, dtypes: self.create(shape, dtypes)) - ) - - reduce_dims = data.draw(strategies.valid_dims(var.dims)) - - self.check_reduce(var, method, dim=reduce_dims) - - -class DataArrayReduceTests: - def check_reduce(self, obj, op, *args, **kwargs): - actual = getattr(obj, op)(*args, **kwargs) - - data = np.asarray(obj.data) - expected = getattr(obj.copy(data=data), op)(*args, **kwargs) - - note(f"actual:\n{actual}") - note(f"expected:\n{expected}") - - assert_identical(actual, expected) - - @staticmethod - def create(shape, dtypes, op): - return strategies.numpy_array(shape, dtypes) - - @pytest.mark.parametrize( - "method", - ( - "all", - "any", - "cumprod", - "cumsum", - "max", - "mean", - "median", - "min", - "prod", - "std", - "sum", - "var", - ), - ) - @given(st.data()) - @settings(deadline=None) - def test_reduce(self, method, data): - arr = data.draw( - strategies.data_array(lambda shape, dtypes: self.create(shape, dtypes)) - ) - - reduce_dims = data.draw(strategies.valid_dims(arr.dims)) - - self.check_reduce(arr, method, dim=reduce_dims) - - -class DatasetReduceTests: - def check_reduce(self, obj, op, *args, **kwargs): - actual = getattr(obj, op)(*args, **kwargs) - - data = {name: np.asarray(obj.data) for name, obj in obj.variables.items()} - expected = getattr(obj.copy(data=data), op)(*args, **kwargs) - - note(f"actual:\n{actual}") - note(f"expected:\n{expected}") - - assert_identical(actual, expected) - - @staticmethod - def create(shape, dtypes): - return strategies.numpy_array(shape, dtypes) - - @pytest.mark.parametrize( - "method", - ( - "all", - "any", - "cumprod", - "cumsum", - "max", - "mean", - "median", - "min", - "prod", - "std", - "sum", - "var", - ), - ) - @given(st.data()) - @settings(deadline=None) - def test_reduce(self, method, data): - ds = data.draw( - strategies.dataset( - lambda shape, dtypes: self.create(shape, dtypes), max_size=5 - ) - ) - - reduce_dims = data.draw(strategies.valid_dims(ds.dims)) - - self.check_reduce(ds, method, dim=reduce_dims) diff --git a/xarray/testing/duckarrays/strategies.py b/xarray/testing/duckarrays/strategies.py deleted file mode 100644 index fe28a0b7c66..00000000000 --- a/xarray/testing/duckarrays/strategies.py +++ /dev/null @@ -1,198 +0,0 @@ -from typing import Any - -import hypothesis.extra.numpy as npst -import hypothesis.strategies as st - -import xarray as xr -from xarray.core.utils import is_dict_like -from xarray.testing.duckarrays import utils - -all_dtypes = ( - npst.integer_dtypes() - | npst.unsigned_integer_dtypes() - | npst.floating_dtypes() - | npst.complex_number_dtypes() -) - - -def numpy_array(shape, dtypes=None): - if dtypes is None: - dtypes = all_dtypes - - def elements(dtype): - max_value = 100 - min_value = 0 if dtype.kind == "u" else -max_value - - return npst.from_dtype( - dtype, allow_infinity=False, min_value=min_value, max_value=max_value - ) - - return dtypes.flatmap( - lambda dtype: npst.arrays(dtype=dtype, shape=shape, elements=elements(dtype)) - ) - - -def dimension_sizes(min_dims, max_dims, min_size, max_size): - sizes = st.lists( - elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), - min_size=min_dims, - max_size=max_dims, - unique_by=lambda x: x[0], - ) - return sizes - - -# Is there a way to do this in general? -# Could make a Protocol... -T_DuckArray = Any - - -@st.composite -def duckarray( - draw, - create_data, - *, - sizes=None, - min_size=1, - max_size=3, - min_dims=1, - max_dims=3, - dtypes=None, -) -> st.SearchStrategy[T_DuckArray]: - if sizes is None: - sizes = draw( - dimension_sizes( - min_size=min_size, - max_size=max_size, - min_dims=min_dims, - max_dims=max_dims, - ) - ) - - if not sizes: - shape = () - else: - _, shape = zip(*sizes) - data = create_data(shape, dtypes) - - return draw(data) - - -@st.composite -def variable( - draw, - create_data, - *, - sizes=None, - min_size=1, - max_size=3, - min_dims=1, - max_dims=3, - dtypes=None, -) -> st.SearchStrategy[xr.Variable]: - if sizes is None: - sizes = draw( - dimension_sizes( - min_size=min_size, - max_size=max_size, - min_dims=min_dims, - max_dims=max_dims, - ) - ) - - if not sizes: - dims = () - shape = () - else: - dims, shape = zip(*sizes) - data = create_data(shape, dtypes) - - return xr.Variable(dims, draw(data)) - - -@st.composite -def data_array( - draw, create_data, *, min_dims=1, max_dims=3, min_size=1, max_size=3, dtypes=None -) -> st.SearchStrategy[xr.DataArray]: - name = draw(st.none() | st.text(min_size=1)) - if dtypes is None: - dtypes = all_dtypes - - sizes = st.lists( - elements=st.tuples(st.text(min_size=1), st.integers(min_size, max_size)), - min_size=min_dims, - max_size=max_dims, - unique_by=lambda x: x[0], - ) - drawn_sizes = draw(sizes) - dims, shape = zip(*drawn_sizes) - - data = draw(create_data(shape, dtypes)) - - return xr.DataArray( - data=data, - name=name, - dims=dims, - ) - - -@st.composite -def dataset( - draw, - create_data, - *, - min_dims=1, - max_dims=3, - min_size=1, - max_size=3, - min_vars=1, - max_vars=3, -) -> st.SearchStrategy[xr.Dataset]: - dtypes = st.just(draw(all_dtypes)) - names = st.text(min_size=1) - sizes = dimension_sizes( - min_size=min_size, max_size=max_size, min_dims=min_dims, max_dims=max_dims - ) - - data_vars = sizes.flatmap( - lambda s: st.dictionaries( - keys=names.filter(lambda n: n not in dict(s)), - values=variable(create_data, sizes=s, dtypes=dtypes), - min_size=min_vars, - max_size=max_vars, - ) - ) - - return xr.Dataset(data_vars=draw(data_vars)) - - -def valid_axis(ndim): - if ndim == 0: - return st.none() | st.just(0) - return st.none() | st.integers(-ndim, ndim - 1) - - -def valid_axes(ndim): - return valid_axis(ndim) | npst.valid_tuple_axes(ndim, min_size=1) - - -def valid_dim(dims): - if not isinstance(dims, list): - dims = [dims] - - ndim = len(dims) - axis = valid_axis(ndim) - return axis.map(lambda axes: utils.valid_dims_from_axes(dims, axes)) - - -def valid_dims(dims): - if is_dict_like(dims): - dims = list(dims.keys()) - elif isinstance(dims, tuple): - dims = list(dims) - elif not isinstance(dims, list): - dims = [dims] - - ndim = len(dims) - axes = valid_axes(ndim) - return axes.map(lambda axes: utils.valid_dims_from_axes(dims, axes)) diff --git a/xarray/testing/strategies.py b/xarray/testing/strategies.py deleted file mode 100644 index d08cbc0b584..00000000000 --- a/xarray/testing/strategies.py +++ /dev/null @@ -1,447 +0,0 @@ -from collections.abc import Hashable, Iterable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Protocol, Union, overload - -try: - import hypothesis.strategies as st -except ImportError as e: - raise ImportError( - "`xarray.testing.strategies` requires `hypothesis` to be installed." - ) from e - -import hypothesis.extra.numpy as npst -import numpy as np -from hypothesis.errors import InvalidArgument - -import xarray as xr -from xarray.core.types import T_DuckArray - -if TYPE_CHECKING: - from xarray.core.types import _DTypeLikeNested, _ShapeLike - - -__all__ = [ - "supported_dtypes", - "names", - "dimension_names", - "dimension_sizes", - "attrs", - "variables", - "unique_subset_of", -] - - -class ArrayStrategyFn(Protocol[T_DuckArray]): - def __call__( - self, - *, - shape: "_ShapeLike", - dtype: "_DTypeLikeNested", - ) -> st.SearchStrategy[T_DuckArray]: - ... - - -def supported_dtypes() -> st.SearchStrategy[np.dtype]: - """ - Generates only those numpy dtypes which xarray can handle. - - Use instead of hypothesis.extra.numpy.scalar_dtypes in order to exclude weirder dtypes such as unicode, byte_string, array, or nested dtypes. - Also excludes datetimes, which dodges bugs with pandas non-nanosecond datetime overflows. - - Requires the hypothesis package to be installed. - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - # TODO should this be exposed publicly? - # We should at least decide what the set of numpy dtypes that xarray officially supports is. - return ( - npst.integer_dtypes() - | npst.unsigned_integer_dtypes() - | npst.floating_dtypes() - | npst.complex_number_dtypes() - ) - - -# TODO Generalize to all valid unicode characters once formatting bugs in xarray's reprs are fixed + docs can handle it. -_readable_characters = st.characters( - categories=["L", "N"], max_codepoint=0x017F -) # only use characters within the "Latin Extended-A" subset of unicode - - -def names() -> st.SearchStrategy[str]: - """ - Generates arbitrary string names for dimensions / variables. - - Requires the hypothesis package to be installed. - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - return st.text( - _readable_characters, - min_size=1, - max_size=5, - ) - - -def dimension_names( - *, - min_dims: int = 0, - max_dims: int = 3, -) -> st.SearchStrategy[list[Hashable]]: - """ - Generates an arbitrary list of valid dimension names. - - Requires the hypothesis package to be installed. - - Parameters - ---------- - min_dims - Minimum number of dimensions in generated list. - max_dims - Maximum number of dimensions in generated list. - """ - - return st.lists( - elements=names(), - min_size=min_dims, - max_size=max_dims, - unique=True, - ) - - -def dimension_sizes( - *, - dim_names: st.SearchStrategy[Hashable] = names(), - min_dims: int = 0, - max_dims: int = 3, - min_side: int = 1, - max_side: Union[int, None] = None, -) -> st.SearchStrategy[Mapping[Hashable, int]]: - """ - Generates an arbitrary mapping from dimension names to lengths. - - Requires the hypothesis package to be installed. - - Parameters - ---------- - dim_names: strategy generating strings, optional - Strategy for generating dimension names. - Defaults to the `names` strategy. - min_dims: int, optional - Minimum number of dimensions in generated list. - Default is 1. - max_dims: int, optional - Maximum number of dimensions in generated list. - Default is 3. - min_side: int, optional - Minimum size of a dimension. - Default is 1. - max_side: int, optional - Minimum size of a dimension. - Default is `min_length` + 5. - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - - if max_side is None: - max_side = min_side + 3 - - return st.dictionaries( - keys=dim_names, - values=st.integers(min_value=min_side, max_value=max_side), - min_size=min_dims, - max_size=max_dims, - ) - - -_readable_strings = st.text( - _readable_characters, - max_size=5, -) -_attr_keys = _readable_strings -_small_arrays = npst.arrays( - shape=npst.array_shapes( - max_side=2, - max_dims=2, - ), - dtype=npst.scalar_dtypes(), -) -_attr_values = st.none() | st.booleans() | _readable_strings | _small_arrays - - -def attrs() -> st.SearchStrategy[Mapping[Hashable, Any]]: - """ - Generates arbitrary valid attributes dictionaries for xarray objects. - - The generated dictionaries can potentially be recursive. - - Requires the hypothesis package to be installed. - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - return st.recursive( - st.dictionaries(_attr_keys, _attr_values), - lambda children: st.dictionaries(_attr_keys, children), - max_leaves=3, - ) - - -@st.composite -def variables( - draw: st.DrawFn, - *, - array_strategy_fn: Union[ArrayStrategyFn, None] = None, - dims: Union[ - st.SearchStrategy[Union[Sequence[Hashable], Mapping[Hashable, int]]], - None, - ] = None, - dtype: st.SearchStrategy[np.dtype] = supported_dtypes(), - attrs: st.SearchStrategy[Mapping] = attrs(), -) -> xr.Variable: - """ - Generates arbitrary xarray.Variable objects. - - Follows the basic signature of the xarray.Variable constructor, but allows passing alternative strategies to - generate either numpy-like array data or dimensions. Also allows specifying the shape or dtype of the wrapped array - up front. - - Passing nothing will generate a completely arbitrary Variable (containing a numpy array). - - Requires the hypothesis package to be installed. - - Parameters - ---------- - array_strategy_fn: Callable which returns a strategy generating array-likes, optional - Callable must only accept shape and dtype kwargs, and must generate results consistent with its input. - If not passed the default is to generate a small numpy array with one of the supported_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 as a mapping the array shape will be passed to array_strategy_fn. - Default is to generate arbitrary dimension names for each axis in data. - dtype: Strategy which generates np.dtype objects, optional - Will be passed in to array_strategy_fn. - Default is to generate any scalar dtype using supported_dtypes. - Be aware that this default set of dtypes includes some not strictly allowed by the array API standard. - attrs: Strategy which generates dicts, optional - Default is to generate a nested attributes dictionary containing arbitrary strings, booleans, integers, Nones, - and numpy arrays. - - Returns - ------- - variable_strategy - Strategy for generating xarray.Variable objects. - - Raises - ------ - ValueError - If a custom array_strategy_fn returns a strategy which generates an example array inconsistent with the shape - & dtype input passed to it. - - Examples - -------- - Generate completely arbitrary Variable objects backed by a numpy array: - - >>> variables().example() # doctest: +SKIP - - array([43506, -16, -151], dtype=int32) - >>> variables().example() # doctest: +SKIP - - array([[[-10000000., -10000000.], - [-10000000., -10000000.]], - [[-10000000., -10000000.], - [ 0., -10000000.]], - [[ 0., -10000000.], - [-10000000., inf]], - [[ -0., -10000000.], - [-10000000., -0.]]], dtype=float32) - Attributes: - śřĴ: {'ĉ': {'iĥf': array([-30117, -1740], dtype=int16)}} - - Generate only Variable objects with certain dimension names: - - >>> variables(dims=st.just(["a", "b"])).example() # doctest: +SKIP - - array([[ 248, 4294967295, 4294967295], - [2412855555, 3514117556, 4294967295], - [ 111, 4294967295, 4294967295], - [4294967295, 1084434988, 51688], - [ 47714, 252, 11207]], dtype=uint32) - - Generate only Variable objects with certain dimension names and lengths: - - >>> variables(dims=st.just({"a": 2, "b": 1})).example() # doctest: +SKIP - - array([[-1.00000000e+007+3.40282347e+038j], - [-2.75034266e-225+2.22507386e-311j]]) - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - - if not isinstance(dims, st.SearchStrategy) and dims is not None: - raise InvalidArgument( - f"dims must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(dims)}. " - "To specify fixed contents, use hypothesis.strategies.just()." - ) - if not isinstance(dtype, st.SearchStrategy) and dtype is not None: - raise InvalidArgument( - f"dtype must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(dtype)}. " - "To specify fixed contents, use hypothesis.strategies.just()." - ) - if not isinstance(attrs, st.SearchStrategy) and attrs is not None: - raise InvalidArgument( - f"attrs must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(attrs)}. " - "To specify fixed contents, use hypothesis.strategies.just()." - ) - - _array_strategy_fn: ArrayStrategyFn - if array_strategy_fn is None: - # For some reason if I move the default value to the function signature definition mypy incorrectly says the ignore is no longer necessary, making it impossible to satisfy mypy - _array_strategy_fn = npst.arrays # type: ignore[assignment] # npst.arrays has extra kwargs that we aren't using later - elif not callable(array_strategy_fn): - raise InvalidArgument( - "array_strategy_fn must be a Callable that accepts the kwargs dtype and shape and returns a hypothesis " - "strategy which generates corresponding array-like objects." - ) - else: - _array_strategy_fn = ( - array_strategy_fn # satisfy mypy that this new variable cannot be None - ) - - _dtype = draw(dtype) - - if dims is not None: - # generate dims first then draw data to match - _dims = draw(dims) - if isinstance(_dims, Sequence): - dim_names = list(_dims) - valid_shapes = npst.array_shapes(min_dims=len(_dims), max_dims=len(_dims)) - _shape = draw(valid_shapes) - array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) - elif isinstance(_dims, (Mapping, dict)): - # should be a mapping of form {dim_names: lengths} - dim_names, _shape = list(_dims.keys()), tuple(_dims.values()) - array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) - else: - raise InvalidArgument( - f"Invalid type returned by dims strategy - drew an object of type {type(dims)}" - ) - else: - # nothing provided, so generate everything consistently - # We still generate the shape first here just so that we always pass shape to array_strategy_fn - _shape = draw(npst.array_shapes()) - array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) - dim_names = draw(dimension_names(min_dims=len(_shape), max_dims=len(_shape))) - - _data = draw(array_strategy) - - if _data.shape != _shape: - raise ValueError( - "array_strategy_fn returned an array object with a different shape than it was passed." - f"Passed {_shape}, but returned {_data.shape}." - "Please either specify a consistent shape via the dims kwarg or ensure the array_strategy_fn callable " - "obeys the shape argument passed to it." - ) - if _data.dtype != _dtype: - raise ValueError( - "array_strategy_fn returned an array object with a different dtype than it was passed." - f"Passed {_dtype}, but returned {_data.dtype}" - "Please either specify a consistent dtype via the dtype kwarg or ensure the array_strategy_fn callable " - "obeys the dtype argument passed to it." - ) - - return xr.Variable(dims=dim_names, data=_data, attrs=draw(attrs)) - - -@overload -def unique_subset_of( - objs: Sequence[Hashable], - *, - min_size: int = 0, - max_size: Union[int, None] = None, -) -> st.SearchStrategy[Sequence[Hashable]]: - ... - - -@overload -def unique_subset_of( - objs: Mapping[Hashable, Any], - *, - min_size: int = 0, - max_size: Union[int, None] = None, -) -> st.SearchStrategy[Mapping[Hashable, Any]]: - ... - - -@st.composite -def unique_subset_of( - draw: st.DrawFn, - objs: Union[Sequence[Hashable], Mapping[Hashable, Any]], - *, - min_size: int = 0, - max_size: Union[int, None] = None, -) -> Union[Sequence[Hashable], Mapping[Hashable, Any]]: - """ - Return a strategy which generates a unique subset of the given objects. - - Each entry in the output subset will be unique (if input was a sequence) or have a unique key (if it was a mapping). - - Requires the hypothesis package to be installed. - - Parameters - ---------- - objs: Union[Sequence[Hashable], Mapping[Hashable, Any]] - Objects from which to sample to produce the subset. - min_size: int, optional - Minimum size of the returned subset. Default is 0. - max_size: int, optional - Maximum size of the returned subset. Default is the full length of the input. - If set to 0 the result will be an empty mapping. - - Returns - ------- - unique_subset_strategy - Strategy generating subset of the input. - - Examples - -------- - >>> unique_subset_of({"x": 2, "y": 3}).example() # doctest: +SKIP - {'y': 3} - >>> unique_subset_of(["x", "y"]).example() # doctest: +SKIP - ['x'] - - See Also - -------- - :ref:`testing.hypothesis`_ - """ - if not isinstance(objs, Iterable): - raise TypeError( - f"Object to sample from must be an Iterable or a Mapping, but received type {type(objs)}" - ) - - if len(objs) == 0: - raise ValueError("Can't sample from a length-zero object.") - - keys = list(objs.keys()) if isinstance(objs, Mapping) else objs - - subset_keys = draw( - st.lists( - st.sampled_from(keys), - unique=True, - min_size=min_size, - max_size=max_size, - ) - ) - - return ( - {k: objs[k] for k in subset_keys} if isinstance(objs, Mapping) else subset_keys - ) diff --git a/xarray/testing/duckarrays/utils.py b/xarray/testing/utils.py similarity index 100% rename from xarray/testing/duckarrays/utils.py rename to xarray/testing/utils.py diff --git a/xarray/tests/duckarrays/test_numpy_array_api.py b/xarray/tests/duckarrays/test_numpy_array_api.py index af28ed28ba6..a842ae114ca 100644 --- a/xarray/tests/duckarrays/test_numpy_array_api.py +++ b/xarray/tests/duckarrays/test_numpy_array_api.py @@ -30,3 +30,14 @@ def array_strategy_fn( dtype: "_DTypeLikeNested", ) -> st.SearchStrategy[T_DuckArray]: return nxps.arrays(shape=shape, dtype=dtype) + + +class TestVariableReductions(duckarrays.VariableReduceTests): + dtypes = nxps.scalar_dtypes() + + @staticmethod + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + return nxps.arrays(shape=shape, dtype=dtype) From 5ab5a7469aa332bf41a78d83da2344f595cbb10e Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:07:12 -0500 Subject: [PATCH 129/140] reinstate strategies module which I accidentally git rmed --- xarray/testing/strategies.py | 447 +++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 xarray/testing/strategies.py diff --git a/xarray/testing/strategies.py b/xarray/testing/strategies.py new file mode 100644 index 00000000000..d08cbc0b584 --- /dev/null +++ b/xarray/testing/strategies.py @@ -0,0 +1,447 @@ +from collections.abc import Hashable, Iterable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Protocol, Union, overload + +try: + import hypothesis.strategies as st +except ImportError as e: + raise ImportError( + "`xarray.testing.strategies` requires `hypothesis` to be installed." + ) from e + +import hypothesis.extra.numpy as npst +import numpy as np +from hypothesis.errors import InvalidArgument + +import xarray as xr +from xarray.core.types import T_DuckArray + +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike + + +__all__ = [ + "supported_dtypes", + "names", + "dimension_names", + "dimension_sizes", + "attrs", + "variables", + "unique_subset_of", +] + + +class ArrayStrategyFn(Protocol[T_DuckArray]): + def __call__( + self, + *, + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + ... + + +def supported_dtypes() -> st.SearchStrategy[np.dtype]: + """ + Generates only those numpy dtypes which xarray can handle. + + Use instead of hypothesis.extra.numpy.scalar_dtypes in order to exclude weirder dtypes such as unicode, byte_string, array, or nested dtypes. + Also excludes datetimes, which dodges bugs with pandas non-nanosecond datetime overflows. + + Requires the hypothesis package to be installed. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + # TODO should this be exposed publicly? + # We should at least decide what the set of numpy dtypes that xarray officially supports is. + return ( + npst.integer_dtypes() + | npst.unsigned_integer_dtypes() + | npst.floating_dtypes() + | npst.complex_number_dtypes() + ) + + +# TODO Generalize to all valid unicode characters once formatting bugs in xarray's reprs are fixed + docs can handle it. +_readable_characters = st.characters( + categories=["L", "N"], max_codepoint=0x017F +) # only use characters within the "Latin Extended-A" subset of unicode + + +def names() -> st.SearchStrategy[str]: + """ + Generates arbitrary string names for dimensions / variables. + + Requires the hypothesis package to be installed. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + return st.text( + _readable_characters, + min_size=1, + max_size=5, + ) + + +def dimension_names( + *, + min_dims: int = 0, + max_dims: int = 3, +) -> st.SearchStrategy[list[Hashable]]: + """ + Generates an arbitrary list of valid dimension names. + + Requires the hypothesis package to be installed. + + Parameters + ---------- + min_dims + Minimum number of dimensions in generated list. + max_dims + Maximum number of dimensions in generated list. + """ + + return st.lists( + elements=names(), + min_size=min_dims, + max_size=max_dims, + unique=True, + ) + + +def dimension_sizes( + *, + dim_names: st.SearchStrategy[Hashable] = names(), + min_dims: int = 0, + max_dims: int = 3, + min_side: int = 1, + max_side: Union[int, None] = None, +) -> st.SearchStrategy[Mapping[Hashable, int]]: + """ + Generates an arbitrary mapping from dimension names to lengths. + + Requires the hypothesis package to be installed. + + Parameters + ---------- + dim_names: strategy generating strings, optional + Strategy for generating dimension names. + Defaults to the `names` strategy. + min_dims: int, optional + Minimum number of dimensions in generated list. + Default is 1. + max_dims: int, optional + Maximum number of dimensions in generated list. + Default is 3. + min_side: int, optional + Minimum size of a dimension. + Default is 1. + max_side: int, optional + Minimum size of a dimension. + Default is `min_length` + 5. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + + if max_side is None: + max_side = min_side + 3 + + return st.dictionaries( + keys=dim_names, + values=st.integers(min_value=min_side, max_value=max_side), + min_size=min_dims, + max_size=max_dims, + ) + + +_readable_strings = st.text( + _readable_characters, + max_size=5, +) +_attr_keys = _readable_strings +_small_arrays = npst.arrays( + shape=npst.array_shapes( + max_side=2, + max_dims=2, + ), + dtype=npst.scalar_dtypes(), +) +_attr_values = st.none() | st.booleans() | _readable_strings | _small_arrays + + +def attrs() -> st.SearchStrategy[Mapping[Hashable, Any]]: + """ + Generates arbitrary valid attributes dictionaries for xarray objects. + + The generated dictionaries can potentially be recursive. + + Requires the hypothesis package to be installed. + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + return st.recursive( + st.dictionaries(_attr_keys, _attr_values), + lambda children: st.dictionaries(_attr_keys, children), + max_leaves=3, + ) + + +@st.composite +def variables( + draw: st.DrawFn, + *, + array_strategy_fn: Union[ArrayStrategyFn, None] = None, + dims: Union[ + st.SearchStrategy[Union[Sequence[Hashable], Mapping[Hashable, int]]], + None, + ] = None, + dtype: st.SearchStrategy[np.dtype] = supported_dtypes(), + attrs: st.SearchStrategy[Mapping] = attrs(), +) -> xr.Variable: + """ + Generates arbitrary xarray.Variable objects. + + Follows the basic signature of the xarray.Variable constructor, but allows passing alternative strategies to + generate either numpy-like array data or dimensions. Also allows specifying the shape or dtype of the wrapped array + up front. + + Passing nothing will generate a completely arbitrary Variable (containing a numpy array). + + Requires the hypothesis package to be installed. + + Parameters + ---------- + array_strategy_fn: Callable which returns a strategy generating array-likes, optional + Callable must only accept shape and dtype kwargs, and must generate results consistent with its input. + If not passed the default is to generate a small numpy array with one of the supported_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 as a mapping the array shape will be passed to array_strategy_fn. + Default is to generate arbitrary dimension names for each axis in data. + dtype: Strategy which generates np.dtype objects, optional + Will be passed in to array_strategy_fn. + Default is to generate any scalar dtype using supported_dtypes. + Be aware that this default set of dtypes includes some not strictly allowed by the array API standard. + attrs: Strategy which generates dicts, optional + Default is to generate a nested attributes dictionary containing arbitrary strings, booleans, integers, Nones, + and numpy arrays. + + Returns + ------- + variable_strategy + Strategy for generating xarray.Variable objects. + + Raises + ------ + ValueError + If a custom array_strategy_fn returns a strategy which generates an example array inconsistent with the shape + & dtype input passed to it. + + Examples + -------- + Generate completely arbitrary Variable objects backed by a numpy array: + + >>> variables().example() # doctest: +SKIP + + array([43506, -16, -151], dtype=int32) + >>> variables().example() # doctest: +SKIP + + array([[[-10000000., -10000000.], + [-10000000., -10000000.]], + [[-10000000., -10000000.], + [ 0., -10000000.]], + [[ 0., -10000000.], + [-10000000., inf]], + [[ -0., -10000000.], + [-10000000., -0.]]], dtype=float32) + Attributes: + śřĴ: {'ĉ': {'iĥf': array([-30117, -1740], dtype=int16)}} + + Generate only Variable objects with certain dimension names: + + >>> variables(dims=st.just(["a", "b"])).example() # doctest: +SKIP + + array([[ 248, 4294967295, 4294967295], + [2412855555, 3514117556, 4294967295], + [ 111, 4294967295, 4294967295], + [4294967295, 1084434988, 51688], + [ 47714, 252, 11207]], dtype=uint32) + + Generate only Variable objects with certain dimension names and lengths: + + >>> variables(dims=st.just({"a": 2, "b": 1})).example() # doctest: +SKIP + + array([[-1.00000000e+007+3.40282347e+038j], + [-2.75034266e-225+2.22507386e-311j]]) + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + + if not isinstance(dims, st.SearchStrategy) and dims is not None: + raise InvalidArgument( + f"dims must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(dims)}. " + "To specify fixed contents, use hypothesis.strategies.just()." + ) + if not isinstance(dtype, st.SearchStrategy) and dtype is not None: + raise InvalidArgument( + f"dtype must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(dtype)}. " + "To specify fixed contents, use hypothesis.strategies.just()." + ) + if not isinstance(attrs, st.SearchStrategy) and attrs is not None: + raise InvalidArgument( + f"attrs must be provided as a hypothesis.strategies.SearchStrategy object (or None), but got type {type(attrs)}. " + "To specify fixed contents, use hypothesis.strategies.just()." + ) + + _array_strategy_fn: ArrayStrategyFn + if array_strategy_fn is None: + # For some reason if I move the default value to the function signature definition mypy incorrectly says the ignore is no longer necessary, making it impossible to satisfy mypy + _array_strategy_fn = npst.arrays # type: ignore[assignment] # npst.arrays has extra kwargs that we aren't using later + elif not callable(array_strategy_fn): + raise InvalidArgument( + "array_strategy_fn must be a Callable that accepts the kwargs dtype and shape and returns a hypothesis " + "strategy which generates corresponding array-like objects." + ) + else: + _array_strategy_fn = ( + array_strategy_fn # satisfy mypy that this new variable cannot be None + ) + + _dtype = draw(dtype) + + if dims is not None: + # generate dims first then draw data to match + _dims = draw(dims) + if isinstance(_dims, Sequence): + dim_names = list(_dims) + valid_shapes = npst.array_shapes(min_dims=len(_dims), max_dims=len(_dims)) + _shape = draw(valid_shapes) + array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) + elif isinstance(_dims, (Mapping, dict)): + # should be a mapping of form {dim_names: lengths} + dim_names, _shape = list(_dims.keys()), tuple(_dims.values()) + array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) + else: + raise InvalidArgument( + f"Invalid type returned by dims strategy - drew an object of type {type(dims)}" + ) + else: + # nothing provided, so generate everything consistently + # We still generate the shape first here just so that we always pass shape to array_strategy_fn + _shape = draw(npst.array_shapes()) + array_strategy = _array_strategy_fn(shape=_shape, dtype=_dtype) + dim_names = draw(dimension_names(min_dims=len(_shape), max_dims=len(_shape))) + + _data = draw(array_strategy) + + if _data.shape != _shape: + raise ValueError( + "array_strategy_fn returned an array object with a different shape than it was passed." + f"Passed {_shape}, but returned {_data.shape}." + "Please either specify a consistent shape via the dims kwarg or ensure the array_strategy_fn callable " + "obeys the shape argument passed to it." + ) + if _data.dtype != _dtype: + raise ValueError( + "array_strategy_fn returned an array object with a different dtype than it was passed." + f"Passed {_dtype}, but returned {_data.dtype}" + "Please either specify a consistent dtype via the dtype kwarg or ensure the array_strategy_fn callable " + "obeys the dtype argument passed to it." + ) + + return xr.Variable(dims=dim_names, data=_data, attrs=draw(attrs)) + + +@overload +def unique_subset_of( + objs: Sequence[Hashable], + *, + min_size: int = 0, + max_size: Union[int, None] = None, +) -> st.SearchStrategy[Sequence[Hashable]]: + ... + + +@overload +def unique_subset_of( + objs: Mapping[Hashable, Any], + *, + min_size: int = 0, + max_size: Union[int, None] = None, +) -> st.SearchStrategy[Mapping[Hashable, Any]]: + ... + + +@st.composite +def unique_subset_of( + draw: st.DrawFn, + objs: Union[Sequence[Hashable], Mapping[Hashable, Any]], + *, + min_size: int = 0, + max_size: Union[int, None] = None, +) -> Union[Sequence[Hashable], Mapping[Hashable, Any]]: + """ + Return a strategy which generates a unique subset of the given objects. + + Each entry in the output subset will be unique (if input was a sequence) or have a unique key (if it was a mapping). + + Requires the hypothesis package to be installed. + + Parameters + ---------- + objs: Union[Sequence[Hashable], Mapping[Hashable, Any]] + Objects from which to sample to produce the subset. + min_size: int, optional + Minimum size of the returned subset. Default is 0. + max_size: int, optional + Maximum size of the returned subset. Default is the full length of the input. + If set to 0 the result will be an empty mapping. + + Returns + ------- + unique_subset_strategy + Strategy generating subset of the input. + + Examples + -------- + >>> unique_subset_of({"x": 2, "y": 3}).example() # doctest: +SKIP + {'y': 3} + >>> unique_subset_of(["x", "y"]).example() # doctest: +SKIP + ['x'] + + See Also + -------- + :ref:`testing.hypothesis`_ + """ + if not isinstance(objs, Iterable): + raise TypeError( + f"Object to sample from must be an Iterable or a Mapping, but received type {type(objs)}" + ) + + if len(objs) == 0: + raise ValueError("Can't sample from a length-zero object.") + + keys = list(objs.keys()) if isinstance(objs, Mapping) else objs + + subset_keys = draw( + st.lists( + st.sampled_from(keys), + unique=True, + min_size=min_size, + max_size=max_size, + ) + ) + + return ( + {k: objs[k] for k in subset_keys} if isinstance(objs, Mapping) else subset_keys + ) From ec7f726ab5d891e5109b711cad353b3d1726e224 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:24:29 -0500 Subject: [PATCH 130/140] reduce tests for numpy array api now pass --- xarray/testing/duckarrays.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/xarray/testing/duckarrays.py b/xarray/testing/duckarrays.py index 4aaacbf5a59..0c6fcf0f8ec 100644 --- a/xarray/testing/duckarrays.py +++ b/xarray/testing/duckarrays.py @@ -10,7 +10,7 @@ import xarray as xr import xarray.testing.strategies as xrst -from xarray import assert_identical +from xarray.testing.assertions import assert_identical from xarray.core.types import T_DuckArray if TYPE_CHECKING: @@ -87,7 +87,7 @@ def array_strategy_fn( ... def check_reduce(self, var, op, dim, *args, **kwargs): - actual = getattr(var, op)(*args, **kwargs) + actual = getattr(var, op)(dim=dim, *args, **kwargs) data = np.asarray(var.data) expected = getattr(var.copy(data=data), op)(*args, **kwargs) @@ -107,16 +107,16 @@ def check_reduce(self, var, op, dim, *args, **kwargs): ( "all", "any", - "cumprod", - "cumsum", - "max", - "mean", - "median", - "min", - "prod", - "std", - "sum", - "var", + # "cumprod", # not in array API + # "cumsum", # not in array API + # "max", # only in array API for real numeric dtypes + # "max", # only in array API for real floating point dtypes + # "median", # not in array API + # "min", # only in array API for real numeric dtypes + # "prod", # only in array API for numeric dtypes + # "std", # TypeError: std() got an unexpected keyword argument 'ddof' + # "sum", # only in array API for numeric dtypes + # "var", # TypeError: std() got an unexpected keyword argument 'ddof' ), ) @given(st.data()) From 843217eddef24682a23a47736a8d8a7fde14485a Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:28:37 -0500 Subject: [PATCH 131/140] use suppress_warning utility --- xarray/testing/duckarrays.py | 2 +- xarray/tests/duckarrays/test_numpy_array_api.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/xarray/testing/duckarrays.py b/xarray/testing/duckarrays.py index 0c6fcf0f8ec..d60e66748d7 100644 --- a/xarray/testing/duckarrays.py +++ b/xarray/testing/duckarrays.py @@ -10,8 +10,8 @@ import xarray as xr import xarray.testing.strategies as xrst -from xarray.testing.assertions import assert_identical from xarray.core.types import T_DuckArray +from xarray.testing.assertions import assert_identical if TYPE_CHECKING: from xarray.core.types import _DTypeLikeNested, _ShapeLike diff --git a/xarray/tests/duckarrays/test_numpy_array_api.py b/xarray/tests/duckarrays/test_numpy_array_api.py index a842ae114ca..e2c3baee05b 100644 --- a/xarray/tests/duckarrays/test_numpy_array_api.py +++ b/xarray/tests/duckarrays/test_numpy_array_api.py @@ -1,4 +1,3 @@ -import warnings from typing import TYPE_CHECKING import hypothesis.strategies as st @@ -6,18 +5,21 @@ from xarray.core.types import T_DuckArray from xarray.testing import duckarrays +from xarray.testing.utils import suppress_warning from xarray.tests import _importorskip if TYPE_CHECKING: from xarray.core.types import _DTypeLikeNested, _ShapeLike -with warnings.catch_warnings(): - # ignore the warning that the array_api is experimental raised by numpy - warnings.simplefilter("ignore") +# ignore the warning that the array_api is experimental raised by numpy +with suppress_warning( + UserWarning, "The numpy.array_api submodule is still experimental. See NEP 47." +): _importorskip("numpy", "1.26.0") import numpy.array_api as nxp + nxps = make_strategies_namespace(nxp) From 9d585ce9bc3307887047de84db95877dbf4cac74 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:33:08 -0500 Subject: [PATCH 132/140] test sparse reductions --- xarray/tests/duckarrays/test_sparse.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 401dd03bcdb..678b4492f00 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -28,9 +28,7 @@ def disable_bottleneck(): # sparse does not support float16 sparse_dtypes = xrst.supported_dtypes().filter( - lambda dtype: ( - not np.issubdtype(dtype, np.float16) - ) + lambda dtype: (not np.issubdtype(dtype, np.float16)) ) @@ -54,8 +52,7 @@ def to_sparse(arr: np.ndarray) -> sparse.COO: class TestVariableConstructors(duckarrays.VariableConstructorTests): - # dtypes = nxps.scalar_dtypes() - array_strategy_fn = sparse_arrays_fn + dtypes = sparse_dtypes() @staticmethod def array_strategy_fn( @@ -66,3 +63,14 @@ def array_strategy_fn( def check_values(self, var, arr): npt.assert_equal(var.to_numpy(), arr.todense()) + + +class TestVariableReductions(duckarrays.VariableReduceTests): + dtypes = nxps.scalar_dtypes() + + @staticmethod + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + return nxps.arrays(shape=shape, dtype=dtype) From 7dc832d74b0ee7544feea14c049476b3d8a6e11b Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:34:11 -0500 Subject: [PATCH 133/140] remove old utilities --- xarray/testing/utils.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/xarray/testing/utils.py b/xarray/testing/utils.py index 913206acb6f..56dd1e83067 100644 --- a/xarray/testing/utils.py +++ b/xarray/testing/utils.py @@ -8,29 +8,3 @@ def suppress_warning(category, message=""): warnings.filterwarnings("ignore", category=category, message=message) yield - - -def create_dimension_names(ndim: int) -> list[str]: - return [f"dim_{n}" for n in range(ndim)] - - -def valid_dims_from_axes(dims, axes): - if axes is None: - return None - - if axes == 0 and len(dims) == 0: - return None - - if isinstance(axes, int): - return dims[axes] - - return [dims[axis] for axis in axes] - - -def valid_axes_from_dims(all_dims, dims): - if dims is None: - return None - elif isinstance(dims, list): - return [all_dims.index(dim) for dim in dims] - else: - return all_dims.index(dims) From 32cbdc21ac0c5bcc88db36c797c262e242ebde89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 04:36:14 +0000 Subject: [PATCH 134/140] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/tests/duckarrays/test_pint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/duckarrays/test_pint.py b/xarray/tests/duckarrays/test_pint.py index fd5c806ff02..c0896e1f514 100644 --- a/xarray/tests/duckarrays/test_pint.py +++ b/xarray/tests/duckarrays/test_pint.py @@ -9,9 +9,9 @@ import hypothesis.strategies as st from hypothesis import note -from xarray.tests import assert_allclose from xarray.testing.duckarrays import base from xarray.testing.duckarrays.base import strategies, utils +from xarray.tests import assert_allclose from xarray.tests.test_units import assert_units_equal, attach_units, strip_units pint = pytest.importorskip("pint") From a002d0b70c674a7293668f0a8d98a5b412ae630e Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:37:20 -0500 Subject: [PATCH 135/140] remove pint tests for now --- xarray/tests/duckarrays/test_pint.py | 181 --------------------------- 1 file changed, 181 deletions(-) delete mode 100644 xarray/tests/duckarrays/test_pint.py diff --git a/xarray/tests/duckarrays/test_pint.py b/xarray/tests/duckarrays/test_pint.py deleted file mode 100644 index fd5c806ff02..00000000000 --- a/xarray/tests/duckarrays/test_pint.py +++ /dev/null @@ -1,181 +0,0 @@ -import numpy as np -import pytest - -# isort: off -# needs to stay here to avoid ImportError for the hypothesis imports -pytest.importorskip("hypothesis") -# isort: on - -import hypothesis.strategies as st -from hypothesis import note - -from xarray.tests import assert_allclose -from xarray.testing.duckarrays import base -from xarray.testing.duckarrays.base import strategies, utils -from xarray.tests.test_units import assert_units_equal, attach_units, strip_units - -pint = pytest.importorskip("pint") -unit_registry = pint.UnitRegistry(force_ndarray_like=True) -Quantity = unit_registry.Quantity - -pytestmark = [pytest.mark.filterwarnings("error::pint.UnitStrippedWarning")] - - -@pytest.fixture(autouse=True) -def disable_bottleneck(): - from xarray import set_options - - with set_options(use_bottleneck=False): - yield - - -all_units = st.sampled_from(["m", "mm", "s", "dimensionless"]) - -tolerances = { - np.float64: 1e-8, - np.float32: 1e-4, - np.float16: 1e-2, - np.complex128: 1e-8, - np.complex64: 1e-4, -} - - -def apply_func(op, var, *args, **kwargs): - dim = kwargs.pop("dim", None) - if dim in var.dims: - axis = utils.valid_axes_from_dims(var.dims, dim) - else: - axis = None - kwargs["axis"] = axis - - arr = var.data - func_name = f"nan{op}" if arr.dtype.kind in "fc" else op - func = getattr(np, func_name, getattr(np, op)) - with utils.suppress_warning(RuntimeWarning): - result = func(arr, *args, **kwargs) - - return getattr(result, "units", None) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), - } - } -) -class TestPintVariableReduceMethods(base.VariableReduceTests): - @st.composite - @staticmethod - def create(draw, shape, dtypes): - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) - - def compute_expected(self, obj, op, *args, **kwargs): - without_units = strip_units(obj) - expected = getattr(without_units, op)(*args, **kwargs) - - units = apply_func(op, obj, *args, **kwargs) - return attach_units(expected, {None: units}) - - def check_reduce(self, obj, op, *args, **kwargs): - if ( - op in ("cumprod",) - and getattr(obj.data, "units", None) != unit_registry.dimensionless - ): - with pytest.raises(pint.DimensionalityError): - getattr(obj, op)(*args, **kwargs) - else: - actual = getattr(obj, op)(*args, **kwargs) - - note(f"actual:\n{actual}") - - expected = self.compute_expected(obj, op, *args, **kwargs) - - note(f"expected:\n{expected}") - - assert_units_equal(actual, expected) - assert_allclose(actual, expected) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), - } - } -) -class TestPintDataArrayReduceMethods(base.DataArrayReduceTests): - @st.composite - @staticmethod - def create(draw, shape, dtypes): - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) - - def compute_expected(self, obj, op, *args, **kwargs): - without_units = strip_units(obj) - expected = getattr(without_units, op)(*args, **kwargs) - units = apply_func(op, obj.variable, *args, **kwargs) - - return attach_units(expected, {obj.name: units}) - - def check_reduce(self, obj, op, *args, **kwargs): - if ( - op in ("cumprod",) - and getattr(obj.data, "units", None) != unit_registry.dimensionless - ): - with pytest.raises(pint.DimensionalityError): - getattr(obj, op)(*args, **kwargs) - else: - actual = getattr(obj, op)(*args, **kwargs) - - note(f"actual:\n{actual}") - - expected = self.compute_expected(obj, op, *args, **kwargs) - - note(f"expected:\n{expected}") - - assert_units_equal(actual, expected) - tol = tolerances.get(obj.dtype.name, 1e-8) - assert_allclose(actual, expected, atol=tol) - - -@pytest.mark.apply_marks( - { - "test_reduce": { - "[prod]": pytest.mark.skip(reason="inconsistent implementation in pint"), - } - } -) -class TestPintDatasetReduceMethods(base.DatasetReduceTests): - @st.composite - @staticmethod - def create(draw, shape, dtypes): - return Quantity(draw(strategies.numpy_array(shape, dtypes)), draw(all_units)) - - def compute_expected(self, obj, op, *args, **kwargs): - without_units = strip_units(obj) - result_without_units = getattr(without_units, op)(*args, **kwargs) - units = { - name: apply_func(op, var, *args, **kwargs) - for name, var in obj.variables.items() - } - attached = attach_units(result_without_units, units) - return attached - - def check_reduce(self, obj, op, *args, **kwargs): - if op in ("cumprod",) and any( - getattr(var.data, "units", None) != unit_registry.dimensionless - for var in obj.data_vars.values() - ): - with pytest.raises(pint.DimensionalityError): - getattr(obj, op)(*args, **kwargs) - else: - actual = getattr(obj, op)(*args, **kwargs) - - note(f"actual:\n{actual}") - - expected = self.compute_expected(obj, op, *args, **kwargs) - - note(f"expected:\n{expected}") - - assert_units_equal(actual, expected) - assert_allclose(actual, expected) From bfc3fe7dd48f248b2c02426eaadd99ca364c0c6b Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Wed, 13 Dec 2023 23:38:52 -0500 Subject: [PATCH 136/140] remove conftest stuff --- xarray/tests/conftest.py | 42 ---------------------------------------- 1 file changed, 42 deletions(-) diff --git a/xarray/tests/conftest.py b/xarray/tests/conftest.py index a09997e4d1b..6a8cf008f9f 100644 --- a/xarray/tests/conftest.py +++ b/xarray/tests/conftest.py @@ -11,48 +11,6 @@ def backend(request): return request.param -def pytest_configure(config): - config.addinivalue_line( - "markers", - "apply_marks(marks): function to attach marks to tests and test variants", - ) - - -def always_sequence(obj): - if not isinstance(obj, (list, tuple)): - obj = [obj] - - return obj - - -def pytest_collection_modifyitems(session, config, items): - for item in items: - mark = item.get_closest_marker("apply_marks") - if mark is None: - continue - - marks = mark.args[0] - if not isinstance(marks, dict): - continue - - possible_marks = marks.get(item.originalname) - if possible_marks is None: - continue - - if not isinstance(possible_marks, dict): - for mark in always_sequence(possible_marks): - item.add_marker(mark) - continue - - variant = item.name[len(item.originalname) :] - to_attach = possible_marks.get(variant) - if to_attach is None: - continue - - for mark in always_sequence(to_attach): - item.add_marker(mark) - - @pytest.fixture(params=[1]) def ds(request, backend): if request.param == 1: From d23eaec6e48abc61f21b8e1285a84e192a12a121 Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Sat, 16 Dec 2023 23:23:35 -0500 Subject: [PATCH 137/140] single class can test variable/dataarray/dataset --- xarray/testing/duckarrays.py | 12 ++++++------ xarray/tests/duckarrays/test_numpy_array_api.py | 4 ++-- xarray/tests/duckarrays/test_sparse.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/xarray/testing/duckarrays.py b/xarray/testing/duckarrays.py index d60e66748d7..dfad51a036d 100644 --- a/xarray/testing/duckarrays.py +++ b/xarray/testing/duckarrays.py @@ -18,8 +18,8 @@ __all__ = [ - "VariableConstructorTests", - "VariableReduceTests", + "ConstructorTests", + "ReduceTests", ] @@ -49,7 +49,7 @@ def check_values(self, var, arr): npt.assert_equal(var.to_numpy(), np.asarray(arr)) -class VariableConstructorTests(ArrayConstructorChecksMixin): +class ConstructorTests(ArrayConstructorChecksMixin): shapes = npst.array_shapes() dtypes = xrst.supported_dtypes() @@ -62,7 +62,7 @@ def array_strategy_fn( ... @given(st.data()) - def test_construct(self, data) -> None: + def test_construct_variable(self, data) -> None: shape = data.draw(self.shapes) dtype = data.draw(self.dtypes) arr = data.draw(self.array_strategy_fn(shape=shape, dtype=dtype)) @@ -75,7 +75,7 @@ def test_construct(self, data) -> None: self.check(var, arr) -class VariableReduceTests: +class ReduceTests: dtypes = xrst.supported_dtypes() @staticmethod @@ -120,7 +120,7 @@ def check_reduce(self, var, op, dim, *args, **kwargs): ), ) @given(st.data()) - def test_reduce(self, method, data): + def test_reduce_variable(self, method, data): """ Test that the reduction applied to an xarray Variable is always equal to the same reduction applied to the underlying array. diff --git a/xarray/tests/duckarrays/test_numpy_array_api.py b/xarray/tests/duckarrays/test_numpy_array_api.py index e2c3baee05b..4c217cf9645 100644 --- a/xarray/tests/duckarrays/test_numpy_array_api.py +++ b/xarray/tests/duckarrays/test_numpy_array_api.py @@ -23,7 +23,7 @@ nxps = make_strategies_namespace(nxp) -class TestVariableConstructors(duckarrays.VariableConstructorTests): +class TestConstructors(duckarrays.ConstructorTests): dtypes = nxps.scalar_dtypes() @staticmethod @@ -34,7 +34,7 @@ def array_strategy_fn( return nxps.arrays(shape=shape, dtype=dtype) -class TestVariableReductions(duckarrays.VariableReduceTests): +class TestReductions(duckarrays.ReduceTests): dtypes = nxps.scalar_dtypes() @staticmethod diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 678b4492f00..1a91c6fd72d 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -51,7 +51,7 @@ def to_sparse(arr: np.ndarray) -> sparse.COO: return to_sparse(np_arr) -class TestVariableConstructors(duckarrays.VariableConstructorTests): +class TestConstructors(duckarrays.ConstructorTests): dtypes = sparse_dtypes() @staticmethod @@ -65,7 +65,7 @@ def check_values(self, var, arr): npt.assert_equal(var.to_numpy(), arr.todense()) -class TestVariableReductions(duckarrays.VariableReduceTests): +class TestReductions(duckarrays.ReduceTests): dtypes = nxps.scalar_dtypes() @staticmethod From 8e3c65545067e39480389c7faa071b8eadddbe8d Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Sat, 16 Dec 2023 23:42:44 -0500 Subject: [PATCH 138/140] test numpy outside of array API --- ...t_numpy_array_api.py => test_array_api.py} | 0 xarray/tests/duckarrays/test_numpy.py | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+) rename xarray/tests/duckarrays/{test_numpy_array_api.py => test_array_api.py} (100%) create mode 100644 xarray/tests/duckarrays/test_numpy.py diff --git a/xarray/tests/duckarrays/test_numpy_array_api.py b/xarray/tests/duckarrays/test_array_api.py similarity index 100% rename from xarray/tests/duckarrays/test_numpy_array_api.py rename to xarray/tests/duckarrays/test_array_api.py diff --git a/xarray/tests/duckarrays/test_numpy.py b/xarray/tests/duckarrays/test_numpy.py new file mode 100644 index 00000000000..2800176529e --- /dev/null +++ b/xarray/tests/duckarrays/test_numpy.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING + +import hypothesis.strategies as st +import hypothesis.extra.numpy as npst + +from xarray.core.types import T_DuckArray +from xarray.testing import duckarrays +from xarray.testing.utils import suppress_warning +from xarray.tests import _importorskip +from xarray.testing.strategies import supported_dtypes + + +if TYPE_CHECKING: + from xarray.core.types import _DTypeLikeNested, _ShapeLike + + +class TestConstructors(duckarrays.ConstructorTests): + dtypes = supported_dtypes() + + @staticmethod + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + return npst.arrays(shape=shape, dtype=dtype) + + +class TestReductions(duckarrays.ReduceTests): + dtypes = supported_dtypes() + + @staticmethod + def array_strategy_fn( + shape: "_ShapeLike", + dtype: "_DTypeLikeNested", + ) -> st.SearchStrategy[T_DuckArray]: + return npst.arrays(shape=shape, dtype=dtype) From c07c690a0b7d71a542f0cc8c9aeadbe7364e1f43 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:05:34 +0000 Subject: [PATCH 139/140] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- xarray/tests/duckarrays/test_numpy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xarray/tests/duckarrays/test_numpy.py b/xarray/tests/duckarrays/test_numpy.py index 2800176529e..f18b70fbc87 100644 --- a/xarray/tests/duckarrays/test_numpy.py +++ b/xarray/tests/duckarrays/test_numpy.py @@ -1,15 +1,12 @@ from typing import TYPE_CHECKING -import hypothesis.strategies as st import hypothesis.extra.numpy as npst +import hypothesis.strategies as st from xarray.core.types import T_DuckArray from xarray.testing import duckarrays -from xarray.testing.utils import suppress_warning -from xarray.tests import _importorskip from xarray.testing.strategies import supported_dtypes - if TYPE_CHECKING: from xarray.core.types import _DTypeLikeNested, _ShapeLike From d2b35c5a183b322b184691a4668d3af0e63ee8fd Mon Sep 17 00:00:00 2001 From: TomNicholas Date: Sun, 31 Dec 2023 11:33:39 -0700 Subject: [PATCH 140/140] narrow dtypes --- xarray/testing/duckarrays.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/xarray/testing/duckarrays.py b/xarray/testing/duckarrays.py index dfad51a036d..cb6450a1d71 100644 --- a/xarray/testing/duckarrays.py +++ b/xarray/testing/duckarrays.py @@ -75,6 +75,10 @@ def test_construct_variable(self, data) -> None: self.check(var, arr) +def is_real_floating(dtype): + return np.issubdtype(dtype, np.number) and np.issubdtype(dtype, np.floating) + + class ReduceTests: dtypes = xrst.supported_dtypes() @@ -103,34 +107,35 @@ def check_reduce(self, var, op, dim, *args, **kwargs): assert_identical(actual, expected) @pytest.mark.parametrize( - "method", + "method, dtype_assumption", ( - "all", - "any", + ("all", lambda x: True), # should work for any dtype + ("any", lambda x: True), # should work for any dtype # "cumprod", # not in array API # "cumsum", # not in array API - # "max", # only in array API for real numeric dtypes - # "max", # only in array API for real floating point dtypes + ("max", is_real_floating), # only in array API for real numeric dtypes # "median", # not in array API - # "min", # only in array API for real numeric dtypes - # "prod", # only in array API for numeric dtypes + ("min", is_real_floating), # only in array API for real numeric dtypes + ("prod", is_real_floating), # only in array API for real numeric dtypes # "std", # TypeError: std() got an unexpected keyword argument 'ddof' - # "sum", # only in array API for numeric dtypes + ("sum", is_real_floating), # only in array API for real numeric dtypes # "var", # TypeError: std() got an unexpected keyword argument 'ddof' ), ) @given(st.data()) - def test_reduce_variable(self, method, data): + def test_reduce_variable(self, method, dtype_assumption, data): """ Test that the reduction applied to an xarray Variable is always equal to the same reduction applied to the underlying array. """ + narrowed_dtypes = self.dtypes.filter(dtype_assumption) + var = data.draw( xrst.variables( array_strategy_fn=self.array_strategy_fn, dims=xrst.dimension_names(min_dims=1), - dtype=self.dtypes, + dtype=narrowed_dtypes, ) )