Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "simtypes"
version = "0.0.13"
version = "0.0.14"
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
description = 'Type checking in runtime without stupid games'
readme = "README.md"
Expand Down
7 changes: 7 additions & 0 deletions tests/typing/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

@pytest.mark.mypy_testing
def test_basic_positives() -> None:
"""Static typing accepts simple matching int and str check calls, and the runtime results are true."""
assert check(5, int)
assert check("kek", str)


@pytest.mark.mypy_testing
def test_positive_with_users_class() -> None:
"""Static typing accepts a locally defined class hint, and the runtime check is true for its instance."""
class SomeClass:
pass

Expand All @@ -19,6 +21,11 @@ class SomeClass:

@pytest.mark.mypy_testing
def test_negative_with_users_class() -> None:
"""
A non-instance value can be passed with a local class hint without a static typing error.

The false runtime result is intentionally not asserted here.
"""
class SomeClass:
pass

Expand Down
113 changes: 113 additions & 0 deletions tests/units/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@


def test_none():
"""
Bare None and NoneType annotations match only the None singleton.

Falsy values, the string "None", and the NoneType class object itself are rejected as values.
"""
assert check(None, None) is True
assert check(None, NoneType) is True

Expand All @@ -33,6 +38,11 @@ def test_none():


def test_built_in_types():
"""
Plain bool, int, float, and str hints match exact runtime types, except for bool-as-int behavior covered elsewhere.

String values that look convertible are rejected instead of being parsed or coerced.
"""
assert check(True, bool)
assert check(1, int)
assert check(1.0, float)
Expand All @@ -53,6 +63,7 @@ def test_built_in_types():


def test_any():
"""typing.Any matches all sampled values, including primitives, collections, None, and type objects, even in strict mode."""
assert check(True, Any)
assert check(False, Any)
assert check(0, Any)
Expand All @@ -65,27 +76,39 @@ def test_any():
assert check(None, Any)
assert check(str, Any)
assert check(-1000, Any)
assert check([1, 'two', None], Any, strict=True)
assert check((1, 'two', None), Any, strict=True)
assert check(str, Any, strict=True)
assert check(None, Any, strict=True)


@pytest.mark.skipif(sys.version_info > (3, 13), reason="Before Python 3.14, you couldn't just use Union as an annotation.")
def test_empty_union_old_pythons():
"""Before the Python 3.14 bare-Union behavior, check(None, Union) raises ValueError for an unusable annotation."""
with pytest.raises(ValueError, match=match('Type must be a valid type object.')):
check(None, Union)


@pytest.mark.skipif(sys.version_info < (3, 14), reason="Before Python 3.14, you couldn't just use Union as an annotation.")
def test_empty_union():
"""On Python 3.14+, bare typing.Union is valid but behaves as an empty union, not Any, and matches none of the sampled values."""
assert not check(None, Union)
assert not check(1, Union)
assert not check('kek', Union)


def test_empty_optional():
"""Bare Optional is invalid: check(None, Optional) raises ValueError rather than matching None or behaving like Optional[Any]."""
with pytest.raises(ValueError, match=match('Type must be a valid type object.')):
check(None, Optional)


def test_union(make_union):
"""
Parameterized unions accept values matching any member type.

Covers both typing.Union and PEP 604 syntax, and rejects None unless it is explicitly included.
"""
assert check(1, make_union(int, str))
assert check('hello', make_union(int, str))
assert check(1.0, make_union(int, float))
Expand All @@ -95,6 +118,11 @@ def test_union(make_union):


def test_union_recursive(make_union):
"""
Nested unions behave the same as equivalent flattened unions.

They accept any member type across typing.Union and | forms, and reject unrelated containers and None.
"""
assert check(1, make_union(int, make_union(float, str)))
assert check(1.0, make_union(int, make_union(float, str)))
assert check('kek', make_union(int, make_union(float, str)))
Expand All @@ -117,6 +145,7 @@ def test_union_recursive(make_union):


def test_bool_is_int(make_optional, make_union):
"""bool values satisfy int annotations, including through Union, Optional, and Optional[Union[...]]."""
assert check(True, int)
assert check(False, int)

Expand All @@ -133,6 +162,11 @@ def test_bool_is_int(make_optional, make_union):


