diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0f96092..2048219 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 @@ -44,7 +44,7 @@ jobs: - name: Run mypy shell: bash - run: mypy simtypes + run: mypy --strict simtypes - name: Run mypy for tests shell: bash diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 3e9eca8..449122e 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 46b58c9..6a8f85d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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', @@ -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"] diff --git a/requirements_dev.txt b/requirements_dev.txt index 2dd4729..6105320 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -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 diff --git a/simtypes/check.py b/simtypes/check.py index 0acdbc3..fe50328 100644 --- a/simtypes/check.py +++ b/simtypes/check.py @@ -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 @@ -15,22 +16,25 @@ 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) @@ -38,17 +42,17 @@ def check(value: Any, type: Type[ExpectedType], strict: bool = False, lists_are_ 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 @@ -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) diff --git a/simtypes/from_string.py b/simtypes/from_string.py index e1751b7..dc86480 100644 --- a/simtypes/from_string.py +++ b/simtypes/from_string.py @@ -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) @@ -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] diff --git a/simtypes/types/ints/natural.py b/simtypes/types/ints/natural.py index 413905e..2ec0aa0 100644 --- a/simtypes/types/ints/natural.py +++ b/simtypes/types/ints/natural.py @@ -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): diff --git a/simtypes/types/ints/non_negative.py b/simtypes/types/ints/non_negative.py index 121ab6f..d550ccd 100644 --- a/simtypes/types/ints/non_negative.py +++ b/simtypes/types/ints/non_negative.py @@ -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): diff --git a/tests/units/test_check.py b/tests/units/test_check.py index d13fc80..1640172 100644 --- a/tests/units/test_check.py +++ b/tests/units/test_check.py @@ -1,4 +1,5 @@ import sys +from unittest.mock import Mock, MagicMock try: 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 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)