diff options
| author | robot-piglet <[email protected]> | 2026-03-13 12:20:52 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-03-13 13:11:55 +0300 |
| commit | 310603350e9efce649a477b43982cb2486b66ef3 (patch) | |
| tree | 3250169445eaae287ff59875fef89f233b6663d6 /contrib/python/typeguard | |
| parent | 014466dc1a5b6ee93284cceef6afdf322721230e (diff) | |
Intermediate changes
commit_hash:990a3ddd65384e3c4083a93c73d55e8ec0b78f02
Diffstat (limited to 'contrib/python/typeguard')
17 files changed, 726 insertions, 228 deletions
diff --git a/contrib/python/typeguard/.dist-info/METADATA b/contrib/python/typeguard/.dist-info/METADATA index e758b265c12..3591701302a 100644 --- a/contrib/python/typeguard/.dist-info/METADATA +++ b/contrib/python/typeguard/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: typeguard -Version: 4.4.4 +Version: 4.5.1 Summary: Run-time type checker for Python Author-email: Alex Grönholm <[email protected]> License-Expression: MIT @@ -34,6 +34,9 @@ Dynamic: license-file .. image:: https://readthedocs.org/projects/typeguard/badge/?version=latest :target: https://typeguard.readthedocs.io/en/latest/?badge=latest :alt: Documentation +.. image:: https://tidelift.com/badges/package/pypi/typeguard + :target: https://tidelift.com/subscription/pkg/pypi-typeguard + :alt: Tidelift This library provides run-time type checking for functions defined with `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ argument (and return) type @@ -41,12 +44,16 @@ annotations, and any arbitrary objects. It can be used together with static type checkers as an additional layer of type safety, to catch type violations that could only be detected at run time. -Two principal ways to do type checking are provided: +Three principal ways to do type checking are provided, each with its pros and cons: #. The ``check_type`` function: * like ``isinstance()``, but supports arbitrary type annotations (within limits) * can be used as a ``cast()`` replacement, but with actual checking of the value +#. The ``check_argument_types()`` and ``check_return_type()`` functions: + + * debugger friendly (except when running with the pydev debugger with the C extension installed) + * does not work reliably with dynamically defined type hints (e.g. in nested functions) #. Code instrumentation: * entire modules, or individual functions (via ``@typechecked``) are recompiled, with diff --git a/contrib/python/typeguard/README.rst b/contrib/python/typeguard/README.rst index fe5896e38b1..f1e09579e1b 100644 --- a/contrib/python/typeguard/README.rst +++ b/contrib/python/typeguard/README.rst @@ -7,6 +7,9 @@ .. image:: https://readthedocs.org/projects/typeguard/badge/?version=latest :target: https://typeguard.readthedocs.io/en/latest/?badge=latest :alt: Documentation +.. image:: https://tidelift.com/badges/package/pypi/typeguard + :target: https://tidelift.com/subscription/pkg/pypi-typeguard + :alt: Tidelift This library provides run-time type checking for functions defined with `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ argument (and return) type @@ -14,12 +17,16 @@ annotations, and any arbitrary objects. It can be used together with static type checkers as an additional layer of type safety, to catch type violations that could only be detected at run time. -Two principal ways to do type checking are provided: +Three principal ways to do type checking are provided, each with its pros and cons: #. The ``check_type`` function: * like ``isinstance()``, but supports arbitrary type annotations (within limits) * can be used as a ``cast()`` replacement, but with actual checking of the value +#. The ``check_argument_types()`` and ``check_return_type()`` functions: + + * debugger friendly (except when running with the pydev debugger with the C extension installed) + * does not work reliably with dynamically defined type hints (e.g. in nested functions) #. Code instrumentation: * entire modules, or individual functions (via ``@typechecked``) are recompiled, with diff --git a/contrib/python/typeguard/tests/dummymodule.py b/contrib/python/typeguard/tests/dummymodule.py index 17ca2263cde..c7245829693 100644 --- a/contrib/python/typeguard/tests/dummymodule.py +++ b/contrib/python/typeguard/tests/dummymodule.py @@ -14,6 +14,7 @@ from typing import ( Sequence, Tuple, Type, + TypedDict, TypeVar, Union, no_type_check, @@ -246,6 +247,13 @@ def unpacking_assign() -> Tuple[int, str]: @typechecked +def unpacking_assign_single_item_tuple() -> str: + x: str + (x,) = ("foo",) + return x + + +@typechecked def unpacking_assign_generator() -> Tuple[int, str]: def genfunc(): yield 1 @@ -351,3 +359,12 @@ def typevar_forwardref(x: Type[T]) -> T: def never_called(x: List["NonExistentType"]) -> List["NonExistentType"]: # noqa: F821 """Regression test for #335.""" return x + + +# Regression test for #536 - forward reference evaluation on Python 3.14 +class ModuleLocalClass: + """A class only available in this module's namespace, not the caller's.""" + + +class TypedDictWithForwardRef(TypedDict): + x: "ModuleLocalClass" diff --git a/contrib/python/typeguard/tests/pep695.py b/contrib/python/typeguard/tests/dummymodule_py312.py index b0a29291839..93f7acbc186 100644 --- a/contrib/python/typeguard/tests/pep695.py +++ b/contrib/python/typeguard/tests/dummymodule_py312.py @@ -1,5 +1,17 @@ from typeguard import typechecked +type Foo = list[int] + + +@typechecked +def func_using_type_alias(x: Foo) -> int: + return x[0] + + +@typechecked +def func_using_type_of_type_alias(x: type[Foo]) -> type: + return x + @typechecked class ParametrizedClass[T]: diff --git a/contrib/python/typeguard/tests/test_checkers.py b/contrib/python/typeguard/tests/test_checkers.py index cc019c7b1f2..bf8b079c42f 100644 --- a/contrib/python/typeguard/tests/test_checkers.py +++ b/contrib/python/typeguard/tests/test_checkers.py @@ -535,6 +535,59 @@ class TestTypedDict: ): check_type({"x": 1, "y": 6, "z": "foo"}, DummyDict) + def test_required_pass(self, typing_provider): + try: + Required = typing_provider.Required + except AttributeError: + pytest.skip(f"'Required' not found in {typing_provider.__name__!r}") + + class DummyDict(typing_provider.TypedDict, total=False): + x1: Required[int] + x2: "Required[int]" + y: int + + check_type({"x1": 1, "x2": 2}, DummyDict) + check_type({"x1": 1, "x2": 2, "y": 3}, DummyDict) + + def test_required_missing(self, typing_provider): + try: + Required = typing_provider.Required + except AttributeError: + pytest.skip(f"'Required' not found in {typing_provider.__name__!r}") + + class DummyDict(typing_provider.TypedDict, total=False): + x1: Required[int] + x2: "Required[int]" + y: int + + with pytest.raises(TypeCheckError, match=r'is missing required key\(s\): "x1"'): + check_type({"x2": 2, "y": 3}, DummyDict) + + with pytest.raises(TypeCheckError, match=r'is missing required key\(s\): "x2"'): + check_type({"x1": 1, "y": 3}, DummyDict) + + def test_required_wrong_type(self, typing_provider): + try: + Required = typing_provider.Required + except AttributeError: + pytest.skip(f"'Required' not found in {typing_provider.__name__!r}") + + class DummyDict(typing_provider.TypedDict, total=False): + x1: Required[int] + x2: "Required[int]" + y: int + + # Ensure inner type is validated correctly (regression test for #533) + with pytest.raises( + TypeCheckError, match=r"value of key 'x1' of dict is not an instance of int" + ): + check_type({"x1": "foo", "x2": 2, "y": 3}, DummyDict) + + with pytest.raises( + TypeCheckError, match=r"value of key 'x2' of dict is not an instance of int" + ): + check_type({"x1": 1, "x2": "foo", "y": 3}, DummyDict) + def test_is_typeddict(self, typing_provider): # Ensure both typing.TypedDict and typing_extensions.TypedDict are recognized class DummyDict(typing_provider.TypedDict): @@ -543,6 +596,57 @@ class TestTypedDict: assert is_typeddict(DummyDict) assert not is_typeddict(dict) + def test_typed_dict_with_forward_ref_from_external_module(self): + """Regression test for #536: forward ref NameError in Python 3.14.""" + import tests.dummymodule + + # Only import the TypedDict, NOT ModuleLocalClass - this tests that forward + # references resolve using the type's module namespace, not the caller's + TypedDictWithForwardRef = tests.dummymodule.TypedDictWithForwardRef + + # Should not raise NameError for forward ref to ModuleLocalClass + check_type({"x": tests.dummymodule.ModuleLocalClass()}, TypedDictWithForwardRef) + + # Should still enforce types correctly + with pytest.raises( + TypeCheckError, match=r"is not an instance of .*ModuleLocalClass" + ): + check_type({"x": "not a ModuleLocalClass"}, TypedDictWithForwardRef) + + def test_extra_items_positive(self, typing_provider): + try: + + class DummyDict(typing_provider.TypedDict, extra_items=Union[str, int]): + x: int + except TypeError as exc: + if "unexpected keyword argument 'extra_items'" in str(exc): + pytest.skip( + f"typing provider {typing_provider!r} does not support extra_items in TypedDict" + ) + else: + raise + + check_type({"x": 6, "y": 7, "z": "foo"}, DummyDict) + + def test_extra_items_bad_type(self, typing_provider): + try: + + class DummyDict(typing_provider.TypedDict, extra_items=Union[str, int]): + x: int + except TypeError as exc: + if "unexpected keyword argument 'extra_items'" in str(exc): + pytest.skip( + f"typing provider {typing_provider!r} does not support extra_items in TypedDict" + ) + else: + raise + + with pytest.raises( + TypeCheckError, + match=r"value of key 'z' of dict did not match any element in the union", + ): + check_type({"x": 6, "y": 7, "z": None}, DummyDict) + class TestList: def test_bad_type(self): @@ -1404,6 +1508,40 @@ class TestProtocol: # check is skipped check_type(Foo(), MyProtocol) + def test_inherited_classmethod(self) -> None: + class MyProtocol(Protocol): + @classmethod + def class_meth(cls) -> None: + pass + + class Base: + @classmethod + def class_meth(cls) -> None: + pass + + class Sub(Base): + pass + + check_type(Sub(), MyProtocol) + check_type(Sub, type[MyProtocol]) + + def test_inherited_staticmethod(self) -> None: + class MyProtocol(Protocol): + @staticmethod + def static_meth() -> None: + pass + + class Base: + @staticmethod + def static_meth() -> None: + pass + + class Sub(Base): + pass + + check_type(Sub(), MyProtocol) + check_type(Sub, type[MyProtocol]) + class TestRecursiveType: def test_valid(self): diff --git a/contrib/python/typeguard/tests/test_functions.py b/contrib/python/typeguard/tests/test_functions.py new file mode 100644 index 00000000000..eeb4c82c67e --- /dev/null +++ b/contrib/python/typeguard/tests/test_functions.py @@ -0,0 +1,75 @@ +from typing import Any + +import pytest +from pytest import raises + +from typeguard import TypeCheckError, check_argument_types, check_return_type + + +class TestCheckArgumentTypes: + def test_success(self) -> None: + def foo(x: int, /, y: str, *args: bool, z: bytes, **kwargs: int) -> None: + check_argument_types() + + foo(1, "foo", True, False, z=b"foo", xyz=657, zzz=111) + + @pytest.mark.parametrize( + "args, kwargs, pattern", + [ + pytest.param( + ("bar", "foo"), + {"z": b"foo"}, + r'argument "x" \(str\) is not an instance of int', + id="posonlyarg", + ), + pytest.param( + (1, 1), + {"z": b"foo"}, + r'argument "y" \(int\) is not an instance of str', + id="posarg", + ), + pytest.param( + (1, "foo"), + {"z": "foo"}, + r'argument "z" \(str\) is not bytes-like', + id="kwonlyarg", + ), + pytest.param( + (1, "foo", 2), + {"z": b"foo"}, + r'item 0 of argument "args" \(tuple\) is not an instance of bool', + id="vararg", + ), + pytest.param( + (1, "foo"), + {"z": b"foo", "xyz": b"foo"}, + r"value of key 'xyz' of argument \"kwargs\" \(dict\) is not an instance of int", + id="varkwarg", + ), + ], + ) + def test_failure( + self, args: tuple[Any], kwargs: dict[str, Any], pattern: str + ) -> None: + def foo(x: int, /, y: str, *args: bool, z: bytes, **kwargs: int) -> None: + check_argument_types() + + with raises(TypeCheckError, match=pattern): + foo(*args, **kwargs) + + +class TestCheckReturnType: + def test_success(self) -> None: + def foo() -> int: + return check_return_type(0) + + foo() + + def test_failure(self) -> None: + def foo() -> int: + return check_return_type("foo") + + with raises( + TypeCheckError, match=r"the return value \(str\) is not an instance of int" + ): + foo() diff --git a/contrib/python/typeguard/tests/test_instrumentation.py b/contrib/python/typeguard/tests/test_instrumentation.py index 4d9929cbac8..59ad01b85cb 100644 --- a/contrib/python/typeguard/tests/test_instrumentation.py +++ b/contrib/python/typeguard/tests/test_instrumentation.py @@ -83,11 +83,11 @@ def deferredannos(method: str): @pytest.fixture(scope="module") -def pep695(method: str): +def dummymodule_py312(method: str): if sys.version_info < (3, 12): - raise pytest.skip("PEP 695 type parameter syntax requires Python 3.12+") + raise pytest.skip("This test requires Python 3.12+") - return _fixture_module("pep695", method) + return _fixture_module("dummymodule_py312", method) def test_type_checked_func(dummymodule): @@ -262,6 +262,10 @@ def test_unpacking_assign(dummymodule): assert dummymodule.unpacking_assign() == (1, "foo") +def test_unpacking_assign_single_item_tuple(dummymodule): + assert dummymodule.unpacking_assign_single_item_tuple() == "foo" + + def test_unpacking_assign_from_generator(dummymodule): assert dummymodule.unpacking_assign_generator() == (1, "foo") @@ -276,10 +280,10 @@ def test_unpacking_assign_star_with_annotation(dummymodule): 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", b"xyzzy", b"1234", "foo") ) == ( 1, - [b"abc", b"bah"], + [b"abc", b"bah", b"xyzzy", b"1234"], "foo", ) @@ -398,22 +402,44 @@ class TestUsesForwardRef: class TestParametrized: - def test_success_func(self, pep695): - assert pep695.parametrized_func(1, "2") == 1 + def test_success_func(self, dummymodule_py312): + assert dummymodule_py312.parametrized_func(1, "2") == 1 - def test_success_method(self, pep695): - assert pep695.ParametrizedClass[int]().method(1, "2") == 1 + def test_success_method(self, dummymodule_py312): + assert dummymodule_py312.ParametrizedClass[int]().method(1, "2") == 1 - def test_failure_func(self, pep695): + def test_failure_func(self, dummymodule_py312): with pytest.raises( TypeCheckError, match=r'argument "y" \(int\) is not an instance of str', ): - pep695.parametrized_func(1, 2) + dummymodule_py312.parametrized_func(1, 2) - def test_failure_method(self, pep695): + def test_failure_method(self, dummymodule_py312): with pytest.raises( TypeCheckError, match=r'argument "y" \(int\) is not an instance of str', ): - pep695.ParametrizedClass[int]().method("str", 2) + dummymodule_py312.ParametrizedClass[int]().method("str", 2) + + +class TestTypeAlias: + def test_success(self, dummymodule_py312): + assert dummymodule_py312.func_using_type_alias([1, 2]) == 1 + + def test_failure(self, dummymodule_py312): + with pytest.raises( + TypeCheckError, + match=r'item 0 of argument "x" \(list\) is not an instance of int', + ): + dummymodule_py312.func_using_type_alias(["foo"]) + + def test_type_arg_success(self, dummymodule_py312): + assert dummymodule_py312.func_using_type_of_type_alias(list) is list + + def test_type_arg_failure(self, dummymodule_py312): + with pytest.raises( + TypeCheckError, + match=r'argument "x" \(class dict\) is not a subclass of list', + ): + dummymodule_py312.func_using_type_of_type_alias(dict) diff --git a/contrib/python/typeguard/tests/test_transformer.py b/contrib/python/typeguard/tests/test_transformer.py index 3fb04627c9c..2e41012298e 100644 --- a/contrib/python/typeguard/tests/test_transformer.py +++ b/contrib/python/typeguard/tests/test_transformer.py @@ -22,11 +22,11 @@ def test_arguments_only() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal def foo(x: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, int)}, memo) + check_argument_types_internal('foo', {'x': (x, int)}, memo) """ ).strip() ) @@ -47,11 +47,11 @@ def test_return_only() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type + from typeguard._functions import check_return_type_internal def foo(x) -> int: memo = TypeCheckMemo(globals(), locals()) - return check_return_type('foo', 6, int, memo) + return check_return_type_internal('foo', 6, int, memo) """ ).strip() ) @@ -78,7 +78,7 @@ class TestGenerator: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type, check_yield_type + from typeguard._functions import check_return_type_internal, check_yield_type from collections.abc import Generator from typing import Any @@ -86,7 +86,7 @@ class TestGenerator: 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) + return check_return_type_internal('foo', 'test', str, memo) """ ).strip() ) @@ -411,12 +411,12 @@ def test_any_in_nested_dict() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal 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) + check_argument_types_internal('foo', {'x': (x, dict[str, dict[str, Any]])}, memo) """ ).strip() ) @@ -426,7 +426,7 @@ def test_avoid_global_names() -> None: node = parse( dedent( """ - memo = TypeCheckMemo = check_argument_types = check_return_type = None + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = None def func1(x: int) -> int: dummy = (memo,) @@ -444,19 +444,19 @@ def test_avoid_global_names() -> None: """ 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 +check_argument_types_internal as check_argument_types_internal_, check_return_type_internal as check_return_type_internal_ + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = None def func1(x: int) -> int: memo_ = TypeCheckMemo_(globals(), locals()) - check_argument_types_('func1', {'x': (x, int)}, memo_) + check_argument_types_internal_('func1', {'x': (x, int)}, memo_) dummy = (memo,) - return check_return_type_('func1', x, int, memo_) + return check_return_type_internal_('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_) + check_argument_types_internal_('func2', {'x': (x, int)}, memo_) + return check_return_type_internal_('func2', x, int, memo_) """ ).strip() ) @@ -467,7 +467,7 @@ def test_avoid_local_names() -> None: dedent( """ def foo(x: int) -> int: - memo = TypeCheckMemo = check_argument_types = check_return_type = None + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = None return x """ ) @@ -480,11 +480,11 @@ def test_avoid_local_names() -> 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_ +check_argument_types_internal as check_argument_types_internal_, check_return_type_internal as check_return_type_internal_ 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_) + check_argument_types_internal_('foo', {'x': (x, int)}, memo_) + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = None + return check_return_type_internal_('foo', x, int, memo_) """ ).strip() ) @@ -495,7 +495,7 @@ def test_avoid_nonlocal_names() -> None: dedent( """ def outer(): - memo = TypeCheckMemo = check_argument_types = check_return_type = None + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = None def foo(x: int) -> int: return x @@ -510,15 +510,15 @@ def test_avoid_nonlocal_names() -> None: == dedent( """ def outer(): - memo = TypeCheckMemo = check_argument_types = check_return_type = None + memo = TypeCheckMemo = check_argument_types_internal = check_return_type_internal = 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_ +check_argument_types_internal as check_argument_types_internal_, check_return_type_internal as check_return_type_internal_ memo_ = TypeCheckMemo_(globals(), locals()) - check_argument_types_('outer.<locals>.foo', {'x': (x, int)}, memo_) - return check_return_type_('outer.<locals>.foo', x, int, memo_) + check_argument_types_internal_('outer.<locals>.foo', {'x': (x, int)}, memo_) + return check_return_type_internal_('outer.<locals>.foo', x, int, memo_) return foo """ ).strip() @@ -544,11 +544,11 @@ def test_method() -> None: def foo(self, x: int) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal 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) + check_argument_types_internal('Foo.foo', {'x': (x, int)}, memo) + return check_return_type_internal('Foo.foo', x, int, memo) """ ).strip() ) @@ -573,11 +573,11 @@ def test_method_posonlyargs() -> None: def foo(self, x: int, /, y: str) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal 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) + check_argument_types_internal('Foo.foo', {'x': (x, int), 'y': (y, str)}, memo) + return check_return_type_internal('Foo.foo', x, int, memo) """ ).strip() ) @@ -604,11 +604,11 @@ def test_classmethod() -> None: @classmethod def foo(cls, x: int) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal 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) + check_argument_types_internal('Foo.foo', {'x': (x, int)}, memo) + return check_return_type_internal('Foo.foo', x, int, memo) """ ).strip() ) @@ -635,12 +635,12 @@ def test_classmethod_posonlyargs() -> None: @classmethod def foo(cls, x: int, /, y: str) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal memo = TypeCheckMemo(globals(), locals(), self_type=cls) - check_argument_types('Foo.foo', {'x': (x, int), 'y': (y, str)}, \ + check_argument_types_internal('Foo.foo', {'x': (x, int), 'y': (y, str)}, \ memo) - return check_return_type('Foo.foo', x, int, memo) + return check_return_type_internal('Foo.foo', x, int, memo) """ ).strip() ) @@ -667,11 +667,11 @@ def test_staticmethod() -> None: @staticmethod def foo(x: int) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('Foo.foo', {'x': (x, int)}, memo) - return check_return_type('Foo.foo', x, int, memo) + check_argument_types_internal('Foo.foo', {'x': (x, int)}, memo) + return check_return_type_internal('Foo.foo', x, int, memo) """ ).strip() ) @@ -695,7 +695,7 @@ def test_new_with_self() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type + from typeguard._functions import check_return_type_internal from typing import Self class Foo: @@ -703,7 +703,7 @@ def test_new_with_self() -> None: def __new__(cls) -> Self: Foo = cls memo = TypeCheckMemo(globals(), locals(), self_type=cls) - return check_return_type('Foo.__new__', super().__new__(cls), \ + return check_return_type_internal('Foo.__new__', super().__new__(cls), \ Self, memo) """ ).strip() @@ -728,14 +728,14 @@ def test_new_with_explicit_class_name() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type + from typeguard._functions import check_return_type_internal 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) + return check_return_type_internal('A.__new__', object.__new__(cls), A, memo) """ ).strip() ) @@ -765,11 +765,11 @@ def test_local_function() -> None: def foo(x: int) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('wrapper.<locals>.foo', {'x': (x, int)}, memo) - return check_return_type('wrapper.<locals>.foo', x, int, memo) + check_argument_types_internal('wrapper.<locals>.foo', {'x': (x, int)}, memo) + return check_return_type_internal('wrapper.<locals>.foo', x, int, memo) def foo2(x: int) -> int: return x @@ -810,13 +810,13 @@ def test_function_local_class_method() -> None: def method(self, x: int) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ -check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal memo = TypeCheckMemo(globals(), locals(), \ self_type=self.__class__) - check_argument_types('wrapper.<locals>.Foo.Bar.method', \ + check_argument_types_internal('wrapper.<locals>.Foo.Bar.method', \ {'x': (x, int)}, memo) - return check_return_type(\ + return check_return_type_internal(\ 'wrapper.<locals>.Foo.Bar.method', x, int, memo) def method2(self, x: int) -> int: @@ -841,11 +841,11 @@ def test_keyword_only_argument() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal def foo(*, x: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, int)}, memo) + check_argument_types_internal('foo', {'x': (x, int)}, memo) """ ).strip() ) @@ -866,11 +866,11 @@ def test_positional_only_argument() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal def foo(x: int, /) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, int)}, memo) + check_argument_types_internal('foo', {'x': (x, int)}, memo) """ ).strip() ) @@ -891,11 +891,11 @@ def test_variable_positional_argument() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal def foo(*args: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'args': (args, tuple[int, ...])}, memo) + check_argument_types_internal('foo', {'args': (args, tuple[int, ...])}, memo) """ ).strip() ) @@ -916,11 +916,11 @@ def test_variable_keyword_argument() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal def foo(**kwargs: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'kwargs': (kwargs, dict[str, int])}, memo) + check_argument_types_internal('foo', {'kwargs': (kwargs, dict[str, int])}, memo) """ ).strip() ) @@ -987,15 +987,15 @@ class TestTypecheckingImport: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, check_return_type + from typeguard._functions import check_argument_types_internal, check_return_type_internal 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) + check_argument_types_internal('foo', {'x': (x, list)}, memo) + return check_return_type_internal('foo', x, list, memo) """ ).strip() ) @@ -1028,7 +1028,7 @@ class TestTypecheckingImport: def foo(x: Any) -> None: memo = TypeCheckMemo(globals(), locals()) y: FooBar = x - z: list[FooBar] = check_variable_assignment([y], [[('z', list)]], \ + z: list[FooBar] = check_variable_assignment([y], [('z', list)], \ memo) """ ).strip() @@ -1115,12 +1115,12 @@ typing.Collection, Sequence]: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal 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) + check_argument_types_internal('foo', {'x': (x, List[Optional[int]])}, memo) """ ).strip() ) @@ -1145,14 +1145,14 @@ typing.Collection, Sequence]: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal 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) + check_argument_types_internal('foo', {'x': (x, Union[Iterable, str])}, memo) """ ).strip() ) @@ -1206,7 +1206,7 @@ class TestAssign: def foo() -> None: memo = TypeCheckMemo(globals(), locals()) - x: int = check_variable_assignment(otherfunc(), [[('x', int)]], \ + x: int = check_variable_assignment(otherfunc(), [('x', int)], \ memo) """ ).strip() @@ -1228,15 +1228,15 @@ memo) == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ + from typeguard._functions import check_argument_types_internal, \ check_variable_assignment def foo(*args: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'args': (args, \ + check_argument_types_internal('foo', {'args': (args, \ tuple[int, ...])}, memo) args = check_variable_assignment((5,), \ -[[('args', tuple[int, ...])]], memo) +[('args', tuple[int, ...])], memo) """ ).strip() ) @@ -1257,15 +1257,15 @@ tuple[int, ...])}, memo) == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ + from typeguard._functions import check_argument_types_internal, \ check_variable_assignment def foo(**kwargs: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'kwargs': (kwargs, \ + check_argument_types_internal('foo', {'kwargs': (kwargs, \ dict[str, int])}, memo) kwargs = check_variable_assignment({'a': 5}, \ -[[('kwargs', dict[str, int])]], memo) +[('kwargs', dict[str, int])], memo) """ ).strip() ) @@ -1295,7 +1295,7 @@ dict[str, int])}, memo) def foo() -> None: memo = TypeCheckMemo(globals(), locals()) x: int | str = check_variable_assignment(otherfunc(), \ -[[('x', Union_[int, str])]], memo) +[('x', Union_[int, str])], memo) """ ).strip() ) @@ -1388,7 +1388,7 @@ dict[str, int])}, memo) x: int z: bytes all = {target} = check_variable_assignment(otherfunc(), \ -[[('all', Any)], [('x', int), ('*y', Any), ('z', bytes)]], memo) +[('all', Any), [('x', int), ('*y', Any), ('z', bytes)]], memo) """ ).strip() ) @@ -1427,6 +1427,44 @@ self_type=self.__class__) ).strip() ) + def test_unpacking_assign_one_item_tuple(self) -> None: + node = parse( + dedent( + """ + class Foo: + + def foo(self) -> str: + x: str + x = 'test' + (x,) = ('test',) + return x + """ + ) + ) + TypeguardTransformer().visit(node) + tuple_target = "(x,)" if sys.version_info < (3, 11) else "x," + assert ( + unparse(node) + == dedent( + f""" + from typeguard import TypeCheckMemo + from typeguard._functions import check_return_type_internal, check_variable_assignment + + class Foo: + + def foo(self) -> str: + memo = TypeCheckMemo(globals(), locals(), \ +self_type=self.__class__) + x: str + x = check_variable_assignment('test', \ +[('x', str)], memo) + {tuple_target} = check_variable_assignment(('test',), \ +[[('x', str)]], memo) + return check_return_type_internal('Foo.foo', x, str, memo) + """ + ).strip() + ) + def test_assignment_annotated_argument(self) -> None: node = parse( dedent( @@ -1442,13 +1480,13 @@ self_type=self.__class__) == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ + from typeguard._functions import check_argument_types_internal, \ 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) + check_argument_types_internal('foo', {'x': (x, int)}, memo) + x = check_variable_assignment(6, [('x', int)], memo) """ ).strip() ) @@ -1498,12 +1536,12 @@ memo)): == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ + from typeguard._functions import check_argument_types_internal, \ check_variable_assignment def foo(x: int) -> None: memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, int)}, memo) + check_argument_types_internal('foo', {'x': (x, int)}, memo) if (x := check_variable_assignment(otherfunc(), [[('x', int)]], memo)): pass """ @@ -1549,7 +1587,7 @@ check_variable_assignment def foo() -> None: memo = TypeCheckMemo(globals(), locals()) x: int - x = check_variable_assignment({function}(x, 6), [[('x', int)]], \ + x = check_variable_assignment({function}(x, 6), [('x', int)], \ memo) """ ).strip() @@ -1592,14 +1630,14 @@ memo) == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, \ + from typeguard._functions import check_argument_types_internal, \ 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) + check_argument_types_internal('foo', {'x': (x, int)}, memo) + x = check_variable_assignment(iadd(x, 6), [('x', int)], memo) """ ).strip() ) @@ -1741,9 +1779,9 @@ def test_dont_leave_empty_ast_container_nodes() -> None: def foo(x: str) -> None: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, str)}, memo) + check_argument_types_internal('foo', {'x': (x, str)}, memo) """ ).strip() ) @@ -1788,9 +1826,9 @@ def test_dont_leave_empty_ast_container_nodes_2() -> None: def foo(x: str) -> None: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, str)}, memo) + check_argument_types_internal('foo', {'x': (x, str)}, memo) """ ).strip() ) @@ -1877,9 +1915,9 @@ def test_dont_parse_annotated_2nd_arg() -> None: def foo(x: Annotated[str, 'foo bar']) -> None: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('foo', {'x': (x, Annotated[str, 'foo bar'])}, memo) + check_argument_types_internal('foo', {'x': (x, Annotated[str, 'foo bar'])}, memo) """ ).strip() ) @@ -1904,9 +1942,9 @@ def test_respect_docstring() -> None: def foo() -> int: """This is a docstring.""" from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type + from typeguard._functions import check_return_type_internal memo = TypeCheckMemo(globals(), locals()) - return check_return_type('foo', 1, int, memo) + return check_return_type_internal('foo', 1, int, memo) ''' ).strip() ) @@ -1933,11 +1971,11 @@ def test_respect_future_import() -> None: """module docstring""" from __future__ import annotations from typeguard import TypeCheckMemo - from typeguard._functions import check_return_type + from typeguard._functions import check_return_type_internal def foo() -> int: memo = TypeCheckMemo(globals(), locals()) - return check_return_type('foo', 1, int, memo) + return check_return_type_internal('foo', 1, int, memo) ''' ).strip() ) @@ -1961,12 +1999,12 @@ def test_literal() -> None: == dedent( """ from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types + from typeguard._functions import check_argument_types_internal 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) + check_argument_types_internal('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 index 770b68bcff6..6fca0cb28ce 100644 --- a/contrib/python/typeguard/tests/test_typechecked.py +++ b/contrib/python/typeguard/tests/test_typechecked.py @@ -571,12 +571,13 @@ def test_debug_instrumentation(monkeypatch, capsys): ---------------------------------------------- def foo(a: str) -> int: from typeguard import TypeCheckMemo - from typeguard._functions import check_argument_types, check_return_type + from typeguard._functions import check_argument_types_internal, \ +check_return_type_internal memo = TypeCheckMemo(globals(), locals()) - check_argument_types('test_debug_instrumentation.<locals>.foo', \ + check_argument_types_internal('test_debug_instrumentation.<locals>.foo', \ {'a': (a, str)}, memo) - return check_return_type('test_debug_instrumentation.<locals>.foo', 6, \ -int, memo) + return check_return_type_internal('test_debug_instrumentation.<locals>.foo', \ +6, int, memo) ---------------------------------------------- """ ) diff --git a/contrib/python/typeguard/tests/ya.make b/contrib/python/typeguard/tests/ya.make index a91224bb36b..6647ca77e28 100644 --- a/contrib/python/typeguard/tests/ya.make +++ b/contrib/python/typeguard/tests/ya.make @@ -5,12 +5,17 @@ PEERDIR( contrib/python/typing-extensions ) +PY_SRCS( + NAMESPACE tests + dummymodule.py +) + TEST_SRCS( conftest.py __init__.py mypy/test_type_annotations.py - pep695.py test_checkers.py + test_functions.py test_importhook.py test_instrumentation.py test_plugins.py diff --git a/contrib/python/typeguard/typeguard/__init__.py b/contrib/python/typeguard/typeguard/__init__.py index 6781cad094b..5eae774194e 100644 --- a/contrib/python/typeguard/typeguard/__init__.py +++ b/contrib/python/typeguard/typeguard/__init__.py @@ -16,6 +16,8 @@ from ._exceptions import TypeCheckError as TypeCheckError from ._exceptions import TypeCheckWarning as TypeCheckWarning from ._exceptions import TypeHintWarning as TypeHintWarning from ._functions import TypeCheckFailCallback as TypeCheckFailCallback +from ._functions import check_argument_types as check_argument_types +from ._functions import check_return_type as check_return_type from ._functions import check_type as check_type from ._functions import warn_on_error as warn_on_error from ._importhook import ImportHookManager as ImportHookManager diff --git a/contrib/python/typeguard/typeguard/_checkers.py b/contrib/python/typeguard/typeguard/_checkers.py index 989409bb40d..6fe087fa455 100644 --- a/contrib/python/typeguard/typeguard/_checkers.py +++ b/contrib/python/typeguard/typeguard/_checkers.py @@ -46,9 +46,15 @@ from ._exceptions import TypeCheckError, TypeHintWarning from ._memo import TypeCheckMemo from ._utils import evaluate_forwardref, get_stacklevel, get_type_name, qualified_name +if sys.version_info >= (3, 15): + from typing import NoExtraItems +else: + from typing_extensions import NoExtraItems + if sys.version_info >= (3, 11): from typing import ( NotRequired, + Required, TypeAlias, get_args, get_origin, @@ -59,6 +65,7 @@ else: from typing_extensions import Any as SubclassableAny from typing_extensions import ( NotRequired, + Required, TypeAlias, get_args, get_origin, @@ -247,16 +254,24 @@ def check_typed_dict( raise TypeCheckError("is not a dict") declared_keys = frozenset(origin_type.__annotations__) - if hasattr(origin_type, "__required_keys__"): - required_keys = set(origin_type.__required_keys__) - else: # py3.8 and lower - required_keys = set(declared_keys) if origin_type.__total__ else set() - + required_keys = set(origin_type.__required_keys__) existing_keys = set(value) - extra_keys = existing_keys - declared_keys - if extra_keys: - keys_formatted = ", ".join(f'"{key}"' for key in sorted(extra_keys, key=repr)) - raise TypeCheckError(f"has unexpected extra key(s): {keys_formatted}") + if extra_keys := existing_keys - declared_keys: + if ( + argtype := getattr(origin_type, "__extra_items__", NoExtraItems) + ) is NoExtraItems: + keys_formatted = ", ".join( + f'"{key}"' for key in sorted(extra_keys, key=repr) + ) + raise TypeCheckError(f"has unexpected extra key(s): {keys_formatted}") + + for key in extra_keys: + argvalue = value[key] + try: + check_type_internal(argvalue, argtype, memo) + except TypeCheckError as exc: + exc.append_path_element(f"value of key {key!r}") + raise # Detect NotRequired fields which are hidden by get_type_hints() type_hints: dict[str, type] = {} @@ -267,11 +282,13 @@ def check_typed_dict( if get_origin(annotation) is NotRequired: required_keys.discard(key) annotation = get_args(annotation)[0] + elif get_origin(annotation) is Required: + required_keys.add(key) + annotation = get_args(annotation)[0] type_hints[key] = annotation - missing_keys = required_keys - existing_keys - if missing_keys: + if missing_keys := required_keys - existing_keys: keys_formatted = ", ".join(f'"{key}"' for key in sorted(missing_keys, key=repr)) raise TypeCheckError(f"is missing required key(s): {keys_formatted}") @@ -471,6 +488,9 @@ def check_class( else: expected_class = args[0] + if type(expected_class) in type_alias_types: + expected_class = expected_class.__value__ + if expected_class is Any: return elif expected_class is typing_extensions.Self: @@ -668,20 +688,26 @@ def check_signature_compatible(subject: type, protocol: type, attrname: str) -> subject_type: typing.Literal["instance", "class", "static"] = "instance" # Check if the protocol-side method is a class method or static method - if attrname in protocol.__dict__: - descriptor = protocol.__dict__[attrname] - if isinstance(descriptor, staticmethod): - protocol_type = "static" - elif isinstance(descriptor, classmethod): - protocol_type = "class" + for klass in protocol.__mro__: + if attrname in klass.__dict__: + descriptor = klass.__dict__[attrname] + if isinstance(descriptor, staticmethod): + protocol_type = "static" + elif isinstance(descriptor, classmethod): + protocol_type = "class" + + break # Check if the subject-side method is a class method or static method - if attrname in subject.__dict__: - descriptor = subject.__dict__[attrname] - if isinstance(descriptor, staticmethod): - subject_type = "static" - elif isinstance(descriptor, classmethod): - subject_type = "class" + for klass in subject.__mro__: + if attrname in klass.__dict__: + descriptor = klass.__dict__[attrname] + if isinstance(descriptor, staticmethod): + subject_type = "static" + elif isinstance(descriptor, classmethod): + subject_type = "class" + + break if protocol_type == "instance" and subject_type != "instance": raise TypeCheckError( @@ -927,6 +953,9 @@ def check_type_internal( return + if type(annotation) in type_alias_types: + annotation = annotation.__value__ + if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock): return @@ -973,7 +1002,9 @@ def check_type_internal( # Equality checks are applied to these -origin_type_checkers = { +origin_type_checkers: dict[ + Any, Callable[[Any, Any, tuple[Any, ...], TypeCheckMemo], None] +] = { bytes: check_byteslike, AbstractSet: check_set, BinaryIO: check_io, @@ -1015,11 +1046,17 @@ origin_type_checkers = { if sys.version_info >= (3, 10): origin_type_checkers[types.UnionType] = check_uniontype origin_type_checkers[typing.TypeGuard] = check_typeguard + if sys.version_info >= (3, 11): origin_type_checkers.update( {typing.LiteralString: check_literal_string, typing.Self: check_self} ) +if sys.version_info >= (3, 12): + type_alias_types = (typing_extensions.TypeAliasType, typing.TypeAliasType) +else: + type_alias_types = (typing_extensions.TypeAliasType,) + def builtin_checker_lookup( origin_type: Any, args: tuple[Any, ...], extras: tuple[Any, ...] diff --git a/contrib/python/typeguard/typeguard/_functions.py b/contrib/python/typeguard/typeguard/_functions.py index ca21c14c0c2..8617b86a219 100644 --- a/contrib/python/typeguard/typeguard/_functions.py +++ b/contrib/python/typeguard/typeguard/_functions.py @@ -3,7 +3,8 @@ from __future__ import annotations import sys import warnings from collections.abc import Sequence -from typing import Any, Callable, NoReturn, TypeVar, Union, overload +from inspect import Parameter, signature +from typing import Any, Callable, NoReturn, TypeVar, Union, get_type_hints, overload from . import _suppression from ._checkers import BINARY_MAGIC_METHODS, check_type_internal @@ -14,7 +15,7 @@ from ._config import ( ) from ._exceptions import TypeCheckError, TypeCheckWarning from ._memo import TypeCheckMemo -from ._utils import get_stacklevel, qualified_name +from ._utils import find_function, get_stacklevel, qualified_name if sys.version_info >= (3, 11): from typing import Literal, Never, TypeAlias @@ -115,7 +116,49 @@ def check_type( return value -def check_argument_types( +def check_argument_types() -> Literal[True]: + """ + Check that the argument values match the annotated types. + + Unless both ``args`` and ``kwargs`` are provided, the information will be retrieved from + the previous stack frame (ie. from the function that called this). + + :return: ``True`` + :raises TypeCheckError: if there is an argument type mismatch + + .. version-changed:: 4.5.0 + This function was restored since its removal in v3.0.0. + + """ + # faster than inspect.currentframe(), but not officially + # supported in all python implementations + frame = sys._getframe(1) + func = find_function(frame) + f_locals = frame.f_locals + memo = TypeCheckMemo(frame.f_globals, frame.f_locals) + if sys.version_info >= (3, 10): + sig = signature(func, globals=frame.f_globals, locals=frame.f_locals) + else: + sig = signature(func) + + arguments = {} + for param in sig.parameters.values(): + if param.annotation is Parameter.empty or param.annotation is Any: + continue + + if param.kind is Parameter.VAR_POSITIONAL: + annotation: Any = tuple[param.annotation, ...] # type: ignore[name-defined] + elif param.kind is Parameter.VAR_KEYWORD: + annotation = dict[str, param.annotation] # type: ignore[name-defined] + else: + annotation = param.annotation + + arguments[param.name] = (f_locals[param.name], annotation) + + return check_argument_types_internal(func.__name__, arguments, memo) + + +def check_argument_types_internal( func_name: str, arguments: dict[str, tuple[Any, Any]], memo: TypeCheckMemo, @@ -146,7 +189,28 @@ def check_argument_types( return True -def check_return_type( +def check_return_type(retval: T) -> T: + """ + Check that the return value is compatible with the return value annotation in the function. + + :param retval: the value about to be returned from the call + :return: ``retval`` unchanged + :raises TypeCheckError: if there is a type mismatch + + .. version-changed:: 4.5.0 + This function was restored since its removal in v3.0.0. + + """ + # faster than inspect.currentframe(), but not officially + # supported in all python implementations + frame = sys._getframe(1) + func = find_function(frame) + memo = TypeCheckMemo(frame.f_globals, frame.f_locals) + type_hints = get_type_hints(func, frame.f_globals, frame.f_locals) + return check_return_type_internal(func.__name__, retval, type_hints["return"], memo) + + +def check_return_type_internal( func_name: str, retval: T, annotation: Any, @@ -243,42 +307,39 @@ def check_yield_type( def check_variable_assignment( - value: Any, targets: Sequence[list[tuple[str, Any]]], memo: TypeCheckMemo + value: Any, + groups: Sequence[list[tuple[str, Any]] | tuple[str, Any]], + memo: TypeCheckMemo, ) -> Any: if _suppression.type_checks_suppressed: return value value_to_return = value - for target in targets: - star_variable_index = next( - (i for i, (varname, _) in enumerate(target) if varname.startswith("*")), - None, - ) - if star_variable_index is not None: - value_to_return = list(value) - remaining_vars = len(target) - 1 - star_variable_index - end_index = len(value_to_return) - remaining_vars - values_to_check = ( - value_to_return[:star_variable_index] - + [value_to_return[star_variable_index:end_index]] - + value_to_return[end_index:] - ) - elif len(target) > 1: - values_to_check = value_to_return = [] - iterator = iter(value) - for _ in target: - try: - values_to_check.append(next(iterator)) - except StopIteration: - raise ValueError( - f"not enough values to unpack (expected {len(target)}, got " - f"{len(values_to_check)})" - ) from None + for targets in groups: + values_to_check: list[tuple[Any, str, Any]] + if isinstance(targets, list): + values_to_check = [] - else: - values_to_check = [value] + # Get all the available values from a generator or arbitrary iterator + if not isinstance(value_to_return, list): + value_to_return = list(value) + + iterator = iter(value_to_return) + for index, (name, annotation) in enumerate(targets): + if name.startswith("*"): + remaining_values = list(iterator) + num_remaining_targets = len(targets) - 1 - index + cutoff_offset = len(remaining_values) - num_remaining_targets + star_values = remaining_values[:cutoff_offset] + iterator = iter(remaining_values[cutoff_offset:]) + values_to_check.append((star_values, name[1:], annotation)) + else: + next_value = next(iterator) + values_to_check.append((next_value, name, annotation)) + else: # single target, no unpacking + values_to_check = [(value,) + targets] - for val, (varname, annotation) in zip(values_to_check, target): + for val, varname, annotation in values_to_check: try: check_type_internal(val, annotation, memo) except TypeCheckError as exc: diff --git a/contrib/python/typeguard/typeguard/_importhook.py b/contrib/python/typeguard/typeguard/_importhook.py index 0d1c6274419..034fc206775 100644 --- a/contrib/python/typeguard/typeguard/_importhook.py +++ b/contrib/python/typeguard/typeguard/_importhook.py @@ -1,6 +1,7 @@ from __future__ import annotations import ast +import os import sys import types from collections.abc import Callable, Iterable, Sequence @@ -56,23 +57,30 @@ class TypeguardLoader(SourceFileLoader): def source_to_code( data: Buffer | str | ast.Module | ast.Expression | ast.Interactive, path: Buffer | str | PathLike[str] = "<string>", + *, + _optimize: int = -1, ) -> CodeType: + if isinstance(path, (str, PathLike)): + filename = path + else: + filename = os.fsdecode(bytes(path)) + if isinstance(data, (ast.Module, ast.Expression, ast.Interactive)): - tree = data + module = data else: if isinstance(data, str): source = data else: source = decode_source(data) - tree = _call_with_frames_removed( + module = _call_with_frames_removed( ast.parse, source, - path, + filename, "exec", ) - tree = TypeguardTransformer().visit(tree) + tree = TypeguardTransformer().visit(module) ast.fix_missing_locations(tree) if global_config.debug_instrumentation and sys.version_info >= (3, 9): @@ -85,7 +93,7 @@ class TypeguardLoader(SourceFileLoader): print("----------------------------------------------", file=sys.stderr) return _call_with_frames_removed( - compile, tree, path, "exec", 0, dont_inherit=True + compile, tree, filename, "exec", 0, dont_inherit=True ) def exec_module(self, module: ModuleType) -> None: diff --git a/contrib/python/typeguard/typeguard/_transformer.py b/contrib/python/typeguard/typeguard/_transformer.py index 7b6dda85263..257b2e3bc48 100644 --- a/contrib/python/typeguard/typeguard/_transformer.py +++ b/contrib/python/typeguard/typeguard/_transformer.py @@ -64,6 +64,9 @@ from copy import deepcopy from dataclasses import dataclass, field from typing import Any, ClassVar, cast, overload +PreliminaryNameTypePair: typing.TypeAlias = tuple[Constant, "expr | None"] +NameTypePair: typing.TypeAlias = tuple[Constant, expr] + generator_names = ( "typing.Generator", "collections.abc.Generator", @@ -767,7 +770,7 @@ class TypeguardTransformer(NodeTransformer): ], ) func_name = self._get_import( - "typeguard._functions", "check_argument_types" + "typeguard._functions", "check_argument_types_internal" ) args = [ self._memo.joined_path, @@ -790,7 +793,7 @@ class TypeguardTransformer(NodeTransformer): ) ): func_name = self._get_import( - "typeguard._functions", "check_return_type" + "typeguard._functions", "check_return_type_internal" ) return_node = Return( Call( @@ -920,7 +923,9 @@ class TypeguardTransformer(NodeTransformer): and self._memo.should_instrument and not self._memo.is_ignored_name(self._memo.return_annotation) ): - func_name = self._get_import("typeguard._functions", "check_return_type") + func_name = self._get_import( + "typeguard._functions", "check_return_type_internal" + ) old_node = node retval = old_node.value or Constant(None) node = Return( @@ -1011,13 +1016,8 @@ class TypeguardTransformer(NodeTransformer): ) targets_arg = List( [ - List( - [ - Tuple( - [Constant(node.target.id), annotation], - ctx=Load(), - ) - ], + Tuple( + [Constant(node.target.id), annotation], ctx=Load(), ) ], @@ -1045,18 +1045,22 @@ class TypeguardTransformer(NodeTransformer): # Only instrument function-local assignments if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)): - preliminary_targets: list[list[tuple[Constant, expr | None]]] = [] + preliminary_targets: list[ + PreliminaryNameTypePair | list[PreliminaryNameTypePair] + ] = [] check_required = False - for target in node.targets: + for node_target in node.targets: elts: Sequence[expr] - if isinstance(target, Name): - elts = [target] - elif isinstance(target, Tuple): - elts = target.elts + if isinstance(node_target, Name): + elts = [node_target] + single_target = True + elif isinstance(node_target, Tuple): + elts = node_target.elts + single_target = False else: continue - annotations_: list[tuple[Constant, expr | None]] = [] + annotations_: list[PreliminaryNameTypePair] = [] for exp in elts: prefix = "" if isinstance(exp, Starred): @@ -1082,33 +1086,49 @@ class TypeguardTransformer(NodeTransformer): else: annotations_.append((Constant(name), None)) - preliminary_targets.append(annotations_) + preliminary_targets.append( + annotations_[0] if single_target else annotations_ + ) if check_required: # Replace missing annotations with typing.Any - targets: list[list[tuple[Constant, expr]]] = [] + targets: list[NameTypePair | list[NameTypePair]] = [] for items in preliminary_targets: - target_list: list[tuple[Constant, expr]] = [] - targets.append(target_list) - for key, expression in items: - if expression is None: - target_list.append((key, self._get_import("typing", "Any"))) - else: + if isinstance(items, list): + target_list: list[NameTypePair] = [] + targets.append(target_list) + for key, expression_or_none in items: + expression = expression_or_none or self._get_import( + "typing", "Any" + ) target_list.append((key, expression)) + else: + key, expression_or_none = items + expression = expression_or_none or self._get_import( + "typing", "Any" + ) + targets.append((key, expression)) func_name = self._get_import( "typeguard._functions", "check_variable_assignment" ) - targets_arg = List( - [ - List( - [Tuple([name, ann], ctx=Load()) for name, ann in target], - ctx=Load(), + elements: list[expr] = [] + for target in targets: + if isinstance(target, list): + elements.append( + List( + [ + Tuple([name, ann], ctx=Load()) + for name, ann in target + ], + ctx=Load(), + ) ) - for target in targets - ], - ctx=Load(), - ) + else: + name_constant, ann = target + elements.append(Tuple([name_constant, ann], ctx=Load())) + + targets_arg = List(elements, ctx=Load()) node.value = Call( func_name, [node.value, targets_arg, self._memo.get_memo_name()], @@ -1187,12 +1207,7 @@ class TypeguardTransformer(NodeTransformer): operator_func, [Name(node.target.id, ctx=Load()), node.value], [] ) targets_arg = List( - [ - List( - [Tuple([Constant(node.target.id), annotation], ctx=Load())], - ctx=Load(), - ) - ], + [Tuple([Constant(node.target.id), annotation], ctx=Load())], ctx=Load(), ) check_call = Call( diff --git a/contrib/python/typeguard/typeguard/_utils.py b/contrib/python/typeguard/typeguard/_utils.py index f61b94d20ad..1dcb9942800 100644 --- a/contrib/python/typeguard/typeguard/_utils.py +++ b/contrib/python/typeguard/typeguard/_utils.py @@ -1,10 +1,11 @@ from __future__ import annotations +import gc import inspect import sys from importlib import import_module from inspect import currentframe -from types import FrameType +from types import CodeType, FrameType, FunctionType from typing import ( TYPE_CHECKING, Any, @@ -16,13 +17,27 @@ from typing import ( get_args, get_origin, ) +from weakref import WeakValueDictionary if TYPE_CHECKING: from ._memo import TypeCheckMemo +_functions_map: WeakValueDictionary[CodeType, FunctionType] = WeakValueDictionary() + if sys.version_info >= (3, 14): def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + # If the ForwardRef has a module, try that module's namespace first. + # This is needed because Python 3.14's ForwardRef.evaluate() requires + # all referenced names to be available in the provided globals/locals. + if getattr(forwardref, "__forward_module__", None): + try: + # Not passing globals / locals defaults to those of the caller + return forwardref.evaluate(type_params=()) + except NameError: + # Fall back to caller's namespace for backwards compatibility + pass + return forwardref.evaluate( globals=memo.globals, locals=memo.locals, type_params=() ) @@ -158,6 +173,40 @@ def get_stacklevel() -> int: return level +def find_function(frame: FrameType) -> Callable[..., Any]: + """ + Return a function object from the garbage collector that matches the frame's code object. + + This process is unreliable as several function objects could use the same code object. + Fortunately the likelihood of this happening with the combination of the function objects + having different type annotations is a very rare occurrence. + + :param frame: a frame object + :return: a function object if one was found, ``None`` if not + + """ + func = _functions_map.get(frame.f_code) + if func is None: + for obj in gc.get_referrers(frame.f_code): + if inspect.isfunction(obj): + if func is None: + # The first match was found + func = obj + else: + # A second match was found + raise LookupError( + "found more than one match when looking for the target function" + ) + + # Cache the result for future lookups + if func is not None: + _functions_map[frame.f_code] = func + else: + raise LookupError("target function not found") + + return func + + @final class Unset: __slots__ = () diff --git a/contrib/python/typeguard/ya.make b/contrib/python/typeguard/ya.make index dffb4f60d5a..69e182cf2b5 100644 --- a/contrib/python/typeguard/ya.make +++ b/contrib/python/typeguard/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.4.4) +VERSION(4.5.1) LICENSE(MIT) |
