Skip to content

Commit

Permalink
Add runtime and typing tests for _guards
Browse files Browse the repository at this point in the history
This also leads to a minor change from __call__ to "apply()" in order
to ensure type-checkers read the types correctly.
  • Loading branch information
sirosen committed Aug 8, 2023
1 parent 24588aa commit 0fb8c69
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/globus_sdk/_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ class _Reducer(t.Generic[T]):
def __init__(self, typ: type[T]):
self.typ = typ

def __call__(self, data: t.Any) -> TypeGuard[T]:
# although it might be nice to use __call__ here, type checkers (read: mypy) do not
# always correctly treat an arbitrary callable TypeGuard as a a guard
def apply(self, data: t.Any) -> TypeGuard[T]:
return isinstance(data, self.typ)

def list_of(self, data: t.Any) -> TypeGuard[list[T]]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def validator(name: str, value: t.Any) -> T:
return validator


str_ = _from_guard(_guards.reduce(str), "a string")
str_ = _from_guard(_guards.reduce(str).apply, "a string")
opt_str = _from_guard(_guards.reduce(str).optional, "a string or null")
opt_bool = _from_guard(_guards.reduce(bool).optional, "a bool or null")
str_list = _from_guard(_guards.reduce(str).list_of, "a list of strings")
Expand Down
51 changes: 51 additions & 0 deletions tests/non-pytest/mypy-ignore-tests/test_guards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# test that the internal _guards module provides valid and well-formed type-guards
import typing as t

from globus_sdk import _guards


def get_any() -> t.Any:
return 1


x = get_any()
t.assert_type(x, t.Any)

# test reduce().apply
if _guards.reduce(str).apply(x):
y1 = x
t.assert_type(y1, str)

# test is_list_of / reduce().list_of
if _guards.is_list_of(x, str):
t.assert_type(x, list[str])
elif _guards.is_list_of(x, int):
t.assert_type(x, list[int])

if _guards.reduce(str).list_of(x):
t.assert_type(x, list[str])
elif _guards.reduce(int).list_of(x):
t.assert_type(x, list[int])

# test is_optional / reduce().optional
if _guards.is_optional(x, float):
t.assert_type(x, float | None)
elif _guards.is_optional(x, bytes):
t.assert_type(x, bytes | None)

if _guards.reduce(float).optional(x):
t.assert_type(x, float | None)
elif _guards.reduce(bytes).optional(x):
t.assert_type(x, bytes | None)


# test is_optional_list_of / reduce().optional_list
if _guards.is_optional_list_of(x, type(None)):
t.assert_type(x, list[None] | None)
elif _guards.is_optional_list_of(x, dict):
t.assert_type(x, list[dict[t.Any, t.Any]] | None)

if _guards.reduce(type(None)).optional_list(x):
t.assert_type(x, list[None] | None)
elif _guards.reduce(dict).optional_list(x):
t.assert_type(x, list[dict[t.Any, t.Any]] | None)
86 changes: 86 additions & 0 deletions tests/unit/test_guards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import pytest

from globus_sdk import _guards


@pytest.mark.parametrize(
"value, typ, ok",
[
# passing
([], str, True),
([1, 2], int, True),
(["1", ""], str, True),
([], list, True),
([[], [1, 2], ["foo"]], list, True),
# failing
([1], str, False),
(["foo"], int, False),
((1, 2), int, False),
(list, list, False),
(list, str, False),
(["foo", 1], str, False),
([1, 2], list, False),
],
)
def test_list_of_guard(value, typ, ok):
assert _guards.is_list_of(value, typ) == ok


@pytest.mark.parametrize(
"value, typ, ok",
[
# passing
(None, str, True),
("foo", str, True),
# failing
(b"foo", str, False),
("", int, False),
(type(None), str, False),
],
)
def test_opt_guard(value, typ, ok):
assert _guards.is_optional(value, typ) == ok


@pytest.mark.parametrize(
"value, typ, ok",
[
# passing
([], str, True),
([], int, True),
([1, 2], int, True),
(["1", ""], str, True),
(None, str, True),
# failing
# NB: the guard checks `list[str] | None`, not `list[str | None]`
([None], str, False),
(b"foo", str, False),
("", str, False),
(type(None), str, False),
],
)
def test_opt_list_guard(value, typ, ok):
assert _guards.is_optional_list_of(value, typ) == ok


def test_reduced_guards():
reduced_str = _guards.reduce(str)

# apply => isinstance
assert reduced_str.apply("foo")
assert not reduced_str.apply(None)

# list_of => is_list_of
assert reduced_str.list_of(["foo", "bar"])
assert not reduced_str.list_of(["foo", "bar", 1])

# optional => is_optional
assert reduced_str.optional("foo")
assert reduced_str.optional(None)
assert not reduced_str.optional(1)

# optional_list => is_optional_list_of
assert reduced_str.optional_list(["foo", "bar"])
assert not reduced_str.optional_list(["foo", "bar", 1])
assert reduced_str.optional_list(None)
assert not reduced_str.optional_list("")

0 comments on commit 0fb8c69

Please sign in to comment.