def test_optional(tuple_type, list_type, make_optional):
"""
Optional annotations accept only None or values of the wrapped type.

Covers typing.Optional[T], T | None, and Optional hints wrapping bare list or tuple types.
"""
assert check(None, make_optional(int))
assert check(1, make_optional(int))
assert check(0, make_optional(int))
Expand Down Expand Up @@ -162,7 +196,29 @@ def test_optional(tuple_type, list_type, make_optional):
assert check((1, 2, 3), make_optional(tuple_type))


def test_optional_with_parameterized_collections_in_strict_mode(make_optional, subscribable_list_type, subscribable_tuple_type):
"""
Strict Optional checks pass strict validation through to parameterized collections.

None still matches, but present list and tuple values must satisfy their inner annotations.
"""
assert check(None, make_optional(subscribable_list_type[int]), strict=True)
assert check([1, 2, 3], make_optional(subscribable_list_type[int]), strict=True)
assert not check(['1', '2', '3'], make_optional(subscribable_list_type[int]), strict=True)
assert not check((1, 2, 3), make_optional(subscribable_list_type[int]), strict=True)

assert check(None, make_optional(subscribable_tuple_type[int, str]), strict=True)
assert check((1, 'kek'), make_optional(subscribable_tuple_type[int, str]), strict=True)
assert not check((1, 2), make_optional(subscribable_tuple_type[int, str]), strict=True)
assert not check([1, 'kek'], make_optional(subscribable_tuple_type[int, str]), strict=True)


def test_optional_union(make_union, make_optional, tuple_type):
"""
Optional unions accept None or any inner union member across typing and PEP 604 spellings.

This differs from ordinary unions, where None is rejected unless it is explicitly included.
"""
assert check(None, make_optional(make_union(int, str)))
assert check(1, make_optional(make_union(int, str)))
assert check('kek', make_optional(make_union(int, str)))
Expand Down Expand Up @@ -193,6 +249,11 @@ def test_optional_union(make_union, make_optional, tuple_type):
],
)
def test_list_without_arguments(list_type, addictional_parameters):
"""
Unsubscripted List/list annotations validate only the outer list type across strict modes.

Mixed contents are accepted when no element type is declared.
"""
assert check([], list_type, **addictional_parameters)
assert check([1, 2, 3], list_type, **addictional_parameters)
assert check(['kek', 'lol'], list_type, **addictional_parameters)
Expand All @@ -218,6 +279,11 @@ def test_list_without_arguments(list_type, addictional_parameters):
],
)
def test_tuple_without_arguments(tuple_type, addictional_parameters):
"""
Unsubscripted Tuple and tuple annotations validate only tuple-ness across strict modes.

Length, nesting, and contents are ignored, while non-tuples are rejected.
"""
assert check((), tuple_type, **addictional_parameters)
assert check((1,), tuple_type, **addictional_parameters)
assert check((None,), tuple_type, **addictional_parameters)
Expand Down Expand Up @@ -248,6 +314,7 @@ def test_tuple_without_arguments(tuple_type, addictional_parameters):
],
)
def test_set_without_arguments(set_type, addictional_parameters):
"""Unsubscripted Set/set annotations accept sets and reject non-set values regardless of strict mode."""
assert check(set(), set_type, **addictional_parameters)
assert check({1}, set_type, **addictional_parameters)
assert check({None}, set_type, **addictional_parameters)
Expand All @@ -273,6 +340,11 @@ def test_set_without_arguments(set_type, addictional_parameters):
],
)
def test_dict_without_arguments(dict_type, addictional_parameters):
"""
Unsubscripted Dict/dict annotations validate only the outer dict type.

Any real dict is accepted across strict modes, while non-dict values are rejected.
"""
assert check({}, dict_type, **addictional_parameters)
assert check({'lol': 'kek'}, dict_type, **addictional_parameters)
assert check({1: 'kek'}, dict_type, **addictional_parameters)
Expand All @@ -291,14 +363,19 @@ def test_dict_without_arguments(dict_type, addictional_parameters):


def test_content_of_list_not_in_strict_mode_is_not_checking(subscribable_list_type):
"""Non-strict List[int]/list[int] checks accept any list regardless of element contents, but still reject non-lists."""
assert check([], subscribable_list_type[int])
assert check(['lol', 'kek'], subscribable_list_type[int])
assert check([1.0, 2.0], subscribable_list_type[int])
assert check([None, None], subscribable_list_type[int])
assert check([None, 'kek', 1, 1.0], subscribable_list_type[int])
assert not check((), subscribable_list_type[int])
assert not check((1, 2, 3), subscribable_list_type[int])
assert not check('kek', subscribable_list_type[int])


