386 lines
12 KiB
Python
386 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Hashable
|
|
from types import EllipsisType
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from xarray.core import duck_array_ops, utils
|
|
from xarray.core.utils import (
|
|
attempt_import,
|
|
either_dict_or_kwargs,
|
|
infix_dims,
|
|
iterate_nested,
|
|
)
|
|
from xarray.tests import assert_array_equal, requires_dask
|
|
|
|
|
|
class TestAlias:
|
|
def test(self):
|
|
def new_method():
|
|
pass
|
|
|
|
old_method = utils.alias(new_method, "old_method")
|
|
assert "deprecated" in old_method.__doc__
|
|
with pytest.warns(Warning, match="deprecated"):
|
|
old_method()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["a", "b", "expected"],
|
|
[
|
|
[np.array(["a"]), np.array(["b"]), np.array(["a", "b"])],
|
|
[np.array([1], dtype="int64"), np.array([2], dtype="int64"), pd.Index([1, 2])],
|
|
],
|
|
)
|
|
def test_maybe_coerce_to_str(a, b, expected):
|
|
index = pd.Index(a).append(pd.Index(b))
|
|
|
|
actual = utils.maybe_coerce_to_str(index, [a, b])
|
|
|
|
assert_array_equal(expected, actual)
|
|
assert expected.dtype == actual.dtype
|
|
|
|
|
|
def test_maybe_coerce_to_str_minimal_str_dtype():
|
|
a = np.array(["a", "a_long_string"])
|
|
index = pd.Index(["a"])
|
|
|
|
actual = utils.maybe_coerce_to_str(index, [a])
|
|
expected = np.array("a")
|
|
|
|
assert_array_equal(expected, actual)
|
|
assert expected.dtype == actual.dtype
|
|
|
|
|
|
class TestArrayEquiv:
|
|
def test_0d(self):
|
|
# verify our work around for pd.isnull not working for 0-dimensional
|
|
# object arrays
|
|
assert duck_array_ops.array_equiv(0, np.array(0, dtype=object))
|
|
assert duck_array_ops.array_equiv(np.nan, np.array(np.nan, dtype=object))
|
|
assert not duck_array_ops.array_equiv(0, np.array(1, dtype=object))
|
|
|
|
|
|
class TestDictionaries:
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self):
|
|
self.x = {"a": "A", "b": "B"}
|
|
self.y = {"c": "C", "b": "B"}
|
|
self.z = {"a": "Z"}
|
|
|
|
def test_equivalent(self):
|
|
assert utils.equivalent(0, 0)
|
|
assert utils.equivalent(np.nan, np.nan)
|
|
assert utils.equivalent(0, np.array(0.0))
|
|
assert utils.equivalent([0], np.array([0]))
|
|
assert utils.equivalent(np.array([0]), [0])
|
|
assert utils.equivalent(np.arange(3), 1.0 * np.arange(3))
|
|
assert not utils.equivalent(0, np.zeros(3))
|
|
|
|
def test_safe(self):
|
|
# should not raise exception:
|
|
utils.update_safety_check(self.x, self.y)
|
|
|
|
def test_unsafe(self):
|
|
with pytest.raises(ValueError):
|
|
utils.update_safety_check(self.x, self.z)
|
|
|
|
def test_compat_dict_intersection(self):
|
|
assert {"b": "B"} == utils.compat_dict_intersection(self.x, self.y)
|
|
assert {} == utils.compat_dict_intersection(self.x, self.z)
|
|
|
|
def test_compat_dict_union(self):
|
|
assert {"a": "A", "b": "B", "c": "C"} == utils.compat_dict_union(self.x, self.y)
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=r"unsafe to merge dictionaries without "
|
|
"overriding values; conflicting key",
|
|
):
|
|
utils.compat_dict_union(self.x, self.z)
|
|
|
|
def test_dict_equiv(self):
|
|
x = {}
|
|
x["a"] = 3
|
|
x["b"] = np.array([1, 2, 3])
|
|
y = {}
|
|
y["b"] = np.array([1.0, 2.0, 3.0])
|
|
y["a"] = 3
|
|
assert utils.dict_equiv(x, y) # two nparrays are equal
|
|
y["b"] = [1, 2, 3] # np.array not the same as a list
|
|
assert utils.dict_equiv(x, y) # nparray == list
|
|
x["b"] = [1.0, 2.0, 3.0]
|
|
assert utils.dict_equiv(x, y) # list vs. list
|
|
x["c"] = None
|
|
assert not utils.dict_equiv(x, y) # new key in x
|
|
x["c"] = np.nan
|
|
y["c"] = np.nan
|
|
assert utils.dict_equiv(x, y) # as intended, nan is nan
|
|
x["c"] = np.inf
|
|
y["c"] = np.inf
|
|
assert utils.dict_equiv(x, y) # inf == inf
|
|
y = dict(y)
|
|
assert utils.dict_equiv(x, y) # different dictionary types are fine
|
|
y["b"] = 3 * np.arange(3)
|
|
assert not utils.dict_equiv(x, y) # not equal when arrays differ
|
|
|
|
def test_frozen(self):
|
|
x = utils.Frozen(self.x)
|
|
with pytest.raises(TypeError):
|
|
x["foo"] = "bar"
|
|
with pytest.raises(TypeError):
|
|
del x["a"]
|
|
with pytest.raises(AttributeError):
|
|
x.update(self.y)
|
|
assert x.mapping == self.x
|
|
assert repr(x) in (
|
|
"Frozen({'a': 'A', 'b': 'B'})",
|
|
"Frozen({'b': 'B', 'a': 'A'})",
|
|
)
|
|
|
|
def test_filtered(self):
|
|
x = utils.FilteredMapping(keys={"a"}, mapping={"a": 1, "b": 2})
|
|
assert "a" in x
|
|
assert "b" not in x
|
|
assert x["a"] == 1
|
|
assert list(x) == ["a"]
|
|
assert len(x) == 1
|
|
assert repr(x) == "FilteredMapping(keys={'a'}, mapping={'a': 1, 'b': 2})"
|
|
assert dict(x) == {"a": 1}
|
|
|
|
|
|
def test_repr_object():
|
|
obj = utils.ReprObject("foo")
|
|
assert repr(obj) == "foo"
|
|
assert isinstance(obj, Hashable)
|
|
assert not isinstance(obj, str)
|
|
|
|
|
|
def test_repr_object_magic_methods():
|
|
o1 = utils.ReprObject("foo")
|
|
o2 = utils.ReprObject("foo")
|
|
o3 = utils.ReprObject("bar")
|
|
o4 = "foo"
|
|
assert o1 == o2
|
|
assert o1 != o3
|
|
assert o1 != o4
|
|
assert hash(o1) == hash(o2)
|
|
assert hash(o1) != hash(o3)
|
|
assert hash(o1) != hash(o4)
|
|
|
|
|
|
def test_is_remote_uri():
|
|
assert utils.is_remote_uri("http://example.com")
|
|
assert utils.is_remote_uri("https://example.com")
|
|
assert not utils.is_remote_uri(" http://example.com")
|
|
assert not utils.is_remote_uri("example.nc")
|
|
|
|
|
|
class Test_is_uniform_and_sorted:
|
|
def test_sorted_uniform(self):
|
|
assert utils.is_uniform_spaced(np.arange(5))
|
|
|
|
def test_sorted_not_uniform(self):
|
|
assert not utils.is_uniform_spaced([-2, 1, 89])
|
|
|
|
def test_not_sorted_uniform(self):
|
|
assert not utils.is_uniform_spaced([1, -1, 3])
|
|
|
|
def test_not_sorted_not_uniform(self):
|
|
assert not utils.is_uniform_spaced([4, 1, 89])
|
|
|
|
def test_two_numbers(self):
|
|
assert utils.is_uniform_spaced([0, 1.7])
|
|
|
|
def test_relative_tolerance(self):
|
|
assert utils.is_uniform_spaced([0, 0.97, 2], rtol=0.1)
|
|
|
|
|
|
class Test_hashable:
|
|
def test_hashable(self):
|
|
for v in [False, 1, (2,), (3, 4), "four"]:
|
|
assert utils.hashable(v)
|
|
for v in [[5, 6], ["seven", "8"], {9: "ten"}]:
|
|
assert not utils.hashable(v)
|
|
|
|
|
|
@requires_dask
|
|
def test_dask_array_is_scalar():
|
|
# regression test for GH1684
|
|
import dask.array as da
|
|
|
|
y = da.arange(8, chunks=4)
|
|
assert not utils.is_scalar(y)
|
|
|
|
|
|
def test_hidden_key_dict():
|
|
hidden_key = "_hidden_key"
|
|
data = {"a": 1, "b": 2, hidden_key: 3}
|
|
data_expected = {"a": 1, "b": 2}
|
|
hkd = utils.HiddenKeyDict(data, [hidden_key])
|
|
assert len(hkd) == 2
|
|
assert hidden_key not in hkd
|
|
for k, v in data_expected.items():
|
|
assert hkd[k] == v
|
|
with pytest.raises(KeyError):
|
|
hkd[hidden_key]
|
|
with pytest.raises(KeyError):
|
|
del hkd[hidden_key]
|
|
|
|
|
|
def test_either_dict_or_kwargs():
|
|
result = either_dict_or_kwargs(dict(a=1), None, "foo")
|
|
expected = dict(a=1)
|
|
assert result == expected
|
|
|
|
result = either_dict_or_kwargs(None, dict(a=1), "foo")
|
|
expected = dict(a=1)
|
|
assert result == expected
|
|
|
|
with pytest.raises(ValueError, match=r"foo"):
|
|
result = either_dict_or_kwargs(dict(a=1), dict(a=1), "foo")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["supplied", "all_", "expected"],
|
|
[
|
|
(list("abc"), list("abc"), list("abc")),
|
|
(["a", ..., "c"], list("abc"), list("abc")),
|
|
(["a", ...], list("abc"), list("abc")),
|
|
(["c", ...], list("abc"), list("cab")),
|
|
([..., "b"], list("abc"), list("acb")),
|
|
([...], list("abc"), list("abc")),
|
|
],
|
|
)
|
|
def test_infix_dims(supplied, all_, expected):
|
|
result = list(infix_dims(supplied, all_))
|
|
assert result == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["supplied", "all_"], [([..., ...], list("abc")), ([...], list("aac"))]
|
|
)
|
|
def test_infix_dims_errors(supplied, all_):
|
|
with pytest.raises(ValueError):
|
|
list(infix_dims(supplied, all_))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["dim", "expected"],
|
|
[
|
|
pytest.param("a", ("a",), id="str"),
|
|
pytest.param(["a", "b"], ("a", "b"), id="list_of_str"),
|
|
pytest.param(["a", 1], ("a", 1), id="list_mixed"),
|
|
pytest.param(["a", ...], ("a", ...), id="list_with_ellipsis"),
|
|
pytest.param(("a", "b"), ("a", "b"), id="tuple_of_str"),
|
|
pytest.param(["a", ("b", "c")], ("a", ("b", "c")), id="list_with_tuple"),
|
|
pytest.param((("b", "c"),), (("b", "c"),), id="tuple_of_tuple"),
|
|
pytest.param({"a", 1}, tuple({"a", 1}), id="non_sequence_collection"),
|
|
pytest.param((), (), id="empty_tuple"),
|
|
pytest.param(set(), (), id="empty_collection"),
|
|
pytest.param(None, None, id="None"),
|
|
pytest.param(..., ..., id="ellipsis"),
|
|
],
|
|
)
|
|
def test_parse_dims_as_tuple(dim, expected) -> None:
|
|
all_dims = ("a", "b", 1, ("b", "c")) # selection of different Hashables
|
|
actual = utils.parse_dims_as_tuple(dim, all_dims, replace_none=False)
|
|
assert actual == expected
|
|
|
|
|
|
def test_parse_dims_set() -> None:
|
|
all_dims = ("a", "b", 1, ("b", "c")) # selection of different Hashables
|
|
dim = {"a", 1}
|
|
actual = utils.parse_dims_as_tuple(dim, all_dims)
|
|
assert set(actual) == dim
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dim", [pytest.param(None, id="None"), pytest.param(..., id="ellipsis")]
|
|
)
|
|
def test_parse_dims_replace_none(dim: None | EllipsisType) -> None:
|
|
all_dims = ("a", "b", 1, ("b", "c")) # selection of different Hashables
|
|
actual = utils.parse_dims_as_tuple(dim, all_dims, replace_none=True)
|
|
assert actual == all_dims
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dim",
|
|
[
|
|
pytest.param("x", id="str_missing"),
|
|
pytest.param(["a", "x"], id="list_missing_one"),
|
|
pytest.param(["x", 2], id="list_missing_all"),
|
|
],
|
|
)
|
|
def test_parse_dims_raises(dim) -> None:
|
|
all_dims = ("a", "b", 1, ("b", "c")) # selection of different Hashables
|
|
with pytest.raises(ValueError, match="'x'"):
|
|
utils.parse_dims_as_tuple(dim, all_dims, check_exists=True)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["dim", "expected"],
|
|
[
|
|
pytest.param("a", ("a",), id="str"),
|
|
pytest.param(["a", "b"], ("a", "b"), id="list"),
|
|
pytest.param([...], ("a", "b", "c"), id="list_only_ellipsis"),
|
|
pytest.param(["a", ...], ("a", "b", "c"), id="list_with_ellipsis"),
|
|
pytest.param(["a", ..., "b"], ("a", "c", "b"), id="list_with_middle_ellipsis"),
|
|
],
|
|
)
|
|
def test_parse_ordered_dims(dim, expected) -> None:
|
|
all_dims = ("a", "b", "c")
|
|
actual = utils.parse_ordered_dims(dim, all_dims)
|
|
assert actual == expected
|
|
|
|
|
|
def test_parse_ordered_dims_raises() -> None:
|
|
all_dims = ("a", "b", "c")
|
|
|
|
with pytest.raises(ValueError, match="'x' do not exist"):
|
|
utils.parse_ordered_dims("x", all_dims, check_exists=True)
|
|
|
|
with pytest.raises(ValueError, match="repeated dims"):
|
|
utils.parse_ordered_dims(["a", ...], all_dims + ("a",))
|
|
|
|
with pytest.raises(ValueError, match="More than one ellipsis"):
|
|
utils.parse_ordered_dims(["a", ..., "b", ...], all_dims)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"nested_list, expected",
|
|
[
|
|
([], []),
|
|
([1], [1]),
|
|
([1, 2, 3], [1, 2, 3]),
|
|
([[1]], [1]),
|
|
([[1, 2], [3, 4]], [1, 2, 3, 4]),
|
|
([[[1, 2, 3], [4]], [5, 6]], [1, 2, 3, 4, 5, 6]),
|
|
],
|
|
)
|
|
def test_iterate_nested(nested_list, expected):
|
|
assert list(iterate_nested(nested_list)) == expected
|
|
|
|
|
|
def test_find_stack_level():
|
|
assert utils.find_stack_level() == 1
|
|
assert utils.find_stack_level(test_mode=True) == 2
|
|
|
|
def f():
|
|
return utils.find_stack_level(test_mode=True)
|
|
|
|
assert f() == 3
|
|
|
|
|
|
def test_attempt_import() -> None:
|
|
"""Test optional dependency handling."""
|
|
np = attempt_import("numpy")
|
|
assert np.__name__ == "numpy"
|
|
|
|
with pytest.raises(ImportError, match="The foo package is required"):
|
|
attempt_import(module="foo")
|
|
with pytest.raises(ImportError, match="The foo package is required"):
|
|
attempt_import(module="foo.bar")
|