diff --git a/README.md b/README.md index 9bfc360..f1d659d 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,15 @@ print(check(-11, NonNegativeInt)) #> False ``` +In addition to other types, simtypes supports an extended type of sentinels from the [`denial`](https://github.com/pomponchik/denial/) library. In short, this is an extended `None`, for cases when we need to distinguish between situations where a value is undefined and situations where it is defined as undefined. Similar to `None`, objects of the `InnerNoneType` class can be used as type hints for themselves: + +```python +from denial import InnerNoneType + +print(check(InnerNoneType('key'), InnerNoneType('key'))) +#> True +``` + ## String deserialization @@ -177,8 +186,9 @@ The library also provides primitive deserialization. Conversion of strings into - `int` - any integers. - `float` - any floating-point numbers, including infinities and [`NaN`](https://en.wikipedia.org/wiki/NaN). - `bool`- the strings `"yes"`, `"True"`, and `"true"` are interpreted as `True`, while `"no"`, `"False"`, or `"false"` are interpreted as `False`. +- `date` or `datetime` - strings representing, respectively, dates or dates + time in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. - `list` - lists in [`json`](https://en.wikipedia.org/wiki/JSON) format are expected. -- `tuple` - lists in [`json`](https://en.wikipedia.org/wiki/JSON) format are expected. This is the only type where the value produced does not match the passed type, the returned value is always a list. +- `tuple` - lists in [`json`](https://en.wikipedia.org/wiki/JSON) format are expected. - `dict` - dicts in [`json`](https://en.wikipedia.org/wiki/JSON) format are expected. Examples: @@ -222,13 +232,21 @@ print(from_string('no', bool)) print(from_string('True', bool)) #> True +# dates and datetimes +from datetime import datetime, date + +print(from_string('2026-01-27', date)) +#> 2026-01-27 +print(from_string('2026-01-27 01:47:29.982044', datetime)) +#> 2026-01-27 01:47:29.982044 + # collections print(from_string('[1, 2, 3]', list[int])) #> [1, 2, 3] print(from_string('[1, 2, 3]', tuple[int, ...])) -#> [1, 2, 3] +#> (1, 2, 3) print(from_string('{"123": [1, 2, 3]}', dict[str, tuple[int, ...]])) -#> {"123": [1, 2, 3]} +#> {'123': (1, 2, 3)} ``` > 👀 If the passed string cannot be interpreted as an object of the specified type, a `TypeError` exception will be raised. diff --git a/pyproject.toml b/pyproject.toml index f8cb12b..81aeca1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,15 @@ build-backend = "setuptools.build_meta" [project] name = "simtypes" -version = "0.0.9" +version = "0.0.10" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'Type checking in runtime without stupid games' readme = "README.md" requires-python = ">=3.8" -dependencies = ['typing_extensions ; python_version <= "3.12"'] +dependencies = [ + 'typing_extensions ; python_version <= "3.12"', + 'denial>=0.0.5', +] classifiers = [ "Operating System :: OS Independent", 'Operating System :: MacOS :: MacOS X', @@ -30,7 +33,7 @@ classifiers = [ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', ] -keywords = ['type check'] +keywords = ['type check', 'deserialization'] [tool.setuptools.package-data] "simtypes" = ["py.typed"] diff --git a/simtypes/check.py b/simtypes/check.py index fe50328..16f5929 100644 --- a/simtypes/check.py +++ b/simtypes/check.py @@ -13,6 +13,8 @@ from typing import List, Type, Union, Any, get_args, get_origin +from denial import InnerNoneType + from simtypes.typing import ExpectedType @@ -26,6 +28,9 @@ def check(value: Any, type_hint: Type[ExpectedType], strict: bool = False, lists elif type_hint is None: return value is None + elif isinstance(type_hint, InnerNoneType): + return type_hint == value + origin_type = get_origin(type_hint) if origin_type is Union or origin_type is UnionType: diff --git a/simtypes/from_string.py b/simtypes/from_string.py index dc86480..a015cf0 100644 --- a/simtypes/from_string.py +++ b/simtypes/from_string.py @@ -1,35 +1,15 @@ -from typing import Type, Any, get_origin +from typing import List, Tuple, Dict, Type, Optional, Union, Any, get_origin, get_args from json import loads, JSONDecodeError from inspect import isclass +from datetime import datetime, date +from collections.abc import Hashable from simtypes import check from simtypes.typing import ExpectedType -def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType: - if not isinstance(value, str): - raise ValueError(f'You can only pass a string as a string. You passed {type(value).__name__}.') - - if expected_type is Any: # type: ignore[comparison-overlap] - return value # type: ignore[return-value] - - origin_type = get_origin(expected_type) - - if any(x in (dict, list, tuple) for x in (expected_type, origin_type)): - type_name = expected_type.__name__ if origin_type is None else origin_type.__name__ - error_message = f'The string "{value}" cannot be interpreted as a {type_name} of the specified format.' - - try: - result: ExpectedType = loads(value) - except JSONDecodeError as e: - raise TypeError(error_message) from e - - if check(result, expected_type, strict=True, lists_are_tuples=True): # type: ignore[operator] - return result - else: - raise TypeError(error_message) - - elif expected_type is str: +def convert_single_value(value: str, expected_type: Type[ExpectedType]) -> ExpectedType: + if expected_type is str: return value # type: ignore[return-value] elif expected_type is bool: @@ -57,7 +37,191 @@ def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType: except ValueError as e: raise TypeError(f'The string "{value}" cannot be interpreted as a floating point number.') from e + if expected_type is datetime: + try: + return datetime.fromisoformat(value) # type: ignore[return-value] + except ValueError as e: + raise TypeError(f'The string "{value}" cannot be interpreted as a datetime object.') from e + + if expected_type is date: + try: + return date.fromisoformat(value) # type: ignore[return-value] + except ValueError as e: + raise TypeError(f'The string "{value}" cannot be interpreted as a date object.') from e + if not isclass(expected_type): raise ValueError('The type must be a valid type object.') raise TypeError(f'Serialization of the type {expected_type.__name__} you passed is not supported. Supported types: int, float, bool, list, dict, tuple.') + + +# TODO: try to abstract fix_lists(), fix_tuples() and fix_dicts() to one function +def fix_lists(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Optional[List[Any]]: + if not isinstance(collection, list) or len(type_hint_arguments) >= 2: + return None + + if not len(type_hint_arguments): + return collection + + type_hint = type_hint_arguments[0] + origin_type = get_origin(type_hint) + type_hint_arguments = get_args(type_hint) + + result = [] + for element in collection: + if any(x in (dict, list, tuple) for x in (type_hint, origin_type)): + fixed_element = fix_iterable_types(element, type_hint_arguments, origin_type, type_hint) + if fixed_element is None: + return None + result.append(fixed_element) + elif type_hint is date or type_hint is datetime: + if not isinstance(element, str): + return None + try: + result.append(convert_single_value(element, type_hint)) + except TypeError: + return None + else: + result.append(element) + + return result + + +def fix_tuples(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Optional[Tuple[Any, ...]]: + if not isinstance(collection, list): + return None + + if not len(type_hint_arguments): + return tuple(collection) + + result = [] + + if len(type_hint_arguments) == 2 and type_hint_arguments[1] is Ellipsis: + type_hint = type_hint_arguments[0] + origin_type = get_origin(type_hint) + type_hint_arguments = get_args(type_hint) + + for element in collection: + if any(x in (dict, list, tuple) for x in (type_hint, origin_type)): + fixed_element = fix_iterable_types(element, type_hint_arguments, origin_type, type_hint) + if fixed_element is None: + return None + result.append(fixed_element) + elif type_hint is date or type_hint is datetime: + if not isinstance(element, str): + return None + try: + result.append(convert_single_value(element, type_hint)) + except TypeError: + return None + else: + result.append(element) + + else: + if len(collection) != len(type_hint_arguments): + return None + + for type_hint, element in zip(type_hint_arguments, collection): + type_hint_arguments = get_args(type_hint) + origin_type = get_origin(type_hint) + if any(x in (dict, list, tuple) for x in (type_hint, origin_type)): + fixed_element = fix_iterable_types(element, type_hint_arguments, origin_type, type_hint) + if fixed_element is None: + return None + result.append(fixed_element) + elif type_hint is date or type_hint is datetime: + if not isinstance(element, str): + return None + try: + result.append(convert_single_value(element, type_hint)) + except TypeError: + return None + else: + result.append(element) + + return tuple(result) + + +def fix_dicts(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Optional[Dict[Hashable, Any]]: + if not isinstance(collection, dict) or len(type_hint_arguments) >= 3 or len(type_hint_arguments) == 1: + return None + + if not len(type_hint_arguments): + return collection + + key_type_hint = type_hint_arguments[0] + value_type_hint = type_hint_arguments[1] + + result = {} + for key, element in collection.items(): + pair = {'key': (key, key_type_hint), 'value': (element, value_type_hint)} + pair_result = {} + + for name, meta in pair.items(): + element, type_hint = meta + origin_type = get_origin(type_hint) + type_hint_arguments = get_args(type_hint) + if any(x in (dict, list, tuple) for x in (type_hint, origin_type)): + fixed_element = fix_iterable_types(element, type_hint_arguments, origin_type, type_hint) + if fixed_element is None: + return None + subresult = fixed_element + elif type_hint is date or type_hint is datetime: + if not isinstance(element, str): + return None + try: + subresult = convert_single_value(element, type_hint) + except TypeError: + return None + else: + subresult = element + pair_result[name] = subresult + + result[pair_result['key']] = pair_result['value'] + + return result + + +def fix_iterable_types(collection: Union[List[Any], Tuple[Any, ...], Dict[Hashable, Any]], type_hint_arguments: Tuple[Any, ...], origin_type: Any, expected_type: Any) -> Optional[Union[List[Any], Tuple[Any, ...], Dict[Hashable, Any]]]: + if list in (origin_type, expected_type): + result = fix_lists(collection, type_hint_arguments) # type: ignore[arg-type] + elif tuple in (origin_type, expected_type): + result = fix_tuples(collection, type_hint_arguments) # type: ignore[assignment, arg-type] + if result is not None: + result = tuple(result) # type: ignore[assignment] + elif dict in (origin_type, expected_type): + result = fix_dicts(collection, type_hint_arguments) # type: ignore[assignment, arg-type] + else: + return None # pragma: no cover + + return result + + +def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType: + if not isinstance(value, str): + raise ValueError(f'You can only pass a string as a string. You passed {type(value).__name__}.') + + if expected_type is Any: # type: ignore[comparison-overlap] + return value # type: ignore[return-value] + + origin_type = get_origin(expected_type) + + if any(x in (dict, list, tuple) for x in (expected_type, origin_type)): + type_name = expected_type.__name__ if origin_type is None else origin_type.__name__ + error = TypeError(f'The string "{value}" cannot be interpreted as a {type_name} of the specified format.') + + try: + result = loads(value) + except JSONDecodeError as e: + raise error from e + + result = fix_iterable_types(result, get_args(expected_type), origin_type, expected_type) + if result is None: + raise error + + if check(result, expected_type, strict=True): # type: ignore[operator] + return result # type: ignore[no-any-return] + else: + raise error + + return convert_single_value(value, expected_type) diff --git a/tests/units/test_check.py b/tests/units/test_check.py index 1640172..141bb5c 100644 --- a/tests/units/test_check.py +++ b/tests/units/test_check.py @@ -11,6 +11,7 @@ import pytest from full_match import match +from denial import InnerNone, InnerNoneType, SentinelType from simtypes import check @@ -492,3 +493,69 @@ def test_pass_mocks_when_its_off(strict_mode, list_type): assert check(Mock(), Mock, strict=strict_mode, pass_mocks=False) assert check(MagicMock(), MagicMock, strict=strict_mode, pass_mocks=False) + + +@pytest.mark.parametrize( + ['strict_mode'], + [ + (False,), + (True,), + ], +) +def test_denial_sentinel(strict_mode): + assert not check(123, SentinelType, strict=strict_mode) + assert not check('None', SentinelType, strict=strict_mode) + + assert check(None, SentinelType, strict=strict_mode) + assert check(InnerNone, SentinelType, strict=strict_mode) + assert check(InnerNoneType(), SentinelType, strict=strict_mode) + assert check(InnerNoneType(123), SentinelType, strict=strict_mode) + assert check(InnerNoneType('lol'), SentinelType, strict=strict_mode) + + +@pytest.mark.parametrize( + ['strict_mode'], + [ + (False,), + (True,), + ], +) +def test_denial_innernonetype(strict_mode): + assert not check(123, InnerNoneType, strict=strict_mode) + assert not check('None', InnerNoneType, strict=strict_mode) + assert not check(None, InnerNoneType, strict=strict_mode) + + assert check(InnerNone, InnerNoneType, strict=strict_mode) + assert check(InnerNoneType(), InnerNoneType, strict=strict_mode) + assert check(InnerNoneType(123), InnerNoneType, strict=strict_mode) + assert check(InnerNoneType('lol'), InnerNoneType, strict=strict_mode) + + +@pytest.mark.parametrize( + ['strict_mode'], + [ + (False,), + (True,), + ], +) +def test_denial_innernone(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) + + 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) + + assert not check(123, InnerNone, strict=strict_mode) + assert not check('None', InnerNone, strict=strict_mode) + assert not check(None, InnerNone, strict=strict_mode) + + assert not check(InnerNoneType(), InnerNoneType(), strict=strict_mode) + assert not check(InnerNoneType(), InnerNone, strict=strict_mode) + assert not check(InnerNoneType(123), InnerNoneType(1234), strict=strict_mode) + assert not check(InnerNoneType('lol'), InnerNoneType('lol-kek'), strict=strict_mode) + + assert check(InnerNone, InnerNone, strict=strict_mode) + assert check(InnerNoneType(123), InnerNoneType(123), strict=strict_mode) + assert check(InnerNoneType('lol'), InnerNoneType('lol'), strict=strict_mode) diff --git a/tests/units/test_from_string.py b/tests/units/test_from_string.py index 238b0c1..72d9544 100644 --- a/tests/units/test_from_string.py +++ b/tests/units/test_from_string.py @@ -1,5 +1,7 @@ from math import inf, isnan from typing import Any +from datetime import date, datetime +from json import dumps import pytest from full_match import match @@ -127,6 +129,7 @@ def test_get_bool_value(): def test_get_list_value(list_type, subscribable_dict_type, subscribable_list_type): assert from_string('[]', list_type) == [] + assert from_string('[1, 2, 3]', list_type) == [1, 2, 3] assert from_string('[]', subscribable_list_type[int]) == [] assert from_string('[]', subscribable_list_type[str]) == [] @@ -171,17 +174,21 @@ 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): - assert from_string('[]', tuple_type) == [] - assert from_string('[]', subscribable_tuple_type[int, ...]) == [] - assert from_string('[]', subscribable_tuple_type[str, ...]) == [] + assert from_string('[]', tuple_type) == () + assert from_string('[1, 2, 3]', tuple_type) == (1, 2, 3) + assert from_string('[]', subscribable_tuple_type[int, ...]) == () + assert from_string('[]', subscribable_tuple_type[str, ...]) == () - assert from_string('[1, 2, 3]', subscribable_tuple_type[int, ...]) == [1, 2, 3] - assert from_string('["lol", "kek"]', subscribable_tuple_type[str, ...]) == ["lol", "kek"] - assert from_string('[1, 2, 3]', subscribable_tuple_type[int, int, int]) == [1, 2, 3] - assert from_string('["lol", "kek"]', subscribable_tuple_type[str, str]) == ["lol", "kek"] + assert from_string('[1, 2, 3]', subscribable_tuple_type[int, ...]) == (1, 2, 3) + assert from_string('["lol", "kek"]', subscribable_tuple_type[str, ...]) == ("lol", "kek") + assert from_string('[1, 2, 3]', subscribable_tuple_type[int, int, int]) == (1, 2, 3) + assert from_string('["lol", "kek"]', subscribable_tuple_type[str, str]) == ("lol", "kek") - assert from_string('[["lol", "kek"], ["lol", "kek"]]', subscribable_tuple_type[subscribable_tuple_type[str, str], ...]) == [["lol", "kek"], ["lol", "kek"]] - assert from_string('[{"lol": "kek"}, {"lol": "kek"}]', subscribable_tuple_type[subscribable_dict_type[str, str], ...]) == [{'lol': 'kek'}, {'lol': 'kek'}] + assert from_string('[["lol", "kek"], ["lol", "kek"]]', subscribable_tuple_type[subscribable_tuple_type[str, str], subscribable_tuple_type[str, str]]) == (("lol", "kek"), ("lol", "kek")) + assert from_string('[{"lol": "kek"}, {"lol": "kek"}]', subscribable_tuple_type[subscribable_dict_type[str, str], subscribable_dict_type[str, str]]) == ({'lol': 'kek'}, {'lol': 'kek'}) + + assert from_string('[["lol", "kek"], ["lol", "kek"]]', subscribable_tuple_type[subscribable_tuple_type[str, str], ...]) == (("lol", "kek"), ("lol", "kek")) + assert from_string('[{"lol": "kek"}, {"lol": "kek"}]', subscribable_tuple_type[subscribable_dict_type[str, str], ...]) == ({'lol': 'kek'}, {'lol': 'kek'}) with pytest.raises(TypeError, match=match('The string "[]" cannot be interpreted as a tuple of the specified format.')): from_string('[]', subscribable_tuple_type[int]) @@ -225,6 +232,7 @@ 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): assert from_string('{}', dict_type) == {} + assert from_string('{"lol": "kek"}', dict_type) == {'lol': 'kek'} assert from_string('{}', subscribable_dict_type[int, int]) == {} assert from_string('{}', subscribable_dict_type[str, str]) == {} assert from_string('{}', subscribable_dict_type[int, str]) == {} @@ -300,3 +308,197 @@ def test_get_dict_value(dict_type, subscribable_list_type, subscribable_dict_typ ) def test_get_any(string): assert from_string(string, Any) == string + + +def test_deserialize_date(): + isoformatted_date = date(2026, 1, 22).isoformat() + + assert from_string(isoformatted_date, date) == date.fromisoformat(isoformatted_date) + + with pytest.raises(TypeError, match=match('The string "kek" cannot be interpreted as a date object.')): + from_string('kek', date) + + +def test_deserialize_datetetime(): + isoformatted_datetime = datetime.now().isoformat() + + assert from_string(isoformatted_datetime, datetime) == datetime.fromisoformat(isoformatted_datetime) + + with pytest.raises(TypeError, match=match('The string "kek" cannot be interpreted as a datetime object.')): + from_string('kek', datetime) + + +def test_deserialize_subscribable_collections_with_datetimes(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type): + isoformatted_datetime = datetime.now().isoformat() + + assert from_string(dumps([isoformatted_datetime]), subscribable_list_type[datetime]) == [datetime.fromisoformat(isoformatted_datetime)] + assert from_string(dumps([isoformatted_datetime]), subscribable_tuple_type[datetime]) == (datetime.fromisoformat(isoformatted_datetime),) + assert from_string(dumps([isoformatted_datetime]), subscribable_tuple_type[datetime, ...]) == (datetime.fromisoformat(isoformatted_datetime),) + assert from_string(dumps({isoformatted_datetime: isoformatted_datetime}), subscribable_dict_type[datetime, datetime]) == {datetime.fromisoformat(isoformatted_datetime): datetime.fromisoformat(isoformatted_datetime)} + assert from_string(dumps({isoformatted_datetime: isoformatted_datetime}), subscribable_dict_type[datetime, str]) == {datetime.fromisoformat(isoformatted_datetime): isoformatted_datetime} + assert from_string(dumps({isoformatted_datetime: isoformatted_datetime}), subscribable_dict_type[str, datetime]) == {isoformatted_datetime: datetime.fromisoformat(isoformatted_datetime)} + + +def test_deserialize_subscribable_collections_with_dates(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type): + isoformatted_date = date(2026, 1, 22).isoformat() + + assert from_string(dumps([isoformatted_date]), subscribable_list_type[date]) == [date.fromisoformat(isoformatted_date)] + assert from_string(dumps([isoformatted_date]), subscribable_tuple_type[date]) == (date.fromisoformat(isoformatted_date),) + assert from_string(dumps([isoformatted_date]), subscribable_tuple_type[date, ...]) == (date.fromisoformat(isoformatted_date),) + assert from_string(dumps({isoformatted_date: isoformatted_date}), subscribable_dict_type[date, date]) == {date.fromisoformat(isoformatted_date): date.fromisoformat(isoformatted_date)} + assert from_string(dumps({isoformatted_date: isoformatted_date}), subscribable_dict_type[date, str]) == {date.fromisoformat(isoformatted_date): isoformatted_date} + assert from_string(dumps({isoformatted_date: isoformatted_date}), subscribable_dict_type[str, date]) == {isoformatted_date: date.fromisoformat(isoformatted_date)} + + +def test_wrong_collection_content(subscribable_list_type, subscribable_tuple_type, subscribable_dict_type, dict_type, list_type, tuple_type): + 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]) + + 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[datetime]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a list of the specified format.')): + from_string(dumps([None]), subscribable_list_type[datetime]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a list of the specified format.')): + from_string(dumps([None]), subscribable_list_type[date]) + + 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]) + + 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[datetime]) + + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([123]), subscribable_tuple_type[date]) + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([123]), subscribable_tuple_type[datetime]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([None]), subscribable_tuple_type[datetime]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([None]), subscribable_tuple_type[date]) + + with pytest.raises(TypeError, match=match('The string "["123"]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps(['123']), subscribable_tuple_type[datetime]) + + with pytest.raises(TypeError, match=match('The string "["123"]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps(['123']), subscribable_tuple_type[date]) + + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([123]), subscribable_tuple_type[date, ...]) + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([123]), subscribable_tuple_type[datetime, ...]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([None]), subscribable_tuple_type[datetime, ...]) + + with pytest.raises(TypeError, match=match('The string "[null]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([None]), subscribable_tuple_type[date, ...]) + + with pytest.raises(TypeError, match=match('The string "["123"]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps(['123']), subscribable_tuple_type[datetime, ...]) + + with pytest.raises(TypeError, match=match('The string "["123"]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps(['123']), subscribable_tuple_type[date, ...]) + + + with pytest.raises(TypeError, match=match('The string "{"kek": 123}" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps({'kek': 123}), subscribable_tuple_type[date]) + + with pytest.raises(TypeError, match=match('The string "{"kek": 123}" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps({'kek': 123}), subscribable_tuple_type[date, ...]) + + with pytest.raises(TypeError, match=match('The string "{"kek": 123}" cannot be interpreted as a list of the specified format.')): + from_string(dumps({'kek': 123}), subscribable_list_type[date]) + + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a list of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_list_type[subscribable_dict_type[str, int]]) + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a list of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_list_type[subscribable_list_type[int]]) + + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_tuple_type[subscribable_dict_type[str, int]]) + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_tuple_type[subscribable_list_type[int]]) + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_tuple_type[subscribable_list_type[int], ...]) + + with pytest.raises(TypeError, match=match('The string "[{"kek": "lol"}]" cannot be interpreted as a tuple of the specified format.')): + from_string(dumps([{'kek': 'lol'}]), subscribable_tuple_type[subscribable_dict_type[str, int], ...]) + + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a dict of the specified format.')): + from_string(dumps([123]), subscribable_dict_type[int, int]) + + with pytest.raises(TypeError, match=match('The string "["123"]" cannot be interpreted as a dict of the specified format.')): + from_string(dumps(['123']), subscribable_dict_type[int, int]) + + with pytest.raises(TypeError, match=match('The string "{"123": "123"}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({123: '123'}), subscribable_dict_type[int, int]) + + with pytest.raises(TypeError, match=match('The string "{"123": 123}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': 123}), subscribable_dict_type[int, int]) + + with pytest.raises(TypeError, match=match('The string "{"123": {"123": "123"}}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': {123: '123'}}), subscribable_dict_type[str, subscribable_dict_type[int, int]]) + + with pytest.raises(TypeError, match=match('The string "{"123": [123, 123]}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': [123, 123]}), subscribable_dict_type[str, subscribable_dict_type[int, int]]) + + with pytest.raises(TypeError, match=match('The string "{"123": {"123": "123"}}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': {123: '123'}}), subscribable_dict_type[str, subscribable_list_type[int]]) + + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a dict of the specified format.')): + from_string(dumps([123]), subscribable_dict_type[int, date]) + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a dict of the specified format.')): + from_string(dumps([123]), subscribable_dict_type[int, datetime]) + + with pytest.raises(TypeError, match=match('The string "{"123": "123"}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({123: '123'}), subscribable_dict_type[int, datetime]) + + with pytest.raises(TypeError, match=match('The string "{"123": "123"}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({123: '123'}), subscribable_dict_type[datetime, str]) + + with pytest.raises(TypeError, match=match('The string "{"123": "123"}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({123: '123'}), subscribable_dict_type[int, date]) + + with pytest.raises(TypeError, match=match('The string "{"123": "123"}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({123: '123'}), subscribable_dict_type[date, str]) + + with pytest.raises(TypeError, match=match('The string "{"123": null}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': None}), subscribable_dict_type[int, datetime]) + + with pytest.raises(TypeError, match=match('The string "{"123": null}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': None}), subscribable_dict_type[int, date]) + + with pytest.raises(TypeError, match=match('The string "{"123": 123}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': 123}), subscribable_dict_type[int, datetime]) + + with pytest.raises(TypeError, match=match('The string "{"123": 123}" cannot be interpreted as a dict of the specified format.')): + from_string(dumps({'123': 123}), subscribable_dict_type[int, date]) + + + with pytest.raises(TypeError, match=match('The string "123" cannot be interpreted as a dict of the specified format.')): + from_string('123', dict_type) + + with pytest.raises(TypeError, match=match('The string "[123]" cannot be interpreted as a dict of the specified format.')): + from_string('[123]', dict_type) + + with pytest.raises(TypeError, match=match('The string "123" cannot be interpreted as a list of the specified format.')): + from_string('123', list_type) + + with pytest.raises(TypeError, match=match('The string "123" cannot be interpreted as a tuple of the specified format.')): + from_string('123', tuple_type)