diff options
| author | robot-piglet <[email protected]> | 2025-12-02 18:05:40 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2025-12-02 18:27:29 +0300 |
| commit | b3dbd1a8804e4ff048b31dcc8e9a4cbac206b4bc (patch) | |
| tree | 5550c45ff0f0cebd785d47aa28bdb24adbb6efc4 /contrib/python/typeguard/tests | |
| parent | d191c438a6de7f1a0a7079cdab06e27377dbcbff (diff) | |
Intermediate changes
commit_hash:5d7bb0b3a16601d453badbe45dcb76f582024a93
Diffstat (limited to 'contrib/python/typeguard/tests')
21 files changed, 5320 insertions, 1892 deletions
diff --git a/contrib/python/typeguard/tests/__init__.py b/contrib/python/typeguard/tests/__init__.py new file mode 100644 index 00000000000..b48bd69cb98 --- /dev/null +++ b/contrib/python/typeguard/tests/__init__.py @@ -0,0 +1,44 @@ +from typing import ( + AbstractSet, + Collection, + Dict, + Generic, + List, + NamedTuple, + NewType, + TypeVar, + Union, +) + +T_Foo = TypeVar("T_Foo") + +TBound = TypeVar("TBound", bound="Parent") +TConstrained = TypeVar("TConstrained", "Parent", int) +TTypingConstrained = TypeVar("TTypingConstrained", List[int], AbstractSet[str]) +TIntStr = TypeVar("TIntStr", int, str) +TIntCollection = TypeVar("TIntCollection", int, Collection[int]) +TParent = TypeVar("TParent", bound="Parent") +TChild = TypeVar("TChild", bound="Child") + + +class Employee(NamedTuple): + name: str + id: int + + +JSONType = Union[str, float, bool, None, List["JSONType"], Dict[str, "JSONType"]] +myint = NewType("myint", int) +mylist = NewType("mylist", List[int]) + + +class FooGeneric(Generic[T_Foo]): + pass + + +class Parent: + pass + + +class Child(Parent): + def method(self, a: int) -> None: + pass diff --git a/contrib/python/typeguard/tests/conftest.py b/contrib/python/typeguard/tests/conftest.py index 2a3132bc4f7..ef8731f022c 100644 --- a/contrib/python/typeguard/tests/conftest.py +++ b/contrib/python/typeguard/tests/conftest.py @@ -1,12 +1,45 @@ +import random import re +import string import sys +import typing +from itertools import count +from pathlib import Path -version_re = re.compile(r'_py(\d)(\d)\.py$') +import pytest +import typing_extensions +version_re = re.compile(r"_py(\d)(\d)\.py$") +pytest_plugins = ["pytester"] -def pytest_ignore_collect(path, config): - match = version_re.search(path.basename) + +def pytest_ignore_collect( + collection_path: Path, config: pytest.Config +) -> typing.Optional[bool]: + match = version_re.search(collection_path.name) if match: version = tuple(int(x) for x in match.groups()) if sys.version_info < version: return True + + return None + + +def sample_set() -> set: + # Create a set which, when iterated, returns "bb" as the first item + for num in count(): + letter = random.choice(string.ascii_lowercase) + dummy_set = {letter, num} + if next(iter(dummy_set)) == letter: + return dummy_set + + + params=[ + pytest.param(typing, id="typing"), + pytest.param(typing_extensions, id="typing_extensions"), + ] +) +def typing_provider(request): + return request.param diff --git a/contrib/python/typeguard/tests/deferredannos.py b/contrib/python/typeguard/tests/deferredannos.py new file mode 100644 index 00000000000..0d2a1672902 --- /dev/null +++ b/contrib/python/typeguard/tests/deferredannos.py @@ -0,0 +1,10 @@ +from typeguard import typechecked + + +@typechecked +def uses_forwardref(x: NotYetDefined) -> NotYetDefined: # noqa: F821 + return x + + +class NotYetDefined: + pass diff --git a/contrib/python/typeguard/tests/dummymodule.py b/contrib/python/typeguard/tests/dummymodule.py index 7578976a1a7..17ca2263cde 100644 --- a/contrib/python/typeguard/tests/dummymodule.py +++ b/contrib/python/typeguard/tests/dummymodule.py @@ -1,47 +1,82 @@ """Module docstring.""" -from __future__ import absolute_import, division -from typing import no_type_check, no_type_check_decorator +import sys +from contextlib import contextmanager +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Callable, + Dict, + Generator, + List, + Literal, + Sequence, + Tuple, + Type, + TypeVar, + Union, + no_type_check, + no_type_check_decorator, + overload, +) -from typeguard import typeguard_ignore +from typeguard import ( + CollectionCheckStrategy, + ForwardRefPolicy, + typechecked, + typeguard_ignore, +) +if sys.version_info >= (3, 10): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec -@no_type_check_decorator -def dummy_decorator(func): - return func +if TYPE_CHECKING: + from nonexistent import Imaginary +T = TypeVar("T", bound="DummyClass") +P = ParamSpec("P") + +if sys.version_info <= (3, 13): + + @no_type_check_decorator + def dummy_decorator(func): + return func + + @dummy_decorator + def non_type_checked_decorated_func(x: int, y: str) -> 6: + # This is to ensure that we avoid using a local variable that's already in use + _call_memo = "foo" # noqa: F841 + return "foo" + + +@typechecked def type_checked_func(x: int, y: int) -> int: return x * y @no_type_check def non_type_checked_func(x: int, y: str) -> 6: - return 'foo' - - -@dummy_decorator -def non_type_checked_decorated_func(x: int, y: str) -> 6: - return 'foo' + return "foo" @typeguard_ignore def non_typeguard_checked_func(x: int, y: str) -> 6: - return 'foo' - - -def dynamic_type_checking_func(arg, argtype, return_annotation): - def inner(x: argtype) -> return_annotation: - return str(x) - - return inner(arg) + return "foo" class Metaclass(type): pass +@typechecked class DummyClass(metaclass=Metaclass): + bar: str + baz: int + def type_checked_method(self, x: int, y: int) -> int: return x * y @@ -67,26 +102,252 @@ class DummyClass(metaclass=Metaclass): def outer(): + @typechecked class Inner: - pass + def get_self(self) -> "Inner": + return self - def create_inner() -> 'Inner': + def create_inner() -> "Inner": return Inner() return create_inner +@typechecked class Outer: class Inner: pass - def create_inner(self) -> 'Inner': + def create_inner(self) -> "Inner": return Outer.Inner() @classmethod - def create_inner_classmethod(cls) -> 'Inner': + def create_inner_classmethod(cls) -> "Inner": return Outer.Inner() @staticmethod - def create_inner_staticmethod() -> 'Inner': + def create_inner_staticmethod() -> "Inner": return Outer.Inner() + + +@contextmanager +@typechecked +def dummy_context_manager() -> Generator[int, None, None]: + yield 1 + + +@overload +def overloaded_func(a: int) -> int: ... + + +@overload +def overloaded_func(a: str) -> str: ... + + +@typechecked +def overloaded_func(a: Union[str, int]) -> Union[str, int]: + return a + + +@typechecked +def missing_return() -> int: + pass + + +def get_inner_class() -> type: + @typechecked + class InnerClass: + def get_self(self) -> "InnerClass": + return self + + return InnerClass + + +def create_local_class_instance() -> object: + class Inner: + pass + + @typechecked + def get_instance() -> "Inner": + return instance + + instance = Inner() + return get_instance() + + +@typechecked +async def async_func(a: int) -> str: + return str(a) + + +@typechecked +def generator_func(yield_value: Any, return_value: Any) -> Generator[int, Any, str]: + yield yield_value + return return_value + + +@typechecked +async def asyncgen_func(yield_value: Any) -> AsyncGenerator[int, Any]: + yield yield_value + + +@typechecked +def pep_604_union_args( + x: "Callable[[], Literal[-1]] | Callable[..., Union[int, str]]", +) -> None: + pass + + +@typechecked +def pep_604_union_retval(x: Any) -> "str | int": + return x + + +@typechecked +def builtin_generic_collections(x: "list[set[int]]") -> Any: + return x + + +@typechecked +def paramspec_function(func: P, args: P.args, kwargs: P.kwargs) -> None: + pass + + +@typechecked +def aug_assign() -> int: + x: int = 1 + x += 1 + return x + + +@typechecked +def multi_assign_single_value() -> Tuple[int, float, complex]: + x: int + y: float + z: complex + x = y = z = 6 + return x, y, z + + +@typechecked +def multi_assign_iterable() -> Tuple[Sequence[int], Sequence[float], Sequence[complex]]: + x: Sequence[int] + y: Sequence[float] + z: Sequence[complex] + x = y = z = [6, 7] + return x, y, z + + +@typechecked +def unpacking_assign() -> Tuple[int, str]: + x: int + x, y = (1, "foo") + return x, y + + +@typechecked +def unpacking_assign_generator() -> Tuple[int, str]: + def genfunc(): + yield 1 + yield "foo" + + x: int + x, y = genfunc() + return x, y + + +@typechecked +def unpacking_assign_star_with_annotation() -> Tuple[int, List[bytes], str]: + x: int + z: str + x, *y, z = (1, b"abc", b"bah", "foo") + return x, y, z + + +@typechecked +def unpacking_assign_star_no_annotation(value: Any) -> Tuple[int, List[bytes], str]: + x: int + y: List[bytes] + z: str + x, *y, z = value + return x, y, z + + +@typechecked +def attribute_assign_unpacking(obj: DummyClass) -> None: + obj.bar, obj.baz = "foo", 123123 + + +@typechecked(forward_ref_policy=ForwardRefPolicy.ERROR) +def override_forward_ref_policy(value: "NonexistentType") -> None: # noqa: F821 + pass + + +@typechecked(typecheck_fail_callback=lambda exc, memo: print(exc)) +def override_typecheck_fail_callback(value: int) -> None: + pass + + +@typechecked(collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS) +def override_collection_check_strategy(value: List[int]) -> None: + pass + + +@typechecked(typecheck_fail_callback=lambda exc, memo: print(exc)) +class OverrideClass: + def override_typecheck_fail_callback(self, value: int) -> None: + pass + + class Inner: + @typechecked + def override_typecheck_fail_callback(self, value: int) -> None: + pass + + +@typechecked +def typed_variable_args( + *args: str, **kwargs: int +) -> Tuple[Tuple[str, ...], Dict[str, int]]: + return args, kwargs + + +@typechecked +def guarded_type_hint_plain(x: "Imaginary") -> "Imaginary": + y: Imaginary = x + return y + + +@typechecked +def guarded_type_hint_subscript_toplevel(x: "Imaginary[int]") -> "Imaginary[int]": + y: Imaginary[int] = x + return y + + +@typechecked +def guarded_type_hint_subscript_nested( + x: List["Imaginary[int]"], +) -> List["Imaginary[int]"]: + y: List[Imaginary[int]] = x + return y + + +@typechecked +def literal(x: Literal["foo"]) -> Literal["foo"]: + y: Literal["foo"] = x + return y + + +@typechecked +def literal_in_union(x: Union[Literal["foo"],]) -> Literal["foo"]: + y: Literal["foo"] = x + return y + + +@typechecked +def typevar_forwardref(x: Type[T]) -> T: + return x() + + +def never_called(x: List["NonExistentType"]) -> List["NonExistentType"]: # noqa: F821 + """Regression test for #335.""" + return x diff --git a/contrib/python/typeguard/tests/mypy/negative.py b/contrib/python/typeguard/tests/mypy/negative.py index 6db0eb2a35b..ec93022f57b 100644 --- a/contrib/python/typeguard/tests/mypy/negative.py +++ b/contrib/python/typeguard/tests/mypy/negative.py @@ -1,4 +1,4 @@ -from typeguard import check_argument_types, check_return_type, typechecked, typeguard_ignore +from typeguard import typechecked, typeguard_ignore @typechecked @@ -8,28 +8,31 @@ def foo(x: int) -> int: @typechecked def bar(x: int) -> int: - return str(x) # error: Incompatible return value type (got "str", expected "int") + return str(x) # noqa: E501 # error: Incompatible return value type (got "str", expected "int") [return-value] @typeguard_ignore def non_typeguard_checked_func(x: int) -> int: - return str(x) # error: Incompatible return value type (got "str", expected "int") + return str(x) # noqa: E501 # error: Incompatible return value type (got "str", expected "int") [return-value] +@typechecked def returns_str() -> str: - return bar(0) # error: Incompatible return value type (got "int", expected "str") + return bar(0) # noqa: E501 # error: Incompatible return value type (got "int", expected "str") [return-value] +@typechecked def arg_type(x: int) -> str: - return check_argument_types() # noqa: E501 # error: Incompatible return value type (got "bool", expected "str") + return True # noqa: E501 # error: Incompatible return value type (got "bool", expected "str") [return-value] +@typechecked def ret_type() -> str: - return check_return_type(False) # noqa: E501 # error: Incompatible return value type (got "bool", expected "str") + return True # noqa: E501 # error: Incompatible return value type (got "bool", expected "str") [return-value] -_ = arg_type(foo) # noqa: E501 # error: Argument 1 to "arg_type" has incompatible type "Callable[[int], int]"; expected "int" -_ = foo("typeguard") # error: Argument 1 to "foo" has incompatible type "str"; expected "int" +_ = arg_type(foo) # noqa: E501 # error: Argument 1 to "arg_type" has incompatible type "Callable[[int], int]"; expected "int" [arg-type] +_ = foo("typeguard") # noqa: E501 # error: Argument 1 to "foo" has incompatible type "str"; expected "int" [arg-type] @typechecked @@ -49,5 +52,5 @@ def create_myclass(x: int) -> MyClass: return MyClass(x) -_ = get_value("foo") # noqa: E501 # error: Argument 1 to "get_value" has incompatible type "str"; expected "MyClass" -_ = MyClass(returns_str()) # noqa: E501 # error: Argument 1 to "MyClass" has incompatible type "str"; expected "int" +_ = get_value("foo") # noqa: E501 # error: Argument 1 to "get_value" has incompatible type "str"; expected "MyClass" [arg-type] +_ = MyClass(returns_str()) # noqa: E501 # error: Argument 1 to "MyClass" has incompatible type "str"; expected "int" [arg-type] diff --git a/contrib/python/typeguard/tests/mypy/positive.py b/contrib/python/typeguard/tests/mypy/positive.py index 2f01bebf362..dc8a350aa47 100644 --- a/contrib/python/typeguard/tests/mypy/positive.py +++ b/contrib/python/typeguard/tests/mypy/positive.py @@ -1,6 +1,6 @@ from typing import Callable -from typeguard import check_argument_types, check_return_type, typechecked +from typeguard import typechecked @typechecked @@ -15,18 +15,17 @@ def takes_callable(f: Callable[[str], str]) -> str: takes_callable(foo) -def has_valid_arguments(x: int, y: str) -> bool: - return check_argument_types() +@typechecked +def has_valid_arguments(x: int, y: str) -> None: + pass def has_valid_return_type(y: str) -> str: - check_return_type(y) return y @typechecked class MyClass: - def __init__(self, x: int) -> None: self.x = x diff --git a/contrib/python/typeguard/tests/mypy/test_type_annotations.py b/contrib/python/typeguard/tests/mypy/test_type_annotations.py index 50ddc50686e..a7ae7c58593 100644 --- a/contrib/python/typeguard/tests/mypy/test_type_annotations.py +++ b/contrib/python/typeguard/tests/mypy/test_type_annotations.py @@ -1,21 +1,23 @@ +import json import os import platform -import re import subprocess -from typing import Dict, List import pytest POSITIVE_FILE = "positive.py" NEGATIVE_FILE = "negative.py" -LINE_PATTERN = NEGATIVE_FILE + ":([0-9]+):" -pytestmark = [pytest.mark.skipif(platform.python_implementation() == 'PyPy', - reason='MyPy does not work with PyPy yet')] +pytestmark = [ + pytest.mark.skipif( + platform.python_implementation() == "PyPy", + reason="MyPy does not work with PyPy yet", + ) +] -def get_mypy_cmd(filename: str) -> List[str]: - return ["mypy", "--strict", filename] +def get_mypy_cmd(filename: str) -> list[str]: + return ["mypy", "-O", "json", "--strict", filename] def get_negative_mypy_output() -> str: @@ -30,7 +32,7 @@ def get_negative_mypy_output() -> str: return output -def get_expected_errors() -> Dict[int, str]: +def get_expected_errors() -> dict[int, str]: """ Extract the expected errors from comments in the negative examples file. """ @@ -42,14 +44,14 @@ def get_expected_errors() -> Dict[int, str]: for idx, line in enumerate(lines): line = line.rstrip() if "# error" in line: - expected[idx + 1] = line[line.index("# error") + 2:] + expected[idx + 1] = line[line.index("# error") + 9 :] # Sanity check. Should update if negative.py changes. assert len(expected) == 9 return expected -def get_mypy_errors() -> Dict[int, str]: +def get_mypy_errors() -> dict[int, str]: """ Extract the errors from running mypy on the negative examples file. """ @@ -57,10 +59,8 @@ def get_mypy_errors() -> Dict[int, str]: got = {} for line in mypy_output.splitlines(): - m = re.match(LINE_PATTERN, line) - if m is None: - continue - got[int(m.group(1))] = line[len(m.group(0)) + 1:] + error = json.loads(line) + got[error["line"]] = f"{error['message']} [{error['code']}]" return got @@ -75,6 +75,8 @@ def chdir_local() -> None: os.chdir(os.path.dirname(__file__)) +# Этот тест ожидает "mypy" в PATH @pytest.mark.usefixtures("chdir_local") def test_positive() -> None: """ @@ -83,6 +85,8 @@ def test_positive() -> None: subprocess.check_call(get_mypy_cmd(POSITIVE_FILE)) +# Этот тест ожидает "mypy" в PATH @pytest.mark.usefixtures("chdir_local") def test_negative() -> None: """ @@ -94,8 +98,8 @@ def test_negative() -> None: if set(got_errors) != set(expected_errors): raise RuntimeError( - "Expected error lines {} does not ".format(set(expected_errors)) + - "match mypy error lines {}.".format(set(got_errors)) + f"Expected error lines {set(expected_errors)} does not " + + f"match mypy error lines {set(got_errors)}." ) mismatches = [ @@ -103,12 +107,8 @@ def test_negative() -> None: for idx in expected_errors if expected_errors[idx] != got_errors[idx] ] - for (idx, expected, got) in mismatches: - print( - "Line {}".format(idx), - "Expected: {}".format(expected), - "Got: {}".format(got), - sep="\n\t" - ) + for idx, expected, got in mismatches: + print(f"Line {idx}", f"Expected: {expected}", f"Got: {got}", sep="\n\t") + if mismatches: raise RuntimeError("Error messages changed") diff --git a/contrib/python/typeguard/tests/pep695.py b/contrib/python/typeguard/tests/pep695.py new file mode 100644 index 00000000000..b0a29291839 --- /dev/null +++ b/contrib/python/typeguard/tests/pep695.py @@ -0,0 +1,12 @@ +from typeguard import typechecked + + +@typechecked +class ParametrizedClass[T]: + def method(self, x: T, y: str) -> T: + return x + + +@typechecked +def parametrized_func[T](x: T, y: str) -> T: + return x diff --git a/contrib/python/typeguard/tests/test_checkers.py b/contrib/python/typeguard/tests/test_checkers.py new file mode 100644 index 00000000000..cc019c7b1f2 --- /dev/null +++ b/contrib/python/typeguard/tests/test_checkers.py @@ -0,0 +1,1526 @@ +import collections.abc +import sys +import types +from contextlib import nullcontext +from datetime import timedelta +from functools import partial +from io import BytesIO, StringIO +from pathlib import Path +from typing import ( + IO, + AbstractSet, + Annotated, + Any, + AnyStr, + BinaryIO, + Callable, + Collection, + ContextManager, + Dict, + ForwardRef, + FrozenSet, + Iterable, + Iterator, + List, + Literal, + Mapping, + MutableMapping, + Optional, + Protocol, + Sequence, + Set, + Sized, + TextIO, + Tuple, + Type, + TypeVar, + Union, +) + +import pytest +from typing_extensions import LiteralString + +from typeguard import ( + CollectionCheckStrategy, + ForwardRefPolicy, + TypeCheckError, + TypeCheckMemo, + TypeHintWarning, + check_type, + check_type_internal, + suppress_type_checks, +) +from typeguard._checkers import is_typeddict +from typeguard._utils import qualified_name + +from . import ( + Child, + Employee, + JSONType, + Parent, + TChild, + TIntStr, + TParent, + TTypingConstrained, + myint, + mylist, +) + +if sys.version_info >= (3, 11): + SubclassableAny = Any +else: + from typing_extensions import Any as SubclassableAny + +if sys.version_info >= (3, 10): + from typing import Concatenate, ParamSpec, TypeGuard +else: + from typing_extensions import Concatenate, ParamSpec, TypeGuard + +P = ParamSpec("P") + + + sys.version_info >= (3, 13), reason="AnyStr is deprecated on Python 3.13" +) +class TestAnyStr: + @pytest.mark.parametrize( + "value", [pytest.param("bar", id="str"), pytest.param(b"bar", id="bytes")] + ) + def test_valid(self, value): + check_type(value, AnyStr) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 4, AnyStr).match( + r"does not match any of the constraints \(bytes, str\)" + ) + + +class TestBytesLike: + @pytest.mark.parametrize( + "value", + [ + pytest.param(b"test", id="bytes"), + pytest.param(bytearray(b"test"), id="bytearray"), + pytest.param(memoryview(b"test"), id="memoryview"), + ], + ) + def test_valid(self, value): + check_type(value, bytes) + + def test_fail(self): + pytest.raises(TypeCheckError, check_type, "test", bytes).match( + r"str is not bytes-like" + ) + + +class TestFloat: + @pytest.mark.parametrize( + "value", [pytest.param(3, id="int"), pytest.param(3.87, id="float")] + ) + def test_valid(self, value): + check_type(value, float) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, "foo", float).match( + r"str is neither float or int" + ) + + +class TestComplexNumber: + @pytest.mark.parametrize( + "value", + [ + pytest.param(3, id="int"), + pytest.param(3.87, id="float"), + pytest.param(3.87 + 8j, id="complex"), + ], + ) + def test_valid(self, value): + check_type(value, complex) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, "foo", complex).match( + "str is neither complex, float or int" + ) + + +class TestCallable: + def test_any_args(self): + def some_callable(x: int, y: str) -> int: + pass + + check_type(some_callable, Callable[..., int]) + + def test_exact_arg_count(self): + def some_callable(x: int, y: str) -> int: + pass + + check_type(some_callable, Callable[[int, str], int]) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Callable[..., int]).match( + "is not callable" + ) + + def test_too_few_arguments(self): + def some_callable(x: int) -> int: + pass + + pytest.raises( + TypeCheckError, check_type, some_callable, Callable[[int, str], int] + ).match( + r"has too few arguments in its declaration; expected 2 but 1 argument\(s\) " + r"declared" + ) + + def test_too_many_arguments(self): + def some_callable(x: int, y: str, z: float) -> int: + pass + + pytest.raises( + TypeCheckError, check_type, some_callable, Callable[[int, str], int] + ).match( + r"has too many mandatory positional arguments in its declaration; expected " + r"2 but 3 mandatory positional argument\(s\) declared" + ) + + def test_mandatory_kwonlyargs(self): + def some_callable(x: int, y: str, *, z: float, bar: str) -> int: + pass + + pytest.raises( + TypeCheckError, check_type, some_callable, Callable[[int, str], int] + ).match(r"has mandatory keyword-only arguments in its declaration: z, bar") + + def test_class(self): + """ + Test that passing a class as a callable does not count the "self" argument + against the ones declared in the Callable specification. + + """ + + class SomeClass: + def __init__(self, x: int, y: str): + pass + + check_type(SomeClass, Callable[[int, str], Any]) + + def test_plain(self): + def callback(a): + pass + + check_type(callback, Callable) + + def test_partial_class(self): + """ + Test that passing a bound method as a callable does not count the "self" + argument against the ones declared in the Callable specification. + + """ + + class SomeClass: + def __init__(self, x: int, y: str): + pass + + check_type(partial(SomeClass, y="foo"), Callable[[int], Any]) + + def test_bound_method(self): + """ + Test that passing a bound method as a callable does not count the "self" + argument against the ones declared in the Callable specification. + + """ + check_type(Child().method, Callable[[int], Any]) + + def test_partial_bound_method(self): + """ + Test that passing a bound method as a callable does not count the "self" + argument against the ones declared in the Callable specification. + + """ + check_type(partial(Child().method, 1), Callable[[], Any]) + + def test_defaults(self): + """ + Test that a callable having "too many" arguments don't raise an error if the + extra arguments have default values. + + """ + + def some_callable(x: int, y: str, z: float = 1.2) -> int: + pass + + check_type(some_callable, Callable[[int, str], Any]) + + def test_builtin(self): + """ + Test that checking a Callable annotation against a builtin callable does not + raise an error. + + """ + check_type([].append, Callable[[int], Any]) + + def test_concatenate(self): + """Test that ``Concatenate`` in the arglist is ignored.""" + check_type([].append, Callable[Concatenate[object, P], Any]) + + def test_positional_only_arg_with_default(self): + def some_callable(x: int = 1, /) -> None: + pass + + check_type(some_callable, Callable[[int], Any]) + + +class TestLiteral: + def test_literal_union(self): + annotation = Union[str, Literal[1, 6, 8]] + check_type(6, annotation) + pytest.raises(TypeCheckError, check_type, 4, annotation).match( + r"int did not match any element in the union:\n" + r" str: is not an instance of str\n" + r" Literal\[1, 6, 8\]: is not any of \(1, 6, 8\)$" + ) + + def test_literal_nested(self): + annotation = Literal[1, Literal["x", "a", Literal["z"]], 6, 8] + check_type("z", annotation) + pytest.raises(TypeCheckError, check_type, 4, annotation).match( + r"int is not any of \(1, 'x', 'a', 'z', 6, 8\)$" + ) + + def test_literal_int_as_bool(self): + pytest.raises(TypeCheckError, check_type, 0, Literal[False]) + pytest.raises(TypeCheckError, check_type, 1, Literal[True]) + + def test_literal_illegal_value(self): + pytest.raises(TypeError, check_type, 4, Literal[1, 1.1]).match( + r"Illegal literal value: 1.1$" + ) + + +class TestMapping: + class DummyMapping(collections.abc.Mapping): + _values = {"a": 1, "b": 10, "c": 100} + + def __getitem__(self, index: str): + return self._values[index] + + def __iter__(self): + return iter(self._values) + + def __len__(self) -> int: + return len(self._values) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Mapping[str, int]).match( + "is not a mapping" + ) + + def test_bad_key_type(self): + pytest.raises( + TypeCheckError, check_type, TestMapping.DummyMapping(), Mapping[int, int] + ).match( + f"key 'a' of {__name__}.TestMapping.DummyMapping is not an instance of int" + ) + + def test_bad_value_type(self): + pytest.raises( + TypeCheckError, check_type, TestMapping.DummyMapping(), Mapping[str, str] + ).match( + f"value of key 'a' of {__name__}.TestMapping.DummyMapping is not an " + f"instance of str" + ) + + def test_bad_key_type_full_check(self): + pytest.raises( + TypeCheckError, + check_type, + {"x": 1, 3: 2}, + Mapping[str, int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("key 3 of dict is not an instance of str") + + def test_bad_value_type_full_check(self): + pytest.raises( + TypeCheckError, + check_type, + {"x": 1, "y": "a"}, + Mapping[str, int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("value of key 'y' of dict is not an instance of int") + + def test_any_value_type(self): + check_type(TestMapping.DummyMapping(), Mapping[str, Any]) + + +class TestMutableMapping: + class DummyMutableMapping(collections.abc.MutableMapping): + _values = {"a": 1, "b": 10, "c": 100} + + def __getitem__(self, index: str): + return self._values[index] + + def __setitem__(self, key, value): + self._values[key] = value + + def __delitem__(self, key): + del self._values[key] + + def __iter__(self): + return iter(self._values) + + def __len__(self) -> int: + return len(self._values) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, MutableMapping[str, int]).match( + "is not a mutable mapping" + ) + + def test_bad_key_type(self): + pytest.raises( + TypeCheckError, + check_type, + TestMutableMapping.DummyMutableMapping(), + MutableMapping[int, int], + ).match( + f"key 'a' of {__name__}.TestMutableMapping.DummyMutableMapping is not an " + f"instance of int" + ) + + def test_bad_value_type(self): + pytest.raises( + TypeCheckError, + check_type, + TestMutableMapping.DummyMutableMapping(), + MutableMapping[str, str], + ).match( + f"value of key 'a' of {__name__}.TestMutableMapping.DummyMutableMapping " + f"is not an instance of str" + ) + + +class TestDict: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Dict[str, int]).match( + "int is not a dict" + ) + + def test_bad_key_type(self): + pytest.raises(TypeCheckError, check_type, {1: 2}, Dict[str, int]).match( + "key 1 of dict is not an instance of str" + ) + + def test_bad_value_type(self): + pytest.raises(TypeCheckError, check_type, {"x": "a"}, Dict[str, int]).match( + "value of key 'x' of dict is not an instance of int" + ) + + def test_bad_key_type_full_check(self): + pytest.raises( + TypeCheckError, + check_type, + {"x": 1, 3: 2}, + Dict[str, int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("key 3 of dict is not an instance of str") + + def test_bad_value_type_full_check(self): + pytest.raises( + TypeCheckError, + check_type, + {"x": 1, "y": "a"}, + Dict[str, int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("value of key 'y' of dict is not an instance of int") + + def test_custom_dict_generator_items(self): + class CustomDict(dict): + def items(self): + for key in self: + yield key, self[key] + + check_type(CustomDict(a=1), Dict[str, int]) + + +class TestTypedDict: + @pytest.mark.parametrize( + "value, total, error_re", + [ + pytest.param({"x": 6, "y": "foo"}, True, None, id="correct"), + pytest.param( + {"y": "foo"}, + True, + r'dict is missing required key\(s\): "x"', + id="missing_x", + ), + pytest.param( + {"x": 6, "y": 3}, True, "dict is not an instance of str", id="wrong_y" + ), + pytest.param( + {"x": 6}, + True, + r'is missing required key\(s\): "y"', + id="missing_y_error", + ), + pytest.param({"x": 6}, False, None, id="missing_y_ok"), + pytest.param( + {"x": "abc"}, False, "dict is not an instance of int", id="wrong_x" + ), + pytest.param( + {"x": 6, "foo": "abc"}, + False, + r'dict has unexpected extra key\(s\): "foo"', + id="unknown_key", + ), + pytest.param( + None, + True, + "is not a dict", + id="not_dict", + ), + ], + ) + def test_typed_dict( + self, value, total: bool, error_re: Optional[str], typing_provider + ): + class DummyDict(typing_provider.TypedDict, total=total): + x: int + y: str + + if error_re: + pytest.raises(TypeCheckError, check_type, value, DummyDict).match(error_re) + else: + check_type(value, DummyDict) + + def test_inconsistent_keys_invalid(self, typing_provider): + class DummyDict(typing_provider.TypedDict): + x: int + + pytest.raises( + TypeCheckError, check_type, {"x": 1, "y": 2, b"z": 3}, DummyDict + ).match(r'dict has unexpected extra key\(s\): "y", "b\'z\'"') + + def test_notrequired_pass(self, typing_provider): + try: + NotRequired = typing_provider.NotRequired + except AttributeError: + pytest.skip(f"'NotRequired' not found in {typing_provider.__name__!r}") + + class DummyDict(typing_provider.TypedDict): + x: int + y: NotRequired[int] + z: "NotRequired[int]" + + check_type({"x": 8}, DummyDict) + + def test_notrequired_fail(self, typing_provider): + try: + NotRequired = typing_provider.NotRequired + except AttributeError: + pytest.skip(f"'NotRequired' not found in {typing_provider.__name__!r}") + + class DummyDict(typing_provider.TypedDict): + x: int + y: NotRequired[int] + z: "NotRequired[int]" + + with pytest.raises( + TypeCheckError, match=r"value of key 'y' of dict is not an instance of int" + ): + check_type({"x": 1, "y": "foo"}, DummyDict) + + with pytest.raises( + TypeCheckError, match=r"value of key 'z' of dict is not an instance of int" + ): + check_type({"x": 1, "y": 6, "z": "foo"}, DummyDict) + + def test_is_typeddict(self, typing_provider): + # Ensure both typing.TypedDict and typing_extensions.TypedDict are recognized + class DummyDict(typing_provider.TypedDict): + x: int + + assert is_typeddict(DummyDict) + assert not is_typeddict(dict) + + +class TestList: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, List[int]).match( + "int is not a list" + ) + + def test_first_check_success(self): + check_type(["aa", "bb", 1], List[str]) + + def test_first_check_empty(self): + check_type([], List[str]) + + def test_first_check_fail(self): + pytest.raises(TypeCheckError, check_type, ["bb"], List[int]).match( + "list is not an instance of int" + ) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + [1, 2, "bb"], + List[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("list is not an instance of int") + + +class TestSequence: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Sequence[int]).match( + "int is not a sequence" + ) + + @pytest.mark.parametrize( + "value", + [pytest.param([1, "bb"], id="list"), pytest.param((1, "bb"), id="tuple")], + ) + def test_first_check_success(self, value): + check_type(value, Sequence[int]) + + def test_first_check_empty(self): + check_type([], Sequence[int]) + + def test_first_check_fail(self): + pytest.raises(TypeCheckError, check_type, ["bb"], Sequence[int]).match( + "list is not an instance of int" + ) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + [1, 2, "bb"], + Sequence[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("list is not an instance of int") + + +class TestAbstractSet: + def test_custom_type(self): + class DummySet(AbstractSet[int]): + def __contains__(self, x: object) -> bool: + return x == 1 + + def __len__(self) -> int: + return 1 + + def __iter__(self) -> Iterator[int]: + yield 1 + + check_type(DummySet(), AbstractSet[int]) + + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, AbstractSet[int]).match( + "int is not a set" + ) + + def test_first_check_fail(self, sample_set): + # Create a set which, when iterated, returns "bb" as the first item + pytest.raises(TypeCheckError, check_type, sample_set, AbstractSet[int]).match( + "set is not an instance of int" + ) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + {1, 2, "bb"}, + AbstractSet[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("set is not an instance of int") + + +class TestSet: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, Set[int]).match("int is not a set") + + def test_valid(self): + check_type({1, 2}, Set[int]) + + def test_first_check_empty(self): + check_type(set(), Set[int]) + + def test_first_check_fail(self, sample_set: set): + pytest.raises(TypeCheckError, check_type, sample_set, Set[int]).match( + "set is not an instance of int" + ) + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + {1, 2, "bb"}, + Set[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("set is not an instance of int") + + +class TestFrozenSet: + def test_bad_type(self): + pytest.raises(TypeCheckError, check_type, 5, FrozenSet[int]).match( + "int is not a frozenset" + ) + + def test_valid(self): + check_type(frozenset({1, 2}), FrozenSet[int]) + + def test_first_check_empty(self): + check_type(frozenset(), FrozenSet[int]) + + def test_first_check_fail(self, sample_set: set): + pytest.raises( + TypeCheckError, check_type, frozenset(sample_set), FrozenSet[int] + ).match("set is not an instance of int") + + def test_full_check_fail(self): + pytest.raises( + TypeCheckError, + check_type, + frozenset({1, 2, "bb"}), + FrozenSet[int], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("set is not an instance of int") + + def test_set_against_frozenset(self, sample_set: set): + pytest.raises(TypeCheckError, check_type, sample_set, FrozenSet[int]).match( + "set is not a frozenset" + ) + + + "annotated_type", + [ + pytest.param(Tuple, id="typing"), + pytest.param( + tuple, + id="builtin", + marks=[ + pytest.mark.skipif( + sys.version_info < (3, 9), + reason="builtins.tuple is not parametrizable before Python 3.9", + ) + ], + ), + ], +) +class TestTuple: + def test_bad_type(self, annotated_type: Any): + pytest.raises(TypeCheckError, check_type, 5, annotated_type[int]).match( + "int is not a tuple" + ) + + def test_first_check_empty(self, annotated_type: Any): + check_type((), annotated_type[int, ...]) + + def test_unparametrized_tuple(self, annotated_type: Any): + check_type((5, "foo"), annotated_type) + + def test_unparametrized_tuple_fail(self, annotated_type: Any): + pytest.raises(TypeCheckError, check_type, 5, annotated_type).match( + "int is not a tuple" + ) + + def test_too_many_elements(self, annotated_type: Any): + pytest.raises( + TypeCheckError, check_type, (1, "aa", 2), annotated_type[int, str] + ).match(r"tuple has wrong number of elements \(expected 2, got 3 instead\)") + + def test_too_few_elements(self, annotated_type: Any): + pytest.raises(TypeCheckError, check_type, (1,), annotated_type[int, str]).match( + r"tuple has wrong number of elements \(expected 2, got 1 instead\)" + ) + + def test_bad_element(self, annotated_type: Any): + pytest.raises( + TypeCheckError, check_type, (1, 2), annotated_type[int, str] + ).match("tuple is not an instance of str") + + def test_ellipsis_bad_element(self, annotated_type: Any): + pytest.raises( + TypeCheckError, check_type, ("blah",), annotated_type[int, ...] + ).match("tuple is not an instance of int") + + def test_ellipsis_bad_element_full_check(self, annotated_type: Any): + pytest.raises( + TypeCheckError, + check_type, + (1, 2, "blah"), + annotated_type[int, ...], + collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, + ).match("tuple is not an instance of int") + + def test_empty_tuple(self, annotated_type: Any): + check_type((), annotated_type[()]) + + def test_empty_tuple_fail(self, annotated_type: Any): + pytest.raises(TypeCheckError, check_type, (1,), annotated_type[()]).match( + "tuple is not an empty tuple" + ) + + +class TestNamedTuple: + def test_valid(self): + check_type(Employee("bob", 1), Employee) + + def test_type_mismatch(self): + pytest.raises(TypeCheckError, check_type, ("bob", 1), Employee).match( + r"tuple is not a named tuple of type __tests__.Employee" + ) + + def test_wrong_field_type(self): + pytest.raises(TypeCheckError, check_type, Employee(2, 1), Employee).match( + r"Employee is not an instance of str" + ) + + +class TestUnion: + @pytest.mark.parametrize( + "value", [pytest.param(6, id="int"), pytest.param("aa", id="str")] + ) + def test_valid(self, value): + check_type(value, Union[str, int]) + + def test_typing_type_fail(self): + pytest.raises(TypeCheckError, check_type, 1, Union[str, Collection]).match( + "int did not match any element in the union:\n" + " str: is not an instance of str\n" + " Collection: is not an instance of collections.abc.Collection" + ) + + @pytest.mark.parametrize( + "annotation", + [ + pytest.param(Union[str, int], id="pep484"), + pytest.param( + ForwardRef("str | int"), + id="pep604", + marks=[ + pytest.mark.skipif( + sys.version_info < (3, 10), reason="Requires Python 3.10+" + ) + ], + ), + ], + ) + @pytest.mark.parametrize( + "value", [pytest.param(6.5, id="float"), pytest.param(b"aa", id="bytes")] + ) + def test_union_fail(self, annotation, value): + qualname = qualified_name(value) + pytest.raises(TypeCheckError, check_type, value, annotation).match( + f"{qualname} did not match any element in the union:\n" + f" str: is not an instance of str\n" + f" int: is not an instance of int" + ) + + @pytest.mark.skipif( + sys.implementation.name != "cpython", + reason="Test relies on CPython's reference counting behavior", + ) + def test_union_reference_leak(self): + class Leak: + def __del__(self): + nonlocal leaked + leaked = False + + def inner1(): + leak = Leak() # noqa: F841 + check_type(b"asdf", Union[str, bytes]) + + leaked = True + inner1() + assert not leaked + + def inner2(): + leak = Leak() # noqa: F841 + check_type(b"asdf", Union[bytes, str]) + + leaked = True + inner2() + assert not leaked + + def inner3(): + leak = Leak() # noqa: F841 + with pytest.raises(TypeCheckError, match="any element in the union:"): + check_type(1, Union[str, bytes]) + + leaked = True + inner3() + assert not leaked + + @pytest.mark.skipif( + sys.implementation.name != "cpython", + reason="Test relies on CPython's reference counting behavior", + ) + @pytest.mark.skipif(sys.version_info < (3, 10), reason="UnionType requires 3.10") + def test_uniontype_reference_leak(self): + class Leak: + def __del__(self): + nonlocal leaked + leaked = False + + def inner1(): + leak = Leak() # noqa: F841 + check_type(b"asdf", str | bytes) + + leaked = True + inner1() + assert not leaked + + def inner2(): + leak = Leak() # noqa: F841 + check_type(b"asdf", bytes | str) + + leaked = True + inner2() + assert not leaked + + def inner3(): + leak = Leak() # noqa: F841 + with pytest.raises(TypeCheckError, match="any element in the union:"): + check_type(1, Union[str, bytes]) + + leaked = True + inner3() + assert not leaked + + @pytest.mark.skipif(sys.version_info < (3, 10), reason="UnionType requires 3.10") + def test_raw_uniontype_success(self): + check_type(str | int, types.UnionType) + + @pytest.mark.skipif(sys.version_info < (3, 10), reason="UnionType requires 3.10") + def test_raw_uniontype_fail(self): + if sys.version_info < (3, 14): + expected_type = r"\w+\.UnionType" + else: + expected_type = "Union" + + with pytest.raises( + TypeCheckError, match=f"class str is not an instance of {expected_type}$" + ): + check_type(str, types.UnionType) + + +class TestTypevar: + def test_bound(self): + check_type(Child(), TParent) + + def test_bound_fail(self): + with pytest.raises(TypeCheckError, match="is not an instance of __tests__.Child"): + check_type(Parent(), TChild) + + @pytest.mark.parametrize( + "value", [pytest.param([6, 7], id="int"), pytest.param({"aa", "bb"}, id="str")] + ) + def test_collection_constraints(self, value): + check_type(value, TTypingConstrained) + + def test_collection_constraints_fail(self): + pytest.raises(TypeCheckError, check_type, {1, 2}, TTypingConstrained).match( + r"set does not match any of the constraints \(List\[int\], " + r"AbstractSet\[str\]\)" + ) + + def test_constraints_fail(self): + pytest.raises(TypeCheckError, check_type, 2.5, TIntStr).match( + r"float does not match any of the constraints \(int, str\)" + ) + + +class TestNewType: + def test_simple_valid(self): + check_type(1, myint) + + def test_simple_bad_value(self): + pytest.raises(TypeCheckError, check_type, "a", myint).match( + r"str is not an instance of int" + ) + + def test_generic_valid(self): + check_type([1], mylist) + + def test_generic_bad_value(self): + pytest.raises(TypeCheckError, check_type, ["a"], mylist).match( + r"item 0 of list is not an instance of int" + ) + + +class TestType: + @pytest.mark.parametrize("annotation", [pytest.param(Type), pytest.param(type)]) + def test_unparametrized(self, annotation: Any): + check_type(TestNewType, annotation) + + @pytest.mark.parametrize("annotation", [pytest.param(Type), pytest.param(type)]) + def test_unparametrized_fail(self, annotation: Any): + pytest.raises(TypeCheckError, check_type, 1, annotation).match( + "int is not a class" + ) + + @pytest.mark.parametrize( + "value", [pytest.param(Parent, id="exact"), pytest.param(Child, id="subclass")] + ) + def test_parametrized(self, value): + check_type(value, Type[Parent]) + + def test_parametrized_fail(self): + pytest.raises(TypeCheckError, check_type, int, Type[str]).match( + "class int is not a subclass of str" + ) + + def test_parametrized_value(self): + check_type(list[str], type[list[str]]) + + @pytest.mark.parametrize( + "value", [pytest.param(str, id="str"), pytest.param(int, id="int")] + ) + def test_union(self, value): + check_type(value, Type[Union[str, int, list]]) + + def test_union_any(self): + check_type(list, Type[Union[str, int, Any]]) + + def test_any(self): + check_type(list, Type[Any]) + + def test_union_fail(self): + pytest.raises( + TypeCheckError, check_type, dict, Type[Union[str, int, list]] + ).match( + "class dict did not match any element in the union:\n" + " str: is not a subclass of str\n" + " int: is not a subclass of int\n" + " list: is not a subclass of list" + ) + + def test_union_typevar(self): + T = TypeVar("T", bound=Parent) + check_type(Child, Type[T]) + + @pytest.mark.parametrize("check_against", [type, Type[Any]]) + def test_generic_aliase(self, check_against): + check_type(dict[str, str], check_against) + check_type(Dict, check_against) + check_type(Dict[str, str], check_against) + + +class TestIO: + @pytest.mark.parametrize( + "annotation", + [ + pytest.param(BinaryIO, id="direct"), + pytest.param(IO[bytes], id="parametrized"), + ], + ) + def test_binary_valid(self, annotation): + check_type(BytesIO(), annotation) + + @pytest.mark.parametrize( + "annotation", + [ + pytest.param(BinaryIO, id="direct"), + pytest.param(IO[bytes], id="parametrized"), + ], + ) + def test_binary_fail(self, annotation): + pytest.raises(TypeCheckError, check_type, StringIO(), annotation).match( + "_io.StringIO is not a binary I/O object" + ) + + def test_binary_real_file(self, tmp_path: Path): + with tmp_path.joinpath("testfile").open("wb") as f: + check_type(f, BinaryIO) + + @pytest.mark.parametrize( + "annotation", + [pytest.param(TextIO, id="direct"), pytest.param(IO[str], id="parametrized")], + ) + def test_text_valid(self, annotation): + check_type(StringIO(), annotation) + + @pytest.mark.parametrize( + "annotation", + [pytest.param(TextIO, id="direct"), pytest.param(IO[str], id="parametrized")], + ) + def test_text_fail(self, annotation): + pytest.raises(TypeCheckError, check_type, BytesIO(), annotation).match( + "_io.BytesIO is not a text based I/O object" + ) + + def test_text_real_file(self, tmp_path: Path): + with tmp_path.joinpath("testfile").open("w") as f: + check_type(f, TextIO) + + +class TestIntersectingProtocol: + SIT = TypeVar("SIT", covariant=True) + + class SizedIterable( + Sized, + Iterable[SIT], + Protocol[SIT], + ): ... + + @pytest.mark.parametrize( + "subject, predicate_type", + ( + pytest.param( + (), + SizedIterable, + id="empty_tuple_unspecialized", + ), + pytest.param( + range(2), + SizedIterable, + id="range", + ), + pytest.param( + (), + SizedIterable[int], + id="empty_tuple_int_specialized", + ), + pytest.param( + (1, 2, 3), + SizedIterable[int], + id="tuple_int_specialized", + ), + pytest.param( + ("1", "2", "3"), + SizedIterable[str], + id="tuple_str_specialized", + ), + ), + ) + def test_valid_member_passes(self, subject: object, predicate_type: type) -> None: + for _ in range(2): # Makes sure that the cache is also exercised + check_type(subject, predicate_type) + + xfail_nested_protocol_checks = pytest.mark.xfail( + reason="false negative due to missing support for nested protocol checks", + ) + + @pytest.mark.parametrize( + "subject, predicate_type", + ( + pytest.param( + (1 for _ in ()), + SizedIterable, + id="generator", + ), + pytest.param( + range(2), + SizedIterable[str], + marks=xfail_nested_protocol_checks, + id="range_str_specialized", + ), + pytest.param( + (1, 2, 3), + SizedIterable[str], + marks=xfail_nested_protocol_checks, + id="int_tuple_str_specialized", + ), + pytest.param( + ("1", "2", "3"), + SizedIterable[int], + marks=xfail_nested_protocol_checks, + id="str_tuple_int_specialized", + ), + ), + ) + def test_raises_for_non_member(self, subject: object, predicate_type: type) -> None: + with pytest.raises(TypeCheckError): + check_type(subject, predicate_type) + + +class TestProtocol: + @pytest.mark.parametrize( + "instantiate", + [pytest.param(True, id="instance"), pytest.param(False, id="class")], + ) + def test_success(self, typing_provider: Any, instantiate: bool) -> None: + class MyProtocol(Protocol): + member: int + + def noargs(self) -> None: + pass + + def posonlyargs(self, a: int, b: str, /) -> None: + pass + + def posargs(self, a: int, b: str, c: float = 2.0) -> None: + pass + + def varargs(self, *args: Any) -> None: + pass + + def varkwargs(self, **kwargs: Any) -> None: + pass + + def varbothargs(self, *args: Any, **kwargs: Any) -> None: + pass + + @staticmethod + def my_static_method(x: int, y: str) -> None: + pass + + @classmethod + def my_class_method(cls, x: int, y: str) -> None: + pass + + class Foo: + member = 1 + + def noargs(self, x: int = 1) -> None: + pass + + def posonlyargs(self, a: int, b: str, c: float = 2.0, /) -> None: + pass + + def posargs(self, *args: Any) -> None: + pass + + def varargs(self, *args: Any, kwarg: str = "foo") -> None: + pass + + def varkwargs(self, **kwargs: Any) -> None: + pass + + def varbothargs(self, *args: Any, **kwargs: Any) -> None: + pass + + # These were intentionally reversed, as this is OK for mypy + @classmethod + def my_static_method(cls, x: int, y: str) -> None: + pass + + @staticmethod + def my_class_method(x: int, y: str) -> None: + pass + + if instantiate: + check_type(Foo(), MyProtocol) + else: + check_type(Foo, type[MyProtocol]) + + @pytest.mark.parametrize( + "instantiate", + [pytest.param(True, id="instance"), pytest.param(False, id="class")], + ) + @pytest.mark.parametrize("subject_class", [object, str, Parent]) + def test_empty_protocol(self, instantiate: bool, subject_class: type[Any]): + class EmptyProtocol(Protocol): + pass + + if instantiate: + check_type(subject_class(), EmptyProtocol) + else: + check_type(subject_class, type[EmptyProtocol]) + + @pytest.mark.parametrize("has_member", [True, False]) + def test_member_checks(self, has_member: bool) -> None: + class MyProtocol(Protocol): + member: int + + class Foo: + def __init__(self, member: int): + if member: + self.member = member + + if has_member: + check_type(Foo(1), MyProtocol) + else: + pytest.raises(TypeCheckError, check_type, Foo(0), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because it has no attribute named " + f"'member'" + ) + + def test_missing_method(self) -> None: + class MyProtocol(Protocol): + def meth(self) -> None: + pass + + class Foo: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because it has no method named " + f"'meth'" + ) + + def test_too_many_posargs(self) -> None: + class MyProtocol(Protocol): + def meth(self) -> None: + pass + + class Foo: + def meth(self, x: str) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method has too " + f"many mandatory positional arguments" + ) + + def test_wrong_posarg_name(self) -> None: + class MyProtocol(Protocol): + def meth(self, x: str) -> None: + pass + + class Foo: + def meth(self, y: str) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + rf"^{qualified_name(Foo)} is not compatible with the " + rf"{MyProtocol.__qualname__} protocol because its 'meth' method has a " + rf"positional argument \(y\) that should be named 'x' at this position" + ) + + def test_too_few_posargs(self) -> None: + class MyProtocol(Protocol): + def meth(self, x: str) -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method has too " + f"few positional arguments" + ) + + def test_no_varargs(self) -> None: + class MyProtocol(Protocol): + def meth(self, *args: Any) -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method should " + f"accept variable positional arguments but doesn't" + ) + + def test_no_kwargs(self) -> None: + class MyProtocol(Protocol): + def meth(self, **kwargs: Any) -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method should " + f"accept variable keyword arguments but doesn't" + ) + + def test_missing_kwarg(self) -> None: + class MyProtocol(Protocol): + def meth(self, *, x: str) -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method is " + f"missing keyword-only arguments: x" + ) + + def test_extra_kwarg(self) -> None: + class MyProtocol(Protocol): + def meth(self) -> None: + pass + + class Foo: + def meth(self, *, x: str) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method has " + f"mandatory keyword-only arguments not present in the protocol: x" + ) + + def test_instance_staticmethod_mismatch(self) -> None: + class MyProtocol(Protocol): + @staticmethod + def meth() -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method should " + f"be a static method but it's an instance method" + ) + + def test_instance_classmethod_mismatch(self) -> None: + class MyProtocol(Protocol): + @classmethod + def meth(cls) -> None: + pass + + class Foo: + def meth(self) -> None: + pass + + pytest.raises(TypeCheckError, check_type, Foo(), MyProtocol).match( + f"^{qualified_name(Foo)} is not compatible with the " + f"{MyProtocol.__qualname__} protocol because its 'meth' method should " + f"be a class method but it's an instance method" + ) + + def test_builtin_signature_check(self) -> None: + class MyProtocol(Protocol): + def attr(self) -> None: + pass + + class Foo: + attr = timedelta + + # Foo.attr is incompatible but timedelta has not inspectable signature so the + # check is skipped + check_type(Foo(), MyProtocol) + + +class TestRecursiveType: + def test_valid(self): + check_type({"a": [1, 2, 3]}, JSONType) + + def test_fail(self): + with pytest.raises( + TypeCheckError, + match=( + "dict did not match any element in the union:\n" + " str: is not an instance of str\n" + " float: is neither float or int\n" + " bool: is not an instance of bool\n" + " NoneType: is not an instance of NoneType\n" + " List\\[JSONType\\]: is not a list\n" + " Dict\\[str, JSONType\\]: value of key 'a' did not match any element " + "in the union:\n" + " str: is not an instance of str\n" + " float: is neither float or int\n" + " bool: is not an instance of bool\n" + " NoneType: is not an instance of NoneType\n" + " List\\[JSONType\\]: is not a list\n" + " Dict\\[str, JSONType\\]: is not a dict" + ), + ): + check_type({"a": (1, 2, 3)}, JSONType) + + +class TestAnnotated: + def test_valid(self): + check_type("aa", Annotated[str, "blah"]) + + def test_fail(self): + pytest.raises(TypeCheckError, check_type, 1, Annotated[str, "blah"]).match( + "int is not an instance of str" + ) + + +class TestLiteralString: + def test_valid(self): + check_type("aa", LiteralString) + + def test_fail(self): + pytest.raises(TypeCheckError, check_type, 1, LiteralString).match( + "int is not an instance of str" + ) + + +class TestTypeGuard: + def test_valid(self): + check_type(True, TypeGuard) + + def test_fail(self): + pytest.raises(TypeCheckError, check_type, 1, TypeGuard).match( + "int is not an instance of bool" + ) + + + "policy, contextmanager", + [ + pytest.param(ForwardRefPolicy.ERROR, pytest.raises(NameError), id="error"), + pytest.param(ForwardRefPolicy.WARN, pytest.warns(TypeHintWarning), id="warn"), + pytest.param(ForwardRefPolicy.IGNORE, nullcontext(), id="ignore"), + ], +) +def test_forward_reference_policy( + policy: ForwardRefPolicy, contextmanager: ContextManager +): + with contextmanager: + check_type(1, ForwardRef("Foo"), forward_ref_policy=policy) # noqa: F821 + + +def test_any(): + assert check_type("aa", Any) == "aa" + + +def test_suppressed_checking(): + with suppress_type_checks(): + assert check_type("aa", int) == "aa" + + +def test_suppressed_checking_exception(): + with pytest.raises(RuntimeError), suppress_type_checks(): + assert check_type("aa", int) == "aa" + raise RuntimeError + + pytest.raises(TypeCheckError, check_type, "aa", int) + + +def test_any_subclass(): + class Foo(SubclassableAny): + pass + + check_type(Foo(), int) + + +def test_none(): + check_type(None, None) + + +def test_return_checked_value(): + value = {"foo": 1} + assert check_type(value, Dict[str, int]) is value + + +def test_imported_str_forward_ref(): + value = {"foo": 1} + memo = TypeCheckMemo(globals(), locals()) + pattern = r"Skipping type check against 'Dict\[str, int\]'" + with pytest.warns(TypeHintWarning, match=pattern): + check_type_internal(value, "Dict[str, int]", memo) + + +def test_check_against_tuple_success(): + check_type(1, (float, Union[str, int])) + + +def test_check_against_tuple_failure(): + pytest.raises(TypeCheckError, check_type, "aa", (int, bytes)) diff --git a/contrib/python/typeguard/tests/test_importhook.py b/contrib/python/typeguard/tests/test_importhook.py index b0f28278da0..7c8de7c8740 100644 --- a/contrib/python/typeguard/tests/test_importhook.py +++ b/contrib/python/typeguard/tests/test_importhook.py @@ -6,106 +6,40 @@ from pathlib import Path import pytest -from typeguard.importhook import TypeguardFinder, install_import_hook +from typeguard import TypeCheckError, TypeguardFinder, install_import_hook +from typeguard._importhook import OPTIMIZATION -import yatest.common as yc +pytestmark = pytest.mark.filterwarnings("error:no type annotations present") +this_dir = Path(__file__).parent +dummy_module_path = this_dir / "dummymodule.py" +cached_module_path = Path( + cache_from_source(str(dummy_module_path), optimization=OPTIMIZATION) +) -this_dir = Path(yc.test_source_path()) -dummy_module_path = this_dir / 'dummymodule.py' -cached_module_path = Path(cache_from_source(str(dummy_module_path), optimization='typeguard')) - [email protected](scope='module') -def dummymodule(): +def import_dummymodule(): if cached_module_path.exists(): cached_module_path.unlink() sys.path.insert(0, str(this_dir)) try: - with install_import_hook('dummymodule'): + with install_import_hook(["dummymodule"]): with warnings.catch_warnings(): - warnings.filterwarnings('error', module='typeguard') - module = import_module('dummymodule') + warnings.filterwarnings("error", module="typeguard") + module = import_module("dummymodule") return module finally: sys.path.remove(str(this_dir)) -def test_cached_module(dummymodule): - assert cached_module_path.is_file() - - -def test_type_checked_func(dummymodule): - assert dummymodule.type_checked_func(2, 3) == 6 - - -def test_type_checked_func_error(dummymodule): - pytest.raises(TypeError, dummymodule.type_checked_func, 2, '3').\ - match('"y" must be int; got str instead') - - -def test_non_type_checked_func(dummymodule): - assert dummymodule.non_type_checked_func('bah', 9) == 'foo' - - -def test_non_type_checked_decorated_func(dummymodule): - assert dummymodule.non_type_checked_decorated_func('bah', 9) == 'foo' - - -def test_typeguard_ignored_func(dummymodule): - assert dummymodule.non_typeguard_checked_func('bah', 9) == 'foo' - - -def test_type_checked_method(dummymodule): - instance = dummymodule.DummyClass() - pytest.raises(TypeError, instance.type_checked_method, 'bah', 9).\ - match('"x" must be int; got str instead') - - -def test_type_checked_classmethod(dummymodule): - pytest.raises(TypeError, dummymodule.DummyClass.type_checked_classmethod, 'bah', 9).\ - match('"x" must be int; got str instead') - - -def test_type_checked_staticmethod(dummymodule): - pytest.raises(TypeError, dummymodule.DummyClass.type_checked_classmethod, 'bah', 9).\ - match('"x" must be int; got str instead') - - [email protected]('argtype, returntype, error', [ - (int, str, None), - (str, str, '"x" must be str; got int instead'), - (int, int, 'type of the return value must be int; got str instead') -], ids=['correct', 'bad_argtype', 'bad_returntype']) -def test_dynamic_type_checking_func(dummymodule, argtype, returntype, error): - if error: - exc = pytest.raises(TypeError, dummymodule.dynamic_type_checking_func, 4, argtype, - returntype) - exc.match(error) - else: - assert dummymodule.dynamic_type_checking_func(4, argtype, returntype) == '4' - - -def test_class_in_function(dummymodule): - create_inner = dummymodule.outer() - retval = create_inner() - assert retval.__class__.__qualname__ == 'outer.<locals>.Inner' - - -def test_inner_class_method(dummymodule): - retval = dummymodule.Outer().create_inner() - assert retval.__class__.__qualname__ == 'Outer.Inner' - - -def test_inner_class_classmethod(dummymodule): - retval = dummymodule.Outer.create_inner_classmethod() - assert retval.__class__.__qualname__ == 'Outer.Inner' - - -def test_inner_class_staticmethod(dummymodule): - retval = dummymodule.Outer.create_inner_staticmethod() - assert retval.__class__.__qualname__ == 'Outer.Inner' +def test_blanket_import(): + dummymodule = import_dummymodule() + try: + pytest.raises(TypeCheckError, dummymodule.type_checked_func, 2, "3").match( + r'argument "y" \(str\) is not an instance of int' + ) + finally: + del sys.modules["dummymodule"] def test_package_name_matching(): @@ -123,3 +57,14 @@ def test_package_name_matching(): assert not finder.should_instrument("spam") assert not finder.should_instrument("ha") assert not finder.should_instrument("spam_eggs") + + [email protected](sys.version_info < (3, 9), reason="Requires ast.unparse()") +def test_debug_instrumentation(monkeypatch, capsys): + monkeypatch.setattr("typeguard.config.debug_instrumentation", True) + import_dummymodule() + out, err = capsys.readouterr() + path_str = str(dummy_module_path) + # в ya make "path_str" разрешается в подкаталог ~/.ya/build/build_root/... + assert f"{path_str!r} after instrumentation:"[1:] in err + assert "class DummyClass" in err diff --git a/contrib/python/typeguard/tests/test_instrumentation.py b/contrib/python/typeguard/tests/test_instrumentation.py new file mode 100644 index 00000000000..4d9929cbac8 --- /dev/null +++ b/contrib/python/typeguard/tests/test_instrumentation.py @@ -0,0 +1,419 @@ +import asyncio +import importlib +import sys +import warnings +from importlib import import_module +from importlib.util import cache_from_source +from pathlib import Path + +import pytest +from pytest import FixtureRequest + +from typeguard import TypeCheckError, install_import_hook, suppress_type_checks +from typeguard._importhook import OPTIMIZATION + +pytestmark = pytest.mark.filterwarnings("error:no type annotations present") +this_dir = Path(__file__).parent +dummy_module_path = this_dir / "dummymodule.py" +instrumented_cached_module_path = Path( + cache_from_source(str(dummy_module_path), optimization=OPTIMIZATION) +) +cached_module_path = Path(cache_from_source(str(dummy_module_path))) + +# This block here is to test the recipe mentioned in the user guide +if "pytest" in sys.modules: + from typeguard import typechecked +else: + from typing import TypeVar + + _T = TypeVar("_T") + + def typechecked(target: _T, **kwargs) -> _T: + return target if target else typechecked + + [email protected](scope="module", params=["typechecked", "importhook"]) +def method(request: FixtureRequest) -> str: + return request.param + + +def _fixture_module(name: str, method: str): + # config.debug_instrumentation = True + sys.path.insert(0, str(this_dir)) + try: + # sys.modules.pop(name, None) + if method == "typechecked": + if cached_module_path.exists(): + cached_module_path.unlink() + + if name in sys.modules: + module = import_module(name) + importlib.reload(module) + else: + module = import_module(name) + return module + + if instrumented_cached_module_path.exists(): + instrumented_cached_module_path.unlink() + + with install_import_hook([name]): + with warnings.catch_warnings(): + warnings.filterwarnings("error", module="typeguard") + if name in sys.modules: + module = import_module(name) + importlib.reload(module) + else: + module = import_module(name) + return module + finally: + sys.path.remove(str(this_dir)) + + [email protected](scope="module") +def dummymodule(method: str): + return _fixture_module("dummymodule", method) + + [email protected](scope="module") +def deferredannos(method: str): + if sys.version_info < (3, 14): + raise pytest.skip("Deferred annotations are only supported in Python 3.14+") + + return _fixture_module("deferredannos", method) + + [email protected](scope="module") +def pep695(method: str): + if sys.version_info < (3, 12): + raise pytest.skip("PEP 695 type parameter syntax requires Python 3.12+") + + return _fixture_module("pep695", method) + + +def test_type_checked_func(dummymodule): + assert dummymodule.type_checked_func(2, 3) == 6 + + +def test_type_checked_func_error(dummymodule): + pytest.raises(TypeCheckError, dummymodule.type_checked_func, 2, "3").match( + r'argument "y" \(str\) is not an instance of int' + ) + + +def test_non_type_checked_func(dummymodule): + assert dummymodule.non_type_checked_func("bah", 9) == "foo" + + +def test_non_type_checked_decorated_func(dummymodule): + assert dummymodule.non_type_checked_func("bah", 9) == "foo" + + +def test_typeguard_ignored_func(dummymodule): + assert dummymodule.non_type_checked_func("bah", 9) == "foo" + + +def test_type_checked_method(dummymodule): + instance = dummymodule.DummyClass() + pytest.raises(TypeCheckError, instance.type_checked_method, "bah", 9).match( + r'argument "x" \(str\) is not an instance of int' + ) + + +def test_type_checked_classmethod(dummymodule): + pytest.raises( + TypeCheckError, dummymodule.DummyClass.type_checked_classmethod, "bah", 9 + ).match(r'argument "x" \(str\) is not an instance of int') + + +def test_type_checked_staticmethod(dummymodule): + pytest.raises( + TypeCheckError, dummymodule.DummyClass.type_checked_staticmethod, "bah", 9 + ).match(r'argument "x" \(str\) is not an instance of int') + + [email protected](reason="No workaround for this has been implemented yet") +def test_inner_class_method(dummymodule): + retval = dummymodule.Outer().create_inner() + assert retval.__class__.__qualname__ == "Outer.Inner" + + [email protected](reason="No workaround for this has been implemented yet") +def test_inner_class_classmethod(dummymodule): + retval = dummymodule.Outer.create_inner_classmethod() + assert retval.__class__.__qualname__ == "Outer.Inner" + + [email protected](reason="No workaround for this has been implemented yet") +def test_inner_class_staticmethod(dummymodule): + retval = dummymodule.Outer.create_inner_staticmethod() + assert retval.__class__.__qualname__ == "Outer.Inner" + + +def test_local_class_instance(dummymodule): + instance = dummymodule.create_local_class_instance() + assert ( + instance.__class__.__qualname__ == "create_local_class_instance.<locals>.Inner" + ) + + +def test_contextmanager(dummymodule): + with dummymodule.dummy_context_manager() as value: + assert value == 1 + + +def test_overload(dummymodule): + dummymodule.overloaded_func(1) + dummymodule.overloaded_func("x") + pytest.raises(TypeCheckError, dummymodule.overloaded_func, b"foo") + + +def test_async_func(dummymodule): + pytest.raises(TypeCheckError, asyncio.run, dummymodule.async_func(b"foo")) + + +def test_generator_valid(dummymodule): + gen = dummymodule.generator_func(6, "foo") + assert gen.send(None) == 6 + try: + gen.send(None) + except StopIteration as exc: + assert exc.value == "foo" + else: + pytest.fail("Generator did not exit") + + +def test_generator_bad_yield_type(dummymodule): + gen = dummymodule.generator_func("foo", "foo") + pytest.raises(TypeCheckError, gen.send, None).match( + r"yielded value \(str\) is not an instance of int" + ) + gen.close() + + +def test_generator_bad_return_type(dummymodule): + gen = dummymodule.generator_func(6, 6) + assert gen.send(None) == 6 + pytest.raises(TypeCheckError, gen.send, None).match( + r"return value \(int\) is not an instance of str" + ) + gen.close() + + +def test_asyncgen_valid(dummymodule): + gen = dummymodule.asyncgen_func(6) + assert asyncio.run(gen.asend(None)) == 6 + + +def test_asyncgen_bad_yield_type(dummymodule): + gen = dummymodule.asyncgen_func("foo") + pytest.raises(TypeCheckError, asyncio.run, gen.asend(None)).match( + r"yielded value \(str\) is not an instance of int" + ) + + +def test_missing_return(dummymodule): + pytest.raises(TypeCheckError, dummymodule.missing_return).match( + r"the return value \(None\) is not an instance of int" + ) + + +def test_pep_604_union_args(dummymodule): + pytest.raises(TypeCheckError, dummymodule.pep_604_union_args, 1.1).match( + r'argument "x" \(float\) did not match any element in the union:' + r"\n Callable\[list, Literal\[-1\]\]: is not callable" + r"\n Callable\[ellipsis, Union\[int, str\]\]: is not callable" + ) + + +def test_pep_604_union_retval(dummymodule): + pytest.raises(TypeCheckError, dummymodule.pep_604_union_retval, 1.1).match( + r"the return value \(float\) did not match any element in the union:" + r"\n str: is not an instance of str" + r"\n int: is not an instance of int" + ) + + +def test_builtin_generic_collections(dummymodule): + pytest.raises(TypeCheckError, dummymodule.builtin_generic_collections, 1.1).match( + r'argument "x" \(float\) is not a list' + ) + + +def test_paramspec(dummymodule): + def foo(a: int, b: str, *, c: bytes) -> None: + pass + + dummymodule.paramspec_function(foo, (1, "bar"), {"c": b"abc"}) + + +def test_augmented_assign(dummymodule): + assert dummymodule.aug_assign() == 2 + + +def test_multi_assign_single_value(dummymodule): + assert dummymodule.multi_assign_single_value() == (6, 6, 6) + + +def test_multi_assign_iterable(dummymodule): + assert dummymodule.multi_assign_iterable() == ([6, 7], [6, 7], [6, 7]) + + +def test_unpacking_assign(dummymodule): + assert dummymodule.unpacking_assign() == (1, "foo") + + +def test_unpacking_assign_from_generator(dummymodule): + assert dummymodule.unpacking_assign_generator() == (1, "foo") + + +def test_unpacking_assign_star_with_annotation(dummymodule): + assert dummymodule.unpacking_assign_star_with_annotation() == ( + 1, + [b"abc", b"bah"], + "foo", + ) + + +def test_unpacking_assign_star_no_annotation_success(dummymodule): + assert dummymodule.unpacking_assign_star_no_annotation( + (1, b"abc", b"bah", "foo") + ) == ( + 1, + [b"abc", b"bah"], + "foo", + ) + + +def test_attribute_assign_unpacking(dummymodule): + foo = dummymodule.DummyClass() + dummymodule.attribute_assign_unpacking(foo) + + +def test_unpacking_assign_star_no_annotation_fail(dummymodule): + with pytest.raises( + TypeCheckError, match=r"value assigned to z \(bytes\) is not an instance of str" + ): + dummymodule.unpacking_assign_star_no_annotation((1, b"abc", b"bah", b"foo")) + + +class TestOptionsOverride: + def test_forward_ref_policy(self, dummymodule): + with pytest.raises(NameError, match="name 'NonexistentType' is not defined"): + dummymodule.override_forward_ref_policy(6) + + def test_typecheck_fail_callback(self, dummymodule, capsys): + dummymodule.override_typecheck_fail_callback("foo") + assert capsys.readouterr().out == ( + 'argument "value" (str) is not an instance of int\n' + ) + + def test_override_collection_check_strategy(self, dummymodule): + with pytest.raises( + TypeCheckError, + match=r'item 1 of argument "value" \(list\) is not an instance of int', + ): + dummymodule.override_collection_check_strategy([1, "foo"]) + + def test_outer_class_typecheck_fail_callback(self, dummymodule, capsys): + dummymodule.OverrideClass().override_typecheck_fail_callback("foo") + assert capsys.readouterr().out == ( + 'argument "value" (str) is not an instance of int\n' + ) + + def test_inner_class_no_overrides(self, dummymodule): + with pytest.raises(TypeCheckError): + dummymodule.OverrideClass.Inner().override_typecheck_fail_callback("foo") + + +class TestVariableArguments: + def test_success(self, dummymodule): + assert dummymodule.typed_variable_args("foo", "bar", a=1, b=8) == ( + ("foo", "bar"), + {"a": 1, "b": 8}, + ) + + def test_args_fail(self, dummymodule): + with pytest.raises( + TypeCheckError, + match=r'item 0 of argument "args" \(tuple\) is not an instance of str', + ): + dummymodule.typed_variable_args(1, a=1, b=8) + + def test_kwargs_fail(self, dummymodule): + with pytest.raises( + TypeCheckError, + match=r'value of key \'a\' of argument "kwargs" \(dict\) is not an ' + r"instance of int", + ): + dummymodule.typed_variable_args("foo", "bar", a="baz") + + +class TestGuardedType: + def test_plain(self, dummymodule): + assert dummymodule.guarded_type_hint_plain("foo") == "foo" + + def test_subscript_toplevel(self, dummymodule): + assert dummymodule.guarded_type_hint_subscript_toplevel("foo") == "foo" + + def test_subscript_nested(self, dummymodule): + assert dummymodule.guarded_type_hint_subscript_nested(["foo"]) == ["foo"] + + +def test_literal(dummymodule): + assert dummymodule.literal("foo") == "foo" + + +def test_literal_in_union(dummymodule): + """Regression test for #372.""" + assert dummymodule.literal_in_union("foo") == "foo" + + +def test_typevar_forwardref(dummymodule): + print(f"id of typevar_forwardref: {id(dummymodule.typevar_forwardref):x}") + instance = dummymodule.typevar_forwardref(dummymodule.DummyClass) + assert isinstance(instance, dummymodule.DummyClass) + + +def test_suppress_annotated_assignment(dummymodule): + with suppress_type_checks(): + assert dummymodule.literal_in_union("foo") == "foo" + + +def test_suppress_annotated_multi_assignment(dummymodule): + with suppress_type_checks(): + assert dummymodule.multi_assign_single_value() == (6, 6, 6) + + +class TestUsesForwardRef: + def test_success(self, deferredannos): + obj = deferredannos.NotYetDefined() + assert deferredannos.uses_forwardref(obj) is obj + + def test_failure(self, deferredannos): + with pytest.raises( + TypeCheckError, + match=r'argument "x" \(int\) is not an instance of deferredannos.NotYetDefined', + ): + deferredannos.uses_forwardref(1) + + +class TestParametrized: + def test_success_func(self, pep695): + assert pep695.parametrized_func(1, "2") == 1 + + def test_success_method(self, pep695): + assert pep695.ParametrizedClass[int]().method(1, "2") == 1 + + def test_failure_func(self, pep695): + with pytest.raises( + TypeCheckError, + match=r'argument "y" \(int\) is not an instance of str', + ): + pep695.parametrized_func(1, 2) + + def test_failure_method(self, pep695): + with pytest.raises( + TypeCheckError, + match=r'argument "y" \(int\) is not an instance of str', + ): + pep695.ParametrizedClass[int]().method("str", 2) diff --git a/contrib/python/typeguard/tests/test_plugins.py b/contrib/python/typeguard/tests/test_plugins.py new file mode 100644 index 00000000000..f01a07419b8 --- /dev/null +++ b/contrib/python/typeguard/tests/test_plugins.py @@ -0,0 +1,26 @@ +from pytest import MonkeyPatch + +from typeguard import load_plugins + + +def test_custom_type_checker(monkeypatch: MonkeyPatch) -> None: + def lookup_func(origin_type, args, extras): + pass + + class FakeEntryPoint: + name = "test" + + def load(self): + return lookup_func + + def fake_entry_points(group): + assert group == "typeguard.checker_lookup" + return [FakeEntryPoint()] + + checker_lookup_functions = [] + monkeypatch.setattr("typeguard._checkers.entry_points", fake_entry_points) + monkeypatch.setattr( + "typeguard._checkers.checker_lookup_functions", checker_lookup_functions + ) + load_plugins() + assert checker_lookup_functions[0] is lookup_func diff --git a/contrib/python/typeguard/tests/test_pytest_plugin.py b/contrib/python/typeguard/tests/test_pytest_plugin.py new file mode 100644 index 00000000000..0c5b04d0ba6 --- /dev/null +++ b/contrib/python/typeguard/tests/test_pytest_plugin.py @@ -0,0 +1,77 @@ +from textwrap import dedent + +import pytest +from pytest import MonkeyPatch, Pytester + +from typeguard import CollectionCheckStrategy, ForwardRefPolicy, TypeCheckConfiguration + + +def config(monkeypatch: MonkeyPatch) -> TypeCheckConfiguration: + config = TypeCheckConfiguration() + monkeypatch.setattr("typeguard._pytest_plugin.global_config", config) + return config + + +def test_config_options(pytester: Pytester, config: TypeCheckConfiguration) -> None: + pytester.makepyprojecttoml( + ''' + [tool.pytest.ini_options] + typeguard-packages = """ + mypackage + otherpackage""" + typeguard-debug-instrumentation = true + typeguard-typecheck-fail-callback = "mypackage:failcallback" + typeguard-forward-ref-policy = "ERROR" + typeguard-collection-check-strategy = "ALL_ITEMS" + ''' + ) + pytester.makepyfile( + mypackage=( + dedent( + """ + def failcallback(): + pass + """ + ) + ) + ) + + pytester.plugins = ["typeguard"] + pytester.syspathinsert() + pytestconfig = pytester.parseconfigure() + assert pytestconfig.getini("typeguard-packages") == ["mypackage", "otherpackage"] + assert config.typecheck_fail_callback.__name__ == "failcallback" + assert config.debug_instrumentation is True + assert config.forward_ref_policy is ForwardRefPolicy.ERROR + assert config.collection_check_strategy is CollectionCheckStrategy.ALL_ITEMS + + +def test_commandline_options( + pytester: Pytester, config: TypeCheckConfiguration +) -> None: + pytester.makepyfile( + mypackage=( + dedent( + """ + def failcallback(): + pass + """ + ) + ) + ) + + pytester.plugins = ["typeguard"] + pytester.syspathinsert() + pytestconfig = pytester.parseconfigure( + "--typeguard-packages=mypackage,otherpackage", + "--typeguard-typecheck-fail-callback=mypackage:failcallback", + "--typeguard-debug-instrumentation", + "--typeguard-forward-ref-policy=ERROR", + "--typeguard-collection-check-strategy=ALL_ITEMS", + ) + assert pytestconfig.getoption("typeguard_packages") == "mypackage,otherpackage" + assert config.typecheck_fail_callback.__name__ == "failcallback" + assert config.debug_instrumentation is True + assert config.forward_ref_policy is ForwardRefPolicy.ERROR + assert config.collection_check_strategy is CollectionCheckStrategy.ALL_ITEMS diff --git a/contrib/python/typeguard/tests/test_suppression.py b/contrib/python/typeguard/tests/test_suppression.py new file mode 100644 index 00000000000..47c433c8a28 --- /dev/null +++ b/contrib/python/typeguard/tests/test_suppression.py @@ -0,0 +1,68 @@ +import pytest + +from typeguard import TypeCheckError, check_type, suppress_type_checks, typechecked + + +def test_contextmanager_typechecked(): + @typechecked + def foo(x: str) -> None: + pass + + with suppress_type_checks(): + foo(1) + + +def test_contextmanager_check_type(): + with suppress_type_checks(): + check_type(1, str) + + +def test_contextmanager_nesting(): + with suppress_type_checks(), suppress_type_checks(): + check_type(1, str) + + pytest.raises(TypeCheckError, check_type, 1, str) + + +def test_contextmanager_exception(): + """ + Test that type check suppression stops even if an exception is raised within the + context manager block. + + """ + with pytest.raises(RuntimeError): + with suppress_type_checks(): + raise RuntimeError + + pytest.raises(TypeCheckError, check_type, 1, str) + + +@suppress_type_checks +def test_decorator_typechecked(): + @typechecked + def foo(x: str) -> None: + pass + + foo(1) + + +@suppress_type_checks +def test_decorator_check_type(): + check_type(1, str) + + +def test_decorator_exception(): + """ + Test that type check suppression stops even if an exception is raised from a + decorated function. + + """ + + @suppress_type_checks + def foo(): + raise RuntimeError + + with pytest.raises(RuntimeError): + foo() + + pytest.raises(TypeCheckError, check_type, 1, str) diff --git a/contrib/python/typeguard/tests/test_transformer.py b/contrib/python/typeguard/tests/test_transformer.py new file mode 100644 index 00000000000..3fb04627c9c --- /dev/null +++ b/contrib/python/typeguard/tests/test_transformer.py @@ -0,0 +1,1972 @@ +import sys +from ast import parse, unparse +from textwrap import dedent + +import pytest + +from typeguard._transformer import TypeguardTransformer + + +def test_arguments_only() -> None: + node = parse( + dedent( + """ + def foo(x: int) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + + def foo(x: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + """ + ).strip() + ) + + +def test_return_only() -> None: + node = parse( + dedent( + """ + def foo(x) -> int: + return 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type + + def foo(x) -> int: + memo = TypeCheckMemo(globals(), locals()) + return check_return_type('foo', 6, int, memo) + """ + ).strip() + ) + + +class TestGenerator: + def test_yield(self) -> None: + node = parse( + dedent( + """ + from collections.abc import Generator + from typing import Any + + def foo(x) -> Generator[int, Any, str]: + yield 2 + yield 6 + return 'test' + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type, check_yield_type + from collections.abc import Generator + from typing import Any + + def foo(x) -> Generator[int, Any, str]: + memo = TypeCheckMemo(globals(), locals()) + yield check_yield_type('foo', 2, int, memo) + yield check_yield_type('foo', 6, int, memo) + return check_return_type('foo', 'test', str, memo) + """ + ).strip() + ) + + def test_no_return_type_check(self) -> None: + node = parse( + dedent( + """ + from collections.abc import Generator + + def foo(x) -> Generator[int, None, None]: + yield 2 + yield 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_send_type, check_yield_type + from collections.abc import Generator + + def foo(x) -> Generator[int, None, None]: + memo = TypeCheckMemo(globals(), locals()) + check_send_type('foo', (yield check_yield_type('foo', 2, int, \ +memo)), None, memo) + check_send_type('foo', (yield check_yield_type('foo', 6, int, \ +memo)), None, memo) + """ + ).strip() + ) + + def test_no_send_type_check(self) -> None: + node = parse( + dedent( + """ + from typing import Any + from collections.abc import Generator + + def foo(x) -> Generator[int, Any, Any]: + yield 2 + yield 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_yield_type + from typing import Any + from collections.abc import Generator + + def foo(x) -> Generator[int, Any, Any]: + memo = TypeCheckMemo(globals(), locals()) + yield check_yield_type('foo', 2, int, memo) + yield check_yield_type('foo', 6, int, memo) + """ + ).strip() + ) + + +class TestAsyncGenerator: + def test_full(self) -> None: + node = parse( + dedent( + """ + from collections.abc import AsyncGenerator + + async def foo(x) -> AsyncGenerator[int, None]: + yield 2 + yield 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_send_type, check_yield_type + from collections.abc import AsyncGenerator + + async def foo(x) -> AsyncGenerator[int, None]: + memo = TypeCheckMemo(globals(), locals()) + check_send_type('foo', (yield check_yield_type('foo', 2, int, \ +memo)), None, memo) + check_send_type('foo', (yield check_yield_type('foo', 6, int, \ +memo)), None, memo) + """ + ).strip() + ) + + def test_no_yield_type_check(self) -> None: + node = parse( + dedent( + """ + from typing import Any + from collections.abc import AsyncGenerator + + async def foo() -> AsyncGenerator[Any, None]: + yield 2 + yield 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_send_type + from typing import Any + from collections.abc import AsyncGenerator + + async def foo() -> AsyncGenerator[Any, None]: + memo = TypeCheckMemo(globals(), locals()) + check_send_type('foo', (yield 2), None, memo) + check_send_type('foo', (yield 6), None, memo) + """ + ).strip() + ) + + def test_no_send_type_check(self) -> None: + node = parse( + dedent( + """ + from typing import Any + from collections.abc import AsyncGenerator + + async def foo() -> AsyncGenerator[int, Any]: + yield 2 + yield 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_yield_type + from typing import Any + from collections.abc import AsyncGenerator + + async def foo() -> AsyncGenerator[int, Any]: + memo = TypeCheckMemo(globals(), locals()) + yield check_yield_type('foo', 2, int, memo) + yield check_yield_type('foo', 6, int, memo) + """ + ).strip() + ) + + +def test_pass_only() -> None: + node = parse( + dedent( + """ + def foo(x) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + def foo(x) -> None: + pass + """ + ).strip() + ) + + + "import_line, decorator", + [ + pytest.param("from typing import no_type_check", "@no_type_check"), + pytest.param("from typeguard import typeguard_ignore", "@typeguard_ignore"), + pytest.param("import typing", "@typing.no_type_check"), + pytest.param("import typeguard", "@typeguard.typeguard_ignore"), + ], +) +def test_no_type_check_decorator(import_line: str, decorator: str) -> None: + node = parse( + dedent( + f""" + {import_line} + + {decorator} + def foo(x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + f""" + {import_line} + + {decorator} + def foo(x: int) -> int: + return x + """ + ).strip() + ) + + + "import_line, annotation", + [ + pytest.param("from typing import Any", "Any"), + pytest.param("from typing import Any as AlterAny", "AlterAny"), + pytest.param("from typing_extensions import Any", "Any"), + pytest.param("from typing_extensions import Any as AlterAny", "AlterAny"), + pytest.param("import typing", "typing.Any"), + pytest.param("import typing as typing_alter", "typing_alter.Any"), + pytest.param("import typing_extensions as typing_alter", "typing_alter.Any"), + ], +) +def test_any_only(import_line: str, annotation: str) -> None: + node = parse( + dedent( + f""" + {import_line} + + def foo(x, y: {annotation}) -> {annotation}: + return 1 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + f""" + {import_line} + + def foo(x, y: {annotation}) -> {annotation}: + return 1 + """ + ).strip() + ) + + +def test_any_in_union() -> None: + node = parse( + dedent( + """ + from typing import Any, Union + + def foo(x, y: Union[Any, None]) -> Union[Any, None]: + return 1 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any, Union + + def foo(x, y: Union[Any, None]) -> Union[Any, None]: + return 1 + """ + ).strip() + ) + + +def test_any_in_pep_604_union() -> None: + node = parse( + dedent( + """ + from typing import Any + + def foo(x, y: Any | None) -> Any | None: + return 1 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any + + def foo(x, y: Any | None) -> Any | None: + return 1 + """ + ).strip() + ) + + +def test_any_in_nested_dict() -> None: + # Regression test for #373 + node = parse( + dedent( + """ + from typing import Any + + def foo(x: dict[str, dict[str, Any]]) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + from typing import Any + + def foo(x: dict[str, dict[str, Any]]) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, dict[str, dict[str, Any]])}, memo) + """ + ).strip() + ) + + +def test_avoid_global_names() -> None: + node = parse( + dedent( + """ + memo = TypeCheckMemo = check_argument_types = check_return_type = None + + def func1(x: int) -> int: + dummy = (memo,) + return x + + def func2(x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo as TypeCheckMemo_ + from typeguard._functions import \ +check_argument_types as check_argument_types_, check_return_type as check_return_type_ + memo = TypeCheckMemo = check_argument_types = check_return_type = None + + def func1(x: int) -> int: + memo_ = TypeCheckMemo_(globals(), locals()) + check_argument_types_('func1', {'x': (x, int)}, memo_) + dummy = (memo,) + return check_return_type_('func1', x, int, memo_) + + def func2(x: int) -> int: + memo_ = TypeCheckMemo_(globals(), locals()) + check_argument_types_('func2', {'x': (x, int)}, memo_) + return check_return_type_('func2', x, int, memo_) + """ + ).strip() + ) + + +def test_avoid_local_names() -> None: + node = parse( + dedent( + """ + def foo(x: int) -> int: + memo = TypeCheckMemo = check_argument_types = check_return_type = None + return x + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + def foo(x: int) -> int: + from typeguard import TypeCheckMemo as TypeCheckMemo_ + from typeguard._functions import \ +check_argument_types as check_argument_types_, check_return_type as check_return_type_ + memo_ = TypeCheckMemo_(globals(), locals()) + check_argument_types_('foo', {'x': (x, int)}, memo_) + memo = TypeCheckMemo = check_argument_types = check_return_type = None + return check_return_type_('foo', x, int, memo_) + """ + ).strip() + ) + + +def test_avoid_nonlocal_names() -> None: + node = parse( + dedent( + """ + def outer(): + memo = TypeCheckMemo = check_argument_types = check_return_type = None + + def foo(x: int) -> int: + return x + + return foo + """ + ) + ) + TypeguardTransformer(["outer", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + def outer(): + memo = TypeCheckMemo = check_argument_types = check_return_type = None + + def foo(x: int) -> int: + from typeguard import TypeCheckMemo as TypeCheckMemo_ + from typeguard._functions import \ +check_argument_types as check_argument_types_, check_return_type as check_return_type_ + memo_ = TypeCheckMemo_(globals(), locals()) + check_argument_types_('outer.<locals>.foo', {'x': (x, int)}, memo_) + return check_return_type_('outer.<locals>.foo', x, int, memo_) + return foo + """ + ).strip() + ) + + +def test_method() -> None: + node = parse( + dedent( + """ + class Foo: + def foo(self, x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer(["Foo", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + class Foo: + + def foo(self, x: int) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals(), self_type=self.__class__) + check_argument_types('Foo.foo', {'x': (x, int)}, memo) + return check_return_type('Foo.foo', x, int, memo) + """ + ).strip() + ) + + +def test_method_posonlyargs() -> None: + node = parse( + dedent( + """ + class Foo: + def foo(self, x: int, /, y: str) -> int: + return x + """ + ) + ) + TypeguardTransformer(["Foo", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + class Foo: + + def foo(self, x: int, /, y: str) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals(), self_type=self.__class__) + check_argument_types('Foo.foo', {'x': (x, int), 'y': (y, str)}, memo) + return check_return_type('Foo.foo', x, int, memo) + """ + ).strip() + ) + + +def test_classmethod() -> None: + node = parse( + dedent( + """ + class Foo: + @classmethod + def foo(cls, x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer(["Foo", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + class Foo: + + @classmethod + def foo(cls, x: int) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals(), self_type=cls) + check_argument_types('Foo.foo', {'x': (x, int)}, memo) + return check_return_type('Foo.foo', x, int, memo) + """ + ).strip() + ) + + +def test_classmethod_posonlyargs() -> None: + node = parse( + dedent( + """ + class Foo: + @classmethod + def foo(cls, x: int, /, y: str) -> int: + return x + """ + ) + ) + TypeguardTransformer(["Foo", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + class Foo: + + @classmethod + def foo(cls, x: int, /, y: str) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals(), self_type=cls) + check_argument_types('Foo.foo', {'x': (x, int), 'y': (y, str)}, \ +memo) + return check_return_type('Foo.foo', x, int, memo) + """ + ).strip() + ) + + +def test_staticmethod() -> None: + node = parse( + dedent( + """ + class Foo: + @staticmethod + def foo(x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer(["Foo", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + class Foo: + + @staticmethod + def foo(x: int) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('Foo.foo', {'x': (x, int)}, memo) + return check_return_type('Foo.foo', x, int, memo) + """ + ).strip() + ) + + +def test_new_with_self() -> None: + node = parse( + dedent( + """ + from typing import Self + + class Foo: + def __new__(cls) -> Self: + return super().__new__(cls) + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type + from typing import Self + + class Foo: + + def __new__(cls) -> Self: + Foo = cls + memo = TypeCheckMemo(globals(), locals(), self_type=cls) + return check_return_type('Foo.__new__', super().__new__(cls), \ +Self, memo) + """ + ).strip() + ) + + +def test_new_with_explicit_class_name() -> None: + # Regression test for #398 + node = parse( + dedent( + """ + class A: + + def __new__(cls) -> 'A': + return object.__new__(cls) + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type + + class A: + + def __new__(cls) -> 'A': + A = cls + memo = TypeCheckMemo(globals(), locals(), self_type=cls) + return check_return_type('A.__new__', object.__new__(cls), A, memo) + """ + ).strip() + ) + + +def test_local_function() -> None: + node = parse( + dedent( + """ + def wrapper(): + def foo(x: int) -> int: + return x + + def foo2(x: int) -> int: + return x + + return foo + """ + ) + ) + TypeguardTransformer(["wrapper", "foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + def wrapper(): + + def foo(x: int) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('wrapper.<locals>.foo', {'x': (x, int)}, memo) + return check_return_type('wrapper.<locals>.foo', x, int, memo) + + def foo2(x: int) -> int: + return x + return foo + """ + ).strip() + ) + + +def test_function_local_class_method() -> None: + node = parse( + dedent( + """ + def wrapper(): + + class Foo: + + class Bar: + + def method(self, x: int) -> int: + return x + + def method2(self, x: int) -> int: + return x + """ + ) + ) + TypeguardTransformer(["wrapper", "Foo", "Bar", "method"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + def wrapper(): + + class Foo: + + class Bar: + + def method(self, x: int) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_return_type + memo = TypeCheckMemo(globals(), locals(), \ +self_type=self.__class__) + check_argument_types('wrapper.<locals>.Foo.Bar.method', \ +{'x': (x, int)}, memo) + return check_return_type(\ +'wrapper.<locals>.Foo.Bar.method', x, int, memo) + + def method2(self, x: int) -> int: + return x + """ + ).strip() + ) + + +def test_keyword_only_argument() -> None: + node = parse( + dedent( + """ + def foo(*, x: int) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + + def foo(*, x: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + """ + ).strip() + ) + + +def test_positional_only_argument() -> None: + node = parse( + dedent( + """ + def foo(x: int, /) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + + def foo(x: int, /) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + """ + ).strip() + ) + + +def test_variable_positional_argument() -> None: + node = parse( + dedent( + """ + def foo(*args: int) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + + def foo(*args: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'args': (args, tuple[int, ...])}, memo) + """ + ).strip() + ) + + +def test_variable_keyword_argument() -> None: + node = parse( + dedent( + """ + def foo(**kwargs: int) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + + def foo(**kwargs: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'kwargs': (kwargs, dict[str, int])}, memo) + """ + ).strip() + ) + + +class TestTypecheckingImport: + """ + Test that annotations imported conditionally on typing.TYPE_CHECKING are not used in + run-time checks. + """ + + def test_direct_references(self) -> None: + node = parse( + dedent( + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + import typing + from typing import Hashable, Sequence + + def foo(x: Hashable, y: typing.Collection, *args: Hashable, \ +**kwargs: typing.Collection) -> Sequence: + bar: typing.Collection + baz: Hashable = 1 + return (1, 2) + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + import typing + from typing import Hashable, Sequence + + def foo(x: Hashable, y: typing.Collection, *args: Hashable, \ +**kwargs: typing.Collection) -> Sequence: + bar: typing.Collection + baz: Hashable = 1 + return (1, 2) + """ + ).strip() + ) + + def test_collection_parameter(self) -> None: + node = parse( + dedent( + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from nonexistent import FooBar + + def foo(x: list[FooBar]) -> list[FooBar]: + return x + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, check_return_type + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from nonexistent import FooBar + + def foo(x: list[FooBar]) -> list[FooBar]: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, list)}, memo) + return check_return_type('foo', x, list, memo) + """ + ).strip() + ) + + def test_variable_annotations(self) -> None: + node = parse( + dedent( + """ + from typing import Any, TYPE_CHECKING + if TYPE_CHECKING: + from nonexistent import FooBar + + def foo(x: Any) -> None: + y: FooBar = x + z: list[FooBar] = [y] + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Any, TYPE_CHECKING + if TYPE_CHECKING: + from nonexistent import FooBar + + def foo(x: Any) -> None: + memo = TypeCheckMemo(globals(), locals()) + y: FooBar = x + z: list[FooBar] = check_variable_assignment([y], [[('z', list)]], \ +memo) + """ + ).strip() + ) + + def test_generator_function(self) -> None: + node = parse( + dedent( + """ + from typing import Any, TYPE_CHECKING + from collections.abc import Generator + if TYPE_CHECKING: + import typing + from typing import Hashable, Sequence + + def foo(x: Hashable, y: typing.Collection) -> Generator[Hashable, \ +typing.Collection, Sequence]: + yield 'foo' + return (1, 2) + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any, TYPE_CHECKING + from collections.abc import Generator + if TYPE_CHECKING: + import typing + from typing import Hashable, Sequence + + def foo(x: Hashable, y: typing.Collection) -> Generator[Hashable, \ +typing.Collection, Sequence]: + yield 'foo' + return (1, 2) + """ + ).strip() + ) + + def test_optional(self) -> None: + node = parse( + dedent( + """ + from typing import Any, Optional, TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Optional[Hashable]) -> Optional[Hashable]: + return x + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any, Optional, TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Optional[Hashable]) -> Optional[Hashable]: + return x + """ + ).strip() + ) + + def test_optional_nested(self) -> None: + node = parse( + dedent( + """ + from typing import Any, List, Optional + + def foo(x: List[Optional[int]]) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + from typing import Any, List, Optional + + def foo(x: List[Optional[int]]) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, List[Optional[int]])}, memo) + """ + ).strip() + ) + + def test_subscript_within_union(self) -> None: + # Regression test for #397 + node = parse( + dedent( + """ + from typing import Any, Iterable, Union, TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Union[Iterable[Hashable], str]) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + from typing import Any, Iterable, Union, TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Union[Iterable[Hashable], str]) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, Union[Iterable, str])}, memo) + """ + ).strip() + ) + + def test_pep604_union(self) -> None: + node = parse( + dedent( + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Hashable | str) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import TYPE_CHECKING + if TYPE_CHECKING: + from typing import Hashable + + def foo(x: Hashable | str) -> None: + pass + """ + ).strip() + ) + + +class TestAssign: + def test_annotated_assign(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x: int = otherfunc() + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int = check_variable_assignment(otherfunc(), [[('x', int)]], \ +memo) + """ + ).strip() + ) + + def test_varargs_assign(self) -> None: + node = parse( + dedent( + """ + def foo(*args: int) -> None: + args = (5,) + """ + ) + ) + TypeguardTransformer().visit(node) + + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_variable_assignment + + def foo(*args: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'args': (args, \ +tuple[int, ...])}, memo) + args = check_variable_assignment((5,), \ +[[('args', tuple[int, ...])]], memo) + """ + ).strip() + ) + + def test_kwargs_assign(self) -> None: + node = parse( + dedent( + """ + def foo(**kwargs: int) -> None: + kwargs = {'a': 5} + """ + ) + ) + TypeguardTransformer().visit(node) + + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_variable_assignment + + def foo(**kwargs: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'kwargs': (kwargs, \ +dict[str, int])}, memo) + kwargs = check_variable_assignment({'a': 5}, \ +[[('kwargs', dict[str, int])]], memo) + """ + ).strip() + ) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="Requires Python < 3.10") + def test_pep604_assign(self) -> None: + node = parse( + dedent( + """ + Union = None + + def foo() -> None: + x: int | str = otherfunc() + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Union as Union_ + Union = None + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int | str = check_variable_assignment(otherfunc(), \ +[[('x', Union_[int, str])]], memo) + """ + ).strip() + ) + + def test_multi_assign(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x: int + z: bytes + x, y, z = otherfunc() + """ + ) + ) + TypeguardTransformer().visit(node) + target = "x, y, z" if sys.version_info >= (3, 11) else "(x, y, z)" + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Any + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int + z: bytes + {target} = check_variable_assignment(otherfunc(), \ +[[('x', int), ('y', Any), ('z', bytes)]], memo) + """ + ).strip() + ) + + def test_star_multi_assign(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x: int + z: bytes + x, *y, z = otherfunc() + """ + ) + ) + TypeguardTransformer().visit(node) + target = "x, *y, z" if sys.version_info >= (3, 11) else "(x, *y, z)" + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Any + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int + z: bytes + {target} = check_variable_assignment(otherfunc(), \ +[[('x', int), ('*y', Any), ('z', bytes)]], memo) + """ + ).strip() + ) + + def test_complex_multi_assign(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x: int + z: bytes + all = x, *y, z = otherfunc() + """ + ) + ) + TypeguardTransformer().visit(node) + target = "x, *y, z" if sys.version_info >= (3, 11) else "(x, *y, z)" + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Any + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int + z: bytes + all = {target} = check_variable_assignment(otherfunc(), \ +[[('all', Any)], [('x', int), ('*y', Any), ('z', bytes)]], memo) + """ + ).strip() + ) + + def test_unpacking_assign_to_self(self) -> None: + node = parse( + dedent( + """ + class Foo: + + def foo(self) -> None: + x: int + (x, self.y) = 1, 'test' + """ + ) + ) + TypeguardTransformer().visit(node) + target = "x, self.y" if sys.version_info >= (3, 11) else "(x, self.y)" + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from typing import Any + + class Foo: + + def foo(self) -> None: + memo = TypeCheckMemo(globals(), locals(), \ +self_type=self.__class__) + x: int + {target} = check_variable_assignment((1, 'test'), \ +[[('x', int), ('self.y', Any)]], memo) + """ + ).strip() + ) + + def test_assignment_annotated_argument(self) -> None: + node = parse( + dedent( + """ + def foo(x: int) -> None: + x = 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_variable_assignment + + def foo(x: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + x = check_variable_assignment(6, [[('x', int)]], memo) + """ + ).strip() + ) + + def test_assignment_expr(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x: int + if x := otherfunc(): + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int + if (x := check_variable_assignment(otherfunc(), [[('x', int)]], \ +memo)): + pass + """ + ).strip() + ) + + def test_assignment_expr_annotated_argument(self) -> None: + node = parse( + dedent( + """ + def foo(x: int) -> None: + if x := otherfunc(): + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_variable_assignment + + def foo(x: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + if (x := check_variable_assignment(otherfunc(), [[('x', int)]], memo)): + pass + """ + ).strip() + ) + + @pytest.mark.parametrize( + "operator, function", + [ + pytest.param("+=", "iadd", id="add"), + pytest.param("-=", "isub", id="subtract"), + pytest.param("*=", "imul", id="multiply"), + pytest.param("@=", "imatmul", id="matrix_multiply"), + pytest.param("/=", "itruediv", id="div"), + pytest.param("//=", "ifloordiv", id="floordiv"), + pytest.param("**=", "ipow", id="power"), + pytest.param("<<=", "ilshift", id="left_bitshift"), + pytest.param(">>=", "irshift", id="right_bitshift"), + pytest.param("&=", "iand", id="and"), + pytest.param("^=", "ixor", id="xor"), + pytest.param("|=", "ior", id="or"), + ], + ) + def test_augmented_assignment(self, operator: str, function: str) -> None: + node = parse( + dedent( + f""" + def foo() -> None: + x: int + x {operator} 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_variable_assignment + from operator import {function} + + def foo() -> None: + memo = TypeCheckMemo(globals(), locals()) + x: int + x = check_variable_assignment({function}(x, 6), [[('x', int)]], \ +memo) + """ + ).strip() + ) + + def test_augmented_assignment_non_annotated(self) -> None: + node = parse( + dedent( + """ + def foo() -> None: + x = 1 + x += 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + def foo() -> None: + x = 1 + x += 6 + """ + ).strip() + ) + + def test_augmented_assignment_annotated_argument(self) -> None: + node = parse( + dedent( + """ + def foo(x: int) -> None: + x += 6 + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, \ +check_variable_assignment + from operator import iadd + + def foo(x: int) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, int)}, memo) + x = check_variable_assignment(iadd(x, 6), [[('x', int)]], memo) + """ + ).strip() + ) + + +def test_argname_typename_conflicts() -> None: + node = parse( + dedent( + """ + from collections.abc import Generator + + def foo(x: kwargs, /, y: args, *args: x, baz: x, **kwargs: y) -> \ +Generator[args, x, kwargs]: + yield y + return x + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from collections.abc import Generator + + def foo(x: kwargs, /, y: args, *args: x, baz: x, **kwargs: y) -> \ +Generator[args, x, kwargs]: + yield y + return x + """ + ).strip() + ) + + +def test_local_assignment_typename_conflicts() -> None: + node = parse( + dedent( + """ + def foo() -> int: + int = 6 + return int + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + def foo() -> int: + int = 6 + return int + """ + ).strip() + ) + + +def test_local_ann_assignment_typename_conflicts() -> None: + node = parse( + dedent( + """ + from typing import Any + + def foo() -> int: + int: Any = 6 + return int + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any + + def foo() -> int: + int: Any = 6 + return int + """ + ).strip() + ) + + +def test_local_named_expr_typename_conflicts() -> None: + node = parse( + dedent( + """ + from typing import Any + + def foo() -> int: + if (int := 6): + pass + return int + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Any + + def foo() -> int: + if (int := 6): + pass + return int + """ + ).strip() + ) + + +def test_dont_leave_empty_ast_container_nodes() -> None: + # Regression test for #352 + node = parse( + dedent( + """ + if True: + + class A: + ... + + def func(): + ... + + def foo(x: str) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + if True: + pass + + def foo(x: str) -> None: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, str)}, memo) + """ + ).strip() + ) + + +def test_dont_leave_empty_ast_container_nodes_2() -> None: + # Regression test for #352 + node = parse( + dedent( + """ + try: + + class A: + ... + + def func(): + ... + + except: + + class A: + ... + + def func(): + ... + + + def foo(x: str) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + try: + pass + except: + pass + + def foo(x: str) -> None: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, str)}, memo) + """ + ).strip() + ) + + +class TestTypeShadowedByArgument: + def test_typing_union(self) -> None: + # Regression test for #394 + node = parse( + dedent( + """ + from __future__ import annotations + from typing import Union + + class A: + ... + + def foo(A: Union[A, None]) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + from __future__ import annotations + from typing import Union + + def foo(A: Union[A, None]) -> None: + pass + """ + ).strip() + ) + + def test_pep604_union(self) -> None: + # Regression test for #395 + node = parse( + dedent( + """ + from __future__ import annotations + + class A: + ... + + def foo(A: A | None) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + from __future__ import annotations + + def foo(A: A | None) -> None: + pass + """ + ).strip() + ) + + +def test_dont_parse_annotated_2nd_arg() -> None: + # Regression test for #352 + node = parse( + dedent( + """ + from typing import Annotated + + def foo(x: Annotated[str, 'foo bar']) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + from typing import Annotated + + def foo(x: Annotated[str, 'foo bar']) -> None: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, Annotated[str, 'foo bar'])}, memo) + """ + ).strip() + ) + + +def test_respect_docstring() -> None: + # Regression test for #359 + node = parse( + dedent( + ''' + def foo() -> int: + """This is a docstring.""" + return 1 + ''' + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + ''' + def foo() -> int: + """This is a docstring.""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type + memo = TypeCheckMemo(globals(), locals()) + return check_return_type('foo', 1, int, memo) + ''' + ).strip() + ) + + +def test_respect_future_import() -> None: + # Regression test for #385 + node = parse( + dedent( + ''' + """module docstring""" + from __future__ import annotations + + def foo() -> int: + return 1 + ''' + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + ''' + """module docstring""" + from __future__ import annotations + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type + + def foo() -> int: + memo = TypeCheckMemo(globals(), locals()) + return check_return_type('foo', 1, int, memo) + ''' + ).strip() + ) + + +def test_literal() -> None: + # Regression test for #399 + node = parse( + dedent( + """ + from typing import Literal + + def foo(x: Literal['a', 'b']) -> None: + pass + """ + ) + ) + TypeguardTransformer().visit(node) + assert ( + unparse(node) + == dedent( + """ + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + from typing import Literal + + def foo(x: Literal['a', 'b']) -> None: + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, Literal['a', 'b'])}, memo) + """ + ).strip() + ) diff --git a/contrib/python/typeguard/tests/test_typechecked.py b/contrib/python/typeguard/tests/test_typechecked.py new file mode 100644 index 00000000000..770b68bcff6 --- /dev/null +++ b/contrib/python/typeguard/tests/test_typechecked.py @@ -0,0 +1,711 @@ +import asyncio +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent +from typing import ( + Any, + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Dict, + Generator, + Iterable, + Iterator, + List, +) +from unittest.mock import Mock + +import pytest + +from typeguard import TypeCheckError, typechecked + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +class TestCoroutineFunction: + def test_success(self): + @typechecked + async def foo(a: int) -> str: + return "test" + + assert asyncio.run(foo(1)) == "test" + + def test_bad_arg(self): + @typechecked + async def foo(a: int) -> str: + return "test" + + with pytest.raises( + TypeCheckError, match=r'argument "a" \(str\) is not an instance of int' + ): + asyncio.run(foo("foo")) + + def test_bad_return(self): + @typechecked + async def foo(a: int) -> str: + return 1 + + with pytest.raises( + TypeCheckError, match=r"return value \(int\) is not an instance of str" + ): + asyncio.run(foo(1)) + + def test_any_return(self): + @typechecked + async def foo() -> Any: + return 1 + + assert asyncio.run(foo()) == 1 + + +class TestGenerator: + def test_generator_bare(self): + @typechecked + def genfunc() -> Generator: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + gen = genfunc() + with pytest.raises(StopIteration) as exc: + value = next(gen) + while True: + value = gen.send(str(value)) + assert isinstance(value, int) + + assert exc.value.value == ["2", "3", "4"] + + def test_generator_annotated(self): + @typechecked + def genfunc() -> Generator[int, str, List[str]]: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + gen = genfunc() + with pytest.raises(StopIteration) as exc: + value = next(gen) + while True: + value = gen.send(str(value)) + assert isinstance(value, int) + + assert exc.value.value == ["2", "3", "4"] + + def test_generator_iterable_bare(self): + @typechecked + def genfunc() -> Iterable: + yield 2 + yield 3 + yield 4 + + values = list(genfunc()) + assert values == [2, 3, 4] + + def test_generator_iterable_annotated(self): + @typechecked + def genfunc() -> Iterable[int]: + yield 2 + yield 3 + yield 4 + + values = list(genfunc()) + assert values == [2, 3, 4] + + def test_generator_iterator_bare(self): + @typechecked + def genfunc() -> Iterator: + yield 2 + yield 3 + yield 4 + + values = list(genfunc()) + assert values == [2, 3, 4] + + def test_generator_iterator_annotated(self): + @typechecked + def genfunc() -> Iterator[int]: + yield 2 + yield 3 + yield 4 + + values = list(genfunc()) + assert values == [2, 3, 4] + + def test_bad_yield_as_generator(self): + @typechecked + def genfunc() -> Generator[int, str, None]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_bad_yield_as_iterable(self): + @typechecked + def genfunc() -> Iterable[int]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_bad_yield_as_iterator(self): + @typechecked + def genfunc() -> Iterator[int]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_generator_bad_send(self): + @typechecked + def genfunc() -> Generator[int, str, None]: + yield 1 + yield 2 + + pass + gen = genfunc() + next(gen) + with pytest.raises(TypeCheckError) as exc: + gen.send(2) + + exc.match(r"value sent to generator \(int\) is not an instance of str") + + def test_generator_bad_return(self): + @typechecked + def genfunc() -> Generator[int, str, str]: + yield 1 + return 6 + + gen = genfunc() + next(gen) + with pytest.raises(TypeCheckError) as exc: + gen.send("foo") + + exc.match(r"return value \(int\) is not an instance of str") + + def test_return_generator(self): + @typechecked + def genfunc() -> Generator[int, None, None]: + yield 1 + + @typechecked + def foo() -> Generator[int, None, None]: + return genfunc() + + foo() + + +class TestAsyncGenerator: + def test_async_generator_bare(self): + @typechecked + async def genfunc() -> AsyncGenerator: + values.append((yield 2)) + values.append((yield 3)) + values.append((yield 4)) + + async def run_generator(): + gen = genfunc() + value = await gen.asend(None) + with pytest.raises(StopAsyncIteration): + while True: + value = await gen.asend(str(value)) + assert isinstance(value, int) + + values = [] + asyncio.run(run_generator()) + assert values == ["2", "3", "4"] + + def test_async_generator_annotated(self): + @typechecked + async def genfunc() -> AsyncGenerator[int, str]: + values.append((yield 2)) + values.append((yield 3)) + values.append((yield 4)) + + async def run_generator(): + gen = genfunc() + value = await gen.asend(None) + with pytest.raises(StopAsyncIteration): + while True: + value = await gen.asend(str(value)) + assert isinstance(value, int) + + values = [] + asyncio.run(run_generator()) + assert values == ["2", "3", "4"] + + def test_generator_iterable_bare(self): + @typechecked + async def genfunc() -> AsyncIterable: + yield 2 + yield 3 + yield 4 + + async def run_generator(): + return [value async for value in genfunc()] + + assert asyncio.run(run_generator()) == [2, 3, 4] + + def test_generator_iterable_annotated(self): + @typechecked + async def genfunc() -> AsyncIterable[int]: + yield 2 + yield 3 + yield 4 + + async def run_generator(): + return [value async for value in genfunc()] + + assert asyncio.run(run_generator()) == [2, 3, 4] + + def test_generator_iterator_bare(self): + @typechecked + async def genfunc() -> AsyncIterator: + yield 2 + yield 3 + yield 4 + + async def run_generator(): + return [value async for value in genfunc()] + + assert asyncio.run(run_generator()) == [2, 3, 4] + + def test_generator_iterator_annotated(self): + @typechecked + async def genfunc() -> AsyncIterator[int]: + yield 2 + yield 3 + yield 4 + + async def run_generator(): + return [value async for value in genfunc()] + + assert asyncio.run(run_generator()) == [2, 3, 4] + + def test_async_bad_yield_as_generator(self): + @typechecked + async def genfunc() -> AsyncGenerator[int, str]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen.__anext__().__await__()) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_async_bad_yield_as_iterable(self): + @typechecked + async def genfunc() -> AsyncIterable[int]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen.__anext__().__await__()) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_async_bad_yield_as_iterator(self): + @typechecked + async def genfunc() -> AsyncIterator[int]: + yield "foo" + + gen = genfunc() + with pytest.raises(TypeCheckError) as exc: + next(gen.__anext__().__await__()) + + exc.match(r"the yielded value \(str\) is not an instance of int") + + def test_async_generator_bad_send(self): + @typechecked + async def genfunc() -> AsyncGenerator[int, str]: + yield 1 + yield 2 + + gen = genfunc() + pytest.raises(StopIteration, next, gen.__anext__().__await__()) + with pytest.raises(TypeCheckError) as exc: + next(gen.asend(2).__await__()) + + exc.match(r"the value sent to generator \(int\) is not an instance of str") + + def test_return_async_generator(self): + @typechecked + async def genfunc() -> AsyncGenerator[int, None]: + yield 1 + + @typechecked + def foo() -> AsyncGenerator[int, None]: + return genfunc() + + foo() + + def test_async_generator_iterate(self): + @typechecked + async def asyncgenfunc() -> AsyncGenerator[int, None]: + yield 1 + + asyncgen = asyncgenfunc() + aiterator = asyncgen.__aiter__() + exc = pytest.raises(StopIteration, aiterator.__anext__().send, None) + assert exc.value.value == 1 + + +class TestSelf: + def test_return_valid(self): + class Foo: + @typechecked + def method(self) -> Self: + return self + + Foo().method() + + def test_return_invalid(self): + class Foo: + @typechecked + def method(self) -> Self: + return 1 + + foo = Foo() + pytest.raises(TypeCheckError, foo.method).match( + rf"the return value \(int\) is not an instance of the self type " + rf"\({__name__}\.{self.__class__.__name__}\.test_return_invalid\." + rf"<locals>\.Foo\)" + ) + + def test_classmethod_return_valid(self): + class Foo: + @classmethod + @typechecked + def method(cls) -> Self: + return Foo() + + Foo.method() + + def test_classmethod_return_invalid(self): + class Foo: + @classmethod + @typechecked + def method(cls) -> Self: + return 1 + + pytest.raises(TypeCheckError, Foo.method).match( + rf"the return value \(int\) is not an instance of the self type " + rf"\({__name__}\.{self.__class__.__name__}\." + rf"test_classmethod_return_invalid\.<locals>\.Foo\)" + ) + + def test_arg_valid(self): + class Foo: + @typechecked + def method(self, another: Self) -> None: + pass + + foo = Foo() + foo2 = Foo() + foo.method(foo2) + + def test_arg_invalid(self): + class Foo: + @typechecked + def method(self, another: Self) -> None: + pass + + foo = Foo() + pytest.raises(TypeCheckError, foo.method, 1).match( + rf'argument "another" \(int\) is not an instance of the self type ' + rf"\({__name__}\.{self.__class__.__name__}\.test_arg_invalid\." + rf"<locals>\.Foo\)" + ) + + def test_classmethod_arg_valid(self): + class Foo: + @classmethod + @typechecked + def method(cls, another: Self) -> None: + pass + + foo = Foo() + Foo.method(foo) + + def test_classmethod_arg_invalid(self): + class Foo: + @classmethod + @typechecked + def method(cls, another: Self) -> None: + pass + + foo = Foo() + pytest.raises(TypeCheckError, foo.method, 1).match( + rf'argument "another" \(int\) is not an instance of the self type ' + rf"\({__name__}\.{self.__class__.__name__}\." + rf"test_classmethod_arg_invalid\.<locals>\.Foo\)" + ) + + def test_self_type_valid(self): + class Foo: + @typechecked + def method(cls, subclass: type[Self]) -> None: + pass + + class Bar(Foo): + pass + + Foo().method(Bar) + + def test_self_type_invalid(self): + class Foo: + @typechecked + def method(cls, subclass: type[Self]) -> None: + pass + + pytest.raises(TypeCheckError, Foo().method, int).match( + rf'argument "subclass" \(class int\) is not a subclass of the self type ' + rf"\({__name__}\.{self.__class__.__name__}\." + rf"test_self_type_invalid\.<locals>\.Foo\)" + ) + + +class TestMock: + def test_mock_argument(self): + @typechecked + def foo(x: int) -> None: + pass + + foo(Mock()) + + def test_return_mock(self): + @typechecked + def foo() -> int: + return Mock() + + foo() + + +def test_decorator_before_classmethod(): + class Foo: + @typechecked + @classmethod + def method(cls, x: int) -> None: + pass + + pytest.raises(TypeCheckError, Foo().method, "bar").match( + r'argument "x" \(str\) is not an instance of int' + ) + + +def test_classmethod(): + @typechecked + class Foo: + @classmethod + def method(cls, x: int) -> None: + pass + + pytest.raises(TypeCheckError, Foo().method, "bar").match( + r'argument "x" \(str\) is not an instance of int' + ) + + +def test_decorator_before_staticmethod(): + class Foo: + @typechecked + @staticmethod + def method(x: int) -> None: + pass + + pytest.raises(TypeCheckError, Foo().method, "bar").match( + r'argument "x" \(str\) is not an instance of int' + ) + + +def test_staticmethod(): + @typechecked + class Foo: + @staticmethod + def method(x: int) -> None: + pass + + pytest.raises(TypeCheckError, Foo().method, "bar").match( + r'argument "x" \(str\) is not an instance of int' + ) + + +def test_retain_dunder_attributes(): + @typechecked + def foo(x: int, y: str = "foo") -> None: + """This is a docstring.""" + + assert foo.__module__ == __name__ + assert foo.__name__ == "foo" + assert foo.__qualname__ == "test_retain_dunder_attributes.<locals>.foo" + assert foo.__doc__ == "This is a docstring." + assert foo.__defaults__ == ("foo",) + + [email protected](sys.version_info < (3, 9), reason="Requires ast.unparse()") +def test_debug_instrumentation(monkeypatch, capsys): + monkeypatch.setattr("typeguard.config.debug_instrumentation", True) + + @typechecked + def foo(a: str) -> int: + return 6 + + out, err = capsys.readouterr() + assert err == dedent( + """\ + Source code of test_debug_instrumentation.<locals>.foo() after instrumentation: + ---------------------------------------------- + def foo(a: str) -> int: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types, check_return_type + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('test_debug_instrumentation.<locals>.foo', \ +{'a': (a, str)}, memo) + return check_return_type('test_debug_instrumentation.<locals>.foo', 6, \ +int, memo) + ---------------------------------------------- + """ + ) + + +def test_keyword_argument_default(): + # Regression test for #305 + @typechecked + def foo(*args, x: "int | None" = None): + pass + + foo() + + +def test_return_type_annotation_refers_to_nonlocal(): + class Internal: + pass + + @typechecked + def foo() -> Internal: + return Internal() + + assert isinstance(foo(), Internal) + + +def test_existing_method_decorator(): + @typechecked + class Foo: + @contextmanager + def method(self, x: int) -> None: + yield x + 1 + + with Foo().method(6) as value: + assert value == 7 + + +# Этот тест не укладывается в 300s + "flags, expected_return_code", + [ + pytest.param([], 1, id="debug"), + pytest.param(["-O"], 0, id="O"), + pytest.param(["-OO"], 0, id="OO"), + ], +) +def test_typechecked_disabled_in_optimized_mode( + tmp_path: Path, flags: List[str], expected_return_code: int +): + code = dedent( + """ + from typeguard import typechecked + + @typechecked + def foo(x: int) -> None: + pass + + foo("a") + """ + ) + script_path = tmp_path / "code.py" + script_path.write_text(code) + process = subprocess.run( + [sys.executable, *flags, str(script_path)], capture_output=True + ) + assert process.returncode == expected_return_code + if process.returncode == 1: + assert process.stderr.strip().endswith( + b'typeguard.TypeCheckError: argument "x" (str) is not an instance of int' + ) + + +def test_reference_imported_name_from_method() -> None: + # Regression test for #362 + @typechecked + class A: + def foo(self) -> Dict[str, Any]: + return {} + + A().foo() + + +def test_getter_setter(): + """Regression test for #355.""" + + @typechecked + class Foo: + def __init__(self, x: int): + self._x = x + + @property + def x(self) -> int: + return self._x + + @x.setter + def x(self, value: int) -> None: + self._x = value + + f = Foo(1) + f.x = 2 + assert f.x == 2 + with pytest.raises(TypeCheckError): + f.x = "foo" + + +def test_duplicate_method(): + class Foo: + def x(self) -> str: + return "first" + + @typechecked() + def x(self, value: int) -> str: # noqa: F811 + return "second" + + assert Foo().x(1) == "second" + with pytest.raises(TypeCheckError): + Foo().x("wrong") + + +def test_duplicate_function(): + @typechecked + def foo() -> list[int]: # noqa: F811 + return [x for x in range(5)] + + foo1 = foo + + @typechecked + def foo() -> list[int]: # noqa: F811 + return [x for x in range(5, 10)] + + assert foo1() == [0, 1, 2, 3, 4] + assert foo() == [5, 6, 7, 8, 9] diff --git a/contrib/python/typeguard/tests/test_typeguard.py b/contrib/python/typeguard/tests/test_typeguard.py deleted file mode 100644 index 6e7e9cda8bb..00000000000 --- a/contrib/python/typeguard/tests/test_typeguard.py +++ /dev/null @@ -1,1548 +0,0 @@ -import gc -import sys -import traceback -import warnings -from abc import abstractproperty -from concurrent.futures import ThreadPoolExecutor -from functools import lru_cache, partial, wraps -from io import BytesIO, StringIO -from typing import ( - AbstractSet, Any, AnyStr, BinaryIO, Callable, Container, Dict, Generator, Generic, Iterable, - Iterator, List, NamedTuple, Sequence, Set, TextIO, Tuple, Type, TypeVar, Union, TypedDict) -from unittest.mock import MagicMock, Mock - -import pytest -from typing_extensions import Literal, NoReturn, Protocol, runtime_checkable - -from typeguard import ( - ForwardRefPolicy, TypeChecker, TypeHintWarning, TypeWarning, check_argument_types, - check_return_type, check_type, function_name, qualified_name, typechecked) - -try: - from typing import Collection -except ImportError: - # Python 3.6.0+ - Collection = None - -try: - from typing import NewType -except ImportError: - myint = None -else: - myint = NewType("myint", int) - - -TBound = TypeVar('TBound', bound='Parent') -TConstrained = TypeVar('TConstrained', 'Parent', int) -TTypingConstrained = TypeVar('TTypingConstrained', List[int], AbstractSet[str]) -TIntStr = TypeVar('TIntStr', int, str) -TIntCollection = TypeVar('TIntCollection', int, Collection) -TParent = TypeVar('TParent', bound='Parent') -TChild = TypeVar('TChild', bound='Child') -T_Foo = TypeVar('T_Foo') -JSONType = Union[str, int, float, bool, None, List['JSONType'], Dict[str, 'JSONType']] - -DummyDict = TypedDict('DummyDict', {'x': int}, total=False) -issue_42059 = pytest.mark.xfail(bool(DummyDict.__required_keys__), - reason='Fails due to upstream bug BPO-42059') -del DummyDict - -Employee = NamedTuple('Employee', [('name', str), ('id', int)]) - - -class FooGeneric(Generic[T_Foo]): - pass - - -class Parent: - pass - - -class Child(Parent): - def method(self, a: int): - pass - - -class StaticProtocol(Protocol): - def meth(self) -> None: - ... - - -@runtime_checkable -class RuntimeProtocol(Protocol): - def meth(self) -> None: - ... - - [email protected](params=[Mock, MagicMock], ids=['mock', 'magicmock']) -def mock_class(request): - return request.param - - [email protected]('inputval, expected', [ - (qualified_name, 'function'), - (Child(), '__tests__.test_typeguard.Child'), - (int, 'int') -], ids=['func', 'instance', 'builtintype']) -def test_qualified_name(inputval, expected): - assert qualified_name(inputval) == expected - - -def test_function_name(): - assert function_name(function_name) == 'typeguard.function_name' - - -def test_check_type_no_memo(): - check_type('foo', [1], List[int]) - - -def test_check_type_bytes(): - pytest.raises(TypeError, check_type, 'foo', 7, bytes).\ - match(r'type of foo must be bytes-like; got int instead') - - -def test_check_type_no_memo_fail(): - pytest.raises(TypeError, check_type, 'foo', ['a'], List[int]).\ - match(r'type of foo\[0\] must be int; got str instead') - - [email protected]('value', ['bar', b'bar'], ids=['str', 'bytes']) -def test_check_type_anystr(value): - check_type('foo', value, AnyStr) - - -def test_check_type_anystr_fail(): - pytest.raises(TypeError, check_type, 'foo', int, AnyStr).\ - match(r'type of foo must match one of the constraints \(bytes, str\); got type instead') - - -def test_check_return_type(): - def foo() -> int: - assert check_return_type(0) - return 0 - - foo() - - -def test_check_return_type_fail(): - def foo() -> int: - assert check_return_type('foo') - return 1 - - pytest.raises(TypeError, foo).match('type of the return value must be int; got str instead') - - -def test_check_return_notimplemented(): - class Foo: - def __eq__(self, other) -> bool: - assert check_return_type(NotImplemented) - return NotImplemented - - assert Foo().__eq__(1) is NotImplemented - - -def test_check_recursive_type(): - check_type('foo', {'a': [1, 2, 3]}, JSONType) - pytest.raises(TypeError, check_type, 'foo', {'a': (1, 2, 3)}, JSONType, globals=globals()).\ - match(r'type of foo must be one of \(str, int, float, (bool, )?NoneType, ' - r'List\[JSONType\], Dict\[str, JSONType\]\); got dict instead') - - -def test_exec_no_namespace(): - from textwrap import dedent - - exec(dedent(""" - from typeguard import typechecked - - @typechecked - def f() -> None: - pass - - """), {}) - - -class TestCheckArgumentTypes: - def test_any_type(self): - def foo(a: Any): - assert check_argument_types() - - foo('aa') - - def test_mock_value(self, mock_class): - def foo(a: str, b: int, c: dict, d: Any) -> int: - assert check_argument_types() - - foo(mock_class(), mock_class(), mock_class(), mock_class()) - - def test_callable_exact_arg_count(self): - def foo(a: Callable[[int, str], int]): - assert check_argument_types() - - def some_callable(x: int, y: str) -> int: - pass - - foo(some_callable) - - def test_callable_bad_type(self): - def foo(a: Callable[..., int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == 'argument "a" must be a callable' - - def test_callable_too_few_arguments(self): - def foo(a: Callable[[int, str], int]): - assert check_argument_types() - - def some_callable(x: int) -> int: - pass - - exc = pytest.raises(TypeError, foo, some_callable) - assert str(exc.value) == ( - 'callable passed as argument "a" has too few arguments in its declaration; expected 2 ' - 'but 1 argument(s) declared') - - def test_callable_too_many_arguments(self): - def foo(a: Callable[[int, str], int]): - assert check_argument_types() - - def some_callable(x: int, y: str, z: float) -> int: - pass - - exc = pytest.raises(TypeError, foo, some_callable) - assert str(exc.value) == ( - 'callable passed as argument "a" has too many arguments in its declaration; expected ' - '2 but 3 argument(s) declared') - - def test_callable_mandatory_kwonlyargs(self): - def foo(a: Callable[[int, str], int]): - assert check_argument_types() - - def some_callable(x: int, y: str, *, z: float, bar: str) -> int: - pass - - exc = pytest.raises(TypeError, foo, some_callable) - assert str(exc.value) == ( - 'callable passed as argument "a" has mandatory keyword-only arguments in its ' - 'declaration: z, bar') - - def test_callable_class(self): - """ - Test that passing a class as a callable does not count the "self" argument "a"gainst the - ones declared in the Callable specification. - - """ - def foo(a: Callable[[int, str], Any]): - assert check_argument_types() - - class SomeClass: - def __init__(self, x: int, y: str): - pass - - foo(SomeClass) - - def test_callable_plain(self): - def foo(a: Callable): - assert check_argument_types() - - def callback(a): - pass - - foo(callback) - - def test_callable_partial_class(self): - """ - Test that passing a bound method as a callable does not count the "self" argument "a"gainst - the ones declared in the Callable specification. - - """ - def foo(a: Callable[[int], Any]): - assert check_argument_types() - - class SomeClass: - def __init__(self, x: int, y: str): - pass - - foo(partial(SomeClass, y='foo')) - - def test_callable_bound_method(self): - """ - Test that passing a bound method as a callable does not count the "self" argument "a"gainst - the ones declared in the Callable specification. - - """ - def foo(callback: Callable[[int], Any]): - assert check_argument_types() - - foo(Child().method) - - def test_callable_partial_bound_method(self): - """ - Test that passing a bound method as a callable does not count the "self" argument "a"gainst - the ones declared in the Callable specification. - - """ - def foo(callback: Callable[[], Any]): - assert check_argument_types() - - foo(partial(Child().method, 1)) - - def test_callable_defaults(self): - """ - Test that a callable having "too many" arguments don't raise an error if the extra - arguments have default values. - - """ - def foo(callback: Callable[[int, str], Any]): - assert check_argument_types() - - def some_callable(x: int, y: str, z: float = 1.2) -> int: - pass - - foo(some_callable) - - def test_callable_builtin(self): - """ - Test that checking a Callable annotation against a builtin callable does not raise an - error. - - """ - def foo(callback: Callable[[int], Any]): - assert check_argument_types() - - foo([].append) - - def test_dict_bad_type(self): - def foo(a: Dict[str, int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == ( - 'type of argument "a" must be a dict; got int instead') - - def test_dict_bad_key_type(self): - def foo(a: Dict[str, int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, {1: 2}) - assert str(exc.value) == 'type of keys of argument "a" must be str; got int instead' - - def test_dict_bad_value_type(self): - def foo(a: Dict[str, int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, {'x': 'a'}) - assert str(exc.value) == "type of argument \"a\"['x'] must be int; got str instead" - - def test_list_bad_type(self): - def foo(a: List[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == ( - 'type of argument "a" must be a list; got int instead') - - def test_list_bad_element(self): - def foo(a: List[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, [1, 2, 'bb']) - assert str(exc.value) == ( - 'type of argument "a"[2] must be int; got str instead') - - def test_sequence_bad_type(self): - def foo(a: Sequence[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == ( - 'type of argument "a" must be a sequence; got int instead') - - def test_sequence_bad_element(self): - def foo(a: Sequence[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, [1, 2, 'bb']) - assert str(exc.value) == ( - 'type of argument "a"[2] must be int; got str instead') - - def test_abstractset_custom_type(self): - class DummySet(AbstractSet[int]): - def __contains__(self, x: object) -> bool: - return x == 1 - - def __len__(self) -> int: - return 1 - - def __iter__(self) -> Iterator[int]: - yield 1 - - def foo(a: AbstractSet[int]): - assert check_argument_types() - - foo(DummySet()) - - def test_abstractset_bad_type(self): - def foo(a: AbstractSet[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == 'type of argument "a" must be a set; got int instead' - - def test_set_bad_type(self): - def foo(a: Set[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == 'type of argument "a" must be a set; got int instead' - - def test_abstractset_bad_element(self): - def foo(a: AbstractSet[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, {1, 2, 'bb'}) - assert str(exc.value) == ( - 'type of elements of argument "a" must be int; got str instead') - - def test_set_bad_element(self): - def foo(a: Set[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, {1, 2, 'bb'}) - assert str(exc.value) == ( - 'type of elements of argument "a" must be int; got str instead') - - def test_tuple_bad_type(self): - def foo(a: Tuple[int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 5) - assert str(exc.value) == ( - 'type of argument "a" must be a tuple; got int instead') - - def test_tuple_too_many_elements(self): - def foo(a: Tuple[int, str]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, (1, 'aa', 2)) - assert str(exc.value) == ('argument "a" has wrong number of elements (expected 2, got 3 ' - 'instead)') - - def test_tuple_too_few_elements(self): - def foo(a: Tuple[int, str]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, (1,)) - assert str(exc.value) == ('argument "a" has wrong number of elements (expected 2, got 1 ' - 'instead)') - - def test_tuple_bad_element(self): - def foo(a: Tuple[int, str]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, (1, 2)) - assert str(exc.value) == ( - 'type of argument "a"[1] must be str; got int instead') - - def test_tuple_ellipsis_bad_element(self): - def foo(a: Tuple[int, ...]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, (1, 2, 'blah')) - assert str(exc.value) == ( - 'type of argument "a"[2] must be int; got str instead') - - def test_namedtuple(self): - def foo(bar: Employee): - assert check_argument_types() - - foo(Employee('bob', 1)) - - def test_namedtuple_type_mismatch(self): - def foo(bar: Employee): - assert check_argument_types() - - pytest.raises(TypeError, foo, ('bob', 1)).\ - match('type of argument "bar" must be a named tuple of type ' - r'(__tests__\.test_typeguard\.)?Employee; got tuple instead') - - def test_namedtuple_wrong_field_type(self): - def foo(bar: Employee): - assert check_argument_types() - - pytest.raises(TypeError, foo, Employee(2, 1)).\ - match('type of argument "bar".name must be str; got int instead') - - @pytest.mark.parametrize('value', [6, 'aa']) - def test_union(self, value): - def foo(a: Union[str, int]): - assert check_argument_types() - - foo(value) - - def test_union_typing_type(self): - def foo(a: Union[str, Collection]): - assert check_argument_types() - - with pytest.raises(TypeError): - foo(1) - - @pytest.mark.parametrize('value', [6.5, b'aa']) - def test_union_fail(self, value): - def foo(a: Union[str, int]): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, value) - assert str(exc.value) == ( - 'type of argument "a" must be one of (str, int); got {} instead'. - format(value.__class__.__name__)) - - @pytest.mark.parametrize('values', [ - (6, 7), - ('aa', 'bb') - ], ids=['int', 'str']) - def test_typevar_constraints(self, values): - def foo(a: TIntStr, b: TIntStr): - assert check_argument_types() - - foo(*values) - - @pytest.mark.parametrize('value', [ - [6, 7], - {'aa', 'bb'} - ], ids=['int', 'str']) - def test_typevar_collection_constraints(self, value): - def foo(a: TTypingConstrained): - assert check_argument_types() - - foo(value) - - def test_typevar_collection_constraints_fail(self): - def foo(a: TTypingConstrained): - assert check_argument_types() - - pytest.raises(TypeError, foo, {1, 2}).\ - match(r'type of argument "a" must match one of the constraints \(List\[int\], ' - r'AbstractSet\[str\]\); got set instead') - - def test_typevar_constraints_fail(self): - def foo(a: TIntStr, b: TIntStr): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 2.5, 'aa') - assert str(exc.value) == ('type of argument "a" must match one of the constraints ' - '(int, str); got float instead') - - def test_typevar_bound(self): - def foo(a: TParent, b: TParent): - assert check_argument_types() - - foo(Child(), Child()) - - def test_typevar_bound_fail(self): - def foo(a: TChild, b: TChild): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, Parent(), Parent()) - assert str(exc.value) == ('type of argument "a" must be __tests__.test_typeguard.Child or one of ' - 'its subclasses; got __tests__.test_typeguard.Parent instead') - - @pytest.mark.skipif(Type is List, reason='typing.Type could not be imported') - def test_class_bad_subclass(self): - def foo(a: Type[Child]): - assert check_argument_types() - - pytest.raises(TypeError, foo, Parent).match( - '"a" must be a subclass of __tests__.test_typeguard.Child; got __tests__.test_typeguard.Parent instead') - - def test_class_any(self): - def foo(a: Type[Any]): - assert check_argument_types() - - foo(str) - - def test_class_union(self): - def foo(a: Type[Union[str, int]]): - assert check_argument_types() - - foo(str) - foo(int) - pytest.raises(TypeError, foo, tuple).\ - match(r'"a" must match one of the following: \(str, int\); got tuple instead') - - def test_wrapped_function(self): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - - @decorator - def foo(a: 'Child'): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, Parent()) - assert str(exc.value) == ('type of argument "a" must be __tests__.test_typeguard.Child; ' - 'got __tests__.test_typeguard.Parent instead') - - def test_mismatching_default_type(self): - def foo(a: str = 1): - assert check_argument_types() - - pytest.raises(TypeError, foo).match('type of argument "a" must be str; got int instead') - - def test_implicit_default_none(self): - """ - Test that if the default value is ``None``, a ``None`` argument can be passed. - - """ - def foo(a: str = None): - assert check_argument_types() - - foo() - - def test_generator(self): - """Test that argument type checking works in a generator function too.""" - def generate(a: int): - assert check_argument_types() - yield a - yield a + 1 - - gen = generate(1) - next(gen) - - def test_wrapped_generator_no_return_type_annotation(self): - """Test that return type checking works in a generator function too.""" - @typechecked - def generate(a: int): - yield a - yield a + 1 - - gen = generate(1) - next(gen) - - def test_varargs(self): - def foo(*args: int): - assert check_argument_types() - - foo(1, 2) - - def test_varargs_fail(self): - def foo(*args: int): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, 1, 'a') - exc.match(r'type of argument "args"\[1\] must be int; got str instead') - - def test_kwargs(self): - def foo(**kwargs: int): - assert check_argument_types() - - foo(a=1, b=2) - - def test_kwargs_fail(self): - def foo(**kwargs: int): - assert check_argument_types() - - exc = pytest.raises(TypeError, foo, a=1, b='a') - exc.match(r'type of argument "kwargs"\[\'b\'\] must be int; got str instead') - - def test_generic(self): - def foo(a: FooGeneric[str]): - assert check_argument_types() - - foo(FooGeneric[str]()) - - @pytest.mark.skipif(myint is None, reason='NewType is not present in the typing module') - def test_newtype(self): - def foo(a: myint) -> int: - assert check_argument_types() - return 42 - - assert foo(1) == 42 - exc = pytest.raises(TypeError, foo, "a") - assert str(exc.value) == 'type of argument "a" must be int; got str instead' - - @pytest.mark.skipif(Collection is None, reason='typing.Collection is not available') - def test_collection(self): - def foo(a: Collection): - assert check_argument_types() - - pytest.raises(TypeError, foo, True).match( - 'type of argument "a" must be collections.abc.Collection; got bool instead') - - def test_binary_io(self): - def foo(a: BinaryIO): - assert check_argument_types() - - foo(BytesIO()) - - def test_text_io(self): - def foo(a: TextIO): - assert check_argument_types() - - foo(StringIO()) - - def test_binary_io_fail(self): - def foo(a: TextIO): - assert check_argument_types() - - pytest.raises(TypeError, foo, BytesIO()).match('must be a text based I/O') - - def test_text_io_fail(self): - def foo(a: BinaryIO): - assert check_argument_types() - - pytest.raises(TypeError, foo, StringIO()).match('must be a binary I/O') - - def test_binary_io_real_file(self, tmpdir): - def foo(a: BinaryIO): - assert check_argument_types() - - with tmpdir.join('testfile').open('wb') as f: - foo(f) - - def test_text_io_real_file(self, tmpdir): - def foo(a: TextIO): - assert check_argument_types() - - with tmpdir.join('testfile').open('w') as f: - foo(f) - - def test_recursive_type(self): - def foo(arg: JSONType) -> None: - assert check_argument_types() - - foo({'a': [1, 2, 3]}) - pytest.raises(TypeError, foo, {'a': (1, 2, 3)}).\ - match(r'type of argument "arg" must be one of \(str, int, float, (bool, )?NoneType, ' - r'List\[Union\[str, int, float, (bool, )?NoneType, List\[JSONType\], ' - r'Dict\[str, JSONType\]\]\], ' - r'Dict\[str, Union\[str, int, float, (bool, )?NoneType, List\[JSONType\], ' - r'Dict\[str, JSONType\]\]\]\); got dict instead') - - -class TestTypeChecked: - def test_typechecked(self): - @typechecked - def foo(a: int, b: str) -> str: - return 'abc' - - assert foo(4, 'abc') == 'abc' - - def test_typechecked_always(self): - @typechecked(always=True) - def foo(a: int, b: str) -> str: - return 'abc' - - assert foo(4, 'abc') == 'abc' - - def test_typechecked_arguments_fail(self): - @typechecked - def foo(a: int, b: str) -> str: - return 'abc' - - exc = pytest.raises(TypeError, foo, 4, 5) - assert str(exc.value) == 'type of argument "b" must be str; got int instead' - - def test_typechecked_return_type_fail(self): - @typechecked - def foo(a: int, b: str) -> str: - return 6 - - exc = pytest.raises(TypeError, foo, 4, 'abc') - assert str(exc.value) == 'type of the return value must be str; got int instead' - - def test_typechecked_return_typevar_fail(self): - T = TypeVar('T', int, float) - - @typechecked - def foo(a: T, b: T) -> T: - return 'a' - - pytest.raises(TypeError, foo, 4, 2).\ - match(r'type of the return value must match one of the constraints \(int, float\); ' - r'got str instead') - - def test_typechecked_no_annotations(self, recwarn): - def foo(a, b): - pass - - typechecked(foo) - - func_name = function_name(foo) - assert len(recwarn) == 1 - assert str(recwarn[0].message) == ( - 'no type annotations present -- not typechecking {}'.format(func_name)) - - def test_return_type_none(self): - """Check that a declared return type of None is respected.""" - @typechecked - def foo() -> None: - return 'a' - - exc = pytest.raises(TypeError, foo) - assert str(exc.value) == 'type of the return value must be NoneType; got str instead' - - def test_return_type_magicmock(self, mock_class): - @typechecked - def foo() -> str: - return mock_class() - - foo() - - @pytest.mark.parametrize('typehint', [ - Callable[..., int], - Callable - ], ids=['parametrized', 'unparametrized']) - def test_callable(self, typehint): - @typechecked - def foo(a: typehint): - pass - - def some_callable() -> int: - pass - - foo(some_callable) - - @pytest.mark.parametrize('typehint', [ - List[int], - List, - list, - ], ids=['parametrized', 'unparametrized', 'plain']) - def test_list(self, typehint): - @typechecked - def foo(a: typehint): - pass - - foo([1, 2]) - - @pytest.mark.parametrize('typehint', [ - Dict[str, int], - Dict, - dict - ], ids=['parametrized', 'unparametrized', 'plain']) - def test_dict(self, typehint): - @typechecked - def foo(a: typehint): - pass - - foo({'x': 2}) - - @pytest.mark.parametrize('typehint, value', [ - (Dict, {'x': 2, 6: 4}), - (List, ['x', 6]), - (Sequence, ['x', 6]), - (Set, {'x', 6}), - (AbstractSet, {'x', 6}), - (Tuple, ('x', 6)), - ], ids=['dict', 'list', 'sequence', 'set', 'abstractset', 'tuple']) - def test_unparametrized_types_mixed_values(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - @pytest.mark.parametrize('typehint', [ - Sequence[str], - Sequence - ], ids=['parametrized', 'unparametrized']) - @pytest.mark.parametrize('value', [('a', 'b'), ['a', 'b'], 'abc'], - ids=['tuple', 'list', 'str']) - def test_sequence(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - @pytest.mark.parametrize('typehint', [ - Iterable[str], - Iterable - ], ids=['parametrized', 'unparametrized']) - @pytest.mark.parametrize('value', [('a', 'b'), ['a', 'b'], 'abc'], - ids=['tuple', 'list', 'str']) - def test_iterable(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - @pytest.mark.parametrize('typehint', [ - Container[str], - Container - ], ids=['parametrized', 'unparametrized']) - @pytest.mark.parametrize('value', [('a', 'b'), ['a', 'b'], 'abc'], - ids=['tuple', 'list', 'str']) - def test_container(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - @pytest.mark.parametrize('typehint', [ - AbstractSet[int], - AbstractSet, - Set[int], - Set, - set - ], ids=['abstract_parametrized', 'abstract', 'parametrized', 'unparametrized', 'plain']) - @pytest.mark.parametrize('value', [set(), {6}]) - def test_set(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - @pytest.mark.parametrize('typehint', [ - Tuple[int, int], - Tuple[int, ...], - Tuple, - tuple - ], ids=['parametrized', 'ellipsis', 'unparametrized', 'plain']) - def test_tuple(self, typehint): - @typechecked - def foo(a: typehint): - pass - - foo((1, 2)) - - def test_empty_tuple(self): - @typechecked - def foo(a: Tuple[()]): - pass - - foo(()) - - @pytest.mark.skipif(Type is List, reason='typing.Type could not be imported') - @pytest.mark.parametrize('typehint', [ - Type[Parent], - Type[TypeVar('UnboundType')], # noqa: F821 - Type[TypeVar('BoundType', bound=Parent)], # noqa: F821 - Type, - type - ], ids=['parametrized', 'unbound-typevar', 'bound-typevar', 'unparametrized', 'plain']) - def test_class(self, typehint): - @typechecked - def foo(a: typehint): - pass - - foo(Child) - - @pytest.mark.skipif(Type is List, reason='typing.Type could not be imported') - def test_class_not_a_class(self): - @typechecked - def foo(a: Type[dict]): - pass - - exc = pytest.raises(TypeError, foo, 1) - exc.match('type of argument "a" must be a type; got int instead') - - @pytest.mark.parametrize('typehint, value', [ - (complex, complex(1, 5)), - (complex, 1.0), - (complex, 1), - (float, 1.0), - (float, 1) - ], ids=['complex-complex', 'complex-float', 'complex-int', 'float-float', 'float-int']) - def test_numbers(self, typehint, value): - @typechecked - def foo(a: typehint): - pass - - foo(value) - - def test_coroutine_correct_return_type(self): - @typechecked - async def foo() -> str: - return 'foo' - - coro = foo() - pytest.raises(StopIteration, coro.send, None) - - def test_coroutine_wrong_return_type(self): - @typechecked - async def foo() -> str: - return 1 - - coro = foo() - pytest.raises(TypeError, coro.send, None).\ - match('type of the return value must be str; got int instead') - - def test_bytearray_bytes(self): - """Test that a bytearray is accepted where bytes are expected.""" - @typechecked - def foo(x: bytes) -> None: - pass - - foo(bytearray([1])) - - def test_bytearray_memoryview(self): - """Test that a bytearray is accepted where bytes are expected.""" - @typechecked - def foo(x: bytes) -> None: - pass - - foo(memoryview(b'foo')) - - def test_class_decorator(self): - @typechecked - class Foo: - @staticmethod - def staticmethod() -> int: - return 'foo' - - @classmethod - def classmethod(cls) -> int: - return 'foo' - - def method(self) -> int: - return 'foo' - - @property - def prop(self) -> int: - return 'foo' - - @property - def prop2(self) -> int: - return 'foo' - - @prop2.setter - def prop2(self, value: int) -> None: - pass - - pattern = 'type of the return value must be int; got str instead' - pytest.raises(TypeError, Foo.staticmethod).match(pattern) - pytest.raises(TypeError, Foo.classmethod).match(pattern) - pytest.raises(TypeError, Foo().method).match(pattern) - - with pytest.raises(TypeError) as raises: - Foo().prop - - assert raises.value.args[0] == pattern - - with pytest.raises(TypeError) as raises: - Foo().prop2 - - assert raises.value.args[0] == pattern - - with pytest.raises(TypeError) as raises: - Foo().prop2 = 'foo' - - assert raises.value.args[0] == 'type of argument "value" must be int; got str instead' - - @pytest.mark.parametrize('annotation', [ - Generator[int, str, List[str]], - Generator, - Iterable[int], - Iterable, - Iterator[int], - Iterator - ], ids=['generator', 'bare_generator', 'iterable', 'bare_iterable', 'iterator', - 'bare_iterator']) - def test_generator(self, annotation): - @typechecked - def genfunc() -> annotation: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - gen = genfunc() - with pytest.raises(StopIteration) as exc: - value = next(gen) - while True: - value = gen.send(str(value)) - assert isinstance(value, int) - - assert exc.value.value == ['2', '3', '4'] - - @pytest.mark.parametrize('annotation', [ - Generator[int, str, None], - Iterable[int], - Iterator[int] - ], ids=['generator', 'iterable', 'iterator']) - def test_generator_bad_yield(self, annotation): - @typechecked - def genfunc() -> annotation: - yield 'foo' - - gen = genfunc() - with pytest.raises(TypeError) as exc: - next(gen) - - exc.match('type of value yielded from generator must be int; got str instead') - - def test_generator_bad_send(self): - @typechecked - def genfunc() -> Generator[int, str, None]: - yield 1 - yield 2 - - gen = genfunc() - next(gen) - with pytest.raises(TypeError) as exc: - gen.send(2) - - exc.match('type of value sent to generator must be str; got int instead') - - def test_generator_bad_return(self): - @typechecked - def genfunc() -> Generator[int, str, str]: - yield 1 - return 6 - - gen = genfunc() - next(gen) - with pytest.raises(TypeError) as exc: - gen.send('foo') - - exc.match('type of return value must be str; got int instead') - - def test_return_generator(self): - @typechecked - def genfunc() -> Generator[int, None, None]: - yield 1 - - @typechecked - def foo() -> Generator[int, None, None]: - return genfunc() - - foo() - - def test_builtin_decorator(self): - @typechecked - @lru_cache() - def func(x: int) -> None: - pass - - func(3) - func(3) - pytest.raises(TypeError, func, 'foo').\ - match('type of argument "x" must be int; got str instead') - - # Make sure that @lru_cache is still being used - cache_info = func.__wrapped__.cache_info() - assert cache_info.hits == 1 - - def test_local_class(self): - @typechecked - class LocalClass: - class Inner: - pass - - def create_inner(self) -> 'Inner': - return self.Inner() - - retval = LocalClass().create_inner() - assert isinstance(retval, LocalClass.Inner) - - def test_local_class_async(self): - @typechecked - class LocalClass: - class Inner: - pass - - async def create_inner(self) -> 'Inner': - return self.Inner() - - coro = LocalClass().create_inner() - exc = pytest.raises(StopIteration, coro.send, None) - retval = exc.value.value - assert isinstance(retval, LocalClass.Inner) - - def test_callable_nonmember(self): - class CallableClass: - def __call__(self): - pass - - @typechecked - class LocalClass: - some_callable = CallableClass() - - def test_inherited_class_method(self): - @typechecked - class Parent: - @classmethod - def foo(cls, x: str) -> str: - return cls.__name__ - - @typechecked - class Child(Parent): - pass - - assert Child.foo('bar') == 'Child' - pytest.raises(TypeError, Child.foo, 1) - - def test_class_property(self): - @typechecked - class Foo: - def __init__(self) -> None: - self.foo = 'foo' - - @property - def prop(self) -> int: - """My property.""" - return 4 - - @property - def prop2(self) -> str: - return self.foo - - @prop2.setter - def prop2(self, value: str) -> None: - self.foo = value - - assert Foo.__dict__["prop"].__doc__.strip() == "My property." - f = Foo() - assert f.prop == 4 - assert f.prop2 == 'foo' - f.prop2 = 'bar' - assert f.prop2 == 'bar' - - with pytest.raises(TypeError) as raises: - f.prop2 = 3 - - assert raises.value.args[0] == 'type of argument "value" must be str; got int instead' - - def test_decorator_factory_no_annotations(self): - class CallableClass: - def __call__(self): - pass - - def decorator_factory(): - def decorator(f): - cmd = CallableClass() - return cmd - - return decorator - - with pytest.warns(UserWarning): - @typechecked - @decorator_factory() - def foo(): - pass - - @pytest.mark.skipif(sys.version_info >= (3, 12), reason="Fail wint Python 3.12") - @pytest.mark.parametrize('annotation', [TBound, TConstrained], ids=['bound', 'constrained']) - def test_typevar_forwardref(self, annotation): - @typechecked - def func(x: annotation) -> None: - pass - - func(Parent()) - func(Child()) - pytest.raises(TypeError, func, 'foo') - - @pytest.mark.parametrize('protocol_cls', [RuntimeProtocol, StaticProtocol]) - def test_protocol(self, protocol_cls): - @typechecked - def foo(arg: protocol_cls) -> None: - pass - - class Foo: - def meth(self) -> None: - pass - - foo(Foo()) - - def test_protocol_fail(self): - @typechecked - def foo(arg: RuntimeProtocol) -> None: - pass - - pytest.raises(TypeError, foo, object()).\ - match(r'type of argument "arg" \(object\) is not compatible with the RuntimeProtocol ' - 'protocol') - - def test_noreturn(self): - @typechecked - def foo() -> NoReturn: - pass - - pytest.raises(TypeError, foo).match(r'foo\(\) was declared never to return but it did') - - def test_recursive_type(self): - @typechecked - def foo(arg: JSONType) -> None: - pass - - foo({'a': [1, 2, 3]}) - pytest.raises(TypeError, foo, {'a': (1, 2, 3)}).\ - match(r'type of argument "arg" must be one of \(str, int, float, (bool, )?NoneType, ' - r'List\[Union\[str, int, float, (bool, )?NoneType, List\[JSONType\], ' - r'Dict\[str, JSONType\]\]\], ' - r'Dict\[str, Union\[str, int, float, (bool, )?NoneType, List\[JSONType\], ' - r'Dict\[str, JSONType\]\]\]\); got dict instead') - - def test_literal(self): - from http import HTTPStatus - - @typechecked - def foo(a: Literal[1, True, 'x', b'y', HTTPStatus.ACCEPTED]): - pass - - foo(HTTPStatus.ACCEPTED) - pytest.raises(TypeError, foo, 4).match(r"must be one of \(1, True, 'x', b'y', " - r"<HTTPStatus.ACCEPTED: 202>\); got 4 instead$") - - def test_literal_union(self): - @typechecked - def foo(a: Union[str, Literal[1, 6, 8]]): - pass - - foo(6) - pytest.raises(TypeError, foo, 4).\ - match(r'must be one of \(str, Literal\[1, 6, 8\]\); got int instead$') - - def test_literal_nested(self): - @typechecked - def foo(a: Literal[1, Literal['x', 'a', Literal['z']], 6, 8]): - pass - - foo('z') - pytest.raises(TypeError, foo, 4).match(r"must be one of \(1, 'x', 'a', 'z', 6, 8\); " - r"got 4 instead$") - - def test_literal_illegal_value(self): - @typechecked - def foo(a: Literal[1, 1.1]): - pass - - pytest.raises(TypeError, foo, 4).match(r"Illegal literal value: 1.1$") - - @pytest.mark.parametrize('value, total, error_re', [ - pytest.param({'x': 6, 'y': 'foo'}, True, None, id='correct'), - pytest.param({'y': 'foo'}, True, r'required key\(s\) \("x"\) missing from argument "arg"', - id='missing_x'), - pytest.param({'x': 6, 'y': 3}, True, - 'type of dict item "y" for argument "arg" must be str; got int instead', - id='wrong_y'), - pytest.param({'x': 6}, True, r'required key\(s\) \("y"\) missing from argument "arg"', - id='missing_y_error'), - pytest.param({'x': 6}, False, None, id='missing_y_ok', marks=[issue_42059]), - pytest.param({'x': 'abc'}, False, - 'type of dict item "x" for argument "arg" must be int; got str instead', - id='wrong_x', marks=[issue_42059]), - pytest.param({'x': 6, 'foo': 'abc'}, False, r'extra key\(s\) \("foo"\) in argument "arg"', - id='unknown_key') - ]) - def test_typed_dict(self, value, total, error_re): - DummyDict = TypedDict('DummyDict', {'x': int, 'y': str}, total=total) - - @typechecked - def foo(arg: DummyDict): - pass - - if error_re: - pytest.raises(TypeError, foo, value).match(error_re) - else: - foo(value) - - def test_class_abstract_property(self): - """Regression test for #206.""" - - @typechecked - class Foo: - @abstractproperty - def dummyproperty(self): - pass - - assert isinstance(Foo.dummyproperty, abstractproperty) - - -class TestTypeChecker: - @pytest.fixture - def executor(self): - executor = ThreadPoolExecutor(1) - yield executor - executor.shutdown() - - @pytest.fixture - def checker(self): - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - return TypeChecker(__name__) - - @staticmethod - def generatorfunc() -> Generator[int, None, None]: - yield 1 - - @staticmethod - def bad_generatorfunc() -> Generator[int, None, None]: - yield 1 - yield 'foo' - - @staticmethod - def error_function() -> float: - return 1 / 0 - - def test_check_call_args(self, checker: TypeChecker): - def foo(a: int): - pass - - with checker, pytest.warns(TypeWarning) as record: - assert checker.active - foo(1) - foo('x') - - assert not checker.active - foo('x') - - assert len(record) == 1 - warning = record[0].message - assert warning.error == 'type of argument "a" must be int; got str instead' - assert warning.func is foo - assert isinstance(warning.stack, list) - buffer = StringIO() - warning.print_stack(buffer) - assert len(buffer.getvalue()) > 100 - - def test_check_return_value(self, checker: TypeChecker): - def foo() -> int: - return 'x' - - with checker, pytest.warns(TypeWarning) as record: - foo() - - assert len(record) == 1 - assert record[0].message.error == 'type of the return value must be int; got str instead' - - def test_threaded_check_call_args(self, checker: TypeChecker, executor): - def foo(a: int): - pass - - with checker, pytest.warns(TypeWarning) as record: - executor.submit(foo, 1).result() - executor.submit(foo, 'x').result() - - executor.submit(foo, 'x').result() - - assert len(record) == 1 - warning = record[0].message - assert warning.error == 'type of argument "a" must be int; got str instead' - assert warning.func is foo - - def test_double_start(self, checker: TypeChecker): - """Test that the same type checker can't be started twice while running.""" - with checker: - pytest.raises(RuntimeError, checker.start).match('type checker already running') - - def test_nested(self): - """Test that nesting of type checker context managers works as expected.""" - def foo(a: int): - pass - - with warnings.catch_warnings(record=True): - warnings.simplefilter('ignore', DeprecationWarning) - parent = TypeChecker(__name__) - child = TypeChecker(__name__) - - with parent, pytest.warns(TypeWarning) as record: - foo('x') - with child: - foo('x') - - assert len(record) == 3 - - def test_existing_profiler(self, checker: TypeChecker): - """ - Test that an existing profiler function is chained with the type checker and restored after - the block is exited. - - """ - def foo(a: int): - pass - - def profiler(frame, event, arg): - nonlocal profiler_run_count - if event in ('call', 'return'): - profiler_run_count += 1 - - if old_profiler: - old_profiler(frame, event, arg) - - profiler_run_count = 0 - old_profiler = sys.getprofile() - sys.setprofile(profiler) - try: - with checker, pytest.warns(TypeWarning) as record: - foo(1) - foo('x') - - assert sys.getprofile() is profiler - finally: - sys.setprofile(old_profiler) - - assert profiler_run_count - assert len(record) == 1 - - def test_generator(self, checker): - with checker, pytest.warns(None) as record: - gen = self.generatorfunc() - assert next(gen) == 1 - - assert len(record) == 0 - - def test_generator_wrong_yield(self, checker): - with checker, pytest.warns(TypeWarning) as record: - gen = self.bad_generatorfunc() - assert list(gen) == [1, 'foo'] - - assert len(record) == 1 - assert 'type of yielded value must be int; got str instead' in str(record[0].message) - - def test_exception(self, checker): - with checker, pytest.warns(None) as record: - pytest.raises(ZeroDivisionError, self.error_function) - - assert len(record) == 0 - - @pytest.mark.parametrize('policy', [ForwardRefPolicy.WARN, ForwardRefPolicy.GUESS], - ids=['warn', 'guess']) - def test_forward_ref_policy_resolution_fails(self, checker, policy): - def unresolvable_annotation(x: 'OrderedDict'): # noqa - pass - - checker.annotation_policy = policy - gc.collect() # prevent find_function() from finding more than one instance of the function - with checker, pytest.warns(TypeHintWarning) as record: - unresolvable_annotation({}) - - assert len(record) == 1 - assert ("unresolvable_annotation: name 'OrderedDict' is not defined" - in str(record[0].message)) - assert 'x' not in unresolvable_annotation.__annotations__ - - def test_forward_ref_policy_guess(self, checker): - import collections - - def unresolvable_annotation(x: 'OrderedDict'): # noqa - pass - - checker.annotation_policy = ForwardRefPolicy.GUESS - with checker, pytest.warns(TypeHintWarning) as record: - unresolvable_annotation(collections.OrderedDict()) - - assert len(record) == 1 - assert str(record[0].message).startswith("Replaced forward declaration 'OrderedDict' in") - assert unresolvable_annotation.__annotations__['x'] is collections.OrderedDict - - -class TestTracebacks: - def test_short_tracebacks(self): - def foo(a: Callable[..., int]): - assert check_argument_types() - - try: - foo(1) - except TypeError: - _, _, tb = sys.exc_info() - parts = traceback.extract_tb(tb) - typeguard_lines = [part for part in parts - if part.filename.endswith("typeguard/__init__.py")] - assert len(typeguard_lines) == 1 diff --git a/contrib/python/typeguard/tests/test_typeguard_py36.py b/contrib/python/typeguard/tests/test_typeguard_py36.py deleted file mode 100644 index 383f7f3353f..00000000000 --- a/contrib/python/typeguard/tests/test_typeguard_py36.py +++ /dev/null @@ -1,189 +0,0 @@ -import sys -import warnings -from typing import Any, AsyncGenerator, AsyncIterable, AsyncIterator, Callable, Dict - -import pytest -from typing_extensions import Protocol, runtime_checkable - -from typeguard import TypeChecker, typechecked - -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict - - -@runtime_checkable -class RuntimeProtocol(Protocol): - member: int - - def meth(self) -> None: - ... - - -class TestTypeChecked: - @pytest.mark.parametrize('annotation', [ - AsyncGenerator[int, str], - AsyncIterable[int], - AsyncIterator[int] - ], ids=['generator', 'iterable', 'iterator']) - def test_async_generator(self, annotation): - async def run_generator(): - @typechecked - async def genfunc() -> annotation: - values.append((yield 2)) - values.append((yield 3)) - values.append((yield 4)) - - gen = genfunc() - - value = await gen.asend(None) - with pytest.raises(StopAsyncIteration): - while True: - value = await gen.asend(str(value)) - assert isinstance(value, int) - - values = [] - coro = run_generator() - try: - for elem in coro.__await__(): - print(elem) - except StopAsyncIteration as exc: - values = exc.value - - assert values == ['2', '3', '4'] - - @pytest.mark.parametrize('annotation', [ - AsyncGenerator[int, str], - AsyncIterable[int], - AsyncIterator[int] - ], ids=['generator', 'iterable', 'iterator']) - def test_async_generator_bad_yield(self, annotation): - @typechecked - async def genfunc() -> annotation: - yield 'foo' - - gen = genfunc() - with pytest.raises(TypeError) as exc: - next(gen.__anext__().__await__()) - - exc.match('type of value yielded from generator must be int; got str instead') - - def test_async_generator_bad_send(self): - @typechecked - async def genfunc() -> AsyncGenerator[int, str]: - yield 1 - yield 2 - - gen = genfunc() - pytest.raises(StopIteration, next, gen.__anext__().__await__()) - with pytest.raises(TypeError) as exc: - next(gen.asend(2).__await__()) - - exc.match('type of value sent to generator must be str; got int instead') - - def test_return_async_generator(self): - @typechecked - async def genfunc() -> AsyncGenerator[int, None]: - yield 1 - - @typechecked - def foo() -> AsyncGenerator[int, None]: - return genfunc() - - foo() - - def test_async_generator_iterate(self): - asyncgen = typechecked(asyncgenfunc)() - aiterator = asyncgen.__aiter__() - exc = pytest.raises(StopIteration, aiterator.__anext__().send, None) - assert exc.value.value == 1 - - def test_typeddict_inherited(self): - class ParentDict(TypedDict): - x: int - - class ChildDict(ParentDict, total=False): - y: int - - @typechecked - def foo(arg: ChildDict): - pass - - foo({'x': 1}) - if sys.version_info[:2] != (3, 8): - # TypedDict is unusable for runtime checking on Python 3.8 - pytest.raises(TypeError, foo, {'y': 1}) - - def test_mapping_is_not_typeddict(self): - """Regression test for #216.""" - - class Foo(Dict[str, Any]): - pass - - @typechecked - def foo(arg: Foo): - pass - - foo(Foo({'x': 1})) - - -async def asyncgenfunc() -> AsyncGenerator[int, None]: - yield 1 - - -async def asyncgeniterablefunc() -> AsyncIterable[int]: - yield 1 - - -async def asyncgeniteratorfunc() -> AsyncIterator[int]: - yield 1 - - -class TestTypeChecker: - @pytest.fixture - def checker(self): - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - return TypeChecker(__name__) - - @pytest.mark.parametrize('func', [asyncgenfunc, asyncgeniterablefunc, asyncgeniteratorfunc], - ids=['generator', 'iterable', 'iterator']) - def test_async_generator(self, checker, func): - """Make sure that the type checker does not complain about the None return value.""" - with checker, pytest.warns(None) as record: - func() - - assert len(record) == 0 - - def test_callable(self): - class command: - # we need an __annotations__ attribute to trigger the code path - whatever: float - - def __init__(self, function: Callable[[int], int]): - self.function = function - - def __call__(self, arg: int) -> None: - self.function(arg) - - @typechecked - @command - def function(arg: int) -> None: - pass - - function(1) - - -def test_protocol_non_method_members(): - @typechecked - def foo(a: RuntimeProtocol): - pass - - class Foo: - member = 1 - - def meth(self) -> None: - pass - - foo(Foo()) diff --git a/contrib/python/typeguard/tests/test_utils.py b/contrib/python/typeguard/tests/test_utils.py new file mode 100644 index 00000000000..acde4bbdf4e --- /dev/null +++ b/contrib/python/typeguard/tests/test_utils.py @@ -0,0 +1,22 @@ +import pytest + +from typeguard._utils import function_name, qualified_name + +from . import Child + + + "inputval, add_class_prefix, expected", + [ + pytest.param(qualified_name, False, "function", id="func"), + pytest.param(Child(), False, "__tests__.Child", id="instance"), + pytest.param(int, False, "int", id="builtintype"), + pytest.param(int, True, "class int", id="builtintype_classprefix"), + ], +) +def test_qualified_name(inputval, add_class_prefix, expected): + assert qualified_name(inputval, add_class_prefix=add_class_prefix) == expected + + +def test_function_name(): + assert function_name(function_name) == "typeguard._utils.function_name" diff --git a/contrib/python/typeguard/tests/test_warn_on_error.py b/contrib/python/typeguard/tests/test_warn_on_error.py new file mode 100644 index 00000000000..184b93b27e6 --- /dev/null +++ b/contrib/python/typeguard/tests/test_warn_on_error.py @@ -0,0 +1,28 @@ +from typing import List + +import pytest + +from typeguard import TypeCheckWarning, check_type, config, typechecked, warn_on_error + + +def test_check_type(recwarn): + with pytest.warns(TypeCheckWarning) as warning: + check_type(1, str, typecheck_fail_callback=warn_on_error) + + assert len(warning.list) == 1 + assert warning.list[0].filename == __file__ + assert warning.list[0].lineno == test_check_type.__code__.co_firstlineno + 2 + + +def test_typechecked(monkeypatch, recwarn): + @typechecked + def foo() -> List[int]: + return ["aa"] # type: ignore[list-item] + + monkeypatch.setattr(config, "typecheck_fail_callback", warn_on_error) + with pytest.warns(TypeCheckWarning) as warning: + foo() + + assert len(warning.list) == 1 + assert warning.list[0].filename == __file__ + assert warning.list[0].lineno == test_typechecked.__code__.co_firstlineno + 3 diff --git a/contrib/python/typeguard/tests/ya.make b/contrib/python/typeguard/tests/ya.make index 0649ac4e9f7..a91224bb36b 100644 --- a/contrib/python/typeguard/tests/ya.make +++ b/contrib/python/typeguard/tests/ya.make @@ -7,10 +7,19 @@ PEERDIR( TEST_SRCS( conftest.py - dummymodule.py + __init__.py + mypy/test_type_annotations.py + pep695.py + test_checkers.py test_importhook.py - test_typeguard.py - test_typeguard_py36.py + test_instrumentation.py + test_plugins.py + test_pytest_plugin.py + test_suppression.py + test_transformer.py + test_typechecked.py + test_utils.py + test_warn_on_error.py ) DATA( |
