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:
Commentator2.0
2025-06-07 00:14:57 +02:00
parent 60830ca5fa
commit afa5b924cd
3 changed files with 130 additions and 24 deletions
+3 -23
View File
@@ -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"
)
+87
View File
@@ -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)
+40 -1
View File
@@ -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)