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 .github/ISSUE_TEMPLATE/question.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: Question or consultation
about: Ask anything about this project
title: ''
labels: guestion
labels: question
assignees: pomponchik

---
Expand Down
4 changes: 0 additions & 4 deletions .ruff.toml

This file was deleted.

1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Python type checking tools are usually very complex. In this case, we have throw
- [**String deserialization**](#string-deserialization)



## Why?

It's been a long time since static type checking tools like [`mypy`](https://github.com/python/mypy) for `Python` have been available, and they've become very complex. The typing system has also become noticeably more complicated, providing us with more and more new types of annotations, new syntax and other tools. It seems that `Python` devs procrastinate endlessly, postponing all the really important [`CPyhton`](https://github.com/python/cpython) improvements in order to add more garbage to [`typing`](https://docs.python.org/3/library/typing.html).
Expand Down
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.11"
version = "0.0.12"
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
description = 'Type checking in runtime without stupid games'
readme = "README.md"
Expand Down
10 changes: 6 additions & 4 deletions simtypes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from simtypes.check import check as check # noqa: F401
from simtypes.from_string import from_string as from_string # noqa: F401
from simtypes.types.ints.natural import NaturalNumber as NaturalNumber # noqa: F401
from simtypes.types.ints.non_negative import NonNegativeInt as NonNegativeInt # noqa: F401
from simtypes.check import check as check
from simtypes.from_string import from_string as from_string
from simtypes.types.ints.natural import NaturalNumber as NaturalNumber
from simtypes.types.ints.non_negative import (
NonNegativeInt as NonNegativeInt,
)
36 changes: 16 additions & 20 deletions simtypes/check.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from inspect import isclass
from unittest.mock import Mock, MagicMock
from unittest.mock import MagicMock, Mock

try:
from types import UnionType # type: ignore[attr-defined, unused-ignore]
Expand All @@ -11,48 +11,45 @@
except ImportError: # pragma: no cover
from typing_extensions import TypeIs

from typing import List, Type, Union, Any, get_args, get_origin
from typing import Any, List, Type, Union, get_args, get_origin

from denial import InnerNoneType

from simtypes.typing import ExpectedType


def check(value: Any, type_hint: Type[ExpectedType], strict: bool = False, lists_are_tuples: bool = False, pass_mocks: bool = True) -> TypeIs[ExpectedType]:
if type_hint is Any: # type: ignore[comparison-overlap]
def check(value: Any, type_hint: Type[ExpectedType], strict: bool = False, lists_are_tuples: bool = False, pass_mocks: bool = True) -> TypeIs[ExpectedType]: # noqa: C901, PLR0911, PLR0912
if type_hint is Any or (isinstance(value, (Mock, MagicMock)) and pass_mocks): # type: ignore[comparison-overlap]
return True

elif (isinstance(value, Mock) or isinstance(value, MagicMock)) and pass_mocks:
return True

elif type_hint is None:
if type_hint is None:
return value is None

elif isinstance(type_hint, InnerNoneType):
if isinstance(type_hint, InnerNoneType):
return type_hint == value

origin_type = get_origin(type_hint)

if origin_type is Union or origin_type is UnionType:
return any(check(value, argument, strict=strict, lists_are_tuples=lists_are_tuples) for argument in get_args(type_hint))

elif origin_type is list and strict:
if origin_type is list and strict:
if not isinstance(value, list):
return False
arguments = get_args(type_hint)
if not arguments:
return True
return all(check(subvalue, arguments[0], strict=strict, lists_are_tuples=lists_are_tuples) for subvalue in value)

elif origin_type is dict and strict:
if origin_type is dict and strict:
if not isinstance(value, dict):
return False
arguments = get_args(type_hint)
if not arguments:
return True
return all(check(key, arguments[0], strict=strict, lists_are_tuples=lists_are_tuples) and check(subvalue, arguments[1], strict=strict, lists_are_tuples=lists_are_tuples) for key, subvalue in value.items())

elif origin_type is tuple and strict:
if origin_type is tuple and strict:
types_to_check: List[Union[Type[list], Type[tuple]]] = [tuple] if not lists_are_tuples else [tuple, list] # type: ignore[type-arg]
if all(not isinstance(value, x) for x in types_to_check):
return False
Expand All @@ -70,14 +67,13 @@ def check(value: Any, type_hint: Type[ExpectedType], strict: bool = False, lists

return all(check(subvalue, expected_subtype, strict=strict, lists_are_tuples=lists_are_tuples) for subvalue, expected_subtype in zip(value, arguments))

else:
if origin_type is not None:
return isinstance(value, origin_type)
if origin_type is not None:
return isinstance(value, origin_type)

if not isclass(type_hint):
raise ValueError('Type must be a valid type object.')
if not isclass(type_hint):
raise ValueError('Type must be a valid type object.')

if type_hint is tuple and lists_are_tuples:
return isinstance(value, tuple) or isinstance(value, list) # pragma: no cover
if type_hint is tuple and lists_are_tuples:
return isinstance(value, (tuple, list)) # pragma: no cover

return isinstance(value, type_hint)
return isinstance(value, type_hint)
28 changes: 13 additions & 15 deletions simtypes/from_string.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
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 datetime import date, datetime
from inspect import isclass
from json import JSONDecodeError, loads
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin

from simtypes import check
from simtypes.typing import ExpectedType


def convert_single_value(value: str, expected_type: Type[ExpectedType]) -> ExpectedType:
def convert_single_value(value: str, expected_type: Type[ExpectedType]) -> ExpectedType: # noqa: PLR0912, PLR0911, C901
if expected_type is str:
return value # type: ignore[return-value]

elif expected_type is bool:
if expected_type is bool:
if value in ('True', 'true', 'yes'):
return True # type: ignore[return-value]
elif value in ('False', 'false', 'no'):
if value in ('False', 'false', 'no'):
return False # type: ignore[return-value]
else:
raise TypeError(f'The string "{value}" cannot be interpreted as a boolean value.')
raise TypeError(f'The string "{value}" cannot be interpreted as a boolean value.')

elif expected_type is int:
if expected_type is int:
try:
return int(value) # type: ignore[return-value]
except ValueError as e:
raise TypeError(f'The string "{value}" cannot be interpreted as an integer.') from e

elif expected_type is float:
if value == '∞' or value == '+∞':
if value in {'∞', '+∞'}:
value = 'inf'
elif value == '-∞':
value = '-inf'
Expand Down Expand Up @@ -87,7 +86,7 @@ def fix_lists(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Op
return result


def fix_tuples(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Optional[Tuple[Any, ...]]:
def fix_tuples(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Optional[Tuple[Any, ...]]: # noqa: PLR0912, PLR0911, C901
if not isinstance(collection, list):
return None

Expand Down Expand Up @@ -158,7 +157,7 @@ def fix_dicts(collection: List[Any], type_hint_arguments: Tuple[Any, ...]) -> Op
pair_result = {}

for name, meta in pair.items():
element, type_hint = meta
element, type_hint = meta # noqa: PLW2901
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)):
Expand Down Expand Up @@ -221,7 +220,6 @@ def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType:

if check(result, expected_type, strict=True): # type: ignore[operator]
return result # type: ignore[no-any-return]
else:
raise error
raise error

return convert_single_value(value, expected_type)
Empty file removed simtypes/types/strings/email.py
Empty file.
1 change: 0 additions & 1 deletion simtypes/typing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import TypeVar


ExpectedType = TypeVar('ExpectedType')
7 changes: 1 addition & 6 deletions tests/units/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import sys
from typing import Tuple, List, Set, Dict, Union, Optional
from typing import Dict, List, Optional, Set, Tuple, Union

import pytest


@pytest.fixture(params=[1, 2])
def new_style(request):
return request.param


@pytest.fixture(params=[Dict, dict])
def dict_type(request):
return request.param
Expand Down
76 changes: 38 additions & 38 deletions tests/units/test_check.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import sys
from unittest.mock import Mock, MagicMock
from unittest.mock import MagicMock, Mock

try:
from types import NoneType # type: ignore[attr-defined]
except ImportError:
NoneType = type(None) # type: ignore[misc]

from typing import Optional, Any, Union
from collections.abc import Sequence
from typing import Any, Optional, Union

import pytest
from full_match import match
from denial import InnerNone, InnerNoneType, SentinelType
from full_match import match

from simtypes import check

Expand Down Expand Up @@ -132,7 +132,7 @@ def test_bool_is_int(make_optional, make_union):
assert check(False, make_optional(make_union(int, str)))


def test_optional(new_style, tuple_type, list_type, make_optional):
def test_optional(tuple_type, list_type, make_optional):
assert check(None, make_optional(int))
assert check(1, make_optional(int))
assert check(0, make_optional(int))
Expand Down Expand Up @@ -185,11 +185,11 @@ def test_optional_union(make_union, make_optional, tuple_type):


@pytest.mark.parametrize(
['addictional_parameters'],
'addictional_parameters',
[
({},),
({'strict': True},),
({'strict': False},),
{},
{'strict': True},
{'strict': False},
],
)
def test_list_without_arguments(list_type, addictional_parameters):
Expand All @@ -210,11 +210,11 @@ def test_list_without_arguments(list_type, addictional_parameters):


@pytest.mark.parametrize(
['addictional_parameters'],
'addictional_parameters',
[
({},),
({'strict': True},),
({'strict': False},),
{},
{'strict': True},
{'strict': False},
],
)
def test_tuple_without_arguments(tuple_type, addictional_parameters):
Expand All @@ -240,11 +240,11 @@ def test_tuple_without_arguments(tuple_type, addictional_parameters):


@pytest.mark.parametrize(
['addictional_parameters'],
'addictional_parameters',
[
({},),
({'strict': True},),
({'strict': False},),
{},
{'strict': True},
{'strict': False},
],
)
def test_set_without_arguments(set_type, addictional_parameters):
Expand All @@ -265,11 +265,11 @@ def test_set_without_arguments(set_type, addictional_parameters):


@pytest.mark.parametrize(
['addictional_parameters'],
'addictional_parameters',
[
({},),
({'strict': True},),
({'strict': False},),
{},
{'strict': True},
{'strict': False},
],
)
def test_dict_without_arguments(dict_type, addictional_parameters):
Expand Down Expand Up @@ -449,17 +449,17 @@ def test_lists_are_tuples_flag_is_true_in_strict_mode(subscribable_tuple_type, s


@pytest.mark.parametrize(
['strict_mode'],
'strict_mode',
[
(False,),
(True,),
False,
True,
],
)
@pytest.mark.parametrize(
['addictional_parameters'],
'addictional_parameters',
[
({'pass_mocks': True},),
({},),
{'pass_mocks': True},
{},
],
)
def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):
Expand All @@ -476,10 +476,10 @@ def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):


@pytest.mark.parametrize(
['strict_mode'],
'strict_mode',
[
(False,),
(True,),
False,
True,
],
)
def test_pass_mocks_when_its_off(strict_mode, list_type):
Expand All @@ -496,10 +496,10 @@ def test_pass_mocks_when_its_off(strict_mode, list_type):


@pytest.mark.parametrize(
['strict_mode'],
'strict_mode',
[
(False,),
(True,),
False,
True,
],
)
def test_denial_sentinel(strict_mode):
Expand All @@ -514,10 +514,10 @@ def test_denial_sentinel(strict_mode):


@pytest.mark.parametrize(
['strict_mode'],
'strict_mode',
[
(False,),
(True,),
False,
True,
],
)
def test_denial_innernonetype(strict_mode):
Expand All @@ -532,10 +532,10 @@ def test_denial_innernonetype(strict_mode):


@pytest.mark.parametrize(
['strict_mode'],
'strict_mode',
[
(False,),
(True,),
False,
True,
],
)
def test_denial_innernone(strict_mode):
Expand Down
Loading
Loading