From 21696bfdfc1292b39e16491cea35ef2ac1fda6ac Mon Sep 17 00:00:00 2001 From: Keewis Date: Sun, 28 Feb 2021 00:39:17 +0100 Subject: [PATCH 001/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] [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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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/124] 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 cc7af83e0e18489d01e3c957afab178039c0017a Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 11:46:53 +0200 Subject: [PATCH 115/124] add special test job for duck arrays --- .github/workflows/ci.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6839bdab347..e8f4fba4c2f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -59,6 +59,9 @@ jobs: - env: "flaky" python-version: "3.10" os: ubuntu-latest + - env: "duckarrays" + python-version: "3.10" + os: ubuntu-latest steps: - uses: actions/checkout@v3 with: @@ -72,10 +75,12 @@ jobs: echo "CONDA_ENV_FILE=ci/requirements/environment-windows.yml" >> $GITHUB_ENV elif [[ "${{ matrix.env }}" != "" ]] ; then - if [[ "${{ matrix.env }}" == "flaky" ]] ; - then + if [[ "${{ matrix.env }}" == "flaky" ]] ; then echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV echo "PYTEST_EXTRA_FLAGS=--run-flaky --run-network-tests" >> $GITHUB_ENV + elif [[ "${{ matrix.env }}" == "duckarrays" ]] ; then + echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV + echo "PYTEST_EXTRA_FLAGS=--run-duckarray-tests xarray/tests/duckarrays/" >> $GITHUB_ENV else echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV fi From 644bf695734ab76a6fa2a9c5d87eb69ad1268c27 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 11:47:40 +0200 Subject: [PATCH 116/124] limit the github annotations to just the standard env --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8f4fba4c2f..917535ea0ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,7 +101,7 @@ jobs: # We only want to install this on one run, because otherwise we'll have # duplicate annotations. - name: Install error reporter - if: ${{ matrix.os }} == 'ubuntu-latest' and ${{ matrix.python-version }} == '3.10' + if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env == "" }} run: | python -m pip install pytest-github-actions-annotate-failures From cae92871b8ac731cdfe262a384360416e1b59526 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 11:54:30 +0200 Subject: [PATCH 117/124] don't run duckarray tests unless explicitly requested --- conftest.py | 9 +++++++++ setup.cfg | 1 + xarray/tests/duckarrays/test_sparse.py | 2 ++ xarray/tests/duckarrays/test_units.py | 5 ++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 862a1a1d0bc..2391cb047c0 100644 --- a/conftest.py +++ b/conftest.py @@ -11,6 +11,11 @@ def pytest_addoption(parser): action="store_true", help="runs tests requiring a network connection", ) + parser.addoption( + "--run-duckarray-tests", + action="store_true", + help="runs the duckarray hypothesis tests", + ) def pytest_runtest_setup(item): @@ -21,6 +26,10 @@ def pytest_runtest_setup(item): pytest.skip( "set --run-network-tests to run test requiring an internet connection" ) + if "duckarrays" in item.keywords and not item.config.getoption( + "--run-duckarray-tests" + ): + pytest.skip("set --run-duckarray-tests option to run duckarray tests") @pytest.fixture(autouse=True) diff --git a/setup.cfg b/setup.cfg index af7d47c2b79..ff89f0a8f7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -144,6 +144,7 @@ markers = flaky: flaky tests network: tests requiring a network connection slow: slow tests + duckarrays: duckarray tests [flake8] ignore = diff --git a/xarray/tests/duckarrays/test_sparse.py b/xarray/tests/duckarrays/test_sparse.py index 86f8a20e3e1..f4c425fff86 100644 --- a/xarray/tests/duckarrays/test_sparse.py +++ b/xarray/tests/duckarrays/test_sparse.py @@ -14,6 +14,8 @@ sparse = pytest.importorskip("sparse") +pytestmark = [pytest.mark.duckarrays()] + @pytest.fixture(autouse=True) def disable_bottleneck(): diff --git a/xarray/tests/duckarrays/test_units.py b/xarray/tests/duckarrays/test_units.py index 67e2a29535b..3f68d5fdaea 100644 --- a/xarray/tests/duckarrays/test_units.py +++ b/xarray/tests/duckarrays/test_units.py @@ -18,7 +18,10 @@ unit_registry = pint.UnitRegistry(force_ndarray_like=True) Quantity = unit_registry.Quantity -pytestmark = [pytest.mark.filterwarnings("error::pint.UnitStrippedWarning")] +pytestmark = [ + pytest.mark.duckarrays(), + pytest.mark.filterwarnings("error::pint.UnitStrippedWarning"), +] @pytest.fixture(autouse=True) From 240cbcf6be554b0ed4f5a8bbcf8d517bd928a111 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 11:57:44 +0200 Subject: [PATCH 118/124] fix the workflow --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 917535ea0ab..83fa7ccb6e0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,7 +101,7 @@ jobs: # We only want to install this on one run, because otherwise we'll have # duplicate annotations. - name: Install error reporter - if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env == "" }} + if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env == '' }} run: | python -m pip install pytest-github-actions-annotate-failures From aa7d893d3173e062888b814a2ae50b698bfdff8b Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 12:01:10 +0200 Subject: [PATCH 119/124] one more fix --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83fa7ccb6e0..59c61010184 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -101,7 +101,7 @@ jobs: # We only want to install this on one run, because otherwise we'll have # duplicate annotations. - name: Install error reporter - if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env == '' }} + if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env }} == '' run: | python -m pip install pytest-github-actions-annotate-failures From fa3402376295ea3d7a35ba65f66c6374a2f011bc Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 12:08:30 +0200 Subject: [PATCH 120/124] try not pinning the os for duckarray tests --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 59c61010184..778e3b2d8e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -61,7 +61,6 @@ jobs: os: ubuntu-latest - env: "duckarrays" python-version: "3.10" - os: ubuntu-latest steps: - uses: actions/checkout@v3 with: From b92f2729411880fa9c305339dd237752b5195b98 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 12:16:48 +0200 Subject: [PATCH 121/124] explicitly write out the new entries --- .github/workflows/ci.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 778e3b2d8e3..981a1e796bb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -61,6 +61,13 @@ jobs: os: ubuntu-latest - env: "duckarrays" python-version: "3.10" + os: "ubuntu-latest" + - env: "duckarrays" + python-version: "3.10" + os: "windows-latest" + - env: "duckarrays" + python-version: "3.10" + os: "macos-latest" steps: - uses: actions/checkout@v3 with: From 4b5f63bf3b75508aaa8ce045cc6adf589056b7d8 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 12:34:14 +0200 Subject: [PATCH 122/124] try removing the variable evaluation --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 981a1e796bb..d9ab53bf3c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -107,7 +107,7 @@ jobs: # We only want to install this on one run, because otherwise we'll have # duplicate annotations. - name: Install error reporter - if: ${{ matrix.os }} == 'ubuntu-latest' && ${{ matrix.python-version }} == '3.10' && ${{ matrix.env }} == '' + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' && matrix.env == '' run: | python -m pip install pytest-github-actions-annotate-failures From 909583c9f3e853e7cc40ff55c25ba7ebea38bbef Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 13:35:05 +0200 Subject: [PATCH 123/124] set the environment file by default --- .github/workflows/ci.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d9ab53bf3c5..019bbfa8759 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -79,13 +79,16 @@ jobs: if [[ ${{ matrix.os }} == windows* ]] ; then echo "CONDA_ENV_FILE=ci/requirements/environment-windows.yml" >> $GITHUB_ENV - elif [[ "${{ matrix.env }}" != "" ]] ; + else + echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV + fi + + if [[ "${{ matrix.env }}" != "" ]] ; then if [[ "${{ matrix.env }}" == "flaky" ]] ; then echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV echo "PYTEST_EXTRA_FLAGS=--run-flaky --run-network-tests" >> $GITHUB_ENV elif [[ "${{ matrix.env }}" == "duckarrays" ]] ; then - echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV echo "PYTEST_EXTRA_FLAGS=--run-duckarray-tests xarray/tests/duckarrays/" >> $GITHUB_ENV else echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV From 2bcdc833d3d855191eff827997f7ed4e26fb4da4 Mon Sep 17 00:00:00 2001 From: Keewis Date: Thu, 11 Aug 2022 13:38:08 +0200 Subject: [PATCH 124/124] don't overwrite the default --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 019bbfa8759..1ebdd7c27cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,8 +93,6 @@ jobs: else echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV fi - else - echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV fi echo "PYTHON_VERSION=${{ matrix.python-version }}" >> $GITHUB_ENV