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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
python-version:
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Run mypy
shell: bash
run: mypy simtypes
run: mypy --strict simtypes

- name: Run mypy for tests
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests_and_coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
python-version:
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]

steps:
- uses: actions/checkout@v4
Expand Down
9 changes: 8 additions & 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.7"
version = "0.0.8"
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
description = 'Type checking in runtime without stupid games'
readme = "README.md"
Expand All @@ -25,6 +25,8 @@ classifiers = [
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: Free Threading',
'Programming Language :: Python :: Free Threading :: 3 - Stable',
'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
Expand All @@ -38,6 +40,11 @@ keywords = ['type check']
paths_to_mutate = "simtypes"
runner = "pytest"

[tool.ruff]
lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503']
lint.select = ["ERA001", "YTT", "ASYNC", "BLE", "B", "A", "COM", "INP", "PIE", "T20", "PT", "RSE", "RET", "SIM", "SLOT", "TID252", "ARG", "PTH", "I", "C90", "N", "E", "W", "D201", "D202", "D419", "F", "PL", "PLE", "PLR", "PLW", "RUF", "TRY201", "TRY400", "TRY401"]
format.quote-style = "single"

[tool.pytest.ini_options]
markers = ["mypy_testing"]

Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ build==1.2.2.post1
twine==6.1.0
mypy==1.14.1
pytest-mypy-testing==0.1.3
ruff==0.9.9
ruff==0.14.6
mutmut==3.2.3
full_match==0.0.3
34 changes: 19 additions & 15 deletions simtypes/check.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from inspect import isclass
from unittest.mock import Mock, MagicMock

try:
from types import UnionType # type: ignore[attr-defined]
from types import UnionType # type: ignore[attr-defined, unused-ignore]
except ImportError: # pragma: no cover
from typing import Union as UnionType # type: ignore[assignment]
from typing import Union as UnionType # type: ignore[assignment, unused-ignore]

try:
from typing import TypeIs # type: ignore[attr-defined]
from typing import TypeIs # type: ignore[attr-defined, unused-ignore]
except ImportError: # pragma: no cover
from typing_extensions import TypeIs

Expand All @@ -15,40 +16,43 @@
from simtypes.typing import ExpectedType


def check(value: Any, type: Type[ExpectedType], strict: bool = False, lists_are_tuples: bool = False) -> TypeIs[ExpectedType]:
if type is Any: # type: ignore[attr-defined]
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]
return True

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

elif type_hint is None:
return value is None

origin_type = get_origin(type)
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))
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 not isinstance(value, list):
return False
arguments = get_args(type)
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 not isinstance(value, dict):
return False
arguments = get_args(type)
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:
types_to_check: List[Union[Type[list], Type[tuple]]] = [tuple] if not lists_are_tuples else [tuple, list]
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

arguments = get_args(type)
arguments = get_args(type_hint)

if not arguments:
return True
Expand All @@ -65,10 +69,10 @@ def check(value: Any, type: Type[ExpectedType], strict: bool = False, lists_are_
if origin_type is not None:
return isinstance(value, origin_type)

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

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

return isinstance(value, type)
return isinstance(value, type_hint)
10 changes: 5 additions & 5 deletions simtypes/from_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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:
if expected_type is Any: # type: ignore[comparison-overlap]
return value # type: ignore[return-value]

origin_type = get_origin(expected_type)
Expand All @@ -20,15 +20,15 @@ def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType:
error_message = f'The string "{value}" cannot be interpreted as a {type_name} of the specified format.'

try:
result = loads(value)
result: ExpectedType = loads(value)
except JSONDecodeError as e:
raise TypeError(error_message) from e

if not check(result, expected_type, strict=True, lists_are_tuples=True): # type: ignore[operator]
if check(result, expected_type, strict=True, lists_are_tuples=True): # type: ignore[operator]
return result
else:
raise TypeError(error_message)

return result

elif expected_type is str:
return value # type: ignore[return-value]

Expand Down
5 changes: 4 additions & 1 deletion simtypes/types/ints/natural.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Any


class NaturalNumberMeta(type):
def __instancecheck__(cls, instance):
def __instancecheck__(cls, instance: Any) -> bool:
return isinstance(instance, int) and instance > 0

class NaturalNumber(metaclass=NaturalNumberMeta):
Expand Down
5 changes: 4 additions & 1 deletion simtypes/types/ints/non_negative.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Any


class NonNegativeIntMeta(type):
def __instancecheck__(cls, instance):
def __instancecheck__(cls, instance: Any) -> bool:
return isinstance(instance, int) and instance >= 0

class NonNegativeInt(metaclass=NonNegativeIntMeta):
Expand Down
48 changes: 48 additions & 0 deletions tests/units/test_check.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from unittest.mock import Mock, MagicMock

try:
from types import NoneType # type: ignore[attr-defined]
Expand Down Expand Up @@ -444,3 +445,50 @@ def test_lists_are_tuples_flag_is_true_in_strict_mode(subscribable_tuple_type, s
assert check([[1, 2, 3], [4, 5, 6]], subscribable_tuple_type[subscribable_tuple_type[int, ...], ...], strict=True, lists_are_tuples=True)
assert check(([1, 2, 3], [4, 5, 6]), subscribable_tuple_type[make_union(subscribable_tuple_type[str, ...], subscribable_tuple_type[int, ...]), ...], strict=True, lists_are_tuples=True)
assert check({1: [1, 2, 3], 2: [4, 5, 6]}, subscribable_dict_type[int, make_union(subscribable_tuple_type[str, ...], subscribable_tuple_type[int, ...])], strict=True, lists_are_tuples=True)


@pytest.mark.parametrize(
['strict_mode'],
[
(False,),
(True,),
],
)
@pytest.mark.parametrize(
['addictional_parameters'],
[
({'pass_mocks': True},),
({},),
],
)
def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):
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)

assert check(MagicMock(), int, strict=strict_mode, **addictional_parameters)
assert check(MagicMock(), str, strict=strict_mode, **addictional_parameters)
assert check(MagicMock(), list_type, strict=strict_mode, **addictional_parameters)

assert check(Mock(), Mock, strict=strict_mode, **addictional_parameters)
assert check(MagicMock(), MagicMock, strict=strict_mode, **addictional_parameters)


@pytest.mark.parametrize(
['strict_mode'],
[
(False,),
(True,),
],
)
def test_pass_mocks_when_its_off(strict_mode, list_type):
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)

assert not check(MagicMock(), int, strict=strict_mode, pass_mocks=False)
assert not check(MagicMock(), str, strict=strict_mode, pass_mocks=False)
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(), MagicMock, strict=strict_mode, pass_mocks=False)
Loading