tests/functional2: improve type checking util
currently, there is a small helper funciton in lang_util to check if something is of a list type generic to improve re-usability, this function is moved to utils and improved to be also check for nested iterables and such Change-Id: I92984daa4c4decf13d340a2ea5e52f724cee800e
This commit is contained in:
@@ -9,8 +9,7 @@ import toml
|
||||
from toml import TomlDecodeError
|
||||
|
||||
from functional2.testlib.fixtures.file_helper import FileDeclaration, CopyFile, AssetSymlink
|
||||
from functional2.testlib.utils import test_base_folder
|
||||
|
||||
from functional2.testlib.utils import test_base_folder, is_value_of_type
|
||||
|
||||
LANG_TEST_ID_PATTERN = "{folder_name}:{test_name}"
|
||||
|
||||
@@ -126,25 +125,6 @@ def _group_lang_tests(tests: list[LangTest]) -> dict[LangTestRunner, list[LangTe
|
||||
return grouped_tests
|
||||
|
||||
|
||||
def _is_list_of_type(value: Any, expected_type: type[Any]) -> bool:
|
||||
"""
|
||||
checks if the given value conforms to the type `list[expected_type]`
|
||||
:param value: value to check
|
||||
:param expected_type: what type each item should be
|
||||
:return: True, if it conforms, False otherwise
|
||||
"""
|
||||
return isinstance(value, list) and all(isinstance(v, expected_type) for v in value)
|
||||
|
||||
|
||||
def _is_list_of_strings(value: Any) -> bool:
|
||||
"""
|
||||
same as `_is_list_of_type` but with type `str` pre-applied
|
||||
:param value: value to check
|
||||
:return: True, if the value conforms to `list[str]` otherwise False
|
||||
"""
|
||||
return _is_list_of_type(value, str)
|
||||
|
||||
|
||||
def _collect_toml_test_group(folder: Path) -> tuple[list[LangTest], list[InvalidLangTest]]:
|
||||
"""
|
||||
Collects all tests, declared by a `test.toml` file within the given folder
|
||||
@@ -185,7 +165,7 @@ def _collect_toml_test_group(folder: Path) -> tuple[list[LangTest], list[Invalid
|
||||
continue
|
||||
|
||||
flags = definition.pop("flags", [])
|
||||
if not _is_list_of_strings(flags):
|
||||
if not is_value_of_type(flags, list[str]):
|
||||
test_errors.append(
|
||||
f"invalid value type for 'flags': {flags}, expected a list of strings"
|
||||
)
|
||||
@@ -198,7 +178,7 @@ def _collect_toml_test_group(folder: Path) -> tuple[list[LangTest], list[Invalid
|
||||
runner = None
|
||||
|
||||
extra_files = definition.pop("extra-files", [])
|
||||
if not _is_list_of_strings(extra_files):
|
||||
if not is_value_of_type(extra_files, list[str]):
|
||||
test_errors.append(
|
||||
f"invalid value type for 'extra_files': {extra_files}, expected a list of strings"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from collections.abc import Callable, Generator
|
||||
from typing import Literal, Any
|
||||
|
||||
import pytest
|
||||
from functional2.testlib.utils import is_value_of_type
|
||||
|
||||
|
||||
def test_list_type_valid():
|
||||
assert is_value_of_type([1, 2, 3], list[int])
|
||||
|
||||
|
||||
def test_list_type_valid_multi_type():
|
||||
assert is_value_of_type([1, "a", 2], list[int | str])
|
||||
|
||||
|
||||
def test_list_type_invalid_single_fail():
|
||||
assert not is_value_of_type([1, 2, "a"], list[int])
|
||||
|
||||
|
||||
def test_list_type_invalid_all_wrong():
|
||||
assert not is_value_of_type([1, 2, 3], list[str])
|
||||
|
||||
|
||||
def test_list_type_nested():
|
||||
assert is_value_of_type([[1], [2, 3], [4]], list[list[int]])
|
||||
|
||||
|
||||
def test_list_type_nested_single_invalid():
|
||||
assert not is_value_of_type([[1], [2, 3], ["a"]], list[list[int]])
|
||||
|
||||
|
||||
def test_weird_type_valid():
|
||||
assert is_value_of_type(42, Literal[42])
|
||||
|
||||
|
||||
def test_type_type():
|
||||
class Foo: ...
|
||||
|
||||
assert is_value_of_type(Foo, type[Foo])
|
||||
|
||||
|
||||
def test_type_doesnt_allow_generator():
|
||||
def foo():
|
||||
yield 1
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported expected_type.+"):
|
||||
is_value_of_type(foo, Generator[int, None, None])
|
||||
|
||||
|
||||
def test_type_doesnt_allow_callable():
|
||||
def foo(a): # noqa: ANN001, ANN202: dummy stuff within testing, nothing external
|
||||
return "a" + str(a)
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported expected_type.+"):
|
||||
is_value_of_type(foo, Callable[[int], str])
|
||||
|
||||
|
||||
def test_type_allows_none():
|
||||
assert is_value_of_type(None, type[None])
|
||||
|
||||
|
||||
def test_doesnt_allow_generic():
|
||||
class X[T]: ...
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported expected_type.+"):
|
||||
is_value_of_type(0, X[int])
|
||||
|
||||
|
||||
def test_type_dict_checks_valid():
|
||||
d = {"a": 1, "b": 2}
|
||||
assert is_value_of_type(d, dict[str, int])
|
||||
|
||||
|
||||
def test_type_dict_key_invalid():
|
||||
d = {"a": 1, 1: 2}
|
||||
assert not is_value_of_type(d, dict[str, int])
|
||||
|
||||
|
||||
def test_type_dict_value_invalid():
|
||||
d = {"a": 1, "b": "2"}
|
||||
assert not is_value_of_type(d, dict[str, int])
|
||||
|
||||
|
||||
def test_type_allows_any():
|
||||
assert is_value_of_type(1, Any)
|
||||
assert is_value_of_type("a", Any)
|
||||
assert is_value_of_type(lambda x: x, Any)
|
||||
@@ -1,4 +1,9 @@
|
||||
import builtins
|
||||
import types
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from types import UnionType
|
||||
from typing import Any, Literal, get_args, get_origin
|
||||
|
||||
from functional2.testlib.fixtures.file_helper import (
|
||||
CopyFile,
|
||||
@@ -7,7 +12,6 @@ from functional2.testlib.fixtures.file_helper import (
|
||||
merge_file_declaration,
|
||||
)
|
||||
|
||||
|
||||
# Things have to be resolved from top to bottom, because otherwise the tests behave flakey
|
||||
# due to the internal file structure
|
||||
|
||||
@@ -86,3 +90,38 @@ def get_functional2_lang_files(additional_files: FileDeclaration | None = None)
|
||||
additional_files,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_value_of_type(value: Any, expected_type: type[Any] | UnionType) -> bool:
|
||||
"""
|
||||
checks if the given value conforms to the type.
|
||||
This function is useful, to check if all values conform to the inner iterable type
|
||||
:param value: value to check
|
||||
:param expected_type: what type each item should be; must be a non-generic type or one of {dict, list, set, UnionType, Literal}
|
||||
:return: True if it conforms, otherwise False
|
||||
"""
|
||||
supported_origin_types = {dict, list, set, type, UnionType, Literal}
|
||||
origin = get_origin(expected_type)
|
||||
if expected_type is Any:
|
||||
return True
|
||||
match origin:
|
||||
case None | types.UnionType:
|
||||
return isinstance(value, expected_type)
|
||||
case typing.Literal:
|
||||
return value in get_args(expected_type)
|
||||
case builtins.type:
|
||||
return value is get_args(expected_type)[0]
|
||||
case builtins.list | builtins.set | builtins.dict:
|
||||
matches_origin = isinstance(value, origin)
|
||||
if not matches_origin:
|
||||
return False
|
||||
matches = all(is_value_of_type(v, get_args(expected_type)[0]) for v in value)
|
||||
if origin is dict:
|
||||
# also check the value side of dicts
|
||||
matches = matches and all(
|
||||
is_value_of_type(v, get_args(expected_type)[1]) for v in value.values()
|
||||
)
|
||||
return matches
|
||||
case _:
|
||||
msg = f"Unsupported expected_type. Must be a non-generic or one of {supported_origin_types!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
Reference in New Issue
Block a user