Skip to content

Commit ee6d3b2

Browse files
authored
Merge pull request #8 from pomponchik/develop
0.0.8
2 parents 09bf757 + 8c95efa commit ee6d3b2

9 files changed

Lines changed: 92 additions & 27 deletions

File tree

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
matrix:
1010
python-version:
11-
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
11+
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
1212

1313
steps:
1414
- uses: actions/checkout@v4
@@ -44,7 +44,7 @@ jobs:
4444

4545
- name: Run mypy
4646
shell: bash
47-
run: mypy simtypes
47+
run: mypy --strict simtypes
4848

4949
- name: Run mypy for tests
5050
shell: bash

.github/workflows/tests_and_coverage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
matrix:
1010
os: [macos-latest, ubuntu-latest, windows-latest]
1111
python-version:
12-
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
12+
["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
1313

1414
steps:
1515
- uses: actions/checkout@v4

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "simtypes"
7-
version = "0.0.7"
7+
version = "0.0.8"
88
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
99
description = 'Type checking in runtime without stupid games'
1010
readme = "README.md"
@@ -25,6 +25,8 @@ classifiers = [
2525
'Programming Language :: Python :: 3.12',
2626
'Programming Language :: Python :: 3.13',
2727
'Programming Language :: Python :: 3.14',
28+
'Programming Language :: Python :: Free Threading',
29+
'Programming Language :: Python :: Free Threading :: 3 - Stable',
2830
'License :: OSI Approved :: MIT License',
2931
'Intended Audience :: Developers',
3032
'Topic :: Software Development :: Libraries',
@@ -38,6 +40,11 @@ keywords = ['type check']
3840
paths_to_mutate = "simtypes"
3941
runner = "pytest"
4042

43+
[tool.ruff]
44+
lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503']
45+
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"]
46+
format.quote-style = "single"
47+
4148
[tool.pytest.ini_options]
4249
markers = ["mypy_testing"]
4350

requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ build==1.2.2.post1
44
twine==6.1.0
55
mypy==1.14.1
66
pytest-mypy-testing==0.1.3
7-
ruff==0.9.9
7+
ruff==0.14.6
88
mutmut==3.2.3
99
full_match==0.0.3

simtypes/check.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from inspect import isclass
2+
from unittest.mock import Mock, MagicMock
23

34
try:
4-
from types import UnionType # type: ignore[attr-defined]
5+
from types import UnionType # type: ignore[attr-defined, unused-ignore]
56
except ImportError: # pragma: no cover
6-
from typing import Union as UnionType # type: ignore[assignment]
7+
from typing import Union as UnionType # type: ignore[assignment, unused-ignore]
78

89
try:
9-
from typing import TypeIs # type: ignore[attr-defined]
10+
from typing import TypeIs # type: ignore[attr-defined, unused-ignore]
1011
except ImportError: # pragma: no cover
1112
from typing_extensions import TypeIs
1213

@@ -15,40 +16,43 @@
1516
from simtypes.typing import ExpectedType
1617

1718

18-
def check(value: Any, type: Type[ExpectedType], strict: bool = False, lists_are_tuples: bool = False) -> TypeIs[ExpectedType]:
19-
if type is Any: # type: ignore[attr-defined]
19+
def check(value: Any, type_hint: Type[ExpectedType], strict: bool = False, lists_are_tuples: bool = False, pass_mocks: bool = True) -> TypeIs[ExpectedType]:
20+
if type_hint is Any: # type: ignore[comparison-overlap]
2021
return True
2122

22-
elif type is None:
23+
elif (isinstance(value, Mock) or isinstance(value, MagicMock)) and pass_mocks:
24+
return True
25+
26+
elif type_hint is None:
2327
return value is None
2428

25-
origin_type = get_origin(type)
29+
origin_type = get_origin(type_hint)
2630

2731
if origin_type is Union or origin_type is UnionType:
28-
return any(check(value, argument, strict=strict, lists_are_tuples=lists_are_tuples) for argument in get_args(type))
32+
return any(check(value, argument, strict=strict, lists_are_tuples=lists_are_tuples) for argument in get_args(type_hint))
2933

3034
elif origin_type is list and strict:
3135
if not isinstance(value, list):
3236
return False
33-
arguments = get_args(type)
37+
arguments = get_args(type_hint)
3438
if not arguments:
3539
return True
3640
return all(check(subvalue, arguments[0], strict=strict, lists_are_tuples=lists_are_tuples) for subvalue in value)
3741

3842
elif origin_type is dict and strict:
3943
if not isinstance(value, dict):
4044
return False
41-
arguments = get_args(type)
45+
arguments = get_args(type_hint)
4246
if not arguments:
4347
return True
4448
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())
4549

4650
elif origin_type is tuple and strict:
47-
types_to_check: List[Union[Type[list], Type[tuple]]] = [tuple] if not lists_are_tuples else [tuple, list]
51+
types_to_check: List[Union[Type[list], Type[tuple]]] = [tuple] if not lists_are_tuples else [tuple, list] # type: ignore[type-arg]
4852
if all(not isinstance(value, x) for x in types_to_check):
4953
return False
5054

51-
arguments = get_args(type)
55+
arguments = get_args(type_hint)
5256

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

68-
if not isclass(type):
72+
if not isclass(type_hint):
6973
raise ValueError('Type must be a valid type object.')
7074

71-
if type is tuple and lists_are_tuples:
75+
if type_hint is tuple and lists_are_tuples:
7276
return isinstance(value, tuple) or isinstance(value, list) # pragma: no cover
7377

74-
return isinstance(value, type)
78+
return isinstance(value, type_hint)

simtypes/from_string.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def from_string(value: str, expected_type: Type[ExpectedType]) -> ExpectedType:
1010
if not isinstance(value, str):
1111
raise ValueError(f'You can only pass a string as a string. You passed {type(value).__name__}.')
1212

13-
if expected_type is Any:
13+
if expected_type is Any: # type: ignore[comparison-overlap]
1414
return value # type: ignore[return-value]
1515

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

2222
try:
23-
result = loads(value)
23+
result: ExpectedType = loads(value)
2424
except JSONDecodeError as e:
2525
raise TypeError(error_message) from e
2626

27-
if not check(result, expected_type, strict=True, lists_are_tuples=True): # type: ignore[operator]
27+
if check(result, expected_type, strict=True, lists_are_tuples=True): # type: ignore[operator]
28+
return result
29+
else:
2830
raise TypeError(error_message)
2931

30-
return result
31-
3232
elif expected_type is str:
3333
return value # type: ignore[return-value]
3434

simtypes/types/ints/natural.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Any
2+
3+
14
class NaturalNumberMeta(type):
2-
def __instancecheck__(cls, instance):
5+
def __instancecheck__(cls, instance: Any) -> bool:
36
return isinstance(instance, int) and instance > 0
47

58
class NaturalNumber(metaclass=NaturalNumberMeta):

simtypes/types/ints/non_negative.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Any
2+
3+
14
class NonNegativeIntMeta(type):
2-
def __instancecheck__(cls, instance):
5+
def __instancecheck__(cls, instance: Any) -> bool:
36
return isinstance(instance, int) and instance >= 0
47

58
class NonNegativeInt(metaclass=NonNegativeIntMeta):

tests/units/test_check.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from unittest.mock import Mock, MagicMock
23

34
try:
45
from types import NoneType # type: ignore[attr-defined]
@@ -444,3 +445,50 @@ def test_lists_are_tuples_flag_is_true_in_strict_mode(subscribable_tuple_type, s
444445
assert check([[1, 2, 3], [4, 5, 6]], subscribable_tuple_type[subscribable_tuple_type[int, ...], ...], strict=True, lists_are_tuples=True)
445446
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)
446447
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)
448+
449+
450+
@pytest.mark.parametrize(
451+
['strict_mode'],
452+
[
453+
(False,),
454+
(True,),
455+
],
456+
)
457+
@pytest.mark.parametrize(
458+
['addictional_parameters'],
459+
[
460+
({'pass_mocks': True},),
461+
({},),
462+
],
463+
)
464+
def test_pass_mocks_when_its_on(strict_mode, list_type, addictional_parameters):
465+
assert check(Mock(), int, strict=strict_mode, **addictional_parameters)
466+
assert check(Mock(), str, strict=strict_mode, **addictional_parameters)
467+
assert check(Mock(), list_type, strict=strict_mode, **addictional_parameters)
468+
469+
assert check(MagicMock(), int, strict=strict_mode, **addictional_parameters)
470+
assert check(MagicMock(), str, strict=strict_mode, **addictional_parameters)
471+
assert check(MagicMock(), list_type, strict=strict_mode, **addictional_parameters)
472+
473+
assert check(Mock(), Mock, strict=strict_mode, **addictional_parameters)
474+
assert check(MagicMock(), MagicMock, strict=strict_mode, **addictional_parameters)
475+
476+
477+
@pytest.mark.parametrize(
478+
['strict_mode'],
479+
[
480+
(False,),
481+
(True,),
482+
],
483+
)
484+
def test_pass_mocks_when_its_off(strict_mode, list_type):
485+
assert not check(Mock(), int, strict=strict_mode, pass_mocks=False)
486+
assert not check(Mock(), str, strict=strict_mode, pass_mocks=False)
487+
assert not check(Mock(), list_type, strict=strict_mode, pass_mocks=False)
488+
489+
assert not check(MagicMock(), int, strict=strict_mode, pass_mocks=False)
490+
assert not check(MagicMock(), str, strict=strict_mode, pass_mocks=False)
491+
assert not check(MagicMock(), list_type, strict=strict_mode, pass_mocks=False)
492+
493+
assert check(Mock(), Mock, strict=strict_mode, pass_mocks=False)
494+
assert check(MagicMock(), MagicMock, strict=strict_mode, pass_mocks=False)

0 commit comments

Comments
 (0)