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 | |
| parent | d191c438a6de7f1a0a7079cdab06e27377dbcbff (diff) | |
Intermediate changes
commit_hash:5d7bb0b3a16601d453badbe45dcb76f582024a93
Diffstat (limited to 'contrib/python')
43 files changed, 9163 insertions, 3483 deletions
diff --git a/contrib/python/typeguard/.dist-info/METADATA b/contrib/python/typeguard/.dist-info/METADATA index 6ca2152dd5a..e758b265c12 100644 --- a/contrib/python/typeguard/.dist-info/METADATA +++ b/contrib/python/typeguard/.dist-info/METADATA @@ -1,77 +1,73 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: typeguard -Version: 2.13.3 +Version: 4.4.4 Summary: Run-time type checker for Python -Home-page: UNKNOWN -Author: Alex Grönholm -Author-email: [email protected] -License: MIT +Author-email: Alex Grönholm <[email protected]> +License-Expression: MIT Project-URL: Documentation, https://typeguard.readthedocs.io/en/latest/ Project-URL: Change log, https://typeguard.readthedocs.io/en/latest/versionhistory.html Project-URL: Source code, https://github.com/agronholm/typeguard Project-URL: Issue tracker, https://github.com/agronholm/typeguard/issues -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.5.3 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst License-File: LICENSE -Provides-Extra: doc -Requires-Dist: sphinx-rtd-theme ; extra == 'doc' -Requires-Dist: sphinx-autodoc-typehints (>=1.2.0) ; extra == 'doc' -Provides-Extra: test -Requires-Dist: pytest ; extra == 'test' -Requires-Dist: typing-extensions ; extra == 'test' -Requires-Dist: mypy ; (platform_python_implementation != "PyPy") and extra == 'test' +Requires-Dist: importlib_metadata>=3.6; python_version < "3.10" +Requires-Dist: typing_extensions>=4.14.0 +Dynamic: license-file -.. image:: https://travis-ci.com/agronholm/typeguard.svg?branch=master - :target: https://travis-ci.com/agronholm/typeguard +.. image:: https://github.com/agronholm/typeguard/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/typeguard/actions/workflows/test.yml :alt: Build Status .. image:: https://coveralls.io/repos/agronholm/typeguard/badge.svg?branch=master&service=github :target: https://coveralls.io/github/agronholm/typeguard?branch=master :alt: Code Coverage .. image:: https://readthedocs.org/projects/typeguard/badge/?version=latest :target: https://typeguard.readthedocs.io/en/latest/?badge=latest + :alt: Documentation 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 annotations. +`PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ argument (and return) type +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. -Four principal ways to do type checking are provided, each with its pros and cons: +Two principal ways to do type checking are provided: -#. the ``check_argument_types()`` and ``check_return_type()`` functions: +#. The ``check_type`` function: - * 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) -#. the ``@typechecked`` decorator: + * like ``isinstance()``, but supports arbitrary type annotations (within limits) + * can be used as a ``cast()`` replacement, but with actual checking of the value +#. Code instrumentation: - * automatically type checks yields and sends of returned generators (regular and async) - * adds an extra frame to the call stack for every call to a decorated function -#. the stack profiler hook (``with TypeChecker('packagename'):``) (deprecated): + * entire modules, or individual functions (via ``@typechecked``) are recompiled, with + type checking code injected into them + * automatically checks function arguments, return values and assignments to annotated + local variables + * for generator functions (regular and async), checks yield and send values + * requires the original source code of the instrumented module(s) to be accessible - * emits warnings instead of raising ``TypeError`` - * requires very few modifications to the code - * multiple TypeCheckers can be stacked/nested - * does not work reliably with dynamically defined type hints (e.g. in nested functions) - * may cause problems with badly behaving debuggers or profilers - * cannot distinguish between an exception being raised and a ``None`` being returned -#. the import hook (``typeguard.importhook.install_import_hook()``): +Two options are provided for code instrumentation: - * automatically annotates classes and functions with ``@typechecked`` on import - * no code changes required in target modules - * requires imports of modules you need to check to be deferred until after the import hook has - been installed - * may clash with other import hooks +#. the ``@typechecked`` function: -See the documentation_ for further instructions. + * can be applied to functions individually +#. the import hook (``typeguard.install_import_hook()``): -.. _documentation: https://typeguard.readthedocs.io/en/latest/ + * automatically instruments targeted modules on import + * no manual code changes required in the target modules + * requires the import hook to be installed before the targeted modules are imported + * may clash with other import hooks +See the documentation_ for further information. +.. _documentation: https://typeguard.readthedocs.io/en/latest/ diff --git a/contrib/python/typeguard/.dist-info/entry_points.txt b/contrib/python/typeguard/.dist-info/entry_points.txt index 0bde2f50de5..47c9d0bd913 100644 --- a/contrib/python/typeguard/.dist-info/entry_points.txt +++ b/contrib/python/typeguard/.dist-info/entry_points.txt @@ -1,3 +1,2 @@ [pytest11] -typeguard = typeguard.pytest_plugin - +typeguard = typeguard._pytest_plugin diff --git a/contrib/python/typeguard/README.rst b/contrib/python/typeguard/README.rst index 6d2ed01bac4..fe5896e38b1 100644 --- a/contrib/python/typeguard/README.rst +++ b/contrib/python/typeguard/README.rst @@ -1,41 +1,46 @@ -.. image:: https://travis-ci.com/agronholm/typeguard.svg?branch=master - :target: https://travis-ci.com/agronholm/typeguard +.. image:: https://github.com/agronholm/typeguard/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/typeguard/actions/workflows/test.yml :alt: Build Status .. image:: https://coveralls.io/repos/agronholm/typeguard/badge.svg?branch=master&service=github :target: https://coveralls.io/github/agronholm/typeguard?branch=master :alt: Code Coverage .. image:: https://readthedocs.org/projects/typeguard/badge/?version=latest :target: https://typeguard.readthedocs.io/en/latest/?badge=latest + :alt: Documentation 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 annotations. +`PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ argument (and return) type +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. -Four principal ways to do type checking are provided, each with its pros and cons: +Two principal ways to do type checking are provided: -#. the ``check_argument_types()`` and ``check_return_type()`` functions: +#. The ``check_type`` function: - * 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) -#. the ``@typechecked`` decorator: + * like ``isinstance()``, but supports arbitrary type annotations (within limits) + * can be used as a ``cast()`` replacement, but with actual checking of the value +#. Code instrumentation: - * automatically type checks yields and sends of returned generators (regular and async) - * adds an extra frame to the call stack for every call to a decorated function -#. the stack profiler hook (``with TypeChecker('packagename'):``) (deprecated): + * entire modules, or individual functions (via ``@typechecked``) are recompiled, with + type checking code injected into them + * automatically checks function arguments, return values and assignments to annotated + local variables + * for generator functions (regular and async), checks yield and send values + * requires the original source code of the instrumented module(s) to be accessible - * emits warnings instead of raising ``TypeError`` - * requires very few modifications to the code - * multiple TypeCheckers can be stacked/nested - * does not work reliably with dynamically defined type hints (e.g. in nested functions) - * may cause problems with badly behaving debuggers or profilers - * cannot distinguish between an exception being raised and a ``None`` being returned -#. the import hook (``typeguard.importhook.install_import_hook()``): +Two options are provided for code instrumentation: - * automatically annotates classes and functions with ``@typechecked`` on import - * no code changes required in target modules - * requires imports of modules you need to check to be deferred until after the import hook has - been installed +#. the ``@typechecked`` function: + + * can be applied to functions individually +#. the import hook (``typeguard.install_import_hook()``): + + * automatically instruments targeted modules on import + * no manual code changes required in the target modules + * requires the import hook to be installed before the targeted modules are imported * may clash with other import hooks -See the documentation_ for further instructions. +See the documentation_ for further information. .. _documentation: https://typeguard.readthedocs.io/en/latest/ diff --git a/contrib/python/typeguard/patches/01-fix-tests.patch b/contrib/python/typeguard/patches/01-fix-tests.patch index f012dca6207..e3c84632cb3 100644 --- a/contrib/python/typeguard/patches/01-fix-tests.patch +++ b/contrib/python/typeguard/patches/01-fix-tests.patch @@ -1,42 +1,72 @@ ---- contrib/python/typeguard/tests/test_importhook.py (index) -+++ contrib/python/typeguard/tests/test_importhook.py (working tree) -@@ -8,7 +8,9 @@ import pytest - - from typeguard.importhook import TypeguardFinder, install_import_hook +--- contrib/python/typeguard/tests/mypy/test_type_annotations.py (index) ++++ contrib/python/typeguard/tests/mypy/test_type_annotations.py (working tree) +@@ -75,6 +75,8 @@ def chdir_local() -> None: + os.chdir(os.path.dirname(__file__)) --this_dir = Path(__file__).parent -+import yatest.common as yc -+ -+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')) -@@ -29,6 +31,7 @@ def dummymodule(): - sys.path.remove(str(this_dir)) ++# Этот тест ожидает "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 - def test_cached_module(dummymodule): - assert cached_module_path.is_file() + @pytest.mark.usefixtures("chdir_local") + def test_negative() -> None: + """ +--- contrib/python/typeguard/tests/test_checkers.py (index) ++++ contrib/python/typeguard/tests/test_checkers.py (working tree) +@@ -771,7 +771,7 @@ class TestNamedTuple: + + 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" ++ r"tuple is not a named tuple of type __tests__.Employee" + ) + + def test_wrong_field_type(self): +@@ -913,7 +913,7 @@ class TestTypevar: + check_type(Child(), TParent) ---- contrib/python/typeguard/tests/test_typeguard.py (index) -+++ contrib/python/typeguard/tests/test_typeguard.py (working tree) -@@ -84,1 +84,1 @@ def mock_class(request): -- (Child(), 'test_typeguard.Child'), -+ (Child(), '__tests__.test_typeguard.Child'), -@@ -467,1 +467,1 @@ class TestCheckArgumentTypes: -- r'(test_typeguard\.)?Employee; got tuple instead') -+ r'(__tests__\.test_typeguard\.)?Employee; got tuple instead') -@@ -547,2 +547,2 @@ class TestCheckArgumentTypes: -- assert str(exc.value) == ('type of argument "a" must be test_typeguard.Child or one of ' -- 'its subclasses; got test_typeguard.Parent instead') -+ 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') -@@ -556,1 +556,1 @@ class TestCheckArgumentTypes: -- '"a" must be a subclass of test_typeguard.Child; got test_typeguard.Parent instead') -+ '"a" must be a subclass of __tests__.test_typeguard.Child; got __tests__.test_typeguard.Parent instead') -@@ -585,2 +585,2 @@ class TestCheckArgumentTypes: -- assert str(exc.value) == ('type of argument "a" must be test_typeguard.Child; ' -- 'got test_typeguard.Parent instead') -+ assert str(exc.value) == ('type of argument "a" must be __tests__.test_typeguard.Child; ' -+ 'got __tests__.test_typeguard.Parent instead') + def test_bound_fail(self): +- with pytest.raises(TypeCheckError, match="is not an instance of tests.Child"): ++ with pytest.raises(TypeCheckError, match="is not an instance of __tests__.Child"): + check_type(Parent(), TChild) + + @pytest.mark.parametrize( +--- contrib/python/typeguard/tests/test_importhook.py (index) ++++ contrib/python/typeguard/tests/test_importhook.py (working tree) +@@ -65,5 +65,6 @@ def test_debug_instrumentation(monkeypatch, capsys): + import_dummymodule() + out, err = capsys.readouterr() + path_str = str(dummy_module_path) +- assert f"Source code of {path_str!r} after instrumentation:" in err ++ # в ya make "path_str" разрешается в подкаталог ~/.ya/build/build_root/... ++ assert f"{path_str!r} after instrumentation:"[1:] in err + assert "class DummyClass" in err +--- contrib/python/typeguard/tests/test_typechecked.py (index) ++++ contrib/python/typeguard/tests/test_typechecked.py (working tree) +@@ -613,6 +613,8 @@ def test_existing_method_decorator(): + assert value == 7 + + ++# Этот тест не укладывается в 300s + @pytest.mark.parametrize( + "flags, expected_return_code", + [ +--- contrib/python/typeguard/tests/test_utils.py (index) ++++ contrib/python/typeguard/tests/test_utils.py (working tree) +@@ -9,7 +9,7 @@ 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(Child(), False, "__tests__.Child", id="instance"), + pytest.param(int, False, "int", id="builtintype"), + pytest.param(int, True, "class int", id="builtintype_classprefix"), + ], diff --git a/contrib/python/typeguard/patches/02-support-new-typing-extensions.patch b/contrib/python/typeguard/patches/02-support-new-typing-extensions.patch deleted file mode 100644 index b296e06bd0b..00000000000 --- a/contrib/python/typeguard/patches/02-support-new-typing-extensions.patch +++ /dev/null @@ -1,16 +0,0 @@ ---- contrib/python/typeguard/tests/test_typeguard.py (index) -+++ contrib/python/typeguard/tests/test_typeguard.py (working tree) -@@ -8,11 +8,11 @@ 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) -+ 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, TypedDict, runtime_checkable -+from typing_extensions import Literal, NoReturn, Protocol, runtime_checkable - - from typeguard import ( - ForwardRefPolicy, TypeChecker, TypeHintWarning, TypeWarning, check_argument_types, diff --git a/contrib/python/typeguard/patches/03-support-python-3.12.patch b/contrib/python/typeguard/patches/03-support-python-3.12.patch deleted file mode 100644 index d0432dd0d3f..00000000000 --- a/contrib/python/typeguard/patches/03-support-python-3.12.patch +++ /dev/null @@ -1,15 +0,0 @@ ---- contrib/python/typeguard/typeguard/importhook.py (index) -+++ contrib/python/typeguard/typeguard/importhook.py (working tree) -@@ -29 +29 @@ class TypeguardTransformer(ast.NodeVisitor): -- elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Str): -+ elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant): ---- contrib/python/typeguard/tests/test_typeguard.py (index) -+++ contrib/python/typeguard/tests/test_typeguard.py (working tree) -@@ -1227,6 +1227,7 @@ class TestTypeChecked: - 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 diff --git a/contrib/python/typeguard/patches/04-support-python-3.12.4.patch b/contrib/python/typeguard/patches/04-support-python-3.12.4.patch deleted file mode 100644 index bed273464aa..00000000000 --- a/contrib/python/typeguard/patches/04-support-python-3.12.4.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- contrib/python/typeguard/typeguard/__init__.py (index) -+++ contrib/python/typeguard/typeguard/__init__.py (working tree) -@@ -265,7 +265,7 @@ def resolve_forwardref(maybe_ref, memo: _TypeCheckMemo): - if sys.version_info < (3, 9, 0): - return evaluate_forwardref(maybe_ref, memo.globals, memo.locals) - else: -- return evaluate_forwardref(maybe_ref, memo.globals, memo.locals, frozenset()) -+ return evaluate_forwardref(maybe_ref, memo.globals, memo.locals, recursive_guard=frozenset()) - - else: - return maybe_ref 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( diff --git a/contrib/python/typeguard/typeguard/__init__.py b/contrib/python/typeguard/typeguard/__init__.py index 33d83d4582c..6781cad094b 100644 --- a/contrib/python/typeguard/typeguard/__init__.py +++ b/contrib/python/typeguard/typeguard/__init__.py @@ -1,1258 +1,48 @@ -__all__ = ('ForwardRefPolicy', 'TypeHintWarning', 'typechecked', 'check_return_type', - 'check_argument_types', 'check_type', 'TypeWarning', 'TypeChecker', - 'typeguard_ignore') +import os +from typing import Any -import collections.abc -import gc -import inspect -import sys -import threading -from collections import OrderedDict -from enum import Enum -from functools import partial, wraps -from inspect import Parameter, isclass, isfunction, isgeneratorfunction -from io import BufferedIOBase, IOBase, RawIOBase, TextIOBase -from traceback import extract_stack, print_stack -from types import CodeType, FunctionType -from typing import ( - IO, TYPE_CHECKING, AbstractSet, Any, AsyncIterable, AsyncIterator, BinaryIO, Callable, Dict, - Generator, Iterable, Iterator, List, NewType, Optional, Sequence, Set, TextIO, Tuple, Type, - TypeVar, Union, get_type_hints, overload) -from unittest.mock import Mock -from warnings import warn -from weakref import WeakKeyDictionary, WeakValueDictionary +from ._checkers import TypeCheckerCallable as TypeCheckerCallable +from ._checkers import TypeCheckLookupCallback as TypeCheckLookupCallback +from ._checkers import check_type_internal as check_type_internal +from ._checkers import checker_lookup_functions as checker_lookup_functions +from ._checkers import load_plugins as load_plugins +from ._config import CollectionCheckStrategy as CollectionCheckStrategy +from ._config import ForwardRefPolicy as ForwardRefPolicy +from ._config import TypeCheckConfiguration as TypeCheckConfiguration +from ._decorators import typechecked as typechecked +from ._decorators import typeguard_ignore as typeguard_ignore +from ._exceptions import InstrumentationWarning as InstrumentationWarning +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_type as check_type +from ._functions import warn_on_error as warn_on_error +from ._importhook import ImportHookManager as ImportHookManager +from ._importhook import TypeguardFinder as TypeguardFinder +from ._importhook import install_import_hook as install_import_hook +from ._memo import TypeCheckMemo as TypeCheckMemo +from ._suppression import suppress_type_checks as suppress_type_checks +from ._utils import Unset as Unset -# Python 3.8+ -try: - from typing_extensions import Literal -except ImportError: - try: - from typing import Literal - except ImportError: - Literal = None +# Re-export imports so they look like they live directly in this package +for value in list(locals().values()): + if getattr(value, "__module__", "").startswith(f"{__name__}."): + value.__module__ = __name__ -# Python 3.5.4+ / 3.6.2+ -try: - from typing_extensions import NoReturn -except ImportError: - try: - from typing import NoReturn - except ImportError: - NoReturn = None -# Python 3.6+ -try: - from inspect import isasyncgen, isasyncgenfunction - from typing import AsyncGenerator -except ImportError: - AsyncGenerator = None +config: TypeCheckConfiguration - def isasyncgen(obj): - return False - def isasyncgenfunction(func): - return False +def __getattr__(name: str) -> Any: + if name == "config": + from ._config import global_config -# Python 3.8+ -try: - from typing import ForwardRef - evaluate_forwardref = ForwardRef._evaluate -except ImportError: - from typing import _ForwardRef as ForwardRef - evaluate_forwardref = ForwardRef._eval_type + return global_config -if sys.version_info >= (3, 10): - from typing import is_typeddict -else: - _typed_dict_meta_types = () - if sys.version_info >= (3, 8): - from typing import _TypedDictMeta - _typed_dict_meta_types += (_TypedDictMeta,) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - try: - from typing_extensions import _TypedDictMeta - _typed_dict_meta_types += (_TypedDictMeta,) - except ImportError: - pass - def is_typeddict(tp) -> bool: - return isinstance(tp, _typed_dict_meta_types) - - -if TYPE_CHECKING: - _F = TypeVar("_F") - - def typeguard_ignore(f: _F) -> _F: - """This decorator is a noop during static type-checking.""" - return f -else: - from typing import no_type_check as typeguard_ignore - - -_type_hints_map = WeakKeyDictionary() # type: Dict[FunctionType, Dict[str, Any]] -_functions_map = WeakValueDictionary() # type: Dict[CodeType, FunctionType] -_missing = object() - -T_CallableOrType = TypeVar('T_CallableOrType', bound=Callable[..., Any]) - -# Lifted from mypy.sharedparse -BINARY_MAGIC_METHODS = { - "__add__", - "__and__", - "__cmp__", - "__divmod__", - "__div__", - "__eq__", - "__floordiv__", - "__ge__", - "__gt__", - "__iadd__", - "__iand__", - "__idiv__", - "__ifloordiv__", - "__ilshift__", - "__imatmul__", - "__imod__", - "__imul__", - "__ior__", - "__ipow__", - "__irshift__", - "__isub__", - "__itruediv__", - "__ixor__", - "__le__", - "__lshift__", - "__lt__", - "__matmul__", - "__mod__", - "__mul__", - "__ne__", - "__or__", - "__pow__", - "__radd__", - "__rand__", - "__rdiv__", - "__rfloordiv__", - "__rlshift__", - "__rmatmul__", - "__rmod__", - "__rmul__", - "__ror__", - "__rpow__", - "__rrshift__", - "__rshift__", - "__rsub__", - "__rtruediv__", - "__rxor__", - "__sub__", - "__truediv__", - "__xor__", -} - - -class ForwardRefPolicy(Enum): - """Defines how unresolved forward references are handled.""" - - ERROR = 1 #: propagate the :exc:`NameError` from :func:`~typing.get_type_hints` - WARN = 2 #: remove the annotation and emit a TypeHintWarning - #: replace the annotation with the argument's class if the qualified name matches, else remove - #: the annotation - GUESS = 3 - - -class TypeHintWarning(UserWarning): - """ - A warning that is emitted when a type hint in string form could not be resolved to an actual - type. - """ - - -class _TypeCheckMemo: - __slots__ = 'globals', 'locals' - - def __init__(self, globals: Dict[str, Any], locals: Dict[str, Any]): - self.globals = globals - self.locals = locals - - -def _strip_annotation(annotation): - if isinstance(annotation, str): - return annotation.strip("'") - else: - return annotation - - -class _CallMemo(_TypeCheckMemo): - __slots__ = 'func', 'func_name', 'arguments', 'is_generator', 'type_hints' - - def __init__(self, func: Callable, frame_locals: Optional[Dict[str, Any]] = None, - args: tuple = None, kwargs: Dict[str, Any] = None, - forward_refs_policy=ForwardRefPolicy.ERROR): - super().__init__(func.__globals__, frame_locals) - self.func = func - self.func_name = function_name(func) - self.is_generator = isgeneratorfunction(func) - signature = inspect.signature(func) - - if args is not None and kwargs is not None: - self.arguments = signature.bind(*args, **kwargs).arguments - else: - assert frame_locals is not None, 'frame must be specified if args or kwargs is None' - self.arguments = frame_locals - - self.type_hints = _type_hints_map.get(func) - if self.type_hints is None: - while True: - if sys.version_info < (3, 5, 3): - frame_locals = dict(frame_locals) - - try: - hints = get_type_hints(func, localns=frame_locals) - except NameError as exc: - if forward_refs_policy is ForwardRefPolicy.ERROR: - raise - - typename = str(exc).split("'", 2)[1] - for param in signature.parameters.values(): - if _strip_annotation(param.annotation) == typename: - break - else: - raise - - func_name = function_name(func) - if forward_refs_policy is ForwardRefPolicy.GUESS: - if param.name in self.arguments: - argtype = self.arguments[param.name].__class__ - stripped = _strip_annotation(param.annotation) - if stripped == argtype.__qualname__: - func.__annotations__[param.name] = argtype - msg = ('Replaced forward declaration {!r} in {} with {!r}' - .format(stripped, func_name, argtype)) - warn(TypeHintWarning(msg)) - continue - - msg = 'Could not resolve type hint {!r} on {}: {}'.format( - param.annotation, function_name(func), exc) - warn(TypeHintWarning(msg)) - del func.__annotations__[param.name] - else: - break - - self.type_hints = OrderedDict() - for name, parameter in signature.parameters.items(): - if name in hints: - annotated_type = hints[name] - - # PEP 428 discourages it by MyPy does not complain - if parameter.default is None: - annotated_type = Optional[annotated_type] - - if parameter.kind == Parameter.VAR_POSITIONAL: - self.type_hints[name] = Tuple[annotated_type, ...] - elif parameter.kind == Parameter.VAR_KEYWORD: - self.type_hints[name] = Dict[str, annotated_type] - else: - self.type_hints[name] = annotated_type - - if 'return' in hints: - self.type_hints['return'] = hints['return'] - - _type_hints_map[func] = self.type_hints - - -def resolve_forwardref(maybe_ref, memo: _TypeCheckMemo): - if isinstance(maybe_ref, ForwardRef): - if sys.version_info < (3, 9, 0): - return evaluate_forwardref(maybe_ref, memo.globals, memo.locals) - else: - return evaluate_forwardref(maybe_ref, memo.globals, memo.locals, recursive_guard=frozenset()) - - else: - return maybe_ref - - -def get_type_name(type_): - name = (getattr(type_, '__name__', None) or getattr(type_, '_name', None) or - getattr(type_, '__forward_arg__', None)) - if name is None: - origin = getattr(type_, '__origin__', None) - name = getattr(origin, '_name', None) - if name is None and not inspect.isclass(type_): - name = type_.__class__.__name__.strip('_') - - args = getattr(type_, '__args__', ()) or getattr(type_, '__values__', ()) - if args != getattr(type_, '__parameters__', ()): - if name == 'Literal': - formatted_args = ', '.join(str(arg) for arg in args) - else: - formatted_args = ', '.join(get_type_name(arg) for arg in args) - - name = '{}[{}]'.format(name, formatted_args) - - module = getattr(type_, '__module__', None) - if module not in (None, 'typing', 'typing_extensions', 'builtins'): - name = module + '.' + name - - return name - - -def find_function(frame) -> Optional[Callable]: - """ - 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 - return None - - # 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 - - -def qualified_name(obj) -> str: - """ - Return the qualified name (e.g. package.module.Type) for the given object. - - Builtins and types from the :mod:`typing` package get special treatment by having the module - name stripped from the generated name. - - """ - type_ = obj if inspect.isclass(obj) else type(obj) - module = type_.__module__ - qualname = type_.__qualname__ - return qualname if module in ('typing', 'builtins') else '{}.{}'.format(module, qualname) - - -def function_name(func: Callable) -> str: - """ - Return the qualified name of the given function. - - Builtins and types from the :mod:`typing` package get special treatment by having the module - name stripped from the generated name. - - """ - # For partial functions and objects with __call__ defined, __qualname__ does not exist - # For functions run in `exec` with a custom namespace, __module__ can be None - module = getattr(func, '__module__', '') or '' - qualname = (module + '.') if module not in ('builtins', '') else '' - return qualname + getattr(func, '__qualname__', repr(func)) - - -def check_callable(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not callable(value): - raise TypeError('{} must be a callable'.format(argname)) - - if getattr(expected_type, "__args__", None): - try: - signature = inspect.signature(value) - except (TypeError, ValueError): - return - - if hasattr(expected_type, '__result__'): - # Python 3.5 - argument_types = expected_type.__args__ - check_args = argument_types is not Ellipsis - else: - # Python 3.6 - argument_types = expected_type.__args__[:-1] - check_args = argument_types != (Ellipsis,) - - if check_args: - # The callable must not have keyword-only arguments without defaults - unfulfilled_kwonlyargs = [ - param.name for param in signature.parameters.values() if - param.kind == Parameter.KEYWORD_ONLY and param.default == Parameter.empty] - if unfulfilled_kwonlyargs: - raise TypeError( - 'callable passed as {} has mandatory keyword-only arguments in its ' - 'declaration: {}'.format(argname, ', '.join(unfulfilled_kwonlyargs))) - - num_mandatory_args = len([ - param.name for param in signature.parameters.values() - if param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) and - param.default is Parameter.empty]) - has_varargs = any(param for param in signature.parameters.values() - if param.kind == Parameter.VAR_POSITIONAL) - - if num_mandatory_args > len(argument_types): - raise TypeError( - 'callable passed as {} has too many arguments in its declaration; expected {} ' - 'but {} argument(s) declared'.format(argname, len(argument_types), - num_mandatory_args)) - elif not has_varargs and num_mandatory_args < len(argument_types): - raise TypeError( - 'callable passed as {} has too few arguments in its declaration; expected {} ' - 'but {} argument(s) declared'.format(argname, len(argument_types), - num_mandatory_args)) - - -def check_dict(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not isinstance(value, dict): - raise TypeError('type of {} must be a dict; got {} instead'. - format(argname, qualified_name(value))) - - if expected_type is not dict: - if (hasattr(expected_type, "__args__") and - expected_type.__args__ not in (None, expected_type.__parameters__)): - key_type, value_type = expected_type.__args__ - if key_type is not Any or value_type is not Any: - for k, v in value.items(): - check_type('keys of {}'.format(argname), k, key_type, memo) - check_type('{}[{!r}]'.format(argname, k), v, value_type, memo) - - -def check_typed_dict(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - declared_keys = frozenset(expected_type.__annotations__) - if hasattr(expected_type, '__required_keys__'): - required_keys = expected_type.__required_keys__ - else: # py3.8 and lower - required_keys = declared_keys if expected_type.__total__ else frozenset() - - existing_keys = frozenset(value) - extra_keys = existing_keys - declared_keys - if extra_keys: - keys_formatted = ', '.join('"{}"'.format(key) for key in sorted(extra_keys)) - raise TypeError('extra key(s) ({}) in {}'.format(keys_formatted, argname)) - - missing_keys = required_keys - existing_keys - if missing_keys: - keys_formatted = ', '.join('"{}"'.format(key) for key in sorted(missing_keys)) - raise TypeError('required key(s) ({}) missing from {}'.format(keys_formatted, argname)) - - for key, argtype in get_type_hints(expected_type).items(): - argvalue = value.get(key, _missing) - if argvalue is not _missing: - check_type('dict item "{}" for {}'.format(key, argname), argvalue, argtype, memo) - - -def check_list(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not isinstance(value, list): - raise TypeError('type of {} must be a list; got {} instead'. - format(argname, qualified_name(value))) - - if expected_type is not list: - if hasattr(expected_type, "__args__") and expected_type.__args__ not in \ - (None, expected_type.__parameters__): - value_type = expected_type.__args__[0] - if value_type is not Any: - for i, v in enumerate(value): - check_type('{}[{}]'.format(argname, i), v, value_type, memo) - - -def check_sequence(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not isinstance(value, collections.abc.Sequence): - raise TypeError('type of {} must be a sequence; got {} instead'. - format(argname, qualified_name(value))) - - if hasattr(expected_type, "__args__") and expected_type.__args__ not in \ - (None, expected_type.__parameters__): - value_type = expected_type.__args__[0] - if value_type is not Any: - for i, v in enumerate(value): - check_type('{}[{}]'.format(argname, i), v, value_type, memo) - - -def check_set(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not isinstance(value, AbstractSet): - raise TypeError('type of {} must be a set; got {} instead'. - format(argname, qualified_name(value))) - - if expected_type is not set: - if hasattr(expected_type, "__args__") and expected_type.__args__ not in \ - (None, expected_type.__parameters__): - value_type = expected_type.__args__[0] - if value_type is not Any: - for v in value: - check_type('elements of {}'.format(argname), v, value_type, memo) - - -def check_tuple(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - # Specialized check for NamedTuples - is_named_tuple = False - if sys.version_info < (3, 8, 0): - is_named_tuple = hasattr(expected_type, '_field_types') # deprecated since python 3.8 - else: - is_named_tuple = hasattr(expected_type, '__annotations__') - - if is_named_tuple: - if not isinstance(value, expected_type): - raise TypeError('type of {} must be a named tuple of type {}; got {} instead'. - format(argname, qualified_name(expected_type), qualified_name(value))) - - if sys.version_info < (3, 8, 0): - field_types = expected_type._field_types - else: - field_types = expected_type.__annotations__ - - for name, field_type in field_types.items(): - check_type('{}.{}'.format(argname, name), getattr(value, name), field_type, memo) - - return - elif not isinstance(value, tuple): - raise TypeError('type of {} must be a tuple; got {} instead'. - format(argname, qualified_name(value))) - - if getattr(expected_type, '__tuple_params__', None): - # Python 3.5 - use_ellipsis = expected_type.__tuple_use_ellipsis__ - tuple_params = expected_type.__tuple_params__ - elif getattr(expected_type, '__args__', None): - # Python 3.6+ - use_ellipsis = expected_type.__args__[-1] is Ellipsis - tuple_params = expected_type.__args__[:-1 if use_ellipsis else None] - else: - # Unparametrized Tuple or plain tuple - return - - if use_ellipsis: - element_type = tuple_params[0] - for i, element in enumerate(value): - check_type('{}[{}]'.format(argname, i), element, element_type, memo) - elif tuple_params == ((),): - if value != (): - raise TypeError('{} is not an empty tuple but one was expected'.format(argname)) - else: - if len(value) != len(tuple_params): - raise TypeError('{} has wrong number of elements (expected {}, got {} instead)' - .format(argname, len(tuple_params), len(value))) - - for i, (element, element_type) in enumerate(zip(value, tuple_params)): - check_type('{}[{}]'.format(argname, i), element, element_type, memo) - - -def check_union(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if hasattr(expected_type, '__union_params__'): - # Python 3.5 - union_params = expected_type.__union_params__ - else: - # Python 3.6+ - union_params = expected_type.__args__ - - for type_ in union_params: - try: - check_type(argname, value, type_, memo) - return - except TypeError: - pass - - typelist = ', '.join(get_type_name(t) for t in union_params) - raise TypeError('type of {} must be one of ({}); got {} instead'. - format(argname, typelist, qualified_name(value))) - - -def check_class(argname: str, value, expected_type, memo: _TypeCheckMemo) -> None: - if not isclass(value): - raise TypeError('type of {} must be a type; got {} instead'.format( - argname, qualified_name(value))) - - # Needed on Python 3.7+ - if expected_type is Type: - return - - if getattr(expected_type, '__origin__', None) in (Type, type): - expected_class = expected_type.__args__[0] - else: - expected_class = expected_type - - if expected_class is Any: - return - elif isinstance(expected_class, TypeVar): - check_typevar(argname, value, expected_class, memo, True) - elif getattr(expected_class, '__origin__', None) is Union: - for arg in expected_class.__args__: - try: - check_class(argname, value, arg, memo) - break - except TypeError: - pass - else: - formatted_args = ', '.join(get_type_name(arg) for arg in expected_class.__args__) - raise TypeError('{} must match one of the following: ({}); got {} instead'.format( - argname, formatted_args, qualified_name(value) - )) - elif not issubclass(value, expected_class): - raise TypeError('{} must be a subclass of {}; got {} instead'.format( - argname, qualified_name(expected_class), qualified_name(value))) - - -def check_typevar(argname: str, value, typevar: TypeVar, memo: _TypeCheckMemo, - subclass_check: bool = False) -> None: - value_type = value if subclass_check else type(value) - subject = argname if subclass_check else 'type of ' + argname - - if typevar.__bound__ is not None: - bound_type = resolve_forwardref(typevar.__bound__, memo) - if not issubclass(value_type, bound_type): - raise TypeError( - '{} must be {} or one of its subclasses; got {} instead' - .format(subject, qualified_name(bound_type), qualified_name(value_type))) - elif typevar.__constraints__: - constraints = [resolve_forwardref(c, memo) for c in typevar.__constraints__] - for constraint in constraints: - try: - check_type(argname, value, constraint, memo) - except TypeError: - pass - else: - break - else: - formatted_constraints = ', '.join(get_type_name(constraint) - for constraint in constraints) - raise TypeError('{} must match one of the constraints ({}); got {} instead' - .format(subject, formatted_constraints, qualified_name(value_type))) - - -def check_literal(argname: str, value, expected_type, memo: _TypeCheckMemo): - def get_args(literal): - try: - args = literal.__args__ - except AttributeError: - # Instance of Literal from typing_extensions - args = literal.__values__ - - retval = [] - for arg in args: - if isinstance(arg, Literal.__class__) or getattr(arg, '__origin__', None) is Literal: - # The first check works on py3.6 and lower, the second one on py3.7+ - retval.extend(get_args(arg)) - elif isinstance(arg, (int, str, bytes, bool, type(None), Enum)): - retval.append(arg) - else: - raise TypeError('Illegal literal value: {}'.format(arg)) - - return retval - - final_args = tuple(get_args(expected_type)) - if value not in final_args: - raise TypeError('the value of {} must be one of {}; got {} instead'. - format(argname, final_args, value)) - - -def check_number(argname: str, value, expected_type): - if expected_type is complex and not isinstance(value, (complex, float, int)): - raise TypeError('type of {} must be either complex, float or int; got {} instead'. - format(argname, qualified_name(value.__class__))) - elif expected_type is float and not isinstance(value, (float, int)): - raise TypeError('type of {} must be either float or int; got {} instead'. - format(argname, qualified_name(value.__class__))) - - -def check_io(argname: str, value, expected_type): - if expected_type is TextIO: - if not isinstance(value, TextIOBase): - raise TypeError('type of {} must be a text based I/O object; got {} instead'. - format(argname, qualified_name(value.__class__))) - elif expected_type is BinaryIO: - if not isinstance(value, (RawIOBase, BufferedIOBase)): - raise TypeError('type of {} must be a binary I/O object; got {} instead'. - format(argname, qualified_name(value.__class__))) - elif not isinstance(value, IOBase): - raise TypeError('type of {} must be an I/O object; got {} instead'. - format(argname, qualified_name(value.__class__))) - - -def check_protocol(argname: str, value, expected_type): - # TODO: implement proper compatibility checking and support non-runtime protocols - if getattr(expected_type, '_is_runtime_protocol', False): - if not isinstance(value, expected_type): - raise TypeError('type of {} ({}) is not compatible with the {} protocol'. - format(argname, type(value).__qualname__, expected_type.__qualname__)) - - -# Equality checks are applied to these -origin_type_checkers = { - AbstractSet: check_set, - Callable: check_callable, - collections.abc.Callable: check_callable, - dict: check_dict, - Dict: check_dict, - list: check_list, - List: check_list, - Sequence: check_sequence, - collections.abc.Sequence: check_sequence, - collections.abc.Set: check_set, - set: check_set, - Set: check_set, - tuple: check_tuple, - Tuple: check_tuple, - type: check_class, - Type: check_class, - Union: check_union -} -_subclass_check_unions = hasattr(Union, '__union_set_params__') -if Literal is not None: - origin_type_checkers[Literal] = check_literal - -generator_origin_types = (Generator, collections.abc.Generator, - Iterator, collections.abc.Iterator, - Iterable, collections.abc.Iterable) -asyncgen_origin_types = (AsyncIterator, collections.abc.AsyncIterator, - AsyncIterable, collections.abc.AsyncIterable) -if AsyncGenerator is not None: - asyncgen_origin_types += (AsyncGenerator,) -if hasattr(collections.abc, 'AsyncGenerator'): - asyncgen_origin_types += (collections.abc.AsyncGenerator,) - - -def check_type(argname: str, value, expected_type, memo: Optional[_TypeCheckMemo] = None, *, - globals: Optional[Dict[str, Any]] = None, - locals: Optional[Dict[str, Any]] = None) -> None: - """ - Ensure that ``value`` matches ``expected_type``. - - The types from the :mod:`typing` module do not support :func:`isinstance` or :func:`issubclass` - so a number of type specific checks are required. This function knows which checker to call - for which type. - - :param argname: name of the argument to check; used for error messages - :param value: value to be checked against ``expected_type`` - :param expected_type: a class or generic type instance - :param globals: dictionary of global variables to use for resolving forward references - (defaults to the calling frame's globals) - :param locals: dictionary of local variables to use for resolving forward references - (defaults to the calling frame's locals) - :raises TypeError: if there is a type mismatch - - """ - if expected_type is Any or isinstance(value, Mock): - return - - if expected_type is None: - # Only happens on < 3.6 - expected_type = type(None) - - if memo is None: - frame = sys._getframe(1) - if globals is None: - globals = frame.f_globals - if locals is None: - locals = frame.f_locals - - memo = _TypeCheckMemo(globals, locals) - - expected_type = resolve_forwardref(expected_type, memo) - origin_type = getattr(expected_type, '__origin__', None) - if origin_type is not None: - checker_func = origin_type_checkers.get(origin_type) - if checker_func: - checker_func(argname, value, expected_type, memo) - else: - check_type(argname, value, origin_type, memo) - elif isclass(expected_type): - if issubclass(expected_type, Tuple): - check_tuple(argname, value, expected_type, memo) - elif issubclass(expected_type, (float, complex)): - check_number(argname, value, expected_type) - elif _subclass_check_unions and issubclass(expected_type, Union): - check_union(argname, value, expected_type, memo) - elif isinstance(expected_type, TypeVar): - check_typevar(argname, value, expected_type, memo) - elif issubclass(expected_type, IO): - check_io(argname, value, expected_type) - elif is_typeddict(expected_type): - check_typed_dict(argname, value, expected_type, memo) - elif getattr(expected_type, '_is_protocol', False): - check_protocol(argname, value, expected_type) - else: - expected_type = (getattr(expected_type, '__extra__', None) or origin_type or - expected_type) - - if expected_type is bytes: - # As per https://github.com/python/typing/issues/552 - if not isinstance(value, (bytearray, bytes, memoryview)): - raise TypeError('type of {} must be bytes-like; got {} instead' - .format(argname, qualified_name(value))) - elif not isinstance(value, expected_type): - raise TypeError( - 'type of {} must be {}; got {} instead'. - format(argname, qualified_name(expected_type), qualified_name(value))) - elif isinstance(expected_type, TypeVar): - # Only happens on < 3.6 - check_typevar(argname, value, expected_type, memo) - elif isinstance(expected_type, Literal.__class__): - # Only happens on < 3.7 when using Literal from typing_extensions - check_literal(argname, value, expected_type, memo) - elif expected_type.__class__ is NewType: - # typing.NewType on Python 3.10+ - return check_type(argname, value, expected_type.__supertype__, memo) - elif (isfunction(expected_type) and - getattr(expected_type, "__module__", None) == "typing" and - getattr(expected_type, "__qualname__", None).startswith("NewType.") and - hasattr(expected_type, "__supertype__")): - # typing.NewType on Python 3.9 and below - return check_type(argname, value, expected_type.__supertype__, memo) - - -def check_return_type(retval, memo: Optional[_CallMemo] = None) -> bool: - """ - 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: ``True`` - :raises TypeError: if there is a type mismatch - - """ - if memo is None: - # faster than inspect.currentframe(), but not officially - # supported in all python implementations - frame = sys._getframe(1) - - try: - func = find_function(frame) - except LookupError: - return True # This can happen with the Pydev/PyCharm debugger extension installed - - memo = _CallMemo(func, frame.f_locals) - - if 'return' in memo.type_hints: - if memo.type_hints['return'] is NoReturn: - raise TypeError('{}() was declared never to return but it did'.format(memo.func_name)) - - try: - check_type('the return value', retval, memo.type_hints['return'], memo) - except TypeError as exc: # suppress unnecessarily long tracebacks - # Allow NotImplemented if this is a binary magic method (__eq__() et al) - if retval is NotImplemented and memo.type_hints['return'] is bool: - # This does (and cannot) not check if it's actually a method - func_name = memo.func_name.rsplit('.', 1)[-1] - if len(memo.arguments) == 2 and func_name in BINARY_MAGIC_METHODS: - return True - - raise TypeError(*exc.args) from None - - return True - - -def check_argument_types(memo: Optional[_CallMemo] = None) -> bool: - """ - 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 TypeError: if there is an argument type mismatch - - """ - if memo is None: - # faster than inspect.currentframe(), but not officially - # supported in all python implementations - frame = sys._getframe(1) - - try: - func = find_function(frame) - except LookupError: - return True # This can happen with the Pydev/PyCharm debugger extension installed - - memo = _CallMemo(func, frame.f_locals) - - for argname, expected_type in memo.type_hints.items(): - if argname != 'return' and argname in memo.arguments: - value = memo.arguments[argname] - description = 'argument "{}"'.format(argname) - try: - check_type(description, value, expected_type, memo) - except TypeError as exc: # suppress unnecessarily long tracebacks - raise TypeError(*exc.args) from None - - return True - - -class TypeCheckedGenerator: - def __init__(self, wrapped: Generator, memo: _CallMemo): - rtype_args = [] - if hasattr(memo.type_hints['return'], "__args__"): - rtype_args = memo.type_hints['return'].__args__ - - self.__wrapped = wrapped - self.__memo = memo - self.__yield_type = rtype_args[0] if rtype_args else Any - self.__send_type = rtype_args[1] if len(rtype_args) > 1 else Any - self.__return_type = rtype_args[2] if len(rtype_args) > 2 else Any - self.__initialized = False - - def __iter__(self): - return self - - def __next__(self): - return self.send(None) - - def __getattr__(self, name: str) -> Any: - return getattr(self.__wrapped, name) - - def throw(self, *args): - return self.__wrapped.throw(*args) - - def close(self): - self.__wrapped.close() - - def send(self, obj): - if self.__initialized: - check_type('value sent to generator', obj, self.__send_type, memo=self.__memo) - else: - self.__initialized = True - - try: - value = self.__wrapped.send(obj) - except StopIteration as exc: - check_type('return value', exc.value, self.__return_type, memo=self.__memo) - raise - - check_type('value yielded from generator', value, self.__yield_type, memo=self.__memo) - return value - - -class TypeCheckedAsyncGenerator: - def __init__(self, wrapped: AsyncGenerator, memo: _CallMemo): - rtype_args = memo.type_hints['return'].__args__ - self.__wrapped = wrapped - self.__memo = memo - self.__yield_type = rtype_args[0] - self.__send_type = rtype_args[1] if len(rtype_args) > 1 else Any - self.__initialized = False - - def __aiter__(self): - return self - - def __anext__(self): - return self.asend(None) - - def __getattr__(self, name: str) -> Any: - return getattr(self.__wrapped, name) - - def athrow(self, *args): - return self.__wrapped.athrow(*args) - - def aclose(self): - return self.__wrapped.aclose() - - async def asend(self, obj): - if self.__initialized: - check_type('value sent to generator', obj, self.__send_type, memo=self.__memo) - else: - self.__initialized = True - - value = await self.__wrapped.asend(obj) - check_type('value yielded from generator', value, self.__yield_type, memo=self.__memo) - return value - - -@overload -def typechecked(*, always: bool = False) -> Callable[[T_CallableOrType], T_CallableOrType]: - ... - - -@overload -def typechecked(func: T_CallableOrType, *, always: bool = False) -> T_CallableOrType: - ... - - -def typechecked(func=None, *, always=False, _localns: Optional[Dict[str, Any]] = None): - """ - Perform runtime type checking on the arguments that are passed to the wrapped function. - - The return value is also checked against the return annotation if any. - - If the ``__debug__`` global variable is set to ``False``, no wrapping and therefore no type - checking is done, unless ``always`` is ``True``. - - This can also be used as a class decorator. This will wrap all type annotated methods, - including ``@classmethod``, ``@staticmethod``, and ``@property`` decorated methods, - in the class with the ``@typechecked`` decorator. - - :param func: the function or class to enable type checking for - :param always: ``True`` to enable type checks even in optimized mode - - """ - if func is None: - return partial(typechecked, always=always, _localns=_localns) - - if not __debug__ and not always: # pragma: no cover - return func - - if isclass(func): - prefix = func.__qualname__ + '.' - for key, attr in func.__dict__.items(): - if inspect.isfunction(attr) or inspect.ismethod(attr) or inspect.isclass(attr): - if attr.__qualname__.startswith(prefix) and getattr(attr, '__annotations__', None): - setattr(func, key, typechecked(attr, always=always, _localns=func.__dict__)) - elif isinstance(attr, (classmethod, staticmethod)): - if getattr(attr.__func__, '__annotations__', None): - wrapped = typechecked(attr.__func__, always=always, _localns=func.__dict__) - setattr(func, key, type(attr)(wrapped)) - elif isinstance(attr, property): - kwargs = dict(doc=attr.__doc__) - for name in ("fset", "fget", "fdel"): - property_func = kwargs[name] = getattr(attr, name) - if property_func is not None and getattr(property_func, '__annotations__', ()): - kwargs[name] = typechecked( - property_func, always=always, _localns=func.__dict__ - ) - - setattr(func, key, attr.__class__(**kwargs)) - - return func - - if not getattr(func, '__annotations__', None): - warn('no type annotations present -- not typechecking {}'.format(function_name(func))) - return func - - # Find the frame in which the function was declared, for resolving forward references later - if _localns is None: - _localns = sys._getframe(1).f_locals - - # Find either the first Python wrapper or the actual function - python_func = inspect.unwrap(func, stop=lambda f: hasattr(f, '__code__')) - - if not getattr(python_func, '__code__', None): - warn('no code associated -- not typechecking {}'.format(function_name(func))) - return func - - def wrapper(*args, **kwargs): - memo = _CallMemo(python_func, _localns, args=args, kwargs=kwargs) - check_argument_types(memo) - retval = func(*args, **kwargs) - try: - check_return_type(retval, memo) - except TypeError as exc: - raise TypeError(*exc.args) from None - - # If a generator is returned, wrap it if its yield/send/return types can be checked - if inspect.isgenerator(retval) or isasyncgen(retval): - return_type = memo.type_hints.get('return') - if return_type: - origin = getattr(return_type, '__origin__', None) - if origin in generator_origin_types: - return TypeCheckedGenerator(retval, memo) - elif origin is not None and origin in asyncgen_origin_types: - return TypeCheckedAsyncGenerator(retval, memo) - - return retval - - async def async_wrapper(*args, **kwargs): - memo = _CallMemo(python_func, _localns, args=args, kwargs=kwargs) - check_argument_types(memo) - retval = await func(*args, **kwargs) - check_return_type(retval, memo) - return retval - - if inspect.iscoroutinefunction(func): - if python_func.__code__ is not async_wrapper.__code__: - return wraps(func)(async_wrapper) - else: - if python_func.__code__ is not wrapper.__code__: - return wraps(func)(wrapper) - - # the target callable was already wrapped - return func - - -class TypeWarning(UserWarning): - """ - A warning that is emitted when a type check fails. - - :ivar str event: ``call`` or ``return`` - :ivar Callable func: the function in which the violation occurred (the called function if event - is ``call``, or the function where a value of the wrong type was returned from if event is - ``return``) - :ivar str error: the error message contained by the caught :class:`TypeError` - :ivar frame: the frame in which the violation occurred - """ - - __slots__ = ('func', 'event', 'message', 'frame') - - def __init__(self, memo: Optional[_CallMemo], event: str, frame, - exception: Union[str, TypeError]): # pragma: no cover - self.func = memo.func - self.event = event - self.error = str(exception) - self.frame = frame - - if self.event == 'call': - caller_frame = self.frame.f_back - event = 'call to {}() from {}:{}'.format( - function_name(self.func), caller_frame.f_code.co_filename, caller_frame.f_lineno) - else: - event = 'return from {}() at {}:{}'.format( - function_name(self.func), self.frame.f_code.co_filename, self.frame.f_lineno) - - super().__init__('[{thread_name}] {event}: {self.error}'.format( - thread_name=threading.current_thread().name, event=event, self=self)) - - @property - def stack(self): - """Return the stack where the last frame is from the target function.""" - return extract_stack(self.frame) - - def print_stack(self, file: TextIO = None, limit: int = None) -> None: - """ - Print the traceback from the stack frame where the target function was run. - - :param file: an open file to print to (prints to stdout if omitted) - :param limit: the maximum number of stack frames to print - - """ - print_stack(self.frame, limit, file) - - -class TypeChecker: - """ - A type checker that collects type violations by hooking into :func:`sys.setprofile`. - - :param packages: list of top level modules and packages or modules to include for type checking - :param all_threads: ``True`` to check types in all threads created while the checker is - running, ``False`` to only check in the current one - :param forward_refs_policy: how to handle unresolvable forward references in annotations - - .. deprecated:: 2.6 - Use :func:`~.importhook.install_import_hook` instead. This class will be removed in v3.0. - """ - - def __init__(self, packages: Union[str, Sequence[str]], *, all_threads: bool = True, - forward_refs_policy: ForwardRefPolicy = ForwardRefPolicy.ERROR): - assert check_argument_types() - warn('TypeChecker has been deprecated and will be removed in v3.0. ' - 'Use install_import_hook() or the pytest plugin instead.', DeprecationWarning) - self.all_threads = all_threads - self.annotation_policy = forward_refs_policy - self._call_memos = {} # type: Dict[Any, _CallMemo] - self._previous_profiler = None - self._previous_thread_profiler = None - self._active = False - - if isinstance(packages, str): - self._packages = (packages,) - else: - self._packages = tuple(packages) - - @property - def active(self) -> bool: - """Return ``True`` if currently collecting type violations.""" - return self._active - - def should_check_type(self, func: Callable) -> bool: - if not func.__annotations__: - # No point in checking if there are no type hints - return False - elif isasyncgenfunction(func): - # Async generators cannot be supported because the return arg is of an opaque builtin - # type (async_generator_wrapped_value) - return False - else: - # Check types if the module matches any of the package prefixes - return any(func.__module__ == package or func.__module__.startswith(package + '.') - for package in self._packages) - - def start(self): - if self._active: - raise RuntimeError('type checker already running') - - self._active = True - - # Install this instance as the current profiler - self._previous_profiler = sys.getprofile() - sys.setprofile(self) - - # If requested, set this instance as the default profiler for all future threads - # (does not affect existing threads) - if self.all_threads: - self._previous_thread_profiler = threading._profile_hook - threading.setprofile(self) - - def stop(self): - if self._active: - if sys.getprofile() is self: - sys.setprofile(self._previous_profiler) - else: # pragma: no cover - warn('the system profiling hook has changed unexpectedly') - - if self.all_threads: - if threading._profile_hook is self: - threading.setprofile(self._previous_thread_profiler) - else: # pragma: no cover - warn('the threading profiling hook has changed unexpectedly') - - self._active = False - - def __enter__(self): - self.start() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop() - - def __call__(self, frame, event: str, arg) -> None: # pragma: no cover - if not self._active: - # This happens if all_threads was enabled and a thread was created when the checker was - # running but was then stopped. The thread's profiler callback can't be reset any other - # way but this. - sys.setprofile(self._previous_thread_profiler) - return - - # If an actual profiler is running, don't include the type checking times in its results - if event == 'call': - try: - func = find_function(frame) - except Exception: - func = None - - if func is not None and self.should_check_type(func): - memo = self._call_memos[frame] = _CallMemo( - func, frame.f_locals, forward_refs_policy=self.annotation_policy) - if memo.is_generator: - return_type_hint = memo.type_hints['return'] - if return_type_hint is not None: - origin = getattr(return_type_hint, '__origin__', None) - if origin in generator_origin_types: - # Check the types of the yielded values - memo.type_hints['return'] = return_type_hint.__args__[0] - else: - try: - check_argument_types(memo) - except TypeError as exc: - warn(TypeWarning(memo, event, frame, exc)) - - if self._previous_profiler is not None: - self._previous_profiler(frame, event, arg) - elif event == 'return': - if self._previous_profiler is not None: - self._previous_profiler(frame, event, arg) - - if arg is None: - # a None return value might mean an exception is being raised but we have no way of - # checking - return - - memo = self._call_memos.get(frame) - if memo is not None: - try: - if memo.is_generator: - check_type('yielded value', arg, memo.type_hints['return'], memo) - else: - check_return_type(arg, memo) - except TypeError as exc: - warn(TypeWarning(memo, event, frame, exc)) - - if not memo.is_generator: - del self._call_memos[frame] - elif self._previous_profiler is not None: - self._previous_profiler(frame, event, arg) +# Automatically load checker lookup functions unless explicitly disabled +if "TYPEGUARD_DISABLE_PLUGIN_AUTOLOAD" not in os.environ: + load_plugins() diff --git a/contrib/python/typeguard/typeguard/_checkers.py b/contrib/python/typeguard/typeguard/_checkers.py new file mode 100644 index 00000000000..989409bb40d --- /dev/null +++ b/contrib/python/typeguard/typeguard/_checkers.py @@ -0,0 +1,1089 @@ +from __future__ import annotations + +import collections.abc +import inspect +import sys +import types +import typing +import warnings +from collections.abc import Mapping, MutableMapping, Sequence +from enum import Enum +from inspect import Parameter, isclass, isfunction +from io import BufferedIOBase, IOBase, RawIOBase, TextIOBase +from itertools import zip_longest +from textwrap import indent +from typing import ( + IO, + AbstractSet, + Annotated, + Any, + BinaryIO, + Callable, + Dict, + ForwardRef, + List, + NewType, + Optional, + Set, + TextIO, + Tuple, + Type, + TypeVar, + Union, +) +from unittest.mock import Mock + +import typing_extensions + +# Must use this because typing.is_typeddict does not recognize +# TypedDict from typing_extensions, and as of version 4.12.0 +# typing_extensions.TypedDict is different from typing.TypedDict +# on all versions. +from typing_extensions import is_typeddict + +from ._config import ForwardRefPolicy +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, 11): + from typing import ( + NotRequired, + TypeAlias, + get_args, + get_origin, + ) + + SubclassableAny = Any +else: + from typing_extensions import Any as SubclassableAny + from typing_extensions import ( + NotRequired, + TypeAlias, + get_args, + get_origin, + ) + +if sys.version_info >= (3, 10): + from importlib.metadata import entry_points + from typing import ParamSpec +else: + from importlib_metadata import entry_points + from typing_extensions import ParamSpec + +TypeCheckerCallable: TypeAlias = Callable[ + [Any, Any, Tuple[Any, ...], TypeCheckMemo], Any +] +TypeCheckLookupCallback: TypeAlias = Callable[ + [Any, Tuple[Any, ...], Tuple[Any, ...]], Optional[TypeCheckerCallable] +] + +checker_lookup_functions: list[TypeCheckLookupCallback] = [] +generic_alias_types: tuple[type, ...] = ( + type(List), + type(List[Any]), + types.GenericAlias, +) + +# Sentinel +_missing = object() + +# Lifted from mypy.sharedparse +BINARY_MAGIC_METHODS = { + "__add__", + "__and__", + "__cmp__", + "__divmod__", + "__div__", + "__eq__", + "__floordiv__", + "__ge__", + "__gt__", + "__iadd__", + "__iand__", + "__idiv__", + "__ifloordiv__", + "__ilshift__", + "__imatmul__", + "__imod__", + "__imul__", + "__ior__", + "__ipow__", + "__irshift__", + "__isub__", + "__itruediv__", + "__ixor__", + "__le__", + "__lshift__", + "__lt__", + "__matmul__", + "__mod__", + "__mul__", + "__ne__", + "__or__", + "__pow__", + "__radd__", + "__rand__", + "__rdiv__", + "__rfloordiv__", + "__rlshift__", + "__rmatmul__", + "__rmod__", + "__rmul__", + "__ror__", + "__rpow__", + "__rrshift__", + "__rshift__", + "__rsub__", + "__rtruediv__", + "__rxor__", + "__sub__", + "__truediv__", + "__xor__", +} + + +def check_callable( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not callable(value): + raise TypeCheckError("is not callable") + + if args: + try: + signature = inspect.signature(value) + except (TypeError, ValueError): + return + + argument_types = args[0] + if isinstance(argument_types, list) and not any( + type(item) is ParamSpec for item in argument_types + ): + # The callable must not have keyword-only arguments without defaults + unfulfilled_kwonlyargs = [ + param.name + for param in signature.parameters.values() + if param.kind == Parameter.KEYWORD_ONLY + and param.default == Parameter.empty + ] + if unfulfilled_kwonlyargs: + raise TypeCheckError( + f"has mandatory keyword-only arguments in its declaration: " + f"{', '.join(unfulfilled_kwonlyargs)}" + ) + + num_positional_args = num_mandatory_pos_args = 0 + has_varargs = False + for param in signature.parameters.values(): + if param.kind in ( + Parameter.POSITIONAL_ONLY, + Parameter.POSITIONAL_OR_KEYWORD, + ): + num_positional_args += 1 + if param.default is Parameter.empty: + num_mandatory_pos_args += 1 + elif param.kind == Parameter.VAR_POSITIONAL: + has_varargs = True + + if num_mandatory_pos_args > len(argument_types): + raise TypeCheckError( + f"has too many mandatory positional arguments in its declaration; " + f"expected {len(argument_types)} but {num_mandatory_pos_args} " + f"mandatory positional argument(s) declared" + ) + elif not has_varargs and num_positional_args < len(argument_types): + raise TypeCheckError( + f"has too few arguments in its declaration; expected " + f"{len(argument_types)} but {num_positional_args} argument(s) " + f"declared" + ) + + +def check_mapping( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is Dict or origin_type is dict: + if not isinstance(value, dict): + raise TypeCheckError("is not a dict") + if origin_type is MutableMapping or origin_type is collections.abc.MutableMapping: + if not isinstance(value, collections.abc.MutableMapping): + raise TypeCheckError("is not a mutable mapping") + elif not isinstance(value, collections.abc.Mapping): + raise TypeCheckError("is not a mapping") + + if args: + key_type, value_type = args + if key_type is not Any or value_type is not Any: + samples = memo.config.collection_check_strategy.iterate_samples( + value.items() + ) + for k, v in samples: + try: + check_type_internal(k, key_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"key {k!r}") + raise + + try: + check_type_internal(v, value_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"value of key {k!r}") + raise + + +def check_typed_dict( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, 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() + + 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}") + + # Detect NotRequired fields which are hidden by get_type_hints() + type_hints: dict[str, type] = {} + for key, annotation in origin_type.__annotations__.items(): + if isinstance(annotation, ForwardRef): + annotation = evaluate_forwardref(annotation, memo) + + if get_origin(annotation) is NotRequired: + required_keys.discard(key) + annotation = get_args(annotation)[0] + + type_hints[key] = annotation + + missing_keys = required_keys - existing_keys + if missing_keys: + keys_formatted = ", ".join(f'"{key}"' for key in sorted(missing_keys, key=repr)) + raise TypeCheckError(f"is missing required key(s): {keys_formatted}") + + for key, argtype in type_hints.items(): + argvalue = value.get(key, _missing) + if argvalue is not _missing: + try: + check_type_internal(argvalue, argtype, memo) + except TypeCheckError as exc: + exc.append_path_element(f"value of key {key!r}") + raise + + +def check_list( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, list): + raise TypeCheckError("is not a list") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, v in enumerate(samples): + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_sequence( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, collections.abc.Sequence): + raise TypeCheckError("is not a sequence") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, v in enumerate(samples): + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_set( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is frozenset: + if not isinstance(value, frozenset): + raise TypeCheckError("is not a frozenset") + elif not isinstance(value, AbstractSet): + raise TypeCheckError("is not a set") + + if args and args != (Any,): + samples = memo.config.collection_check_strategy.iterate_samples(value) + for v in samples: + try: + check_type_internal(v, args[0], memo) + except TypeCheckError as exc: + exc.append_path_element(f"[{v}]") + raise + + +def check_tuple( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + # Specialized check for NamedTuples + if field_types := getattr(origin_type, "__annotations__", None): + if not isinstance(value, origin_type): + raise TypeCheckError( + f"is not a named tuple of type {qualified_name(origin_type)}" + ) + + for name, field_type in field_types.items(): + try: + check_type_internal(getattr(value, name), field_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"attribute {name!r}") + raise + + return + elif not isinstance(value, tuple): + raise TypeCheckError("is not a tuple") + + if args: + use_ellipsis = args[-1] is Ellipsis + tuple_params = args[: -1 if use_ellipsis else None] + else: + # Unparametrized Tuple or plain tuple + return + + if use_ellipsis: + element_type = tuple_params[0] + samples = memo.config.collection_check_strategy.iterate_samples(value) + for i, element in enumerate(samples): + try: + check_type_internal(element, element_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + elif tuple_params == ((),): + if value != (): + raise TypeCheckError("is not an empty tuple") + else: + if len(value) != len(tuple_params): + raise TypeCheckError( + f"has wrong number of elements (expected {len(tuple_params)}, got " + f"{len(value)} instead)" + ) + + for i, (element, element_type) in enumerate(zip(value, tuple_params)): + try: + check_type_internal(element, element_type, memo) + except TypeCheckError as exc: + exc.append_path_element(f"item {i}") + raise + + +def check_union( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + errors: dict[str, TypeCheckError] = {} + try: + for type_ in args: + try: + check_type_internal(value, type_, memo) + return + except TypeCheckError as exc: + errors[get_type_name(type_)] = exc + + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + finally: + del errors # avoid creating ref cycle + + raise TypeCheckError(f"did not match any element in the union:\n{formatted_errors}") + + +def check_uniontype( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not args: + return check_instance(value, types.UnionType, (), memo) + + errors: dict[str, TypeCheckError] = {} + try: + for type_ in args: + try: + check_type_internal(value, type_, memo) + return + except TypeCheckError as exc: + errors[get_type_name(type_)] = exc + + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + finally: + del errors # avoid creating ref cycle + + raise TypeCheckError(f"did not match any element in the union:\n{formatted_errors}") + + +def check_class( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isclass(value) and not isinstance(value, generic_alias_types): + raise TypeCheckError("is not a class") + + if not args: + return + + if isinstance(args[0], ForwardRef): + expected_class = evaluate_forwardref(args[0], memo) + else: + expected_class = args[0] + + if expected_class is Any: + return + elif expected_class is typing_extensions.Self: + check_self(value, get_origin(expected_class), get_args(expected_class), memo) + elif getattr(expected_class, "_is_protocol", False): + check_protocol(value, expected_class, (), memo) + elif isinstance(expected_class, TypeVar): + check_typevar(value, expected_class, (), memo, subclass_check=True) + elif get_origin(expected_class) is Union: + errors: dict[str, TypeCheckError] = {} + try: + for arg in get_args(expected_class): + if arg is Any: + return + + try: + check_class(value, type, (arg,), memo) + return + except TypeCheckError as exc: + errors[get_type_name(arg)] = exc + else: + formatted_errors = indent( + "\n".join(f"{key}: {error}" for key, error in errors.items()), " " + ) + raise TypeCheckError( + f"did not match any element in the union:\n{formatted_errors}" + ) + finally: + del errors # avoid creating ref cycle + else: + if isinstance(expected_class, generic_alias_types): + expected_class = get_origin(expected_class) + + if isinstance(value, generic_alias_types): + value = get_origin(value) + + if not issubclass(value, expected_class): + raise TypeCheckError( + f"is not a subclass of {qualified_name(expected_class)}" + ) + + +def check_newtype( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, origin_type.__supertype__, memo) + + +def check_instance( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, origin_type): + raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}") + + +def check_typevar( + value: Any, + origin_type: TypeVar, + args: tuple[Any, ...], + memo: TypeCheckMemo, + *, + subclass_check: bool = False, +) -> None: + if origin_type.__bound__ is not None: + annotation = ( + type[origin_type.__bound__] if subclass_check else origin_type.__bound__ + ) + check_type_internal(value, annotation, memo) + elif origin_type.__constraints__: + for constraint in origin_type.__constraints__: + annotation = Type[constraint] if subclass_check else constraint + try: + check_type_internal(value, annotation, memo) + except TypeCheckError: + pass + else: + break + else: + formatted_constraints = ", ".join( + get_type_name(constraint) for constraint in origin_type.__constraints__ + ) + raise TypeCheckError( + f"does not match any of the constraints ({formatted_constraints})" + ) + + +def _is_literal_type(typ: object) -> bool: + return typ is typing.Literal or typ is typing_extensions.Literal + + +def check_literal( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + def get_literal_args(literal_args: tuple[Any, ...]) -> tuple[Any, ...]: + retval: list[Any] = [] + for arg in literal_args: + if _is_literal_type(get_origin(arg)): + retval.extend(get_literal_args(arg.__args__)) + elif arg is None or isinstance(arg, (int, str, bytes, bool, Enum)): + retval.append(arg) + else: + raise TypeError( + f"Illegal literal value: {arg}" + ) # TypeError here is deliberate + + return tuple(retval) + + final_args = tuple(get_literal_args(args)) + try: + index = final_args.index(value) + except ValueError: + pass + else: + if type(final_args[index]) is type(value): + return + + formatted_args = ", ".join(repr(arg) for arg in final_args) + raise TypeCheckError(f"is not any of ({formatted_args})") from None + + +def check_literal_string( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, str, memo) + + +def check_typeguard( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + check_type_internal(value, bool, memo) + + +def check_none( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if value is not None: + raise TypeCheckError("is not None") + + +def check_number( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is complex and not isinstance(value, (complex, float, int)): + raise TypeCheckError("is neither complex, float or int") + elif origin_type is float and not isinstance(value, (float, int)): + raise TypeCheckError("is neither float or int") + + +def check_io( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if origin_type is TextIO or (origin_type is IO and args == (str,)): + if not isinstance(value, TextIOBase): + raise TypeCheckError("is not a text based I/O object") + elif origin_type is BinaryIO or (origin_type is IO and args == (bytes,)): + if not isinstance(value, (RawIOBase, BufferedIOBase)): + raise TypeCheckError("is not a binary I/O object") + elif not isinstance(value, IOBase): + raise TypeCheckError("is not an I/O object") + + +def check_signature_compatible(subject: type, protocol: type, attrname: str) -> None: + subject_attr = getattr(subject, attrname) + try: + subject_sig = inspect.signature(subject_attr) + except ValueError: + return # this can happen with builtins where the signature cannot be retrieved + + protocol_sig = inspect.signature(getattr(protocol, attrname)) + protocol_type: typing.Literal["instance", "class", "static"] = "instance" + 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" + + # 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" + + if protocol_type == "instance" and subject_type != "instance": + raise TypeCheckError( + f"should be an instance method but it's a {subject_type} method" + ) + elif protocol_type != "instance" and subject_type == "instance": + raise TypeCheckError( + f"should be a {protocol_type} method but it's an instance method" + ) + + expected_varargs = any( + param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.VAR_POSITIONAL + ) + has_varargs = any( + param + for param in subject_sig.parameters.values() + if param.kind is Parameter.VAR_POSITIONAL + ) + if expected_varargs and not has_varargs: + raise TypeCheckError("should accept variable positional arguments but doesn't") + + protocol_has_varkwargs = any( + param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.VAR_KEYWORD + ) + subject_has_varkwargs = any( + param + for param in subject_sig.parameters.values() + if param.kind is Parameter.VAR_KEYWORD + ) + if protocol_has_varkwargs and not subject_has_varkwargs: + raise TypeCheckError("should accept variable keyword arguments but doesn't") + + # Check that the callable has at least the expect amount of positional-only + # arguments (and no extra positional-only arguments without default values) + if not has_varargs: + protocol_args = [ + param + for param in protocol_sig.parameters.values() + if param.kind + in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + ] + subject_args = [ + param + for param in subject_sig.parameters.values() + if param.kind + in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD) + ] + + # Remove the "self" parameter from the protocol arguments to match + if protocol_type == "instance": + protocol_args.pop(0) + + # Remove the "self" parameter from the subject arguments to match + if subject_type == "instance": + subject_args.pop(0) + + for protocol_arg, subject_arg in zip_longest(protocol_args, subject_args): + if protocol_arg is None: + if subject_arg.default is Parameter.empty: + raise TypeCheckError("has too many mandatory positional arguments") + + break + + if subject_arg is None: + raise TypeCheckError("has too few positional arguments") + + if ( + protocol_arg.kind is Parameter.POSITIONAL_OR_KEYWORD + and subject_arg.kind is Parameter.POSITIONAL_ONLY + ): + raise TypeCheckError( + f"has an argument ({subject_arg.name}) that should not be " + f"positional-only" + ) + + if ( + protocol_arg.kind is Parameter.POSITIONAL_OR_KEYWORD + and protocol_arg.name != subject_arg.name + ): + raise TypeCheckError( + f"has a positional argument ({subject_arg.name}) that should be " + f"named {protocol_arg.name!r} at this position" + ) + + protocol_kwonlyargs = { + param.name: param + for param in protocol_sig.parameters.values() + if param.kind is Parameter.KEYWORD_ONLY + } + subject_kwonlyargs = { + param.name: param + for param in subject_sig.parameters.values() + if param.kind is Parameter.KEYWORD_ONLY + } + if not subject_has_varkwargs: + # Check that the signature has at least the required keyword-only arguments, and + # no extra mandatory keyword-only arguments + if missing_kwonlyargs := [ + param.name + for param in protocol_kwonlyargs.values() + if param.name not in subject_kwonlyargs + ]: + raise TypeCheckError( + "is missing keyword-only arguments: " + ", ".join(missing_kwonlyargs) + ) + + if not protocol_has_varkwargs: + if extra_kwonlyargs := [ + param.name + for param in subject_kwonlyargs.values() + if param.default is Parameter.empty + and param.name not in protocol_kwonlyargs + ]: + raise TypeCheckError( + "has mandatory keyword-only arguments not present in the protocol: " + + ", ".join(extra_kwonlyargs) + ) + + +def check_protocol( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + origin_annotations = typing.get_type_hints(origin_type) + for attrname in sorted(typing_extensions.get_protocol_members(origin_type)): + if (annotation := origin_annotations.get(attrname)) is not None: + try: + subject_member = getattr(value, attrname) + except AttributeError: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because it has no attribute named {attrname!r}" + ) from None + + try: + check_type_internal(subject_member, annotation, memo) + except TypeCheckError as exc: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} attribute {exc}" + ) from None + elif callable(getattr(origin_type, attrname)): + try: + subject_member = getattr(value, attrname) + except AttributeError: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because it has no method named {attrname!r}" + ) from None + + if not callable(subject_member): + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} attribute is not a callable" + ) + + # TODO: implement assignability checks for parameter and return value + # annotations + subject = value if isclass(value) else value.__class__ + try: + check_signature_compatible(subject, origin_type, attrname) + except TypeCheckError as exc: + raise TypeCheckError( + f"is not compatible with the {origin_type.__qualname__} " + f"protocol because its {attrname!r} method {exc}" + ) from None + + +def check_byteslike( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if not isinstance(value, (bytearray, bytes, memoryview)): + raise TypeCheckError("is not bytes-like") + + +def check_self( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + if memo.self_type is None: + raise TypeCheckError("cannot be checked against Self outside of a method call") + + if isclass(value): + if not issubclass(value, memo.self_type): + raise TypeCheckError( + f"is not a subclass of the self type ({qualified_name(memo.self_type)})" + ) + elif not isinstance(value, memo.self_type): + raise TypeCheckError( + f"is not an instance of the self type ({qualified_name(memo.self_type)})" + ) + + +def check_paramspec( + value: Any, + origin_type: Any, + args: tuple[Any, ...], + memo: TypeCheckMemo, +) -> None: + pass # No-op for now + + +def check_type_internal( + value: Any, + annotation: Any, + memo: TypeCheckMemo, +) -> None: + """ + Check that the given object is compatible with the given type annotation. + + This function should only be used by type checker callables. Applications should use + :func:`~.check_type` instead. + + :param value: the value to check + :param annotation: the type annotation to check against + :param memo: a memo object containing configuration and information necessary for + looking up forward references + """ + + if isinstance(annotation, ForwardRef): + try: + annotation = evaluate_forwardref(annotation, memo) + except NameError: + if memo.config.forward_ref_policy is ForwardRefPolicy.ERROR: + raise + elif memo.config.forward_ref_policy is ForwardRefPolicy.WARN: + warnings.warn( + f"Cannot resolve forward reference {annotation.__forward_arg__!r}", + TypeHintWarning, + stacklevel=get_stacklevel(), + ) + + return + + if annotation is Any or annotation is SubclassableAny or isinstance(value, Mock): + return + + # Skip type checks if value is an instance of a class that inherits from Any + if not isclass(value) and SubclassableAny in type(value).__bases__: + return + + extras: tuple[Any, ...] + origin_type = get_origin(annotation) + if origin_type is Annotated: + annotation, *extras_ = get_args(annotation) + extras = tuple(extras_) + origin_type = get_origin(annotation) + else: + extras = () + + if origin_type is not None: + args = get_args(annotation) + + # Compatibility hack to distinguish between unparametrized and empty tuple + # (tuple[()]), necessary due to https://github.com/python/cpython/issues/91137 + if origin_type in (tuple, Tuple) and annotation is not Tuple and not args: + args = ((),) + else: + origin_type = annotation + args = () + + for lookup_func in checker_lookup_functions: + checker = lookup_func(origin_type, args, extras) + if checker: + checker(value, origin_type, args, memo) + return + + if isclass(origin_type): + if not isinstance(value, origin_type): + raise TypeCheckError(f"is not an instance of {qualified_name(origin_type)}") + elif type(origin_type) is str: # noqa: E721 + warnings.warn( + f"Skipping type check against {origin_type!r}; this looks like a " + f"string-form forward reference imported from another module", + TypeHintWarning, + stacklevel=get_stacklevel(), + ) + + +# Equality checks are applied to these +origin_type_checkers = { + bytes: check_byteslike, + AbstractSet: check_set, + BinaryIO: check_io, + Callable: check_callable, + collections.abc.Callable: check_callable, + complex: check_number, + dict: check_mapping, + Dict: check_mapping, + float: check_number, + frozenset: check_set, + IO: check_io, + list: check_list, + List: check_list, + typing.Literal: check_literal, + Mapping: check_mapping, + MutableMapping: check_mapping, + None: check_none, + collections.abc.Mapping: check_mapping, + collections.abc.MutableMapping: check_mapping, + Sequence: check_sequence, + collections.abc.Sequence: check_sequence, + collections.abc.Set: check_set, + set: check_set, + Set: check_set, + TextIO: check_io, + tuple: check_tuple, + Tuple: check_tuple, + type: check_class, + Type: check_class, + Union: check_union, + # On some versions of Python, these may simply be re-exports from "typing", + # but exactly which Python versions is subject to change. + # It's best to err on the safe side and just always specify these. + typing_extensions.Literal: check_literal, + typing_extensions.LiteralString: check_literal_string, + typing_extensions.Self: check_self, + typing_extensions.TypeGuard: check_typeguard, +} +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} + ) + + +def builtin_checker_lookup( + origin_type: Any, args: tuple[Any, ...], extras: tuple[Any, ...] +) -> TypeCheckerCallable | None: + checker = origin_type_checkers.get(origin_type) + if checker is not None: + return checker + elif is_typeddict(origin_type): + return check_typed_dict + elif isclass(origin_type) and issubclass( + origin_type, + Tuple, # type: ignore[arg-type] + ): + # NamedTuple + return check_tuple + elif getattr(origin_type, "_is_protocol", False): + return check_protocol + elif isinstance(origin_type, ParamSpec): + return check_paramspec + elif isinstance(origin_type, TypeVar): + return check_typevar + elif origin_type.__class__ is NewType: + # typing.NewType on Python 3.10+ + return check_newtype + elif ( + isfunction(origin_type) + and getattr(origin_type, "__module__", None) == "typing" + and getattr(origin_type, "__qualname__", "").startswith("NewType.") + and hasattr(origin_type, "__supertype__") + ): + # typing.NewType on Python 3.9 + return check_newtype + + return None + + +checker_lookup_functions.append(builtin_checker_lookup) + + +def load_plugins() -> None: + """ + Load all type checker lookup functions from entry points. + + All entry points from the ``typeguard.checker_lookup`` group are loaded, and the + returned lookup functions are added to :data:`typeguard.checker_lookup_functions`. + + .. note:: This function is called implicitly on import, unless the + ``TYPEGUARD_DISABLE_PLUGIN_AUTOLOAD`` environment variable is present. + """ + + for ep in entry_points(group="typeguard.checker_lookup"): + try: + plugin = ep.load() + except Exception as exc: + warnings.warn( + f"Failed to load plugin {ep.name!r}: {qualified_name(exc)}: {exc}", + stacklevel=2, + ) + continue + + if not callable(plugin): + warnings.warn( + f"Plugin {ep} returned a non-callable object: {plugin!r}", stacklevel=2 + ) + continue + + checker_lookup_functions.insert(0, plugin) diff --git a/contrib/python/typeguard/typeguard/_config.py b/contrib/python/typeguard/typeguard/_config.py new file mode 100644 index 00000000000..c3097640147 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_config.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import Enum, auto +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from ._functions import TypeCheckFailCallback + +T = TypeVar("T") + + +class ForwardRefPolicy(Enum): + """ + Defines how unresolved forward references are handled. + + Members: + + * ``ERROR``: propagate the :exc:`NameError` when the forward reference lookup fails + * ``WARN``: emit a :class:`~.TypeHintWarning` if the forward reference lookup fails + * ``IGNORE``: silently skip checks for unresolveable forward references + """ + + ERROR = auto() + WARN = auto() + IGNORE = auto() + + +class CollectionCheckStrategy(Enum): + """ + Specifies how thoroughly the contents of collections are type checked. + + This has an effect on the following built-in checkers: + + * ``AbstractSet`` + * ``Dict`` + * ``List`` + * ``Mapping`` + * ``Set`` + * ``Tuple[<type>, ...]`` (arbitrarily sized tuples) + + Members: + + * ``FIRST_ITEM``: check only the first item + * ``ALL_ITEMS``: check all items + """ + + FIRST_ITEM = auto() + ALL_ITEMS = auto() + + def iterate_samples(self, collection: Iterable[T]) -> Iterable[T]: + if self is CollectionCheckStrategy.FIRST_ITEM: + try: + return [next(iter(collection))] + except StopIteration: + return () + else: + return collection + + +@dataclass +class TypeCheckConfiguration: + """ + You can change Typeguard's behavior with these settings. + + .. attribute:: typecheck_fail_callback + :type: Callable[[TypeCheckError, TypeCheckMemo], Any] + + Callable that is called when type checking fails. + + Default: ``None`` (the :exc:`~.TypeCheckError` is raised directly) + + .. attribute:: forward_ref_policy + :type: ForwardRefPolicy + + Specifies what to do when a forward reference fails to resolve. + + Default: ``WARN`` + + .. attribute:: collection_check_strategy + :type: CollectionCheckStrategy + + Specifies how thoroughly the contents of collections (list, dict, etc.) are + type checked. + + Default: ``FIRST_ITEM`` + + .. attribute:: debug_instrumentation + :type: bool + + If set to ``True``, the code of modules or functions instrumented by typeguard + is printed to ``sys.stderr`` after the instrumentation is done + + Default: ``False`` + """ + + forward_ref_policy: ForwardRefPolicy = ForwardRefPolicy.WARN + typecheck_fail_callback: TypeCheckFailCallback | None = None + collection_check_strategy: CollectionCheckStrategy = ( + CollectionCheckStrategy.FIRST_ITEM + ) + debug_instrumentation: bool = False + + +global_config = TypeCheckConfiguration() diff --git a/contrib/python/typeguard/typeguard/_decorators.py b/contrib/python/typeguard/typeguard/_decorators.py new file mode 100644 index 00000000000..9b7521bd9e7 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_decorators.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +import ast +import inspect +import sys +from collections.abc import Sequence +from functools import partial +from inspect import isclass, isfunction +from types import CodeType, FrameType, FunctionType +from typing import TYPE_CHECKING, Any, Callable, ForwardRef, TypeVar, cast, overload +from warnings import warn + +from ._config import CollectionCheckStrategy, ForwardRefPolicy, global_config +from ._exceptions import InstrumentationWarning +from ._functions import TypeCheckFailCallback +from ._transformer import TypeguardTransformer +from ._utils import Unset, function_name, get_stacklevel, is_method_of, unset + +T_CallableOrType = TypeVar("T_CallableOrType", bound=Callable[..., Any]) + +if TYPE_CHECKING: + from typeshed.stdlib.types import _Cell + + def typeguard_ignore(arg: T_CallableOrType) -> T_CallableOrType: + """This decorator is a noop during static type-checking.""" + return arg + +else: + from typing import no_type_check as typeguard_ignore # noqa: F401 + + +def make_cell(value: object) -> _Cell: + return (lambda: value).__closure__[0] # type: ignore[index] + + +def find_target_function( + new_code: CodeType, target_path: Sequence[str], firstlineno: int +) -> CodeType | None: + for const in new_code.co_consts: + if isinstance(const, CodeType): + new_path = ( + target_path[1:] if const.co_name == target_path[0] else target_path + ) + if not new_path: + if const.co_firstlineno == firstlineno: + return const + + continue + + if target_code := find_target_function(const, new_path, firstlineno): + return target_code + + return None + + +def instrument(f: T_CallableOrType) -> FunctionType | str: + if not getattr(f, "__code__", None): + return "no code associated" + elif not getattr(f, "__module__", None): + return "__module__ attribute is not set" + elif f.__code__.co_filename == "<stdin>": + return "cannot instrument functions defined in a REPL" + elif hasattr(f, "__wrapped__"): + return ( + "@typechecked only supports instrumenting functions wrapped with " + "@classmethod, @staticmethod or @property" + ) + + target_path = [item for item in f.__qualname__.split(".") if item != "<locals>"] + module_source = inspect.getsource(sys.modules[f.__module__]) + module_ast = ast.parse(module_source) + instrumentor = TypeguardTransformer(target_path, f.__code__.co_firstlineno) + instrumentor.visit(module_ast) + + if not instrumentor.target_node or instrumentor.target_lineno is None: + return "instrumentor did not find the target function" + + module_code = compile(module_ast, f.__code__.co_filename, "exec", dont_inherit=True) + new_code = find_target_function( + module_code, target_path, instrumentor.target_lineno + ) + if not new_code: + return "cannot find the target function in the AST" + + if global_config.debug_instrumentation and sys.version_info >= (3, 9): + # Find the matching AST node, then unparse it to source and print to stdout + print( + f"Source code of {f.__qualname__}() after instrumentation:" + "\n----------------------------------------------", + file=sys.stderr, + ) + print(ast.unparse(instrumentor.target_node), file=sys.stderr) + print( + "----------------------------------------------", + file=sys.stderr, + ) + + closure = f.__closure__ + if new_code.co_freevars != f.__code__.co_freevars: + # Create a new closure and find values for the new free variables + frame = cast(FrameType, inspect.currentframe()) + frame = cast(FrameType, frame.f_back) + frame_locals = cast(FrameType, frame.f_back).f_locals + cells: list[_Cell] = [] + for key in new_code.co_freevars: + if key in instrumentor.names_used_in_annotations: + # Find the value and make a new cell from it + value = frame_locals.get(key) or ForwardRef(key) + cells.append(make_cell(value)) + else: + # Reuse the cell from the existing closure + assert f.__closure__ + cells.append(f.__closure__[f.__code__.co_freevars.index(key)]) + + closure = tuple(cells) + + new_function = FunctionType(new_code, f.__globals__, f.__name__, closure=closure) + new_function.__module__ = f.__module__ + new_function.__name__ = f.__name__ + new_function.__qualname__ = f.__qualname__ + new_function.__doc__ = f.__doc__ + new_function.__defaults__ = f.__defaults__ + new_function.__kwdefaults__ = f.__kwdefaults__ + + if sys.version_info >= (3, 12): + new_function.__type_params__ = f.__type_params__ + + if sys.version_info >= (3, 14): + new_function.__annotate__ = f.__annotate__ + else: + new_function.__annotations__ = f.__annotations__ + + return new_function + + +@overload +def typechecked( + *, + forward_ref_policy: ForwardRefPolicy | Unset = unset, + typecheck_fail_callback: TypeCheckFailCallback | Unset = unset, + collection_check_strategy: CollectionCheckStrategy | Unset = unset, + debug_instrumentation: bool | Unset = unset, +) -> Callable[[T_CallableOrType], T_CallableOrType]: ... + + +@overload +def typechecked(target: T_CallableOrType) -> T_CallableOrType: ... + + +def typechecked( + target: T_CallableOrType | None = None, + *, + forward_ref_policy: ForwardRefPolicy | Unset = unset, + typecheck_fail_callback: TypeCheckFailCallback | Unset = unset, + collection_check_strategy: CollectionCheckStrategy | Unset = unset, + debug_instrumentation: bool | Unset = unset, +) -> Any: + """ + Instrument the target function to perform run-time type checking. + + This decorator recompiles the target function, injecting code to type check + arguments, return values, yield values (excluding ``yield from``) and assignments to + annotated local variables. + + This can also be used as a class decorator. This will instrument all type annotated + methods, including :func:`@classmethod <classmethod>`, + :func:`@staticmethod <staticmethod>`, and :class:`@property <property>` decorated + methods in the class. + + .. note:: When Python is run in optimized mode (``-O`` or ``-OO``, this decorator + is a no-op). This is a feature meant for selectively introducing type checking + into a code base where the checks aren't meant to be run in production. + + :param target: the function or class to enable type checking for + :param forward_ref_policy: override for + :attr:`.TypeCheckConfiguration.forward_ref_policy` + :param typecheck_fail_callback: override for + :attr:`.TypeCheckConfiguration.typecheck_fail_callback` + :param collection_check_strategy: override for + :attr:`.TypeCheckConfiguration.collection_check_strategy` + :param debug_instrumentation: override for + :attr:`.TypeCheckConfiguration.debug_instrumentation` + + """ + if target is None: + return partial( + typechecked, + forward_ref_policy=forward_ref_policy, + typecheck_fail_callback=typecheck_fail_callback, + collection_check_strategy=collection_check_strategy, + debug_instrumentation=debug_instrumentation, + ) + + if not __debug__: + return target + + if isclass(target): + for key, attr in target.__dict__.items(): + if is_method_of(attr, target): + retval = instrument(attr) + if isfunction(retval): + setattr(target, key, retval) + elif isinstance(attr, (classmethod, staticmethod)): + if is_method_of(attr.__func__, target): + retval = instrument(attr.__func__) + if isfunction(retval): + wrapper = attr.__class__(retval) + setattr(target, key, wrapper) + elif isinstance(attr, property): + kwargs: dict[str, Any] = dict(doc=attr.__doc__) + for name in ("fset", "fget", "fdel"): + property_func = kwargs[name] = getattr(attr, name) + if is_method_of(property_func, target): + retval = instrument(property_func) + if isfunction(retval): + kwargs[name] = retval + + setattr(target, key, attr.__class__(**kwargs)) + + return target + + # Find either the first Python wrapper or the actual function + wrapper_class: ( + type[classmethod[Any, Any, Any]] | type[staticmethod[Any, Any]] | None + ) = None + if isinstance(target, (classmethod, staticmethod)): + wrapper_class = target.__class__ + target = target.__func__ # type: ignore[assignment] + + retval = instrument(target) + if isinstance(retval, str): + warn( + f"{retval} -- not typechecking {function_name(target)}", + InstrumentationWarning, + stacklevel=get_stacklevel(), + ) + return target + + if wrapper_class is None: + return retval + else: + return wrapper_class(retval) diff --git a/contrib/python/typeguard/typeguard/_exceptions.py b/contrib/python/typeguard/typeguard/_exceptions.py new file mode 100644 index 00000000000..625437a6499 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_exceptions.py @@ -0,0 +1,42 @@ +from collections import deque +from typing import Deque + + +class TypeHintWarning(UserWarning): + """ + A warning that is emitted when a type hint in string form could not be resolved to + an actual type. + """ + + +class TypeCheckWarning(UserWarning): + """Emitted by typeguard's type checkers when a type mismatch is detected.""" + + def __init__(self, message: str): + super().__init__(message) + + +class InstrumentationWarning(UserWarning): + """Emitted when there's a problem with instrumenting a function for type checks.""" + + def __init__(self, message: str): + super().__init__(message) + + +class TypeCheckError(Exception): + """ + Raised by typeguard's type checkers when a type mismatch is detected. + """ + + def __init__(self, message: str): + super().__init__(message) + self._path: Deque[str] = deque() + + def append_path_element(self, element: str) -> None: + self._path.append(element) + + def __str__(self) -> str: + if self._path: + return " of ".join(self._path) + " " + str(self.args[0]) + else: + return str(self.args[0]) diff --git a/contrib/python/typeguard/typeguard/_functions.py b/contrib/python/typeguard/typeguard/_functions.py new file mode 100644 index 00000000000..ca21c14c0c2 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_functions.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +import sys +import warnings +from collections.abc import Sequence +from typing import Any, Callable, NoReturn, TypeVar, Union, overload + +from . import _suppression +from ._checkers import BINARY_MAGIC_METHODS, check_type_internal +from ._config import ( + CollectionCheckStrategy, + ForwardRefPolicy, + TypeCheckConfiguration, +) +from ._exceptions import TypeCheckError, TypeCheckWarning +from ._memo import TypeCheckMemo +from ._utils import get_stacklevel, qualified_name + +if sys.version_info >= (3, 11): + from typing import Literal, Never, TypeAlias +else: + from typing_extensions import Literal, Never, TypeAlias + +T = TypeVar("T") +TypeCheckFailCallback: TypeAlias = Callable[[TypeCheckError, TypeCheckMemo], Any] + + +@overload +def check_type( + value: object, + expected_type: type[T], + *, + forward_ref_policy: ForwardRefPolicy = ..., + typecheck_fail_callback: TypeCheckFailCallback | None = ..., + collection_check_strategy: CollectionCheckStrategy = ..., +) -> T: ... + + +@overload +def check_type( + value: object, + expected_type: Any, + *, + forward_ref_policy: ForwardRefPolicy = ..., + typecheck_fail_callback: TypeCheckFailCallback | None = ..., + collection_check_strategy: CollectionCheckStrategy = ..., +) -> Any: ... + + +def check_type( + value: object, + expected_type: Any, + *, + forward_ref_policy: ForwardRefPolicy = TypeCheckConfiguration().forward_ref_policy, + typecheck_fail_callback: TypeCheckFailCallback | None = ( + TypeCheckConfiguration().typecheck_fail_callback + ), + collection_check_strategy: CollectionCheckStrategy = ( + TypeCheckConfiguration().collection_check_strategy + ), +) -> Any: + """ + Ensure that ``value`` matches ``expected_type``. + + The types from the :mod:`typing` module do not support :func:`isinstance` or + :func:`issubclass` so a number of type specific checks are required. This function + knows which checker to call for which type. + + This function wraps :func:`~.check_type_internal` in the following ways: + + * Respects type checking suppression (:func:`~.suppress_type_checks`) + * Forms a :class:`~.TypeCheckMemo` from the current stack frame + * Calls the configured type check fail callback if the check fails + + Note that this function is independent of the globally shared configuration in + :data:`typeguard.config`. This means that usage within libraries is safe from being + affected configuration changes made by other libraries or by the integrating + application. Instead, configuration options have the same default values as their + corresponding fields in :class:`TypeCheckConfiguration`. + + :param value: value to be checked against ``expected_type`` + :param expected_type: a class or generic type instance, or a tuple of such things + :param forward_ref_policy: see :attr:`TypeCheckConfiguration.forward_ref_policy` + :param typecheck_fail_callback: + see :attr`TypeCheckConfiguration.typecheck_fail_callback` + :param collection_check_strategy: + see :attr:`TypeCheckConfiguration.collection_check_strategy` + :return: ``value``, unmodified + :raises TypeCheckError: if there is a type mismatch + + """ + if type(expected_type) is tuple: + expected_type = Union[expected_type] + + config = TypeCheckConfiguration( + forward_ref_policy=forward_ref_policy, + typecheck_fail_callback=typecheck_fail_callback, + collection_check_strategy=collection_check_strategy, + ) + + if _suppression.type_checks_suppressed or expected_type is Any: + return value + + frame = sys._getframe(1) + memo = TypeCheckMemo(frame.f_globals, frame.f_locals, config=config) + try: + check_type_internal(value, expected_type, memo) + except TypeCheckError as exc: + exc.append_path_element(qualified_name(value, add_class_prefix=True)) + if config.typecheck_fail_callback: + config.typecheck_fail_callback(exc, memo) + else: + raise + + return value + + +def check_argument_types( + func_name: str, + arguments: dict[str, tuple[Any, Any]], + memo: TypeCheckMemo, +) -> Literal[True]: + if _suppression.type_checks_suppressed: + return True + + for argname, (value, annotation) in arguments.items(): + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError( + f"{func_name}() was declared never to be called but it was" + ) + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(value, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(value, add_class_prefix=True) + exc.append_path_element(f'argument "{argname}" ({qualname})') + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return True + + +def check_return_type( + func_name: str, + retval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return retval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError(f"{func_name}() was declared never to return but it did") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(retval, annotation, memo) + except TypeCheckError as exc: + # Allow NotImplemented if this is a binary magic method (__eq__() et al) + if retval is NotImplemented and annotation is bool: + # This does (and cannot) not check if it's actually a method + func_name = func_name.rsplit(".", 1)[-1] + if func_name in BINARY_MAGIC_METHODS: + return retval + + qualname = qualified_name(retval, add_class_prefix=True) + exc.append_path_element(f"the return value ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return retval + + +def check_send_type( + func_name: str, + sendval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return sendval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError( + f"{func_name}() was declared never to be sent a value to but it was" + ) + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(sendval, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(sendval, add_class_prefix=True) + exc.append_path_element(f"the value sent to generator ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return sendval + + +def check_yield_type( + func_name: str, + yieldval: T, + annotation: Any, + memo: TypeCheckMemo, +) -> T: + if _suppression.type_checks_suppressed: + return yieldval + + if annotation is NoReturn or annotation is Never: + exc = TypeCheckError(f"{func_name}() was declared never to yield but it did") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise exc + + try: + check_type_internal(yieldval, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(yieldval, add_class_prefix=True) + exc.append_path_element(f"the yielded value ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return yieldval + + +def check_variable_assignment( + value: Any, targets: Sequence[list[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 + + else: + values_to_check = [value] + + for val, (varname, annotation) in zip(values_to_check, target): + try: + check_type_internal(val, annotation, memo) + except TypeCheckError as exc: + qualname = qualified_name(val, add_class_prefix=True) + exc.append_path_element(f"value assigned to {varname} ({qualname})") + if memo.config.typecheck_fail_callback: + memo.config.typecheck_fail_callback(exc, memo) + else: + raise + + return value_to_return + + +def warn_on_error(exc: TypeCheckError, memo: TypeCheckMemo) -> None: + """ + Emit a warning on a type mismatch. + + This is intended to be used as an error handler in + :attr:`TypeCheckConfiguration.typecheck_fail_callback`. + + """ + warnings.warn(TypeCheckWarning(str(exc)), stacklevel=get_stacklevel()) diff --git a/contrib/python/typeguard/typeguard/_importhook.py b/contrib/python/typeguard/typeguard/_importhook.py new file mode 100644 index 00000000000..0d1c6274419 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_importhook.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import ast +import sys +import types +from collections.abc import Callable, Iterable, Sequence +from importlib.abc import MetaPathFinder +from importlib.machinery import ModuleSpec, SourceFileLoader +from importlib.util import cache_from_source, decode_source +from inspect import isclass +from os import PathLike +from types import CodeType, ModuleType, TracebackType +from typing import TypeVar +from unittest.mock import patch + +from ._config import global_config +from ._transformer import TypeguardTransformer + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing_extensions import Buffer + +if sys.version_info >= (3, 11): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec + +if sys.version_info >= (3, 10): + from importlib.metadata import PackageNotFoundError, version +else: + from importlib_metadata import PackageNotFoundError, version + +try: + OPTIMIZATION = "typeguard" + "".join(version("typeguard").split(".")[:3]) +except PackageNotFoundError: + OPTIMIZATION = "typeguard" + +P = ParamSpec("P") +T = TypeVar("T") + + +# The name of this function is magical +def _call_with_frames_removed( + f: Callable[P, T], *args: P.args, **kwargs: P.kwargs +) -> T: + return f(*args, **kwargs) + + +def optimized_cache_from_source(path: str, debug_override: bool | None = None) -> str: + return cache_from_source(path, debug_override, optimization=OPTIMIZATION) + + +class TypeguardLoader(SourceFileLoader): + @staticmethod + def source_to_code( + data: Buffer | str | ast.Module | ast.Expression | ast.Interactive, + path: Buffer | str | PathLike[str] = "<string>", + ) -> CodeType: + if isinstance(data, (ast.Module, ast.Expression, ast.Interactive)): + tree = data + else: + if isinstance(data, str): + source = data + else: + source = decode_source(data) + + tree = _call_with_frames_removed( + ast.parse, + source, + path, + "exec", + ) + + tree = TypeguardTransformer().visit(tree) + ast.fix_missing_locations(tree) + + if global_config.debug_instrumentation and sys.version_info >= (3, 9): + print( + f"Source code of {path!r} after instrumentation:\n" + "----------------------------------------------", + file=sys.stderr, + ) + print(ast.unparse(tree), file=sys.stderr) + print("----------------------------------------------", file=sys.stderr) + + return _call_with_frames_removed( + compile, tree, path, "exec", 0, dont_inherit=True + ) + + def exec_module(self, module: ModuleType) -> None: + # Use a custom optimization marker – the import lock should make this monkey + # patch safe + with patch( + "importlib._bootstrap_external.cache_from_source", + optimized_cache_from_source, + ): + super().exec_module(module) + + +class TypeguardFinder(MetaPathFinder): + """ + Wraps another path finder and instruments the module with + :func:`@typechecked <typeguard.typechecked>` if :meth:`should_instrument` returns + ``True``. + + Should not be used directly, but rather via :func:`~.install_import_hook`. + + .. versionadded:: 2.6 + """ + + def __init__(self, packages: list[str] | None, original_pathfinder: MetaPathFinder): + self.packages = packages + self._original_pathfinder = original_pathfinder + + def find_spec( + self, + fullname: str, + path: Sequence[str] | None, + target: types.ModuleType | None = None, + ) -> ModuleSpec | None: + if self.should_instrument(fullname): + spec = self._original_pathfinder.find_spec(fullname, path, target) + if spec is not None and isinstance(spec.loader, SourceFileLoader): + spec.loader = TypeguardLoader(spec.loader.name, spec.loader.path) + return spec + + return None + + def should_instrument(self, module_name: str) -> bool: + """ + Determine whether the module with the given name should be instrumented. + + :param module_name: full name of the module that is about to be imported (e.g. + ``xyz.abc``) + + """ + if self.packages is None: + return True + + for package in self.packages: + if module_name == package or module_name.startswith(package + "."): + return True + + return False + + +class ImportHookManager: + """ + A handle that can be used to uninstall the Typeguard import hook. + """ + + def __init__(self, hook: MetaPathFinder): + self.hook = hook + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType, + ) -> None: + self.uninstall() + + def uninstall(self) -> None: + """Uninstall the import hook.""" + try: + sys.meta_path.remove(self.hook) + except ValueError: + pass # already removed + + +def install_import_hook( + packages: Iterable[str] | None = None, + *, + cls: type[TypeguardFinder] = TypeguardFinder, +) -> ImportHookManager: + """ + Install an import hook that instruments functions for automatic type checking. + + This only affects modules loaded **after** this hook has been installed. + + :param packages: an iterable of package names to instrument, or ``None`` to + instrument all packages + :param cls: a custom meta path finder class + :return: a context manager that uninstalls the hook on exit (or when you call + ``.uninstall()``) + + .. versionadded:: 2.6 + + """ + if packages is None: + target_packages: list[str] | None = None + elif isinstance(packages, str): + target_packages = [packages] + else: + target_packages = list(packages) + + for finder in sys.meta_path: + if ( + isclass(finder) + and finder.__name__ == "PathFinder" + and hasattr(finder, "find_spec") + ): + break + else: + raise RuntimeError("Cannot find a PathFinder in sys.meta_path") + + hook = cls(target_packages, finder) + sys.meta_path.insert(0, hook) + return ImportHookManager(hook) diff --git a/contrib/python/typeguard/typeguard/_memo.py b/contrib/python/typeguard/typeguard/_memo.py new file mode 100644 index 00000000000..1d0d80c66d7 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_memo.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Any + +from typeguard._config import TypeCheckConfiguration, global_config + + +class TypeCheckMemo: + """ + Contains information necessary for type checkers to do their work. + + .. attribute:: globals + :type: dict[str, Any] + + Dictionary of global variables to use for resolving forward references. + + .. attribute:: locals + :type: dict[str, Any] + + Dictionary of local variables to use for resolving forward references. + + .. attribute:: self_type + :type: type | None + + When running type checks within an instance method or class method, this is the + class object that the first argument (usually named ``self`` or ``cls``) refers + to. + + .. attribute:: config + :type: TypeCheckConfiguration + + Contains the configuration for a particular set of type checking operations. + """ + + __slots__ = "globals", "locals", "self_type", "config" + + def __init__( + self, + globals: dict[str, Any], + locals: dict[str, Any], + *, + self_type: type | None = None, + config: TypeCheckConfiguration = global_config, + ): + self.globals = globals + self.locals = locals + self.self_type = self_type + self.config = config diff --git a/contrib/python/typeguard/typeguard/_pytest_plugin.py b/contrib/python/typeguard/typeguard/_pytest_plugin.py new file mode 100644 index 00000000000..7b2f494ec7d --- /dev/null +++ b/contrib/python/typeguard/typeguard/_pytest_plugin.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import sys +import warnings +from typing import TYPE_CHECKING, Any, Literal + +from typeguard._config import CollectionCheckStrategy, ForwardRefPolicy, global_config +from typeguard._exceptions import InstrumentationWarning +from typeguard._importhook import install_import_hook +from typeguard._utils import qualified_name, resolve_reference + +if TYPE_CHECKING: + from pytest import Config, Parser + + +def pytest_addoption(parser: Parser) -> None: + def add_ini_option( + opt_type: ( + Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None + ), + ) -> None: + parser.addini( + group.options[-1].names()[0][2:], + group.options[-1].attrs()["help"], + opt_type, + ) + + group = parser.getgroup("typeguard") + group.addoption( + "--typeguard-packages", + action="store", + help="comma separated name list of packages and modules to instrument for " + "type checking, or :all: to instrument all modules loaded after typeguard", + ) + add_ini_option("linelist") + + group.addoption( + "--typeguard-debug-instrumentation", + action="store_true", + help="print all instrumented code to stderr", + ) + add_ini_option("bool") + + group.addoption( + "--typeguard-typecheck-fail-callback", + action="store", + help=( + "a module:varname (e.g. typeguard:warn_on_error) reference to a function " + "that is called (with the exception, and memo object as arguments) to " + "handle a TypeCheckError" + ), + ) + add_ini_option("string") + + group.addoption( + "--typeguard-forward-ref-policy", + action="store", + choices=list(ForwardRefPolicy.__members__), + help=( + "determines how to deal with unresolveable forward references in type " + "annotations" + ), + ) + add_ini_option("string") + + group.addoption( + "--typeguard-collection-check-strategy", + action="store", + choices=list(CollectionCheckStrategy.__members__), + help="determines how thoroughly to check collections (list, dict, etc)", + ) + add_ini_option("string") + + +def pytest_configure(config: Config) -> None: + def getoption(name: str) -> Any: + return config.getoption(name.replace("-", "_")) or config.getini(name) + + packages: list[str] | None = [] + if packages_option := config.getoption("typeguard_packages"): + packages = [pkg.strip() for pkg in packages_option.split(",")] + elif packages_ini := config.getini("typeguard-packages"): + packages = packages_ini + + if packages: + if packages == [":all:"]: + packages = None + else: + already_imported_packages = sorted( + package for package in packages if package in sys.modules + ) + if already_imported_packages: + warnings.warn( + f"typeguard cannot check these packages because they are already " + f"imported: {', '.join(already_imported_packages)}", + InstrumentationWarning, + stacklevel=1, + ) + + install_import_hook(packages=packages) + + debug_option = getoption("typeguard-debug-instrumentation") + if debug_option: + global_config.debug_instrumentation = True + + fail_callback_option = getoption("typeguard-typecheck-fail-callback") + if fail_callback_option: + callback = resolve_reference(fail_callback_option) + if not callable(callback): + raise TypeError( + f"{fail_callback_option} ({qualified_name(callback.__class__)}) is not " + f"a callable" + ) + + global_config.typecheck_fail_callback = callback + + forward_ref_policy_option = getoption("typeguard-forward-ref-policy") + if forward_ref_policy_option: + forward_ref_policy = ForwardRefPolicy.__members__[forward_ref_policy_option] + global_config.forward_ref_policy = forward_ref_policy + + collection_check_strategy_option = getoption("typeguard-collection-check-strategy") + if collection_check_strategy_option: + collection_check_strategy = CollectionCheckStrategy.__members__[ + collection_check_strategy_option + ] + global_config.collection_check_strategy = collection_check_strategy diff --git a/contrib/python/typeguard/typeguard/_suppression.py b/contrib/python/typeguard/typeguard/_suppression.py new file mode 100644 index 00000000000..bbbfbfbe8ef --- /dev/null +++ b/contrib/python/typeguard/typeguard/_suppression.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import sys +from collections.abc import Callable, Generator +from contextlib import contextmanager +from functools import update_wrapper +from threading import Lock +from typing import ContextManager, TypeVar, overload + +if sys.version_info >= (3, 10): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec + +P = ParamSpec("P") +T = TypeVar("T") + +type_checks_suppressed = 0 +type_checks_suppress_lock = Lock() + + +@overload +def suppress_type_checks(func: Callable[P, T]) -> Callable[P, T]: ... + + +@overload +def suppress_type_checks() -> ContextManager[None]: ... + + +def suppress_type_checks( + func: Callable[P, T] | None = None, +) -> Callable[P, T] | ContextManager[None]: + """ + Temporarily suppress all type checking. + + This function has two operating modes, based on how it's used: + + #. as a context manager (``with suppress_type_checks(): ...``) + #. as a decorator (``@suppress_type_checks``) + + When used as a context manager, :func:`check_type` and any automatically + instrumented functions skip the actual type checking. These context managers can be + nested. + + When used as a decorator, all type checking is suppressed while the function is + running. + + Type checking will resume once no more context managers are active and no decorated + functions are running. + + Both operating modes are thread-safe. + + """ + + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + global type_checks_suppressed + + with type_checks_suppress_lock: + type_checks_suppressed += 1 + + assert func is not None + try: + return func(*args, **kwargs) + finally: + with type_checks_suppress_lock: + type_checks_suppressed -= 1 + + def cm() -> Generator[None, None, None]: + global type_checks_suppressed + + with type_checks_suppress_lock: + type_checks_suppressed += 1 + + try: + yield + finally: + with type_checks_suppress_lock: + type_checks_suppressed -= 1 + + if func is None: + # Context manager mode + return contextmanager(cm)() + else: + # Decorator mode + update_wrapper(wrapper, func) + return wrapper diff --git a/contrib/python/typeguard/typeguard/_transformer.py b/contrib/python/typeguard/typeguard/_transformer.py new file mode 100644 index 00000000000..7b6dda85263 --- /dev/null +++ b/contrib/python/typeguard/typeguard/_transformer.py @@ -0,0 +1,1228 @@ +from __future__ import annotations + +import ast +import builtins +import sys +import typing +from ast import ( + AST, + Add, + AnnAssign, + Assign, + AsyncFunctionDef, + Attribute, + AugAssign, + BinOp, + BitAnd, + BitOr, + BitXor, + Call, + ClassDef, + Constant, + Dict, + Div, + Expr, + Expression, + FloorDiv, + FunctionDef, + If, + Import, + ImportFrom, + List, + Load, + LShift, + MatMult, + Mod, + Module, + Mult, + Name, + NamedExpr, + NodeTransformer, + NodeVisitor, + Pass, + Pow, + Return, + RShift, + Starred, + Store, + Sub, + Subscript, + Tuple, + Yield, + YieldFrom, + alias, + copy_location, + expr, + fix_missing_locations, + keyword, + walk, +) +from collections import defaultdict +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from copy import deepcopy +from dataclasses import dataclass, field +from typing import Any, ClassVar, cast, overload + +generator_names = ( + "typing.Generator", + "collections.abc.Generator", + "typing.Iterator", + "collections.abc.Iterator", + "typing.Iterable", + "collections.abc.Iterable", + "typing.AsyncIterator", + "collections.abc.AsyncIterator", + "typing.AsyncIterable", + "collections.abc.AsyncIterable", + "typing.AsyncGenerator", + "collections.abc.AsyncGenerator", +) +anytype_names = ( + "typing.Any", + "typing_extensions.Any", +) +literal_names = ( + "typing.Literal", + "typing_extensions.Literal", +) +annotated_names = ( + "typing.Annotated", + "typing_extensions.Annotated", +) +ignore_decorators = ( + "typing.no_type_check", + "typeguard.typeguard_ignore", +) +aug_assign_functions = { + Add: "iadd", + Sub: "isub", + Mult: "imul", + MatMult: "imatmul", + Div: "itruediv", + FloorDiv: "ifloordiv", + Mod: "imod", + Pow: "ipow", + LShift: "ilshift", + RShift: "irshift", + BitAnd: "iand", + BitXor: "ixor", + BitOr: "ior", +} + + +@dataclass +class TransformMemo: + node: Module | ClassDef | FunctionDef | AsyncFunctionDef | None + parent: TransformMemo | None + path: tuple[str, ...] + joined_path: Constant = field(init=False) + return_annotation: expr | None = None + yield_annotation: expr | None = None + send_annotation: expr | None = None + is_async: bool = False + local_names: set[str] = field(init=False, default_factory=set) + imported_names: dict[str, str] = field(init=False, default_factory=dict) + ignored_names: set[str] = field(init=False, default_factory=set) + load_names: defaultdict[str, dict[str, Name]] = field( + init=False, default_factory=lambda: defaultdict(dict) + ) + has_yield_expressions: bool = field(init=False, default=False) + has_return_expressions: bool = field(init=False, default=False) + memo_var_name: Name | None = field(init=False, default=None) + should_instrument: bool = field(init=False, default=True) + variable_annotations: dict[str, expr] = field(init=False, default_factory=dict) + configuration_overrides: dict[str, Any] = field(init=False, default_factory=dict) + code_inject_index: int = field(init=False, default=0) + + def __post_init__(self) -> None: + elements: list[str] = [] + memo = self + while isinstance(memo.node, (ClassDef, FunctionDef, AsyncFunctionDef)): + elements.insert(0, memo.node.name) + if not memo.parent: + break + + memo = memo.parent + if isinstance(memo.node, (FunctionDef, AsyncFunctionDef)): + elements.insert(0, "<locals>") + + self.joined_path = Constant(".".join(elements)) + + # Figure out where to insert instrumentation code + if self.node: + for index, child in enumerate(self.node.body): + if isinstance(child, ImportFrom) and child.module == "__future__": + # (module only) __future__ imports must come first + continue + elif ( + isinstance(child, Expr) + and isinstance(child.value, Constant) + and isinstance(child.value.value, str) + ): + continue # docstring + + self.code_inject_index = index + break + + def get_unused_name(self, name: str) -> str: + memo: TransformMemo | None = self + while memo is not None: + if name in memo.local_names: + memo = self + name += "_" + else: + memo = memo.parent + + self.local_names.add(name) + return name + + def is_ignored_name(self, expression: expr | Expr | None) -> bool: + top_expression = ( + expression.value if isinstance(expression, Expr) else expression + ) + + if isinstance(top_expression, Attribute) and isinstance( + top_expression.value, Name + ): + name = top_expression.value.id + elif isinstance(top_expression, Name): + name = top_expression.id + else: + return False + + memo: TransformMemo | None = self + while memo is not None: + if name in memo.ignored_names: + return True + + memo = memo.parent + + return False + + def get_memo_name(self) -> Name: + if not self.memo_var_name: + self.memo_var_name = Name(id="memo", ctx=Load()) + + return self.memo_var_name + + def get_import(self, module: str, name: str) -> Name: + if module in self.load_names and name in self.load_names[module]: + return self.load_names[module][name] + + qualified_name = f"{module}.{name}" + if name in self.imported_names and self.imported_names[name] == qualified_name: + return Name(id=name, ctx=Load()) + + alias = self.get_unused_name(name) + node = self.load_names[module][name] = Name(id=alias, ctx=Load()) + self.imported_names[name] = qualified_name + return node + + def insert_imports(self, node: Module | FunctionDef | AsyncFunctionDef) -> None: + """Insert imports needed by injected code.""" + if not self.load_names: + return + + # Insert imports after any "from __future__ ..." imports and any docstring + for modulename, names in self.load_names.items(): + aliases = [ + alias(orig_name, new_name.id if orig_name != new_name.id else None) + for orig_name, new_name in sorted(names.items()) + ] + node.body.insert(self.code_inject_index, ImportFrom(modulename, aliases, 0)) + + def name_matches(self, expression: expr | Expr | None, *names: str) -> bool: + if expression is None: + return False + + path: list[str] = [] + top_expression = ( + expression.value if isinstance(expression, Expr) else expression + ) + + if isinstance(top_expression, Subscript): + top_expression = top_expression.value + elif isinstance(top_expression, Call): + top_expression = top_expression.func + + while isinstance(top_expression, Attribute): + path.insert(0, top_expression.attr) + top_expression = top_expression.value + + if not isinstance(top_expression, Name): + return False + + if top_expression.id in self.imported_names: + translated = self.imported_names[top_expression.id] + elif hasattr(builtins, top_expression.id): + translated = "builtins." + top_expression.id + else: + translated = top_expression.id + + path.insert(0, translated) + joined_path = ".".join(path) + if joined_path in names: + return True + elif self.parent: + return self.parent.name_matches(expression, *names) + else: + return False + + def get_config_keywords(self) -> list[keyword]: + if self.parent and isinstance(self.parent.node, ClassDef): + overrides = self.parent.configuration_overrides.copy() + else: + overrides = {} + + overrides.update(self.configuration_overrides) + return [keyword(key, value) for key, value in overrides.items()] + + +class NameCollector(NodeVisitor): + def __init__(self) -> None: + self.names: set[str] = set() + + def visit_Import(self, node: Import) -> None: + for name in node.names: + self.names.add(name.asname or name.name) + + def visit_ImportFrom(self, node: ImportFrom) -> None: + for name in node.names: + self.names.add(name.asname or name.name) + + def visit_Assign(self, node: Assign) -> None: + for target in node.targets: + if isinstance(target, Name): + self.names.add(target.id) + + def visit_NamedExpr(self, node: NamedExpr) -> Any: + if isinstance(node.target, Name): + self.names.add(node.target.id) + + def visit_FunctionDef(self, node: FunctionDef) -> None: + pass + + def visit_ClassDef(self, node: ClassDef) -> None: + pass + + +class GeneratorDetector(NodeVisitor): + """Detects if a function node is a generator function.""" + + contains_yields: bool = False + in_root_function: bool = False + + def visit_Yield(self, node: Yield) -> Any: + self.contains_yields = True + + def visit_YieldFrom(self, node: YieldFrom) -> Any: + self.contains_yields = True + + def visit_ClassDef(self, node: ClassDef) -> Any: + pass + + def visit_FunctionDef(self, node: FunctionDef | AsyncFunctionDef) -> Any: + if not self.in_root_function: + self.in_root_function = True + self.generic_visit(node) + self.in_root_function = False + + def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> Any: + self.visit_FunctionDef(node) + + +class AnnotationTransformer(NodeTransformer): + type_substitutions: ClassVar[dict[str, tuple[str, str]]] = { + "builtins.dict": ("typing", "Dict"), + "builtins.list": ("typing", "List"), + "builtins.tuple": ("typing", "Tuple"), + "builtins.set": ("typing", "Set"), + "builtins.frozenset": ("typing", "FrozenSet"), + } + + def __init__(self, transformer: TypeguardTransformer): + self.transformer = transformer + self._memo = transformer._memo + self._level = 0 + + def visit(self, node: AST) -> Any: + # Don't process Literals + if isinstance(node, expr) and self._memo.name_matches(node, *literal_names): + return node + + self._level += 1 + new_node = super().visit(node) + self._level -= 1 + + if isinstance(new_node, Expression) and not hasattr(new_node, "body"): + return None + + # Return None if this new node matches a variation of typing.Any + if ( + self._level == 0 + and isinstance(new_node, expr) + and self._memo.name_matches(new_node, *anytype_names) + ): + return None + + return new_node + + def visit_BinOp(self, node: BinOp) -> Any: + self.generic_visit(node) + + if isinstance(node.op, BitOr): + # If either branch of the BinOp has been transformed to `None`, it means + # that a type in the union was ignored, so the entire annotation should be + # ignored + if not hasattr(node, "left") or not hasattr(node, "right"): + return None + + # Return Any if either side is Any + if self._memo.name_matches(node.left, *anytype_names): + return node.left + elif self._memo.name_matches(node.right, *anytype_names): + return node.right + + # Turn union types to typing.Union constructs on Python 3.9 + if sys.version_info < (3, 10): + union_name = self.transformer._get_import("typing", "Union") + return Subscript( + value=union_name, + slice=Tuple(elts=[node.left, node.right], ctx=Load()), + ctx=Load(), + ) + + return node + + def visit_Attribute(self, node: Attribute) -> Any: + if self._memo.is_ignored_name(node): + return None + + return node + + def visit_Subscript(self, node: Subscript) -> Any: + if self._memo.is_ignored_name(node.value): + return None + + # The subscript of typing(_extensions).Literal can be any arbitrary string, so + # don't try to evaluate it as code + if node.slice: + if isinstance(node.slice, Tuple): + if self._memo.name_matches(node.value, *annotated_names): + # Only treat the first argument to typing.Annotated as a potential + # forward reference + items = cast( + typing.List[expr], + [self.visit(node.slice.elts[0])] + node.slice.elts[1:], + ) + else: + items = cast( + typing.List[expr], + [self.visit(item) for item in node.slice.elts], + ) + + # If this is a Union and any of the items is Any, erase the entire + # annotation + if self._memo.name_matches(node.value, "typing.Union") and any( + item is None + or ( + isinstance(item, expr) + and self._memo.name_matches(item, *anytype_names) + ) + for item in items + ): + return None + + # If all items in the subscript were Any, erase the subscript entirely + if all(item is None for item in items): + return node.value + + for index, item in enumerate(items): + if item is None: + items[index] = self.transformer._get_import("typing", "Any") + + node.slice.elts = items + else: + self.generic_visit(node) + + # If the transformer erased the slice entirely, just return the node + # value without the subscript (unless it's Optional, in which case erase + # the node entirely + if self._memo.name_matches( + node.value, "typing.Optional" + ) and not hasattr(node, "slice"): + return None + if sys.version_info >= (3, 9) and not hasattr(node, "slice"): + return node.value + elif sys.version_info < (3, 9) and not hasattr(node.slice, "value"): + return node.value + + return node + + def visit_Name(self, node: Name) -> Any: + if self._memo.is_ignored_name(node): + return None + + return node + + def visit_Call(self, node: Call) -> Any: + # Don't recurse into calls + return node + + def visit_Constant(self, node: Constant) -> Any: + if isinstance(node.value, str): + expression = ast.parse(node.value, mode="eval") + new_node = self.visit(expression) + if new_node: + return copy_location(new_node.body, node) + else: + return None + + return node + + +class TypeguardTransformer(NodeTransformer): + def __init__( + self, target_path: Sequence[str] | None = None, target_lineno: int | None = None + ) -> None: + self._target_path = tuple(target_path) if target_path else None + self._memo = self._module_memo = TransformMemo(None, None, ()) + self.names_used_in_annotations: set[str] = set() + self.target_node: FunctionDef | AsyncFunctionDef | None = None + self.target_lineno = target_lineno + + def generic_visit(self, node: AST) -> AST: + has_non_empty_body_initially = bool(getattr(node, "body", None)) + initial_type = type(node) + + node = super().generic_visit(node) + + if ( + type(node) is initial_type + and has_non_empty_body_initially + and hasattr(node, "body") + and not node.body + ): + # If we have still the same node type after transformation + # but we've optimised it's body away, we add a `pass` statement. + node.body = [Pass()] + + return node + + @contextmanager + def _use_memo( + self, node: ClassDef | FunctionDef | AsyncFunctionDef + ) -> Generator[None, Any, None]: + new_memo = TransformMemo(node, self._memo, self._memo.path + (node.name,)) + old_memo = self._memo + self._memo = new_memo + + if isinstance(node, (FunctionDef, AsyncFunctionDef)): + new_memo.should_instrument = ( + self._target_path is None or new_memo.path == self._target_path + ) + if new_memo.should_instrument: + # Check if the function is a generator function + detector = GeneratorDetector() + detector.visit(node) + + # Extract yield, send and return types where possible from a subscripted + # annotation like Generator[int, str, bool] + return_annotation = deepcopy(node.returns) + if detector.contains_yields and new_memo.name_matches( + return_annotation, *generator_names + ): + if isinstance(return_annotation, Subscript): + if isinstance(return_annotation.slice, Tuple): + items = return_annotation.slice.elts + else: + items = [return_annotation.slice] + + if len(items) > 0: + new_memo.yield_annotation = self._convert_annotation( + items[0] + ) + + if len(items) > 1: + new_memo.send_annotation = self._convert_annotation( + items[1] + ) + + if len(items) > 2: + new_memo.return_annotation = self._convert_annotation( + items[2] + ) + else: + new_memo.return_annotation = self._convert_annotation( + return_annotation + ) + + if isinstance(node, AsyncFunctionDef): + new_memo.is_async = True + + yield + self._memo = old_memo + + def _get_import(self, module: str, name: str) -> Name: + memo = self._memo if self._target_path else self._module_memo + return memo.get_import(module, name) + + @overload + def _convert_annotation(self, annotation: None) -> None: ... + + @overload + def _convert_annotation(self, annotation: expr) -> expr: ... + + def _convert_annotation(self, annotation: expr | None) -> expr | None: + if annotation is None: + return None + + # Convert PEP 604 unions (x | y) and generic built-in collections where + # necessary, and undo forward references + new_annotation = cast(expr, AnnotationTransformer(self).visit(annotation)) + if isinstance(new_annotation, expr): + new_annotation = ast.copy_location(new_annotation, annotation) + + # Store names used in the annotation + names = {node.id for node in walk(new_annotation) if isinstance(node, Name)} + self.names_used_in_annotations.update(names) + + return new_annotation + + def visit_Name(self, node: Name) -> Name: + self._memo.local_names.add(node.id) + return node + + def visit_Module(self, node: Module) -> Module: + self._module_memo = self._memo = TransformMemo(node, None, ()) + self.generic_visit(node) + self._module_memo.insert_imports(node) + + fix_missing_locations(node) + return node + + def visit_Import(self, node: Import) -> Import: + for name in node.names: + self._memo.local_names.add(name.asname or name.name) + self._memo.imported_names[name.asname or name.name] = name.name + + return node + + def visit_ImportFrom(self, node: ImportFrom) -> ImportFrom: + for name in node.names: + if name.name != "*": + alias = name.asname or name.name + self._memo.local_names.add(alias) + self._memo.imported_names[alias] = f"{node.module}.{name.name}" + + return node + + def visit_ClassDef(self, node: ClassDef) -> ClassDef | None: + self._memo.local_names.add(node.name) + + # Eliminate top level classes not belonging to the target path + if ( + self._target_path is not None + and not self._memo.path + and node.name != self._target_path[0] + ): + return None + + with self._use_memo(node): + for decorator in node.decorator_list.copy(): + if self._memo.name_matches(decorator, "typeguard.typechecked"): + # Remove the decorator to prevent duplicate instrumentation + node.decorator_list.remove(decorator) + + # Store any configuration overrides + if isinstance(decorator, Call) and decorator.keywords: + self._memo.configuration_overrides.update( + {kw.arg: kw.value for kw in decorator.keywords if kw.arg} + ) + + self.generic_visit(node) + return node + + def visit_FunctionDef( + self, node: FunctionDef | AsyncFunctionDef + ) -> FunctionDef | AsyncFunctionDef | None: + """ + Injects type checks for function arguments, and for a return of None if the + function is annotated to return something else than Any or None, and the body + ends without an explicit "return". + + """ + self._memo.local_names.add(node.name) + + # Eliminate top level functions not belonging to the target path + if ( + self._target_path is not None + and not self._memo.path + and node.name != self._target_path[0] + ): + return None + + # Skip instrumentation if we're instrumenting the whole module and the function + # contains either @no_type_check or @typeguard_ignore + if self._target_path is None: + for decorator in node.decorator_list: + if self._memo.name_matches(decorator, *ignore_decorators): + return node + + with self._use_memo(node): + arg_annotations: dict[str, Any] = {} + if self._target_path is None or self._memo.path == self._target_path: + # Find line number we're supposed to match against + if node.decorator_list: + first_lineno = node.decorator_list[0].lineno + else: + first_lineno = node.lineno + + for decorator in node.decorator_list.copy(): + if self._memo.name_matches(decorator, "typing.overload"): + # Remove overloads entirely + return None + elif self._memo.name_matches(decorator, "typeguard.typechecked"): + # Remove the decorator to prevent duplicate instrumentation + node.decorator_list.remove(decorator) + + # Store any configuration overrides + if isinstance(decorator, Call) and decorator.keywords: + self._memo.configuration_overrides = { + kw.arg: kw.value for kw in decorator.keywords if kw.arg + } + + if self.target_lineno == first_lineno: + assert self.target_node is None + self.target_node = node + if node.decorator_list: + self.target_lineno = node.decorator_list[0].lineno + else: + self.target_lineno = node.lineno + + all_args = node.args.posonlyargs + node.args.args + node.args.kwonlyargs + + # Ensure that any type shadowed by the positional or keyword-only + # argument names are ignored in this function + for arg in all_args: + self._memo.ignored_names.add(arg.arg) + + # Ensure that any type shadowed by the variable positional argument name + # (e.g. "args" in *args) is ignored this function + if node.args.vararg: + self._memo.ignored_names.add(node.args.vararg.arg) + + # Ensure that any type shadowed by the variable keywrod argument name + # (e.g. "kwargs" in *kwargs) is ignored this function + if node.args.kwarg: + self._memo.ignored_names.add(node.args.kwarg.arg) + + for arg in all_args: + annotation = self._convert_annotation(deepcopy(arg.annotation)) + if annotation: + arg_annotations[arg.arg] = annotation + + if node.args.vararg: + annotation_ = self._convert_annotation(node.args.vararg.annotation) + if annotation_: + container = Name("tuple", ctx=Load()) + subscript_slice = Tuple( + [ + annotation_, + Constant(Ellipsis), + ], + ctx=Load(), + ) + arg_annotations[node.args.vararg.arg] = Subscript( + container, subscript_slice, ctx=Load() + ) + + if node.args.kwarg: + annotation_ = self._convert_annotation(node.args.kwarg.annotation) + if annotation_: + container = Name("dict", ctx=Load()) + subscript_slice = Tuple( + [ + Name("str", ctx=Load()), + annotation_, + ], + ctx=Load(), + ) + arg_annotations[node.args.kwarg.arg] = Subscript( + container, subscript_slice, ctx=Load() + ) + + if arg_annotations: + self._memo.variable_annotations.update(arg_annotations) + + self.generic_visit(node) + + if arg_annotations: + annotations_dict = Dict( + keys=[Constant(key) for key in arg_annotations.keys()], + values=[ + Tuple([Name(key, ctx=Load()), annotation], ctx=Load()) + for key, annotation in arg_annotations.items() + ], + ) + func_name = self._get_import( + "typeguard._functions", "check_argument_types" + ) + args = [ + self._memo.joined_path, + annotations_dict, + self._memo.get_memo_name(), + ] + node.body.insert( + self._memo.code_inject_index, Expr(Call(func_name, args, [])) + ) + + # Add a checked "return None" to the end if there's no explicit return + # Skip if the return annotation is None or Any + if ( + self._memo.return_annotation + and (not self._memo.is_async or not self._memo.has_yield_expressions) + and not isinstance(node.body[-1], Return) + and ( + not isinstance(self._memo.return_annotation, Constant) + or self._memo.return_annotation.value is not None + ) + ): + func_name = self._get_import( + "typeguard._functions", "check_return_type" + ) + return_node = Return( + Call( + func_name, + [ + self._memo.joined_path, + Constant(None), + self._memo.return_annotation, + self._memo.get_memo_name(), + ], + [], + ) + ) + + # Replace a placeholder "pass" at the end + if isinstance(node.body[-1], Pass): + copy_location(return_node, node.body[-1]) + del node.body[-1] + + node.body.append(return_node) + + # Insert code to create the call memo, if it was ever needed for this + # function + if self._memo.memo_var_name: + memo_kwargs: dict[str, Any] = {} + if self._memo.parent and isinstance(self._memo.parent.node, ClassDef): + for decorator in node.decorator_list: + if ( + isinstance(decorator, Name) + and decorator.id == "staticmethod" + ): + break + elif ( + isinstance(decorator, Name) + and decorator.id == "classmethod" + ): + arglist = node.args.posonlyargs or node.args.args + memo_kwargs["self_type"] = Name( + id=arglist[0].arg, ctx=Load() + ) + break + else: + if arglist := node.args.posonlyargs or node.args.args: + if node.name == "__new__": + memo_kwargs["self_type"] = Name( + id=arglist[0].arg, ctx=Load() + ) + else: + memo_kwargs["self_type"] = Attribute( + Name(id=arglist[0].arg, ctx=Load()), + "__class__", + ctx=Load(), + ) + + # Construct the function reference + # Nested functions get special treatment: the function name is added + # to free variables (and the closure of the resulting function) + names: list[str] = [node.name] + memo = self._memo.parent + while memo: + if isinstance(memo.node, (FunctionDef, AsyncFunctionDef)): + # This is a nested function. Use the function name as-is. + del names[:-1] + break + elif not isinstance(memo.node, ClassDef): + break + + names.insert(0, memo.node.name) + memo = memo.parent + + config_keywords = self._memo.get_config_keywords() + if config_keywords: + memo_kwargs["config"] = Call( + self._get_import("dataclasses", "replace"), + [self._get_import("typeguard._config", "global_config")], + config_keywords, + ) + + self._memo.memo_var_name.id = self._memo.get_unused_name("memo") + memo_store_name = Name(id=self._memo.memo_var_name.id, ctx=Store()) + globals_call = Call(Name(id="globals", ctx=Load()), [], []) + locals_call = Call(Name(id="locals", ctx=Load()), [], []) + memo_expr = Call( + self._get_import("typeguard", "TypeCheckMemo"), + [globals_call, locals_call], + [keyword(key, value) for key, value in memo_kwargs.items()], + ) + node.body.insert( + self._memo.code_inject_index, + Assign([memo_store_name], memo_expr), + ) + + self._memo.insert_imports(node) + + # Special case the __new__() method to create a local alias from the + # class name to the first argument (usually "cls") + if ( + isinstance(node, FunctionDef) + and node.args + and self._memo.parent is not None + and isinstance(self._memo.parent.node, ClassDef) + and node.name == "__new__" + ): + first_args_expr = Name(node.args.args[0].arg, ctx=Load()) + cls_name = Name(self._memo.parent.node.name, ctx=Store()) + node.body.insert( + self._memo.code_inject_index, + Assign([cls_name], first_args_expr), + ) + + # Rmove any placeholder "pass" at the end + if isinstance(node.body[-1], Pass): + del node.body[-1] + + return node + + def visit_AsyncFunctionDef( + self, node: AsyncFunctionDef + ) -> FunctionDef | AsyncFunctionDef | None: + return self.visit_FunctionDef(node) + + def visit_Return(self, node: Return) -> Return: + """This injects type checks into "return" statements.""" + self.generic_visit(node) + if ( + self._memo.return_annotation + 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") + old_node = node + retval = old_node.value or Constant(None) + node = Return( + Call( + func_name, + [ + self._memo.joined_path, + retval, + self._memo.return_annotation, + self._memo.get_memo_name(), + ], + [], + ) + ) + copy_location(node, old_node) + + return node + + def visit_Yield(self, node: Yield) -> Yield | Call: + """ + This injects type checks into "yield" expressions, checking both the yielded + value and the value sent back to the generator, when appropriate. + + """ + self._memo.has_yield_expressions = True + self.generic_visit(node) + + if ( + self._memo.yield_annotation + and self._memo.should_instrument + and not self._memo.is_ignored_name(self._memo.yield_annotation) + ): + func_name = self._get_import("typeguard._functions", "check_yield_type") + yieldval = node.value or Constant(None) + node.value = Call( + func_name, + [ + self._memo.joined_path, + yieldval, + self._memo.yield_annotation, + self._memo.get_memo_name(), + ], + [], + ) + + if ( + self._memo.send_annotation + and self._memo.should_instrument + and not self._memo.is_ignored_name(self._memo.send_annotation) + ): + func_name = self._get_import("typeguard._functions", "check_send_type") + old_node = node + call_node = Call( + func_name, + [ + self._memo.joined_path, + old_node, + self._memo.send_annotation, + self._memo.get_memo_name(), + ], + [], + ) + copy_location(call_node, old_node) + return call_node + + return node + + def visit_AnnAssign(self, node: AnnAssign) -> Any: + """ + This injects a type check into a local variable annotation-assignment within a + function body. + + """ + self.generic_visit(node) + + if ( + isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) + and node.annotation + and isinstance(node.target, Name) + ): + self._memo.ignored_names.add(node.target.id) + annotation = self._convert_annotation(deepcopy(node.annotation)) + if annotation: + self._memo.variable_annotations[node.target.id] = annotation + if node.value: + func_name = self._get_import( + "typeguard._functions", "check_variable_assignment" + ) + targets_arg = List( + [ + List( + [ + Tuple( + [Constant(node.target.id), annotation], + ctx=Load(), + ) + ], + ctx=Load(), + ) + ], + ctx=Load(), + ) + node.value = Call( + func_name, + [ + node.value, + targets_arg, + self._memo.get_memo_name(), + ], + [], + ) + + return node + + def visit_Assign(self, node: Assign) -> Any: + """ + This injects a type check into a local variable assignment within a function + body. The variable must have been annotated earlier in the function body. + + """ + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)): + preliminary_targets: list[list[tuple[Constant, expr | None]]] = [] + check_required = False + for target in node.targets: + elts: Sequence[expr] + if isinstance(target, Name): + elts = [target] + elif isinstance(target, Tuple): + elts = target.elts + else: + continue + + annotations_: list[tuple[Constant, expr | None]] = [] + for exp in elts: + prefix = "" + if isinstance(exp, Starred): + exp = exp.value + prefix = "*" + + path: list[str] = [] + while isinstance(exp, Attribute): + path.insert(0, exp.attr) + exp = exp.value + + if isinstance(exp, Name): + if not path: + self._memo.ignored_names.add(exp.id) + + path.insert(0, exp.id) + name = prefix + ".".join(path) + if len(path) == 1 and ( + annotation := self._memo.variable_annotations.get(exp.id) + ): + annotations_.append((Constant(name), annotation)) + check_required = True + else: + annotations_.append((Constant(name), None)) + + preliminary_targets.append(annotations_) + + if check_required: + # Replace missing annotations with typing.Any + targets: list[list[tuple[Constant, expr]]] = [] + 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: + target_list.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(), + ) + for target in targets + ], + ctx=Load(), + ) + node.value = Call( + func_name, + [node.value, targets_arg, self._memo.get_memo_name()], + [], + ) + + return node + + def visit_NamedExpr(self, node: NamedExpr) -> Any: + """This injects a type check into an assignment expression (a := foo()).""" + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) and isinstance( + node.target, Name + ): + self._memo.ignored_names.add(node.target.id) + + # Bail out if no matching annotation is found + annotation = self._memo.variable_annotations.get(node.target.id) + if annotation is None: + return node + + func_name = self._get_import( + "typeguard._functions", "check_variable_assignment" + ) + node.value = Call( + func_name, + [ + node.value, + List( + [ + List( + [ + Tuple( + [Constant(node.target.id), annotation], + ctx=Load(), + ) + ], + ctx=Load(), + ) + ], + ctx=Load(), + ), + self._memo.get_memo_name(), + ], + [], + ) + + return node + + def visit_AugAssign(self, node: AugAssign) -> Any: + """ + This injects a type check into an augmented assignment expression (a += 1). + + """ + self.generic_visit(node) + + # Only instrument function-local assignments + if isinstance(self._memo.node, (FunctionDef, AsyncFunctionDef)) and isinstance( + node.target, Name + ): + # Bail out if no matching annotation is found + annotation = self._memo.variable_annotations.get(node.target.id) + if annotation is None: + return node + + # Bail out if the operator is not found (newer Python version?) + try: + operator_func_name = aug_assign_functions[node.op.__class__] + except KeyError: + return node + + operator_func = self._get_import("operator", operator_func_name) + operator_call = Call( + operator_func, [Name(node.target.id, ctx=Load()), node.value], [] + ) + targets_arg = List( + [ + List( + [Tuple([Constant(node.target.id), annotation], ctx=Load())], + ctx=Load(), + ) + ], + ctx=Load(), + ) + check_call = Call( + self._get_import("typeguard._functions", "check_variable_assignment"), + [ + operator_call, + targets_arg, + self._memo.get_memo_name(), + ], + [], + ) + return Assign(targets=[node.target], value=check_call) + + return node + + def visit_If(self, node: If) -> Any: + """ + This blocks names from being collected from a module-level + "if typing.TYPE_CHECKING:" block, so that they won't be type checked. + + """ + self.generic_visit(node) + + if ( + self._memo is self._module_memo + and isinstance(node.test, Name) + and self._memo.name_matches(node.test, "typing.TYPE_CHECKING") + ): + collector = NameCollector() + collector.visit(node) + self._memo.ignored_names.update(collector.names) + + return node diff --git a/contrib/python/typeguard/typeguard/_utils.py b/contrib/python/typeguard/typeguard/_utils.py new file mode 100644 index 00000000000..f61b94d20ad --- /dev/null +++ b/contrib/python/typeguard/typeguard/_utils.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import inspect +import sys +from importlib import import_module +from inspect import currentframe +from types import FrameType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ForwardRef, + Union, + cast, + final, + get_args, + get_origin, +) + +if TYPE_CHECKING: + from ._memo import TypeCheckMemo + +if sys.version_info >= (3, 14): + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + return forwardref.evaluate( + globals=memo.globals, locals=memo.locals, type_params=() + ) +elif sys.version_info >= (3, 13): + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + return forwardref._evaluate( + memo.globals, memo.locals, type_params=(), recursive_guard=frozenset() + ) +else: + + def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any: + try: + return forwardref._evaluate( + memo.globals, memo.locals, recursive_guard=frozenset() + ) + except NameError: + if sys.version_info < (3, 10): + # Try again, with the type substitutions (list -> List etc.) in place + new_globals = memo.globals.copy() + new_globals.setdefault("Union", Union) + + return forwardref._evaluate( + new_globals, memo.locals or new_globals, recursive_guard=frozenset() + ) + + raise + + +def get_type_name(type_: Any) -> str: + name: str + for attrname in "__name__", "_name", "__forward_arg__": + candidate = getattr(type_, attrname, None) + if isinstance(candidate, str): + name = candidate + break + else: + origin = get_origin(type_) + candidate = getattr(origin, "_name", None) + if candidate is None: + candidate = type_.__class__.__name__.strip("_") + + if isinstance(candidate, str): + name = candidate + else: + return "(unknown)" + + args = get_args(type_) + if args: + if name == "Literal": + formatted_args = ", ".join(repr(arg) for arg in args) + else: + formatted_args = ", ".join(get_type_name(arg) for arg in args) + + name += f"[{formatted_args}]" + + # For ForwardRefs, use the module stored on the object if available + if hasattr(type_, "__forward_module__"): + module = type_.__forward_module__ + else: + module = getattr(type_, "__module__", None) + if module and module not in (None, "typing", "typing_extensions", "builtins"): + name = module + "." + name + + return name + + +def qualified_name(obj: Any, *, add_class_prefix: bool = False) -> str: + """ + Return the qualified name (e.g. package.module.Type) for the given object. + + Builtins and types from the :mod:`typing` package get special treatment by having + the module name stripped from the generated name. + + """ + if obj is None: + return "None" + elif inspect.isclass(obj): + prefix = "class " if add_class_prefix else "" + type_ = obj + else: + prefix = "" + type_ = type(obj) + + module = type_.__module__ + qualname = type_.__qualname__ + name = qualname if module in ("typing", "builtins") else f"{module}.{qualname}" + return prefix + name + + +def function_name(func: Callable[..., Any]) -> str: + """ + Return the qualified name of the given function. + + Builtins and types from the :mod:`typing` package get special treatment by having + the module name stripped from the generated name. + + """ + # For partial functions and objects with __call__ defined, __qualname__ does not + # exist + module = getattr(func, "__module__", "") + qualname = (module + ".") if module not in ("builtins", "") else "" + return qualname + getattr(func, "__qualname__", repr(func)) + + +def resolve_reference(reference: str) -> Any: + modulename, varname = reference.partition(":")[::2] + if not modulename or not varname: + raise ValueError(f"{reference!r} is not a module:varname reference") + + obj = import_module(modulename) + for attr in varname.split("."): + obj = getattr(obj, attr) + + return obj + + +def is_method_of(obj: object, cls: type) -> bool: + return ( + inspect.isfunction(obj) + and obj.__module__ == cls.__module__ + and obj.__qualname__.startswith(cls.__qualname__ + ".") + ) + + +def get_stacklevel() -> int: + level = 1 + frame = cast(FrameType, currentframe()).f_back + while frame and frame.f_globals.get("__name__", "").startswith("typeguard."): + level += 1 + frame = frame.f_back + + return level + + +@final +class Unset: + __slots__ = () + + def __repr__(self) -> str: + return "<unset>" + + +unset = Unset() diff --git a/contrib/python/typeguard/typeguard/importhook.py b/contrib/python/typeguard/typeguard/importhook.py deleted file mode 100644 index d4e237fe7de..00000000000 --- a/contrib/python/typeguard/typeguard/importhook.py +++ /dev/null @@ -1,162 +0,0 @@ -import ast -import sys -from importlib.abc import MetaPathFinder -from importlib.machinery import SourceFileLoader -from importlib.util import cache_from_source, decode_source -from inspect import isclass -from typing import Iterable, Type -from unittest.mock import patch - - -# The name of this function is magical -def _call_with_frames_removed(f, *args, **kwargs): - return f(*args, **kwargs) - - -def optimized_cache_from_source(path, debug_override=None): - return cache_from_source(path, debug_override, optimization='typeguard') - - -class TypeguardTransformer(ast.NodeVisitor): - def __init__(self) -> None: - self._parents = [] - - def visit_Module(self, node: ast.Module): - # Insert "import typeguard" after any "from __future__ ..." imports - for i, child in enumerate(node.body): - if isinstance(child, ast.ImportFrom) and child.module == '__future__': - continue - elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant): - continue # module docstring - else: - node.body.insert(i, ast.Import(names=[ast.alias('typeguard', None)])) - break - - self._parents.append(node) - self.generic_visit(node) - self._parents.pop() - return node - - def visit_ClassDef(self, node: ast.ClassDef): - node.decorator_list.append( - ast.Attribute(ast.Name(id='typeguard', ctx=ast.Load()), 'typechecked', ast.Load()) - ) - self._parents.append(node) - self.generic_visit(node) - self._parents.pop() - return node - - def visit_FunctionDef(self, node: ast.FunctionDef): - # Let the class level decorator handle the methods of a class - if isinstance(self._parents[-1], ast.ClassDef): - return node - - has_annotated_args = any(arg for arg in node.args.args if arg.annotation) - has_annotated_return = bool(node.returns) - if has_annotated_args or has_annotated_return: - node.decorator_list.insert( - 0, - ast.Attribute(ast.Name(id='typeguard', ctx=ast.Load()), 'typechecked', ast.Load()) - ) - - self._parents.append(node) - self.generic_visit(node) - self._parents.pop() - return node - - -class TypeguardLoader(SourceFileLoader): - def source_to_code(self, data, path, *, _optimize=-1): - source = decode_source(data) - tree = _call_with_frames_removed(compile, source, path, 'exec', ast.PyCF_ONLY_AST, - dont_inherit=True, optimize=_optimize) - tree = TypeguardTransformer().visit(tree) - ast.fix_missing_locations(tree) - return _call_with_frames_removed(compile, tree, path, 'exec', - dont_inherit=True, optimize=_optimize) - - def exec_module(self, module): - # Use a custom optimization marker – the import lock should make this monkey patch safe - with patch('importlib._bootstrap_external.cache_from_source', optimized_cache_from_source): - return super().exec_module(module) - - -class TypeguardFinder(MetaPathFinder): - """ - Wraps another path finder and instruments the module with ``@typechecked`` if - :meth:`should_instrument` returns ``True``. - - Should not be used directly, but rather via :func:`~.install_import_hook`. - - .. versionadded:: 2.6 - - """ - - def __init__(self, packages, original_pathfinder): - self.packages = packages - self._original_pathfinder = original_pathfinder - - def find_spec(self, fullname, path=None, target=None): - if self.should_instrument(fullname): - spec = self._original_pathfinder.find_spec(fullname, path, target) - if spec is not None and isinstance(spec.loader, SourceFileLoader): - spec.loader = TypeguardLoader(spec.loader.name, spec.loader.path) - return spec - - return None - - def should_instrument(self, module_name: str) -> bool: - """ - Determine whether the module with the given name should be instrumented. - - :param module_name: full name of the module that is about to be imported (e.g. ``xyz.abc``) - - """ - for package in self.packages: - if module_name == package or module_name.startswith(package + '.'): - return True - - return False - - -class ImportHookManager: - def __init__(self, hook: MetaPathFinder): - self.hook = hook - - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_val, exc_tb): - self.uninstall() - - def uninstall(self): - try: - sys.meta_path.remove(self.hook) - except ValueError: - pass # already removed - - -def install_import_hook(packages: Iterable[str], *, - cls: Type[TypeguardFinder] = TypeguardFinder) -> ImportHookManager: - """ - Install an import hook that decorates classes and functions with ``@typechecked``. - - This only affects modules loaded **after** this hook has been installed. - - :return: a context manager that uninstalls the hook on exit (or when you call ``.uninstall()``) - - .. versionadded:: 2.6 - - """ - if isinstance(packages, str): - packages = [packages] - - for i, finder in enumerate(sys.meta_path): - if isclass(finder) and finder.__name__ == 'PathFinder' and hasattr(finder, 'find_spec'): - break - else: - raise RuntimeError('Cannot find a PathFinder in sys.meta_path') - - hook = cls(packages, finder) - sys.meta_path.insert(0, hook) - return ImportHookManager(hook) diff --git a/contrib/python/typeguard/typeguard/pytest_plugin.py b/contrib/python/typeguard/typeguard/pytest_plugin.py deleted file mode 100644 index caa128b5474..00000000000 --- a/contrib/python/typeguard/typeguard/pytest_plugin.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys - -from typeguard.importhook import install_import_hook - - -def pytest_addoption(parser): - group = parser.getgroup('typeguard') - group.addoption('--typeguard-packages', action='store', - help='comma separated name list of packages and modules to instrument for ' - 'type checking') - - -def pytest_configure(config): - value = config.getoption("typeguard_packages") - if not value: - return - - packages = [pkg.strip() for pkg in value.split(",")] - - already_imported_packages = sorted( - package for package in packages if package in sys.modules - ) - if already_imported_packages: - message = ( - "typeguard cannot check these packages because they " - "are already imported: {}" - ) - raise RuntimeError(message.format(", ".join(already_imported_packages))) - - install_import_hook(packages=packages) diff --git a/contrib/python/typeguard/ya.make b/contrib/python/typeguard/ya.make index 53dd3e96730..dffb4f60d5a 100644 --- a/contrib/python/typeguard/ya.make +++ b/contrib/python/typeguard/ya.make @@ -2,17 +2,30 @@ PY3_LIBRARY() -VERSION(2.13.3) +VERSION(4.4.4) LICENSE(MIT) +PEERDIR( + contrib/python/typing-extensions +) + NO_LINT() PY_SRCS( TOP_LEVEL typeguard/__init__.py - typeguard/importhook.py - typeguard/pytest_plugin.py + typeguard/_checkers.py + typeguard/_config.py + typeguard/_decorators.py + typeguard/_exceptions.py + typeguard/_functions.py + typeguard/_importhook.py + typeguard/_memo.py + typeguard/_pytest_plugin.py + typeguard/_suppression.py + typeguard/_transformer.py + typeguard/_utils.py ) RESOURCE_FILES( |
