From b02c1f83bed5966b9a66702c89f7c8fb690edafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 1 Jul 2026 14:27:40 +0300 Subject: [PATCH 1/3] Bump version to 0.0.14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00f6b23..ab42346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From 3147f947c677935538cccd39a6a9936d2cc6ce97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 1 Jul 2026 14:28:30 +0300 Subject: [PATCH 2/3] Add some docstrings --- tests/typing/test_check.py | 7 ++ tests/units/test_check.py | 80 +++++++++++++++++++++ tests/units/test_from_string.py | 48 +++++++++++++ tests/units/types/ints/test_natural.py | 2 + tests/units/types/ints/test_non_negative.py | 2 + 5 files changed, 139 insertions(+) diff --git a/tests/typing/test_check.py b/tests/typing/test_check.py index 329e944..fad618d 100644 --- a/tests/typing/test_check.py +++ b/tests/typing/test_check.py @@ -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 @@ -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 diff --git a/tests/units/test_check.py b/tests/units/test_check.py index 08e9b3a..5e5ad45 100644 --- a/tests/units/test_check.py +++ b/tests/units/test_check.py @@ -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 @@ -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) @@ -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.""" assert check(True, Any) assert check(False, Any) assert check(0, Any) @@ -69,23 +80,31 @@ def test_any(): @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)) @@ -95,6 +114,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))) @@ -117,6 +141,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) @@ -133,6 +158,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)) @@ -163,6 +193,11 @@ def test_optional(tuple_type, list_type, make_optional): 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))) @@ -193,6 +228,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) @@ -218,6 +258,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) @@ -248,6 +293,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) @@ -273,6 +319,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) @@ -291,6 +342,7 @@ 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.""" assert check([], subscribable_list_type[int]) assert check(['lol', 'kek'], subscribable_list_type[int]) assert check([1.0, 2.0], subscribable_list_type[int]) @@ -299,6 +351,7 @@ def test_content_of_list_not_in_strict_mode_is_not_checking(subscribable_list_ty 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]) @@ -307,6 +360,7 @@ 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 key and value types.""" 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]) @@ -316,6 +370,7 @@ def test_content_of_dict_not_in_strict_mode_is_not_checking(subscribable_dict_ty @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 checks only the set origin and ignores element types.""" assert check(set(), set[int]) assert check(set(['lol', 'kek']), set[int]) assert check(set([1, 'kek']), set[int]) @@ -325,6 +380,7 @@ def test_content_of_set_not_in_strict_mode_is_not_checking(): 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) @@ -336,6 +392,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 @@ -348,6 +405,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) @@ -357,11 +415,17 @@ 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 still pass.""" assert check((1, 2, 3), Sequence[str]) assert 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) @@ -381,6 +445,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) @@ -403,6 +472,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) @@ -429,6 +503,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) @@ -463,6 +538,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) @@ -483,6 +559,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) @@ -503,6 +580,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) @@ -521,6 +599,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) @@ -539,6 +618,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) diff --git a/tests/units/test_from_string.py b/tests/units/test_from_string.py index 820fe6c..77754fb 100644 --- a/tests/units/test_from_string.py +++ b/tests/units/test_from_string.py @@ -10,6 +10,7 @@ def test_value_is_not_string(): + """Reject non-string input with ValueError before deserialization, for both parsed and passthrough target types.""" with pytest.raises(ValueError, match=match('You can only pass a string as a string. You passed int.')): from_string(5, int) @@ -18,6 +19,7 @@ def test_value_is_not_string(): def test_type_is_not_type(): + """Reject non-type expected_type arguments with ValueError, including integers and string annotations that are API misuse rather than names to resolve.""" with pytest.raises(ValueError, match=match('The type must be a valid type object.')): from_string('lol', 5) @@ -26,6 +28,7 @@ def test_type_is_not_type(): def test_not_supported_data_type(): + """A custom unsupported target class raises the unsupported-type TypeError; the message names the class and includes the supported-types help text.""" class SuperType: pass @@ -34,11 +37,17 @@ class SuperType: def test_get_string_value(): + """Explicit str deserialization returns valid string input unchanged.""" assert from_string('kek', str) == 'kek' assert from_string('lol', str) == 'lol' def test_get_int_value(): + """ + from_string(..., int) parses decimal integer strings, including negatives, leading zeroes, and underscores. + + Non-integer text, float-looking strings, the string "True", and empty strings raise the integer-specific TypeError. + """ assert from_string('1', int) == 1 assert from_string('1000', int) == 1000 assert from_string('1000000000000', int) == 1000000000000 @@ -63,6 +72,11 @@ def test_get_int_value(): def test_get_float_value(): + """ + Float parsing accepts decimal and integer-shaped strings with underscores, the sampled inf/NaN spellings, and Unicode infinity symbols. + + The string "True", empty strings, and non-numeric text raise the floating-point TypeError. + """ assert from_string('1.0', float) == 1.0 assert from_string('1000.0', float) == 1000.0 assert from_string('1000000000000.0', float) == 1000000000000.0 @@ -109,6 +123,11 @@ def test_get_float_value(): def test_get_bool_value(): + """ + Boolean deserialization accepts only the exact string tokens "yes", "True", "true", "False", "false", and "no". + + Empty, unknown, or near-match strings raise the boolean-specific TypeError. + """ assert from_string('yes', bool) == True assert from_string('True', bool) == True assert from_string('true', bool) == True @@ -128,6 +147,11 @@ def test_get_bool_value(): def test_get_list_value(list_type, subscribable_dict_type, subscribable_list_type): + """ + JSON arrays decode into bare and typed lists, including nested typed lists and lists of parameterized dicts. + + Malformed JSON, wrong element types, and mismatched nested collection types raise the standardized list TypeError. + """ assert from_string('[]', list_type) == [] assert from_string('[1, 2, 3]', list_type) == [1, 2, 3] assert from_string('[]', subscribable_list_type[int]) == [] @@ -174,6 +198,11 @@ def test_get_list_value(list_type, subscribable_dict_type, subscribable_list_typ def test_get_tuple_value(tuple_type, subscribable_tuple_type, subscribable_dict_type): + """ + JSON arrays decode into bare, fixed-length, variadic, and nested typed tuples, including parameterized dict elements. + + Malformed input, wrong arity, and nested type errors raise the standardized tuple TypeError. + """ assert from_string('[]', tuple_type) == () assert from_string('[1, 2, 3]', tuple_type) == (1, 2, 3) assert from_string('[]', subscribable_tuple_type[int, ...]) == () @@ -231,6 +260,11 @@ def test_get_tuple_value(tuple_type, subscribable_tuple_type, subscribable_dict_ def test_get_dict_value(dict_type, subscribable_list_type, subscribable_dict_type): + """ + JSON objects decode into bare and parameterized dicts when keys and values satisfy the annotations. + + JSON string keys are not coerced to int, so non-empty int-key dict hints fail; invalid JSON, value mismatches, and bad nested values raise the standardized dict TypeError. + """ assert from_string('{}', dict_type) == {} assert from_string('{"lol": "kek"}', dict_type) == {'lol': 'kek'} assert from_string('{}', subscribable_dict_type[int, int]) == {} @@ -307,10 +341,16 @@ def test_get_dict_value(dict_type, subscribable_list_type, subscribable_dict_typ ], ) def test_get_any(string): + """from_string with Any returns input strings unchanged, even when they look like JSON or numbers.""" assert from_string(string, Any) == string def test_deserialize_date(): + """ + Scalar ISO dates deserialize like date.fromisoformat. + + Invalid scalar dates raise the date-specific TypeError. + """ isoformatted_date = date(2026, 1, 22).isoformat() assert from_string(isoformatted_date, date) == date.fromisoformat(isoformatted_date) @@ -320,6 +360,11 @@ def test_deserialize_date(): def test_deserialize_datetetime(): + """ + ISO datetime strings deserialize like datetime.fromisoformat. + + Invalid scalar datetime strings raise the datetime-specific TypeError. + """ isoformatted_datetime = datetime.now().isoformat() assert from_string(isoformatted_datetime, datetime) == datetime.fromisoformat(isoformatted_datetime) @@ -329,6 +374,7 @@ def test_deserialize_datetetime(): def test_deserialize_subscribable_collections_with_datetimes(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type): + """Typed list, tuple, and dict hints deserialize ISO datetime strings in elements and dict keys or values; dict keys or values annotated as str remain strings.""" isoformatted_datetime = datetime.now().isoformat() assert from_string(dumps([isoformatted_datetime]), subscribable_list_type[datetime]) == [datetime.fromisoformat(isoformatted_datetime)] @@ -340,6 +386,7 @@ def test_deserialize_subscribable_collections_with_datetimes(subscribable_list_t def test_deserialize_subscribable_collections_with_dates(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type): + """Typed list, tuple, and dict hints deserialize ISO date strings in elements and dict keys or values; dict keys or values annotated as str remain strings.""" isoformatted_date = date(2026, 1, 22).isoformat() assert from_string(dumps([isoformatted_date]), subscribable_list_type[date]) == [date.fromisoformat(isoformatted_date)] @@ -351,6 +398,7 @@ def test_deserialize_subscribable_collections_with_dates(subscribable_list_type, def test_wrong_collection_content(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type, dict_type, list_type, tuple_type): # noqa: PLR0915, PLR0913 + """Invalid JSON collection shapes, nested collection mismatches, bad element/key/value types, and failed date/datetime conversions raise the relevant collection TypeError.""" with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a list of the specified format.')): from_string(dumps([123]), subscribable_list_type[date]) diff --git a/tests/units/types/ints/test_natural.py b/tests/units/types/ints/test_natural.py index 033133c..c8bf37b 100644 --- a/tests/units/types/ints/test_natural.py +++ b/tests/units/types/ints/test_natural.py @@ -2,6 +2,7 @@ def test_basic_isinstance(): + """Direct isinstance recognizes positive ints as NaturalNumber and rejects zero, negatives, and the sampled string input.""" assert isinstance(5, NaturalNumber) assert isinstance(1, NaturalNumber) @@ -11,6 +12,7 @@ def test_basic_isinstance(): def test_basic_check(): + """check accepts positive integers as NaturalNumber and rejects zero, negatives, and the sampled string input through the public API.""" assert check(5, NaturalNumber) assert check(1, NaturalNumber) diff --git a/tests/units/types/ints/test_non_negative.py b/tests/units/types/ints/test_non_negative.py index 241cc43..45d5cfd 100644 --- a/tests/units/types/ints/test_non_negative.py +++ b/tests/units/types/ints/test_non_negative.py @@ -2,6 +2,7 @@ def test_basic_isinstance(): + """isinstance accepts positive integers and zero for NonNegativeInt, while rejecting negatives and non-int values.""" assert isinstance(5, NonNegativeInt) assert isinstance(1, NonNegativeInt) assert isinstance(0, NonNegativeInt) @@ -12,6 +13,7 @@ def test_basic_isinstance(): def test_basic_check(): + """check(..., NonNegativeInt) accepts non-negative ints and rejects negatives, strings, and floats, matching isinstance behavior.""" assert check(5, NonNegativeInt) assert check(1, NonNegativeInt) assert check(0, NonNegativeInt) From 2faeee559565d9da7fed62933b0fc54951adac27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=91=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 1 Jul 2026 16:07:34 +0300 Subject: [PATCH 3/3] Make some tests stronger --- tests/units/test_check.py | 43 +++++++++++++++++++++++++++++---- tests/units/test_from_string.py | 14 +++++------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tests/units/test_check.py b/tests/units/test_check.py index 5e5ad45..f500896 100644 --- a/tests/units/test_check.py +++ b/tests/units/test_check.py @@ -63,7 +63,7 @@ def test_built_in_types(): def test_any(): - """typing.Any matches all sampled values, including primitives, collections, None, and type objects.""" + """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) @@ -76,6 +76,10 @@ 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.") @@ -192,6 +196,23 @@ 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. @@ -342,12 +363,15 @@ 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.""" + """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): @@ -360,23 +384,29 @@ 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 key and value types.""" + """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 checks only the set origin and ignores element types.""" + """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(): @@ -415,9 +445,10 @@ 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 still pass.""" + """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): @@ -569,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) diff --git a/tests/units/test_from_string.py b/tests/units/test_from_string.py index 77754fb..4753488 100644 --- a/tests/units/test_from_string.py +++ b/tests/units/test_from_string.py @@ -124,17 +124,17 @@ def test_get_float_value(): def test_get_bool_value(): """ - Boolean deserialization accepts only the exact string tokens "yes", "True", "true", "False", "false", and "no". + Boolean deserialization returns True/False singletons only for the exact accepted string tokens. Empty, unknown, or near-match strings raise the boolean-specific TypeError. """ - assert from_string('yes', bool) == True - assert from_string('True', bool) == True - assert from_string('true', bool) == True + assert from_string('yes', bool) is True + assert from_string('True', bool) is True + assert from_string('true', bool) is True - assert from_string('False', bool) == False - assert from_string('false', bool) == False - assert from_string('no', bool) == False + assert from_string('False', bool) is False + assert from_string('false', bool) is False + assert from_string('no', bool) is False with pytest.raises(TypeError, match=match('The string "kek" cannot be interpreted as a boolean value.')): from_string('kek', bool)