def test_content_of_tuple_not_in_strict_mode_is_not_checking(subscribable_tuple_type):
"""Non-strict tuple[int] and Tuple[int] checks validate only the outer tuple, ignoring element types and arity."""
assert check((), subscribable_tuple_type[int])
assert check(('lol', 'kek'), subscribable_tuple_type[int])
assert check((1.0, 2.0), subscribable_tuple_type[int])
Expand All @@ -307,24 +384,33 @@ def test_content_of_tuple_not_in_strict_mode_is_not_checking(subscribable_tuple_


def test_content_of_dict_not_in_strict_mode_is_not_checking(subscribable_dict_type):
"""Non-strict Dict[int, int]/dict[int, int] validation accepts any dict regardless of contents, but still rejects non-dicts."""
assert check({}, subscribable_dict_type[int, int])
assert check({1: 'kek'}, subscribable_dict_type[int, int])
assert check({'lol': 'kek'}, subscribable_dict_type[int, int])
assert check({'lol': 1}, subscribable_dict_type[int, int])
assert check({1.0: 1}, subscribable_dict_type[int, int])
assert not check([], subscribable_dict_type[int, int])
assert not check([(1, 2)], subscribable_dict_type[int, int])
assert not check('{1: 1}', subscribable_dict_type[int, int])


@pytest.mark.skipif(sys.version_info < (3, 9), reason='Subscribing to objects became available in Python 3.9')
def test_content_of_set_not_in_strict_mode_is_not_checking():
"""Non-strict set[int] validation accepts any set regardless of element contents, but still rejects non-sets."""
assert check(set(), set[int])
assert check(set(['lol', 'kek']), set[int])
assert check(set([1, 'kek']), set[int])
assert check(set([1, None]), set[int])
assert check(set([None, None]), set[int])
assert check(set(['1', '2']), set[int])
assert not check([], set[int])
assert not check(('1', '2'), set[int])
assert not check('kek', set[int])


def test_try_to_pass_not_type_object_as_type():
"""check raises ValueError for non-type, non-annotation second arguments, including string pseudo-annotations."""
with pytest.raises(ValueError, match=match('Type must be a valid type object.')):
check(1, 1)

Expand All @@ -336,6 +422,7 @@ def test_try_to_pass_not_type_object_as_type():


def test_simple_isinstance():
"""check mirrors isinstance for a user-defined class, matching instances and rejecting unrelated values and class-name strings."""
class SomeType:
pass

Expand All @@ -348,6 +435,7 @@ class SomeType:


def test_sequence():
"""Bare Sequence uses ABC membership: lists, tuples, and strings pass, while scalar non-sequences fail."""
assert check([1, 2, 3], Sequence)
assert check((1, 2, 3), Sequence)
assert check('kek', Sequence)
Expand All @@ -357,11 +445,18 @@ def test_sequence():

@pytest.mark.skipif(sys.version_info < (3, 9), reason='Subscribing to objects became available in Python 3.9')
def test_sequence_is_not_checking_content():
"""Sequence[str] checks only the sequence origin, so integer lists and tuples pass while non-sequence containers fail."""
assert check((1, 2, 3), Sequence[str])
assert check([1, 2, 3], Sequence[str])
assert not check({1, 2, 3}, Sequence[str])


def test_list_with_values_in_strict_mode(subscribable_list_type, make_union):
"""
Strict mode recursively checks parameterized lists.

Empty lists pass, nested unions and lists are checked, and wrong element types or non-list values fail.
"""
assert check([], subscribable_list_type[int], strict=True)
assert check([], subscribable_list_type[str], strict=True)

Expand All @@ -381,6 +476,11 @@ def test_list_with_values_in_strict_mode(subscribable_list_type, make_union):


def test_dict_with_values_in_strict_mode(subscribable_dict_type, subscribable_list_type, make_union):
"""
Strict dict handling checks real dicts recursively.

Every key and value annotation is validated, including nested containers and unions, while empty dicts pass.
"""
assert check({}, subscribable_dict_type[int, int], strict=True)
assert check({}, subscribable_dict_type[str, str], strict=True)

Expand All @@ -403,6 +503,11 @@ def test_dict_with_values_in_strict_mode(subscribable_dict_type, subscribable_li


def test_tuple_with_values_in_strict_mode(subscribable_tuple_type, make_union):
"""
Strict tuple checks validate tuple contents and enforce arity for fixed-length annotations.

Variadic tuple annotations and tuple/union nesting are checked recursively; list and string inputs are rejected.
"""
assert not check((), subscribable_tuple_type[int], strict=True)
assert not check((), subscribable_tuple_type[str], strict=True)
assert check((), subscribable_tuple_type[int, ...], strict=True)
Expand All @@ -429,6 +534,7 @@ def test_tuple_with_values_in_strict_mode(subscribable_tuple_type, make_union):


def test_lists_are_tuples_flag_is_true_in_strict_mode(subscribable_tuple_type, subscribable_list_type, subscribable_dict_type, make_union):
"""With strict=True and lists_are_tuples=True, lists can satisfy tuple annotations recursively, including inside unions, lists, and dict values."""
assert check(["123"], subscribable_list_type[str], strict=True, lists_are_tuples=True)
assert check(["123"], subscribable_tuple_type[str, ...], strict=True, lists_are_tuples=True)
assert check(("123",), subscribable_tuple_type[str, ...], strict=True, lists_are_tuples=True)
Expand Down Expand Up @@ -463,6 +569,7 @@ def test_lists_are_tuples_flag_is_true_in_strict_mode(subscribable_tuple_type, s
],
)
def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):
"""With default pass_mocks or pass_mocks=True, Mock and MagicMock pass unrelated hints in both strict modes."""
assert check(Mock(), int, strict=strict_mode, **addictional_parameters)
assert check(Mock(), str, strict=strict_mode, **addictional_parameters)
assert check(Mock(), list_type, strict=strict_mode, **addictional_parameters)
Expand All @@ -483,6 +590,7 @@ def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):
],
)
def test_pass_mocks_when_its_off(strict_mode, list_type):
"""Disabling pass_mocks rejects mocks for unrelated hints while keeping normal Mock and MagicMock isinstance matches."""
assert not check(Mock(), int, strict=strict_mode, pass_mocks=False)
assert not check(Mock(), str, strict=strict_mode, pass_mocks=False)
assert not check(Mock(), list_type, strict=strict_mode, pass_mocks=False)
Expand All @@ -492,6 +600,8 @@ def test_pass_mocks_when_its_off(strict_mode, list_type):
assert not check(MagicMock(), list_type, strict=strict_mode, pass_mocks=False)

assert check(Mock(), Mock, strict=strict_mode, pass_mocks=False)
assert check(MagicMock(), Mock, strict=strict_mode, pass_mocks=False)
assert not check(Mock(), MagicMock, strict=strict_mode, pass_mocks=False)
assert check(MagicMock(), MagicMock, strict=strict_mode, pass_mocks=False)


Expand All @@ -503,6 +613,7 @@ def test_pass_mocks_when_its_off(strict_mode, list_type):
],
)
def test_denial_sentinel(strict_mode):
"""SentinelType accepts None, InnerNone, and InnerNoneType(...) instances in both strict modes while rejecting ordinary values."""
assert not check(123, SentinelType, strict=strict_mode)
assert not check('None', SentinelType, strict=strict_mode)

Expand All @@ -521,6 +632,7 @@ def test_denial_sentinel(strict_mode):
],
)
def test_denial_innernonetype(strict_mode):
"""InnerNoneType accepts InnerNone and any InnerNoneType(...) instance while rejecting ordinary values and real None."""
assert not check(123, InnerNoneType, strict=strict_mode)
assert not check('None', InnerNoneType, strict=strict_mode)
assert not check(None, InnerNoneType, strict=strict_mode)
Expand All @@ -539,6 +651,7 @@ def test_denial_innernonetype(strict_mode):
],
)
def test_denial_innernone(strict_mode):
"""Concrete denial sentinels match only equal sentinel values, not merely other InnerNoneType instances, regardless of strict mode."""
assert not check(123, InnerNoneType(123), strict=strict_mode)
assert not check('None', InnerNoneType(123), strict=strict_mode)
assert not check(None, InnerNoneType(123), strict=strict_mode)
Expand Down
Loading
Loading