diff --git a/.gitignore b/.gitignore
index 286b826..1aff8d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
.pydevproject
.idea
.tox
-.coverage
+.coverage*
.cache
.eggs/
*.egg-info/
@@ -11,3 +11,6 @@ __pycache__/
docs/_build/
dist/
build/
+.mypy_cache/
+.pytest_cache/
+.ruff_cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..02f80cf
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,37 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-case-conflict
+ - id: check-merge-conflict
+ - id: check-symlinks
+ - id: check-toml
+ - id: check-yaml
+ - id: debug-statements
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args: [ "--fix=lf" ]
+ - id: trailing-whitespace
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.7.2
+ hooks:
+ - id: ruff
+ args: [--fix, --show-fixes]
+ - id: ruff-format
+
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.13.0
+ hooks:
+ - id: mypy
+ additional_dependencies: [ "typing_extensions" ]
+ exclude: "^tests/"
+
+ - repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.10.0
+ hooks:
+ - id: rst-backticks
+ - id: rst-directive-colons
+ - id: rst-inline-touching-normal
+ci:
+ autoupdate_schedule: quarterly
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..48ac4e1
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,16 @@
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+sphinx:
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+python:
+ install:
+ - method: pip
+ path: .
+ extra_requirements: [doc]
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 47f53cb..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-language: python
-sudo: false
-
-stages:
- - name: test
- - name: deploy to pypi
- if: type = push AND tag =~ ^\d+\.\d+\.\d+
-
-jobs:
- fast_finish: true
- include:
- - env: TOXENV=flake8
-
- - env: TOXENV=pypy3
- cache: pip
- python: pypy3
-
- - env: TOXENV=py34
- python: "3.4"
- after_success: &after_success
- - pip install coveralls
- - coveralls
-
- - env: TOXENV=py35
- python: "3.5.0"
- after_success: *after_success
-
- - env: TOXENV=py35
- python: "3.5.2"
- after_success: *after_success
-
- - env: TOXENV=py36
- python: "3.6"
- after_success: *after_success
-
- - env: TOXENV=py37
- python: "3.7"
- dist: xenial
- sudo: required
- after_success: *after_success
-
- - stage: deploy to pypi
- install: skip
- script: skip
- deploy:
- provider: pypi
- user: agronholm
- password:
- secure: gRSVobMY46ku8LMU/CkbhoawxDKZK0bQge2jBZwMt6UNK8X/Cu/mYHS7pihWRqGWacURLp7WZGeUB9ouHfGVIqlc8KQvdS4IgTHgo/CyZVG6AytyRPj+by9tmmYGh58J6DBstTD8c3h6pVytV4f0GcPJh+Cqfgfa6TKLF9dstxZELl5U4W46Po1Rk6Jk0GmhA7qKUd6/Y9fNRPntuEABFNGca8zTDinYTBzhQ6FbbuXfaF4FQkx3EvPm72ruagNkjCBkKXeqSr80Zxl0pPK5imW9VxhumnCm+DwStZ1dISQhEoJzK3b9GIllcFFWF4vUkTlEv9T+yZMhVyrJ+BBqPfKq1eNOALyWSVBTwWmjTez1AD0nNC4s5HegvKf2PF8F7y3EGUm+TLyKN3gC3LX14MHDJz4GJXY7n9gPW8syXU3npc1+bmaf1yfnR1BxncJQmru8nlmrpjG86w9qBH4BBSlkpTF7M82vcFKQ2w9BGZwoQ9vvzduMGRXCwgONfor+UPaRVarLyc+0j6HLXsC+EI9JN1PcDWF7WTj+PizYERB+U9PpjgniAKffGvhUUxjJvVnD9f/6CIKq4qjlTLv8C7PMwx1MVbE+p1JTxq530rQ55RMXicJCvhkn9mM0ytz/JUrDryOGDqmKiWVp53F+yLJHsUy8sN/zT3s6n8tf/Zg=
- distributions: sdist bdist_wheel
- on:
- tags: true
-
-python: "3.4"
-
-install: pip install tox
-
-script: tox
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
deleted file mode 100644
index 6687c1d..0000000
--- a/CHANGELOG.rst
+++ /dev/null
@@ -1,121 +0,0 @@
-Version history
-===============
-
-This library adheres to `Semantic Versioning `_.
-
-**2.2.2** (2018-08-13)
-
-- Fixed false positive when checking a callable against the plain ``typing.Callable`` on Python 3.7
-
-**2.2.1** (2018-08-12)
-
-- Argument type annotations are no longer unioned with the types of their default values, except in
- the case of ``None`` as the default value (although PEP 484 still recommends against this)
-- Fixed some generic types (``typing.Collection`` among others) producing false negatives on
- Python 3.7
-- Shortened unnecessarily long tracebacks by raising a new ``TypeError`` based on the old one
-- Allowed type checking against arbitrary types by removing the requirement to supply a call memo
- to ``check_type()``
-- Fixed ``AttributeError`` when running with the pydev debugger extension installed
-- Fixed getting type names on ``typing.*`` on Python 3.7 (fix by Dale Jung)
-
-**2.2.0** (2018-07-08)
-
-- Fixed compatibility with Python 3.7
-- Removed support for Python 3.3
-- Added support for ``typing.NewType`` (contributed by reinhrst)
-
-**2.1.4** (2018-01-07)
-
-- Removed support for backports.typing, as it has been removed from PyPI
-- Fixed checking of the numeric tower (complex -> float -> int) according to PEP 484
-
-**2.1.3** (2017-03-13)
-
-- Fixed type checks against generic classes
-
-**2.1.2** (2017-03-12)
-
-- Fixed leak of function objects (should've used a ``WeakValueDictionary`` instead of
- ``WeakKeyDictionary``)
-- Fixed obscure failure of TypeChecker when it's unable to find the function object
-- Fixed parametrized ``Type`` not working with type variables
-- Fixed type checks against variable positional and keyword arguments
-
-**2.1.1** (2016-12-20)
-
-- Fixed formatting of README.rst so it renders properly on PyPI
-
-**2.1.0** (2016-12-17)
-
-- Added support for ``typings.Type`` (available in Python 3.5.2+)
-- Added a third, ``sys.setprofile()`` based type checking approach (``typeguard.TypeChecker``)
-- Changed certain type error messages to display "function" instead of the function's qualified
- name
-
-**2.0.2** (2016-12-17)
-
-- More Python 3.6 compatibility fixes (along with a broader test suite)
-
-**2.0.1** (2016-12-10)
-
-- Fixed additional Python 3.6 compatibility issues
-
-**2.0.0** (2016-12-10)
-
-- **BACKWARD INCOMPATIBLE** Dropped Python 3.2 support
-- Fixed incompatibility with Python 3.6
-- Use ``inspect.signature()`` in place of ``inspect.getfullargspec``
-- Added support for ``typing.NamedTuple``
-
-**1.2.3** (2016-09-13)
-
-- Fixed ``@typechecked`` skipping the check of return value type when the type annotation was
- ``None``
-
-**1.2.2** (2016-08-23)
-
-- Fixed checking of homogenous Tuple declarations (``Tuple[bool, ...]``)
-
-**1.2.1** (2016-06-29)
-
-- Use ``backports.typing`` when possible to get new features on older Pythons
-- Fixed incompatibility with Python 3.5.2
-
-**1.2.0** (2016-05-21)
-
-- Fixed argument counting when a class is checked against a Callable specification
-- Fixed argument counting when a functools.partial object is checked against a Callable
- specification
-- Added checks against mandatory keyword-only arguments when checking against a Callable
- specification
-
-**1.1.3** (2016-05-09)
-
-- Gracefully exit if ``check_type_arguments`` can't find a reference to the current function
-
-**1.1.2** (2016-05-08)
-
-- Fixed TypeError when checking a builtin function against a parametrized Callable
-
-**1.1.1** (2016-01-03)
-
-- Fixed improper argument counting with bound methods when typechecking callables
-
-**1.1.0** (2016-01-02)
-
-- Eliminated the need to pass a reference to the currently executing function to
- ``check_argument_types()``
-
-**1.0.2** (2016-01-02)
-
-- Fixed types of default argument values not being considered as valid for the argument
-
-**1.0.1** (2016-01-01)
-
-- Fixed type hints retrieval being done for the wrong callable in cases where the callable was
- wrapped with one or more decorators
-
-**1.0.0** (2015-12-28)
-
-- Initial release
diff --git a/README.rst b/README.rst
index 7c7242f..fe5896e 100644
--- a/README.rst
+++ b/README.rst
@@ -1,131 +1,46 @@
-.. image:: https://travis-ci.org/agronholm/typeguard.svg?branch=master
- :target: https://travis-ci.org/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 argument type annotations.
+This library provides run-time type checking for functions defined with
+`PEP 484 `_ 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.
-The ``typing`` module introduced in Python 3.5 (and available on PyPI for older versions of
-Python 3) is supported. See below for details.
+Two principal ways to do type checking are provided:
-There are three principal ways to use type checking, each with its pros and cons:
+#. The ``check_type`` function:
-#. calling ``check_argument_types()`` from within the function body:
+ * 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:
- * debugger friendly (except when running with the pydev debugger with the C extension installed)
- * cannot check the type of the return value
- * does not work reliably with dynamically defined type hints (e.g. in nested functions)
-#. decorating the function with ``@typechecked``:
+ * 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
- * 100% reliable at finding the function object to be checked (does not need to check the garbage
- collector)
- * can check the type of the return value
- * adds an extra frame to the call stack for every call to a decorated function
-#. using ``with TypeChecker('packagename'):``:
+Two options are provided for code instrumentation:
- * emits warnings instead of raising ``TypeError``
- * eliminates boilerplate
- * multiple TypeCheckers can be stacked/nested
- * noninvasive (only records type violations; does not raise exceptions)
- * does not work reliably with dynamically defined type hints (e.g. in nested functions)
- * may cause problems with badly behaving debuggers or profilers
+#. the ``@typechecked`` function:
-If a function is called with incompatible argument types or a ``@typechecked`` decorated function
-returns a value incompatible with the declared type, a descriptive ``TypeError`` exception is
-raised.
+ * can be applied to functions individually
+#. the import hook (``typeguard.install_import_hook()``):
-Type checks can be fairly expensive so it is recommended to run Python in "optimized" mode
-(``python -O`` or setting the ``PYTHONOPTIMIZE`` environment variable) when running code containing
-type checks in production. The optimized mode will disable the type checks, by virtue of removing
-all ``assert`` statements and setting the ``__debug__`` constant to ``False``.
+ * 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
-Using ``check_argument_types()``:
+See the documentation_ for further information.
-.. code-block:: python3
-
- from typeguard import check_argument_types
-
- def some_function(a: int, b: float, c: str, *args: str):
- assert check_argument_types()
- ...
-
-Using ``@typechecked``:
-
-.. code-block:: python3
-
- from typeguard import typechecked
-
- @typechecked
- def some_function(a: int, b: float, c: str, *args: str) -> bool:
- ...
-
-To enable type checks even in optimized mode:
-
-.. code-block:: python3
-
- @typechecked(always=True)
- def foo(a: str, b: int, c: Union[str, int]) -> bool:
- ...
-
-Using ``TypeChecker``:
-
-.. code-block:: python3
-
- from warnings import filterwarnings
-
- from typeguard import TypeChecker, TypeWarning
-
- # Display all TypeWarnings, not just the first one
- filterwarnings('always', category=TypeWarning)
-
- # Run your entire application inside this context block
- with TypeChecker(['mypackage', 'otherpackage']):
- mypackage.run_app()
-
- # Alternatively, manually start (and stop) the checker:
- checker = TypeChecker('mypackage')
- checker.start()
- mypackage.start_app()
-
-.. hint:: Some other things you can do with ``TypeChecker``:
-
- * display all warnings from the start with ``python -W always::typeguard.TypeWarning``
- * redirect them to logging using ``logging.captureWarnings()``
- * record warnings in your pytest test suite and fail test(s) if you get any
- (see the `pytest documentation `_ about that)
-
-To directly check a value against the specified type:
-
-.. code-block:: python3
-
- from typeguard import check_type
-
- check_type('variablename', [1234], List[int])
-
-
-The following types from the ``typing`` package have specialized support:
-
-============== ============================================================
-Type Notes
-============== ============================================================
-``Callable`` Argument count is checked but types are not (yet)
-``Dict`` Keys and values are typechecked
-``List`` Contents are typechecked
-``NamedTuple`` Field values are typechecked
-``Set`` Contents are typechecked
-``Tuple`` Contents are typechecked
-``Type``
-``TypeVar`` Constraints, bound types and co/contravariance are supported
- but custom generic types are not (due to type erasure)
-``Union``
-============== ============================================================
-
-
-Project links
--------------
-
-* `Change log `_
-* `Source repository `_
-* `Issue tracker `_
+.. _documentation: https://typeguard.readthedocs.io/en/latest/
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644
index 0000000..2c8afeb
--- /dev/null
+++ b/debian/.gitignore
@@ -0,0 +1 @@
+/files
diff --git a/debian/changelog b/debian/changelog
index 4564037..b313174 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,45 @@
+python-typeguard (4.4.1-1) unstable; urgency=medium
+
+ * Team upload.
+ * New upstream release.
+
+ -- Colin Watson Mon, 04 Nov 2024 12:55:28 +0000
+
+python-typeguard (4.4.0-1) unstable; urgency=medium
+
+ * Team upload.
+ * New upstream release.
+
+ -- Colin Watson Tue, 29 Oct 2024 13:04:44 +0000
+
+python-typeguard (4.3.0-1) unstable; urgency=medium
+
+ * Team upload.
+
+ [ Alexandre Detiste ]
+ * New upstream version (4.2.1).
+
+ [ Colin Watson ]
+ * New upstream version (4.3.0):
+ - Fixed test suite incompatibility with pytest 8.2 (closes: #1073433).
+ * Switch to autopkgtest-pkg-pybuild.
+ * Standards-Version: 4.7.0 (no changes required).
+
+ -- Colin Watson Tue, 30 Jul 2024 23:28:23 +0100
+
+python-typeguard (4.1.5-1) unstable; urgency=medium
+
+ * Team upload.
+ * New upstream version 4.1.5
+
+ [ Debian Janitor ]
+ * Bump debhelper from old 12 to 13.
+ * Set upstream metadata fields: Repository-Browse,
+ Bug-Database, Bug-Submit, Repository.
+ * Update standards version to 4.6.2, no changes needed.
+
+ -- Alexandre Detiste Tue, 02 Jan 2024 22:13:00 +0100
+
python-typeguard (2.2.2-2) unstable; urgency=medium
* debian/control
diff --git a/debian/control b/debian/control
index 2fb6e1e..06f779c 100644
--- a/debian/control
+++ b/debian/control
@@ -3,21 +3,26 @@ Section: python
Priority: optional
Maintainer: Debian Python Team
Uploaders: Joel Cross
-Build-Depends: debhelper (>= 12),
- debhelper-compat (= 12),
- dh-python,
- python3-all,
- python3-setuptools,
- python3-setuptools-scm
-Standards-Version: 4.3.0
+Build-Depends:
+ debhelper-compat (= 13),
+ dh-sequence-python3,
+ python3-all,
+ pybuild-plugin-pyproject,
+ python3-pytest,
+ mypy,
+ python3-setuptools,
+ python3-setuptools-scm,
+ python3-typing-extensions (>= 4.10.0),
+Rules-Requires-Root: no
+Standards-Version: 4.7.0
Homepage: https://github.com/agronholm/typeguard
Vcs-Git: https://salsa.debian.org/python-team/packages/python-typeguard.git
Vcs-Browser: https://salsa.debian.org/python-team/packages/python-typeguard
-Testsuite: autopkgtest-pkg-python
+Testsuite: autopkgtest-pkg-pybuild
Package: python3-typeguard
Architecture: all
-Depends: ${python3:Depends}, ${misc:Depends}
+Depends: ${misc:Depends}, ${python3:Depends}
Description: Run-time type checker for Python
This library provides run-time type checking for functions defined with
argument type annotations. This can be done in one of three ways:
diff --git a/debian/rules b/debian/rules
index 59551c2..0c7996b 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,44 +1,11 @@
#!/usr/bin/make -f
-# See debhelper(7) (uncomment to enable)
-# output every command that modifies files on the build system.
+
#export DH_VERBOSE = 1
export PYBUILD_NAME=typeguard
%:
- dh $@ --with python3 --buildsystem=pybuild
-
-# The following workaround is necessary because the author's name
-# contains a special character on "setup.cfg". This triggers the
-# following error:
-#
-# dh clean --with python3 --buildsystem=pybuild
-# dh_auto_clean -O--buildsystem=pybuild
-# I: pybuild base:217: python3.6 setup.py clean
-# Traceback (most recent call last):
-# File "setup.py", line 9, in
-# 'setuptools_scm >= 1.7.0'
-# File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 142, in setup
-# _install_setup_requires(attrs)
-# File "/usr/lib/python3/dist-packages/setuptools/__init__.py", line 135, in _install_setup_requires
-# dist.parse_config_files(ignore_option_errors=True)
-# File "/usr/lib/python3/dist-packages/setuptools/dist.py", line 564, in parse_config_files
-# _Distribution.parse_config_files(self, filenames=filenames)
-# File "/usr/lib/python3.6/distutils/dist.py", line 395, in parse_config_files
-# parser.read(filename)
-# File "/usr/lib/python3.6/configparser.py", line 697, in read
-# self._read(fp, filename)
-# File "/usr/lib/python3.6/configparser.py", line 1015, in _read
-# for lineno, line in enumerate(fp, start=1):
-# File "/usr/lib/python3.6/encodings/ascii.py", line 26, in decode
-# return codecs.ascii_decode(input, self.errors)[0]
-# UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 127: ordinal not in range(128)
-# E: pybuild pybuild:338: clean: plugin distutils failed with: exit code=1: python3.6 setup.py clean
-# dh_auto_clean: pybuild --clean -i python{version} -p "3.6 3.7" returned exit code 13
-# make: *** [debian/rules:9: clean] Error 25
-# gbp:error: '/usr/bin/git-pbuilder' failed: it exited with 2
-override_dh_auto_clean:
- LC_ALL=C.UTF-8 dh_auto_clean
+ dh $@ --buildsystem=pybuild
override_dh_installchangelogs:
- dh_installchangelogs CHANGELOG.rst
+ dh_installchangelogs docs/versionhistory.rst
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
new file mode 100644
index 0000000..a369d4f
--- /dev/null
+++ b/debian/upstream/metadata
@@ -0,0 +1,5 @@
+---
+Bug-Database: https://github.com/agronholm/typeguard/issues
+Bug-Submit: https://github.com/agronholm/typeguard/issues/new
+Repository: https://github.com/agronholm/typeguard.git
+Repository-Browse: https://github.com/agronholm/typeguard
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..d449ff7
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,77 @@
+API reference
+=============
+
+.. module:: typeguard
+
+Type checking
+-------------
+
+.. autofunction:: check_type
+
+.. autodecorator:: typechecked
+
+Import hook
+-----------
+
+.. autofunction:: install_import_hook
+
+.. autoclass:: TypeguardFinder
+ :members:
+
+.. autoclass:: ImportHookManager
+ :members:
+
+Configuration
+-------------
+
+.. data:: config
+ :type: TypeCheckConfiguration
+
+ The global configuration object.
+
+ Used by :func:`@typechecked <.typechecked>` and :func:`.install_import_hook`, and
+ notably **not used** by :func:`.check_type`.
+
+.. autoclass:: TypeCheckConfiguration
+ :members:
+
+.. autoclass:: CollectionCheckStrategy
+
+.. autoclass:: Unset
+
+.. autoclass:: ForwardRefPolicy
+
+.. autofunction:: warn_on_error
+
+Custom checkers
+---------------
+
+.. autofunction:: check_type_internal
+
+.. autofunction:: load_plugins
+
+.. data:: checker_lookup_functions
+ :type: list[Callable[[Any, Tuple[Any, ...], Tuple[Any, ...]], Optional[Callable[[Any, Any, Tuple[Any, ...], TypeCheckMemo], Any]]]]
+
+ A list of callables that are used to look up a checker callable for an annotation.
+
+.. autoclass:: TypeCheckMemo
+ :members:
+
+Type check suppression
+----------------------
+
+.. autodecorator:: typeguard_ignore
+
+.. autofunction:: suppress_type_checks
+
+Exceptions and warnings
+-----------------------
+
+.. autoexception:: InstrumentationWarning
+
+.. autoexception:: TypeCheckError
+
+.. autoexception:: TypeCheckWarning
+
+.. autoexception:: TypeHintWarning
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..59324be
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+from importlib.metadata import version as get_version
+
+from packaging.version import parse
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx_autodoc_typehints",
+]
+
+templates_path = ["_templates"]
+source_suffix = ".rst"
+master_doc = "index"
+project = "Typeguard"
+author = "Alex Grönholm"
+copyright = "2015, " + author
+
+v = parse(get_version("typeguard"))
+version = v.base_version
+release = v.public
+
+language = "en"
+
+exclude_patterns = ["_build"]
+pygments_style = "sphinx"
+autodoc_default_options = {"members": True}
+autodoc_type_aliases = {
+ "TypeCheckerCallable": "typeguard.TypeCheckerCallable",
+ "TypeCheckFailCallback": "typeguard.TypeCheckFailCallback",
+ "TypeCheckLookupCallback": "typeguard.TypeCheckLookupCallback",
+}
+todo_include_todos = False
+
+html_theme = "sphinx_rtd_theme"
+htmlhelp_basename = "typeguarddoc"
+
+intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..9140189
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1,86 @@
+Contributing to Typeguard
+=========================
+
+.. highlight:: bash
+
+If you wish to contribute a fix or feature to Typeguard, please follow the following
+guidelines.
+
+When you make a pull request against the main Typeguard codebase, Github runs the test
+suite against your modified code. Before making a pull request, you should ensure that
+the modified code passes tests and code quality checks locally.
+
+Running the test suite
+----------------------
+
+You can run the test suite two ways: either with tox_, or by running pytest_ directly.
+
+To run tox_ against all supported (of those present on your system) Python versions::
+
+ tox
+
+Tox will handle the installation of dependencies in separate virtual environments.
+
+To pass arguments to the underlying pytest_ command, you can add them after ``--``, like
+this::
+
+ tox -- -k somekeyword
+
+To use pytest directly, you can set up a virtual environment and install the project in
+development mode along with its test dependencies (virtualenv activation demonstrated
+for Linux and macOS; on Windows you need ``venv\Scripts\activate`` instead)::
+
+ python -m venv venv
+ source venv/bin/activate
+ pip install -e .[test]
+
+Now you can just run pytest_::
+
+ pytest
+
+Building the documentation
+--------------------------
+
+To build the documentation, run ``tox -e docs``. This will place the documentation in
+``build/sphinx/html`` where you can open ``index.html`` to view the formatted
+documentation.
+
+Typeguard uses ReadTheDocs_ to automatically build the documentation so the above
+procedure is only necessary if you are modifying the documentation and wish to check the
+results before committing.
+
+Typeguard uses pre-commit_ to perform several code style/quality checks. It is
+recommended to activate pre-commit_ on your local clone of the repository (using
+``pre-commit install``) to ensure that your changes will pass the same checks on GitHub.
+
+Making a pull request on Github
+-------------------------------
+
+To get your changes merged to the main codebase, you need a Github account.
+
+#. Fork the repository (if you don't have your own fork of it yet) by navigating to the
+ `main Typeguard repository`_ and clicking on "Fork" near the top right corner.
+#. Clone the forked repository to your local machine with
+ ``git clone git@github.com/yourusername/typeguard``.
+#. Create a branch for your pull request, like ``git checkout -b myfixname``
+#. Make the desired changes to the code base.
+#. Commit your changes locally. If your changes close an existing issue, add the text
+ ``Fixes #XXX.`` or ``Closes #XXX.`` to the commit message (where XXX is the issue
+ number).
+#. Push the changeset(s) to your forked repository (``git push``)
+#. Navigate to Pull requests page on the original repository (not your fork) and click
+ "New pull request"
+#. Click on the text "compare across forks".
+#. Select your own fork as the head repository and then select the correct branch name.
+#. Click on "Create pull request".
+
+If you have trouble, consult the `pull request making guide`_ on opensource.com.
+
+.. _Docker: https://docs.docker.com/desktop/#download-and-install
+.. _docker compose: https://docs.docker.com/compose/
+.. _tox: https://tox.readthedocs.io/en/latest/install.html
+.. _pre-commit: https://pre-commit.com/#installation
+.. _pytest: https://pypi.org/project/pytest/
+.. _ReadTheDocs: https://readthedocs.org/
+.. _main Typeguard repository: https://github.com/agronholm/typeguard
+.. _pull request making guide: https://opensource.com/article/19/7/create-pull-request-github
diff --git a/docs/extending.rst b/docs/extending.rst
new file mode 100644
index 0000000..4b9f888
--- /dev/null
+++ b/docs/extending.rst
@@ -0,0 +1,113 @@
+Extending Typeguard
+===================
+
+.. py:currentmodule:: typeguard
+
+Adding new type checkers
+------------------------
+
+The range of types supported by Typeguard can be extended by writing a
+**type checker lookup function** and one or more **type checker functions**. The former
+will return one of the latter, or ``None`` if the given value does not match any of your
+custom type checker functions.
+
+The lookup function receives three arguments:
+
+#. The origin type (the annotation with any arguments stripped from it)
+#. The previously stripped out generic arguments, if any
+#. Extra arguments from the :class:`~typing.Annotated` annotation, if any
+
+For example, if the annotation was ``tuple``,, the lookup function would be called with
+``tuple, (), ()``. If the type was parametrized, like ``tuple[str, int]``, it would be
+called with ``tuple, (str, int), ()``. If the annotation was
+``Annotated[tuple[str, int], "foo", "bar"]``, the arguments would instead be
+``tuple, (str, int), ("foo", "bar")``.
+
+The checker function receives four arguments:
+
+#. The value to be type checked
+#. The origin type
+#. The generic arguments from the annotation (empty tuple when the annotation was not
+ parametrized)
+#. The memo object (:class:`~.TypeCheckMemo`)
+
+There are a couple of things to take into account when writing a type checker:
+
+#. If your type checker function needs to do further type checks (such as type checking
+ items in a collection), you need to use :func:`~.check_type_internal` (and pass
+ along ``memo`` to it)
+#. If you're type checking collections, your checker function should respect the
+ :attr:`~.TypeCheckConfiguration.collection_check_strategy` setting, available from
+ :attr:`~.TypeCheckMemo.config`
+
+.. versionchanged:: 4.0
+ In Typeguard 4.0, checker functions **must** respect the settings in
+ ``memo.config``, rather than the global configuration
+
+The following example contains a lookup function and type checker for a custom class
+(``MySpecialType``)::
+
+ from __future__ import annotations
+ from inspect import isclass
+ from typing import Any
+
+ from typeguard import TypeCheckError, TypeCheckerCallable, TypeCheckMemo
+
+
+ class MySpecialType:
+ pass
+
+
+ def check_my_special_type(
+ value: Any, origin_type: Any, args: tuple[Any, ...], memo: TypeCheckMemo
+ ) -> None:
+ if not isinstance(value, MySpecialType):
+ raise TypeCheckError('is not my special type')
+
+
+ def my_checker_lookup(
+ origin_type: Any, args: tuple[Any, ...], extras: tuple[Any, ...]
+ ) -> TypeCheckerCallable | None:
+ if isclass(origin_type) and issubclass(origin_type, MySpecialType):
+ return check_my_special_type
+
+ return None
+
+Registering your type checker lookup function with Typeguard
+------------------------------------------------------------
+
+Just writing a type checker lookup function doesn't do anything by itself. You'll have
+to advertise your type checker lookup function to Typeguard somehow. There are two ways
+to do that (pick just one):
+
+#. Append to :data:`typeguard.checker_lookup_functions`
+#. Add an `entry point`_ to your project in the ``typeguard.checker_lookup`` group
+
+If you're packaging your project with standard packaging tools, it may be better to add
+an entry point instead of registering it manually, because manual registration requires
+the registration code to run first before the lookup function can work.
+
+To manually register the type checker lookup function with Typeguard::
+
+ from typeguard import checker_lookup_functions
+
+ checker_lookup_functions.append(my_checker_lookup)
+
+For adding entry points to your project packaging metadata, the exact method may vary
+depending on your packaging tool of choice, but the standard way (supported at least by
+recent versions of ``setuptools``) is to add this to ``pyproject.toml``:
+
+.. code-block:: toml
+
+ [project.entry-points]
+ typeguard.checker_lookup = {myplugin = "myapp.my_plugin_module:my_checker_lookup"}
+
+The configuration above assumes that the **globally unique** (within the
+``typeguard.checker_lookup`` namespace) entry point name for your lookup function is
+``myplugin``, it lives in the ``myapp.my_plugin_module`` and the name of the function
+there is ``my_checker_lookup``.
+
+.. note:: After modifying your project configuration, you may have to reinstall it in
+ order for the entry point to become discoverable.
+
+.. _entry point: https://docs.python.org/3/library/importlib.metadata.html#entry-points
diff --git a/docs/features.rst b/docs/features.rst
new file mode 100644
index 0000000..3141456
--- /dev/null
+++ b/docs/features.rst
@@ -0,0 +1,225 @@
+Features
+=========
+
+.. py:currentmodule:: typeguard
+
+What does Typeguard check?
+--------------------------
+
+The following type checks are implemented in Typeguard:
+
+* Types of arguments passed to instrumented functions
+* Types of values returned from instrumented functions
+* Types of values yielded from instrumented generator functions
+* Types of values sent to instrumented generator functions
+* Types of values assigned to local variables within instrumented functions
+
+What does Typeguard NOT check?
+------------------------------
+
+The following type checks are not yet supported in Typeguard:
+
+* Types of values assigned to class or instance variables
+* Types of values assigned to global or nonlocal variables
+* Stubs defined with :func:`@overload ` (the implementation is checked
+ if instrumented)
+* ``yield from`` statements in generator functions
+* ``ParamSpec`` and ``Concatenate`` are currently ignored
+* Types where they are shadowed by arguments with the same name (e.g.
+ ``def foo(x: type, type: str): ...``)
+
+Other limitations
+-----------------
+
+Local references to nested classes
+++++++++++++++++++++++++++++++++++
+
+Forward references from methods pointing to non-local nested classes cannot currently be
+resolved::
+
+ class Outer:
+ class Inner:
+ pass
+
+ # Cannot be resolved as the name is no longer available
+ def method(self) -> "Inner":
+ return Outer.Inner()
+
+This shortcoming may be resolved in a future release.
+
+Using :func:`@typechecked ` on top of other decorators
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+As :func:`@typechecked ` works by recompiling the target function with
+instrumentation added, it needs to replace all the references to the original function
+with the new one. This could be impossible when it's placed on top of another decorator
+that wraps the original function. It has no way of telling that other decorator that the
+target function should be switched to a new one. To work around this limitation, either
+place :func:`@typechecked ` at the bottom of the decorator stack, or use
+the import hook instead.
+
+Protocol checking
++++++++++++++++++
+
+As of version 4.3.0, Typeguard can check instances and classes against Protocols,
+regardless of whether they were annotated with
+:func:`@runtime_checkable `.
+
+The only current limitation is that argument annotations are not checked for
+compatibility, however this should be covered by static type checkers pretty well.
+
+Special considerations for ``if TYPE_CHECKING:``
+------------------------------------------------
+
+Both the import hook and :func:`@typechecked ` avoid checking against
+anything imported in a module-level ``if TYPE_CHECKING:`` (or
+``if typing.TYPE_CHECKING:``) block, since those types will not be available at run
+time. Therefore, no errors or warnings are emitted for such annotations, even when they
+would normally not be found.
+
+Support for generator functions
+-------------------------------
+
+For generator functions, the checks applied depend on the function's return annotation.
+For example, the following function gets its yield, send and return values type
+checked::
+
+ from collections.abc import Generator
+
+ def my_generator() -> Generator[int, str, bool]:
+ a = yield 6
+ return True
+
+In contrast, the following generator function only gets its yield value checked::
+
+ from collections.abc import Iterator
+
+ def my_generator() -> Iterator[int]:
+ a = yield 6
+ return True
+
+Asynchronous generators work just the same way, except they don't support returning
+values other than ``None``, so the annotation only has two items::
+
+ from collections.abc import AsyncGenerator
+
+ async def my_generator() -> AsyncGenerator[int, str]:
+ a = yield 6
+
+Overall, the following type annotations will work for generator function type checking:
+
+* :class:`typing.Generator`
+* :class:`collections.abc.Generator`
+* :class:`typing.Iterator`
+* :class:`collections.abc.Iterator`
+* :class:`typing.Iterable`
+* :class:`collections.abc.Iterable`
+* :class:`typing.AsyncIterator`
+* :class:`collections.abc.AsyncIterator`
+* :class:`typing.AsyncIterable`
+* :class:`collections.abc.AsyncIterable`
+* :class:`typing.AsyncGenerator`
+* :class:`collections.abc.AsyncGenerator`
+
+Support for PEP 604 unions on Pythons older than 3.10
+-----------------------------------------------------
+
+The :pep:`604` ``X | Y`` notation was introduced in Python 3.10, but it can be used with
+older Python versions in modules where ``from __future__ import annotations`` is
+present. Typeguard contains a special parser that lets it convert these to older
+:class:`~typing.Union` annotations internally.
+
+Support for generic built-in collection types on Pythons older than 3.9
+-----------------------------------------------------------------------
+
+The built-in collection types (:class:`list`, :class:`tuple`, :class:`dict`,
+:class:`set` and :class:`frozenset`) gained support for generics in Python 3.9.
+For earlier Python versions, Typeguard provides a way to work with such annotations by
+substituting them with the equivalent :mod:`typing` types. The only requirement for this
+to work is the use of ``from __future__ import annotations`` in all such modules.
+
+Support for mock objects
+------------------------
+
+Typeguard handles the :class:`unittest.mock.Mock` class (and its subclasses) specially,
+bypassing any type checks when encountering instances of these classes. Note that any
+"spec" class passed to the mock object is currently not respected.
+
+Supported standard library annotations
+--------------------------------------
+
+The following types from the standard library have specialized support:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Type(s)
+ - Notes
+ * - :class:`typing.Any`
+ - Any type passes type checks against this annotation. Inheriting from ``Any``
+ (:class:`typing.Any` on Python 3.11+, or ``typing.extensions.Any``) will pass any
+ type check
+ * - :class:`typing.Annotated`
+ - Original annotation is unwrapped and typechecked normally
+ * - :class:`BinaryIO`
+ - Specialized instance checks are performed
+ * - | :class:`typing.Callable`
+ | :class:`collections.abc.Callable`
+ - Argument count is checked but types are not (yet)
+ * - | :class:`dict`
+ | :class:`typing.Dict`
+ - Keys and values are typechecked
+ * - :class:`typing.IO`
+ - Specialized instance checks are performed
+ * - | :class:`list`
+ | :class:`typing.List`
+ - Contents are typechecked
+ * - :class:`typing.Literal`
+ -
+ * - :class:`typing.LiteralString`
+ - Checked as :class:`str`
+ * - | :class:`typing.Mapping`
+ | :class:`typing.MutableMapping`
+ | :class:`collections.abc.Mapping`
+ | :class:`collections.abc.MutableMapping`
+ - Keys and values are typechecked
+ * - :class:`typing.NamedTuple`
+ - Field values are typechecked
+ * - | :class:`typing.Never`
+ | :class:`typing.NoReturn`
+ - Supported in argument and return type annotations
+ * - :class:`typing.Protocol`
+ - Run-time protocols are checked with :func:`isinstance`, others are ignored
+ * - :class:`typing.Self`
+ -
+ * - | :class:`set`
+ | :class:`frozenset`
+ | :class:`typing.Set`
+ | :class:`typing.AbstractSet`
+ - Contents are typechecked
+ * - | :class:`typing.Sequence`
+ | :class:`collections.abc.Sequence`
+ - Contents are typechecked
+ * - :class:`typing.TextIO`
+ - Specialized instance checks are performed
+ * - | :class:`tuple`
+ | :class:`typing.Tuple`
+ - Contents are typechecked
+ * - | :class:`type`
+ | :class:`typing.Type`
+ -
+ * - :class:`typing.TypeGuard`
+ - Checked as :class:`bool`
+ * - :class:`typing.TypedDict`
+ - Contents are typechecked; On Python 3.8 and earlier, ``total`` from superclasses
+ is not respected (see `#101`_ for more information); On Python 3.9.0, false
+ positives can happen when constructing :class:`typing.TypedDict` classes using
+ old-style syntax (see `issue 42059`_)
+ * - :class:`typing.TypeVar`
+ - Constraints and bound types are typechecked
+ * - :class:`typing.Union`
+ - :pep:`604` unions are supported on all Python versions when
+ ``from __future__ import annotations`` is used
+
+.. _#101: https://github.com/agronholm/typeguard/issues/101
+.. _issue 42059: https://bugs.python.org/issue42059
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..e7a4e29
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,18 @@
+Typeguard
+=========
+
+.. include:: ../README.rst
+ :end-before: See the
+
+Quick links
+-----------
+
+.. toctree::
+ :maxdepth: 1
+
+ userguide
+ features
+ extending
+ contributing
+ api
+ versionhistory
diff --git a/docs/userguide.rst b/docs/userguide.rst
new file mode 100644
index 0000000..fa2dc16
--- /dev/null
+++ b/docs/userguide.rst
@@ -0,0 +1,280 @@
+User guide
+==========
+
+.. py:currentmodule:: typeguard
+
+Checking types directly
+-----------------------
+
+The most straightfoward way to do type checking with Typeguard is with
+:func:`.check_type`. It can be used as as a beefed-up version of :func:`isinstance` that
+also supports checking against annotations in the :mod:`typing` module::
+
+ from typeguard import check_type
+
+ # Raises TypeCheckError if there's a problem
+ check_type([1234], List[int])
+
+It's also useful for safely casting the types of objects dynamically constructed from
+external sources::
+
+ import json
+ from typing import List, TypedDict
+
+ from typeguard import check_type
+
+ # Example contents of "people.json":
+ # [
+ # {"name": "John Smith", "phone": "111-123123", "address": "123 Main Street"},
+ # {"name": "Jane Smith", "phone": "111-456456", "address": "123 Main Street"}
+ # ]
+
+ class Person(TypedDict):
+ name: str
+ phone: str
+ address: str
+
+ with open("people.json") as f:
+ people = check_type(json.load(f), List[Person])
+
+With this code, static type checkers will recognize the type of ``people`` to be
+``List[Person]``.
+
+Using the decorator
+-------------------
+
+The :func:`@typechecked ` decorator is the simplest way to add type
+checking on a case-by-case basis. It can be used on functions directly, or on entire
+classes, in which case all the contained methods are instrumented::
+
+ from typeguard import typechecked
+
+ @typechecked
+ def some_function(a: int, b: float, c: str, *args: str) -> bool:
+ ...
+ return retval
+
+ @typechecked
+ class SomeClass:
+ # All type annotated methods (including static and class methods and properties)
+ # are type checked.
+ # Does not apply to inner classes!
+ def method(x: int) -> int:
+ ...
+
+The decorator instruments functions by fetching the source code, parsing it to an
+abstract syntax tree using :func:`ast.parse`, modifying it to add type checking, and
+finally compiling the modified AST into byte code. This code is then used to make a new
+function object that is used to replace the original one.
+
+To explicitly set type checking options on a per-function basis, you can pass them as
+keyword arguments to :func:`@typechecked `::
+
+ from typeguard import CollectionCheckStrategy, typechecked
+
+ @typechecked(collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS)
+ def some_function(a: int, b: float, c: str, *args: str) -> bool:
+ ...
+ return retval
+
+This also allows you to override the global options for specific functions when using
+the import hook.
+
+.. note:: You should always place this decorator closest to the original function,
+ as it will not work when there is another decorator wrapping the function.
+ For the same reason, when you use it on a class that has wrapping decorators on
+ its methods, such methods will not be instrumented. In contrast, the import hook
+ has no such restrictions.
+
+Using the import hook
+---------------------
+
+The import hook, when active, automatically instruments all type annotated functions to
+type check arguments, return values and values yielded by or sent to generator
+functions. This allows for a non-invasive method of run time type checking. This method
+does not modify the source code on disk, but instead modifies its AST (Abstract Syntax
+Tree) when the module is loaded.
+
+Using the import hook is as straightforward as installing it before you import any
+modules you wish to be type checked. Give it the name of your top level package (or a
+list of package names)::
+
+ from typeguard import install_import_hook
+
+ install_import_hook('myapp')
+ from myapp import some_module # import only AFTER installing the hook, or it won't take effect
+
+If you wish, you can uninstall the import hook::
+
+ manager = install_import_hook('myapp')
+ from myapp import some_module
+ manager.uninstall()
+
+or using the context manager approach::
+
+ with install_import_hook('myapp'):
+ from myapp import some_module
+
+You can also customize the logic used to select which modules to instrument::
+
+ from typeguard import TypeguardFinder, install_import_hook
+
+ class CustomFinder(TypeguardFinder):
+ def should_instrument(self, module_name: str):
+ # disregard the module names list and instrument all loaded modules
+ return True
+
+ install_import_hook('', cls=CustomFinder)
+
+.. _forwardrefs:
+
+Notes on forward reference handling
+-----------------------------------
+
+The internal type checking functions, injected to instrumented code by either
+:func:`@typechecked ` or the import hook, use the "naked" versions of any
+annotations, undoing any quotations in them (and the effects of
+``from __future__ import annotations``). As such, in instrumented code, the
+:attr:`~.TypeCheckConfiguration.forward_ref_policy` only applies when using type
+variables containing forward references, or type aliases likewise containing forward
+references.
+
+To facilitate the use of types only available to static type checkers, Typeguard
+recognizes module-level imports guarded by ``if typing.TYPE_CHECKING:`` or
+``if TYPE_CHECKING:`` (add the appropriate :mod:`typing` imports). Imports made within
+such blocks on the module level will be replaced in calls to internal type checking
+functions with :data:`~typing.Any`.
+
+Using the pytest plugin
+-----------------------
+
+Typeguard comes with a plugin for pytest (v7.0 or newer) that installs the import hook
+(explained in the previous section). To use it, run ``pytest`` with the appropriate
+``--typeguard-packages`` option. For example, if you wanted to instrument the
+``foo.bar`` and ``xyz`` packages for type checking, you can do the following:
+
+.. code-block:: bash
+
+ pytest --typeguard-packages=foo.bar,xyz
+
+It is also possible to set option for the pytest plugin using pytest's own
+configuration. For example, here's how you might specify several options in
+``pyproject.toml``:
+
+.. code-block:: toml
+
+ [tool.pytest.ini_options]
+ typeguard-packages = """
+ foo.bar
+ xyz"""
+ typeguard-debug-instrumentation = true
+ typeguard-typecheck-fail-callback = "mypackage:failcallback"
+ typeguard-forward-ref-policy = "ERROR"
+ typeguard-collection-check-strategy = "ALL_ITEMS"
+
+See the next section for details on how the individual options work.
+
+.. note:: There is currently no support for specifying a customized module finder.
+
+Setting configuration options
+-----------------------------
+
+There are several configuration options that can be set that influence how type checking
+is done. The :data:`typeguard.config` (which is of type
+:class:`~.TypeCheckConfiguration`) controls the options applied to code instrumented via
+either :func:`@typechecked <.typechecked>` or the import hook. The
+:func:`~.check_type`, function, however, uses the built-in defaults and is not affected
+by the global configuration, so you must pass any configuration overrides explicitly
+with each call.
+
+You can also override specific configuration options in instrumented functions (or
+entire classes) by passing keyword arguments to :func:`@typechecked <.typechecked>`.
+You can do this even if you're using the import hook, as the import hook will remove the
+decorator to ensure that no double instrumentation takes place. If you're using the
+import hook to type check your code only during tests and don't want to include
+``typeguard`` as a run-time dependency, you can use a dummy replacement for the
+decorator.
+
+For example, the following snippet will only import the decorator during a pytest_ run::
+
+ import sys
+
+ 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
+
+.. _pytest: https://docs.pytest.org/
+
+Suppressing type checks
+-----------------------
+
+Temporarily disabling type checks
++++++++++++++++++++++++++++++++++
+
+If you need to temporarily suppress type checking, you can use the
+:func:`~.suppress_type_checks` function, either as a context manager or a decorator, to
+skip the checks::
+
+ from typeguard import check_type, suppress_type_checks
+
+ with suppress_type_checks():
+ check_type(1, str) # would fail without the suppression
+
+ @suppress_type_checks
+ def my_suppressed_function(x: int) -> None:
+ ...
+
+Suppression state is tracked globally. Suppression ends only when all the context
+managers have exited and all calls to decorated functions have returned.
+
+Permanently suppressing type checks for selected functions
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+To exclude specific functions from run time type checking, you can use one of the
+following decorators:
+
+ * :func:`@typeguard_ignore `: prevents the decorated
+ function from being instrumentated by the import hook
+ * :func:`@no_type_check `: as above, but disables static type
+ checking too
+
+For example, calling the function defined below will not result in a type check error
+when the containing module is instrumented by the import hook::
+
+ from typeguard import typeguard_ignore
+
+ @typeguard_ignore
+ def f(x: int) -> int:
+ return str(x)
+
+.. warning:: The :func:`@no_type_check_decorator `
+ decorator is not currently recognized by Typeguard.
+
+Suppressing the ``@typechecked`` decorator in production
+--------------------------------------------------------
+
+If you're using the :func:`@typechecked ` decorator to gradually introduce
+run-time type checks to your code base, you can disable the checks in production by
+running Python in optimized mode (as opposed to debug mode which is the default mode).
+You can do this by either starting Python with the ``-O`` or ``-OO`` option, or by
+setting the PYTHONOPTIMIZE_ environment variable. This will cause
+:func:`@typechecked ` to become a no-op when the import hook is not being
+used to instrument the code.
+
+.. _PYTHONOPTIMIZE: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONOPTIMIZE
+
+Debugging instrumented code
+---------------------------
+
+If you find that your code behaves in an unexpected fashion with the Typeguard
+instrumentation in place, you should set the ``typeguard.config.debug_instrumentation``
+flag to ``True``. This will print all the instrumented code after the modifications,
+which you can check to find the reason for the unexpected behavior.
+
+If you're using the pytest plugin, you can also pass the
+``--typeguard-debug-instrumentation`` and ``-s`` flags together for the same effect.
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
new file mode 100644
index 0000000..45880a2
--- /dev/null
+++ b/docs/versionhistory.rst
@@ -0,0 +1,575 @@
+Version history
+===============
+
+This library adheres to
+`Semantic Versioning 2.0 `_.
+
+**4.4.1** (2024-11-03)
+
+- Dropped Python 3.8 support
+- Changed the signature of ``typeguard_ignore()`` to be compatible with
+ ``typing.no_type_check()`` (PR by @jolaf)
+- Avoid creating reference cycles when type checking uniontypes and classes
+- Fixed checking of variable assignments involving tuple unpacking
+ (`#486 `_)
+- Fixed ``TypeError`` when checking a class against ``type[Self]``
+ (`#481 `_)
+- Fixed checking of protocols on the class level (against ``type[SomeProtocol]``)
+ (`#498 `_)
+- Fixed ``Self`` checks in instance/class methods that have positional-only arguments
+- Fixed explicit checks of PEP 604 unions against ``types.UnionType``
+ (`#467 `_)
+- Fixed checks against annotations wrapped in ``NotRequired`` not being run unless the
+ ``NotRequired`` is a forward reference
+ (`#454 `_)
+- Fixed the ``pytest_ignore_collect`` hook in the pytest plugin blocking default pytest
+ collection ignoring behavior by returning ``None`` instead of ``False``
+ (PR by @mgorny)
+
+**4.4.0** (2024-10-27)
+
+- Added proper checking for method signatures in protocol checks
+ (`#465 `_)
+- Fixed basic support for intersection protocols
+ (`#490 `_; PR by @antonagestam)
+- Fixed protocol checks running against the class of an instance and not the instance
+ itself (this produced wrong results for non-method member checks)
+
+**4.3.0** (2024-05-27)
+
+- Added support for checking against static protocols
+- Fixed some compatibility problems when running on Python 3.13
+ (`#460 `_; PR by @JelleZijlstra)
+- Fixed test suite incompatibility with pytest 8.2
+ (`#461 `_)
+- Fixed pytest plugin crashing on pytest version older than v7.0.0 (even if it's just
+ present) (`#343 `_)
+
+**4.2.1** (2023-03-24)
+
+- Fixed missing ``typing_extensions`` dependency for Python 3.12
+ (`#444 `_)
+- Fixed deprecation warning in the test suite on Python 3.13
+ (`#444 `_)
+
+**4.2.0** (2023-03-23)
+
+- Added support for specifying options for the pytest plugin via pytest config files
+ (`#440 `_)
+- Avoid creating reference cycles when type checking unions (PR by Shantanu)
+- Fixed ``Optional[...]`` being removed from the AST if it was located within a
+ subscript (`#442 `_)
+- Fixed ``TypedDict`` from ``typing_extensions`` not being recognized as one
+ (`#443 `_)
+- Fixed ``typing`` types (``dict[str, int]``, ``List[str]``, etc.) not passing checks
+ against ``type`` or ``Type``
+ (`#432 `_, PR by Yongxin Wang)
+- Fixed detection of optional fields (``NotRequired[...]``) in ``TypedDict`` when using
+ forward references (`#424 `_)
+- Fixed mapping checks against Django's ``MultiValueDict``
+ (`#419 `_)
+
+**4.1.5** (2023-09-11)
+
+- Fixed ``Callable`` erroneously rejecting a callable that has the requested amount of
+ positional arguments but they have defaults
+ (`#400 `_)
+- Fixed a regression introduced in v4.1.4 where the elements of ``Literal`` got quotes
+ removed from them by the AST transformer
+ (`#399 `_)
+
+**4.1.4** (2023-09-10)
+
+- Fixed ``AttributeError`` where the transformer removed elements from a PEP 604 union
+ (`#384 `_)
+- Fixed ``AttributeError: 'Subscript' object has no attribute 'slice'`` when
+ encountering an annotation with a subscript containing an ignored type (imported
+ within an ``if TYPE_CHECKING:`` block)
+ (`#397 `_)
+- Fixed type checking not being skipped when the target is a union (PEP 604 or
+ ``typing.Union``) where one of the elements is an ignored type (shadowed by an
+ argument, variable assignment or an ``if TYPE_CHECKING`` import)
+ (`#394 `_,
+ `#395 `_)
+- Fixed type checking of class instances created in ``__new__()`` in cases such as enums
+ where this method is already invoked before the class has finished initializing
+ (`#398 `_)
+
+**4.1.3** (2023-08-27)
+
+- Dropped Python 3.7 support
+- Fixed ``@typechecked`` optimization causing compilation of instrumented code to fail
+ when any block was left empty by the AST transformer (eg ``if`` or
+ ``try`` / ``except`` blocks)
+ (`#352 `_)
+- Fixed placement of injected typeguard imports with respect to ``__future__`` imports
+ and module docstrings (`#385 `_)
+
+**4.1.2** (2023-08-18)
+
+- Fixed ``Any`` being removed from a subscript that still contains other elements
+ (`#373 `_)
+
+**4.1.1** (2023-08-16)
+
+- Fixed ``suppress_type_checks()`` causing annotated variable assignments to always
+ assign ``None`` (`#380 `_)
+
+**4.1.0** (2023-07-30)
+
+- Added support for passing a tuple as ``expected_type`` to ``check_type()``, making it
+ more of a drop-in replacement for ``isinstance()``
+ (`#371 `_)
+- Fixed regression where ``Literal`` inside a ``Union`` had quotes stripped from its
+ contents, thus typically causing ``NameError`` to be raised when run
+ (`#372 `_)
+
+**4.0.1** (2023-07-27)
+
+- Fixed handling of ``typing_extensions.Literal`` on Python 3.8 and 3.9 when
+ ``typing_extensions>=4.6.0`` is installed
+ (`#363 `_; PR by Alex Waygood)
+- Fixed ``NameError`` when generated type checking code references an imported name from
+ a method (`#362 `_)
+- Fixed docstrings disappearing from instrumented functions
+ (`#359 `_)
+- Fixed ``@typechecked`` failing to instrument functions when there are more than one
+ function within the same scope
+ (`#355 `_)
+- Fixed ``frozenset`` not being checked
+ (`#367 `_)
+
+**4.0.0** (2023-05-12)
+
+- No changes
+
+**4.0.0rc6** (2023-05-07)
+
+- Fixed ``@typechecked`` optimization causing compilation of instrumented code to fail
+ when an ``if`` block was left empty by the AST transformer
+ (`#352 `_)
+- Fixed the AST transformer trying to parse the second argument of ``typing.Annotated``
+ as a forward reference (`#353 `_)
+
+**4.0.0rc5** (2023-05-01)
+
+- Added ``InstrumentationWarning`` to the public API
+- Changed ``@typechecked`` to skip instrumentation in optimized mode, as in typeguard
+ 2.x
+- Avoid type checks where the types in question are shadowed by local variables
+- Fixed instrumentation using ``typing.Optional`` without a subscript when the subscript
+ value was erased due to being an ignored import
+- Fixed ``TypeError: isinstance() arg 2 must be a type or tuple of types`` when
+ instrumented code tries to check a value against a naked (``str``, not ``ForwardRef``)
+ forward reference
+- Fixed instrumentation using the wrong "self" type in the ``__new__()`` method
+
+**4.0.0rc4** (2023-04-15)
+
+- Fixed imports guarded by ``if TYPE_CHECKING:`` when used with subscripts
+ (``SomeType[...]``) being replaced with ``Any[...]`` instead of just ``Any``
+- Fixed instrumentation inadvertently mutating a function's annotations on Python 3.7
+ and 3.8
+- Fixed ``Concatenate[...]`` in ``Callable`` parameters causing ``TypeError`` to be
+ raised
+- Fixed type checks for ``*args`` or ``**kwargs`` not being suppressed when their types
+ are unusable (guarded by ``if TYPE_CHECKING:`` or otherwise)
+- Fixed ``TypeError`` when checking against a generic ``NewType``
+- Don't try to check types shadowed by argument names (e.g.
+ ``def foo(x: type, type: str): ...``)
+- Don't check against unions where one of the elements is ``Any``
+
+**4.0.0rc3** (2023-04-10)
+
+- Fixed ``typing.Literal`` subscript contents being evaluated as forward references
+- Fixed resolution of forward references in type aliases
+
+**4.0.0rc2** (2023-04-08)
+
+- The ``.pyc`` files now use a version-based optimization suffix in the file names so as
+ not to cause the interpreter to load potentially faulty/incompatible cached bytecode
+ generated by older versions
+- Fixed typed variable positional and keyword arguments causing compilation errors on
+ Python 3.7 and 3.8
+- Fixed compilation error when a type annotation contains a type guarded by
+ ``if TYPE_CHECKING:``
+
+**4.0.0rc1** (2023-04-02)
+
+- **BACKWARD INCOMPATIBLE** ``check_type()`` no longer uses the global configuration.
+ It now uses the default configuration values, unless overridden with an explicit
+ ``config`` argument.
+- **BACKWARD INCOMPATIBLE** Removed ``CallMemo`` from the API
+- **BACKWARD INCOMPATIBLE** Required checkers to use the configuration from
+ ``memo.config``, rather than the global configuration
+- Added keyword arguments to ``@typechecked``, allowing users to override settings on a
+ per-function basis
+- Added support for using ``suppress_type_checks()`` as a decorator
+- Added support for type checking against nonlocal classes defined within the same
+ parent function as the instrumented function
+- Changed instrumentation to statically copy the function annotations to avoid having to
+ look up the function object at run time
+- Improved support for avoiding type checks against imports declared in
+ ``if TYPE_CHECKING:`` blocks
+- Fixed ``check_type`` not returning the passed value when checking against ``Any``, or
+ when type checking is being suppressed
+- Fixed ``suppress_type_checks()`` not ending the suppression if the context block
+ raises an exception
+- Fixed checking non-dictionary objects against a ``TypedDict`` annotation
+ (PR by Tolker-KU)
+
+**3.0.2** (2023-03-22)
+
+- Improved warnings by ensuring that they target user code and not Typeguard internal
+ code
+- Fixed ``warn_on_error()`` not showing where the type violation actually occurred
+- Fixed local assignment to ``*args`` or ``**kwargs`` being type checked incorrectly
+- Fixed ``TypeError`` on ``check_type(..., None)``
+- Fixed unpacking assignment not working with a starred variable (``x, *y = ...``) in
+ the target tuple
+- Fixed variable multi-assignment (``a = b = c = ...``) being type checked incorrectly
+
+**3.0.1** (2023-03-16)
+
+- Improved the documentation
+- Fixed assignment unpacking (``a, b = ...``) being checked incorrectly
+- Fixed ``@typechecked`` attempting to instrument wrapper decorators such as
+ ``@contextmanager`` when applied to a class
+- Fixed ``py.typed`` missing from the wheel when not building from a git checkout
+
+**3.0.0** (2023-03-15)
+
+- **BACKWARD INCOMPATIBLE** Dropped the ``argname``, ``memo``, ``globals`` and
+ ``locals`` arguments from ``check_type()``
+- **BACKWARD INCOMPATIBLE** Removed the ``check_argument_types()`` and
+ ``check_return_type()`` functions (use ``@typechecked`` instead)
+- **BACKWARD INCOMPATIBLE** Moved ``install_import_hook`` to be directly importable
+ from the ``typeguard`` module
+- **BACKWARD INCOMPATIBLE** Changed the checking of collections (list, set, dict,
+ sequence, mapping) to only check the first item by default. To get the old behavior,
+ set ``typeguard.config.collection_check_strategy`` to
+ ``CollectionCheckStrategy.ALL_ITEMS``
+- **BACKWARD INCOMPATIBLE** Type checking failures now raise
+ ``typeguard.TypeCheckError`` instead of ``TypeError``
+- Dropped Python 3.5 and 3.6 support
+- Dropped the deprecated profiler hook (``TypeChecker``)
+- Added a configuration system
+- Added support for custom type checking functions
+- Added support for PEP 604 union types (``X | Y``) on all Python versions
+- Added support for generic built-in collection types (``list[int]`` et al) on all
+ Python versions
+- Added support for checking arbitrary ``Mapping`` types
+- Added support for the ``Self`` type
+- Added support for ``typing.Never`` (and ``typing_extensions.Never``)
+- Added support for ``Never`` and ``NoReturn`` in argument annotations
+- Added support for ``LiteralString``
+- Added support for ``TypeGuard``
+- Added support for the subclassable ``Any`` on Python 3.11 and ``typing_extensions``
+- Added the possibility to have the import hook instrument all packages
+- Added the ``suppress_type_checks()`` context manager function for temporarily
+ disabling type checks
+- Much improved error messages showing where the type check failed
+- Made it possible to apply ``@typechecked`` on top of ``@classmethod`` /
+ ``@staticmethod`` (PR by jacobpbrugh)
+- Changed ``check_type()`` to return the passed value, so it can be used (to an extent)
+ in place of ``typing.cast()``, but with run-time type checking
+- Replaced custom implementation of ``is_typeddict()`` with the implementation from
+ ``typing_extensions`` v4.1.0
+- Emit ``InstrumentationWarning`` instead of raising ``RuntimeError`` from the pytest
+ plugin if modules in the target package have already been imported
+- Fixed ``TypeError`` when checking against ``TypedDict`` when the value has mixed types
+ among the extra keys (PR by biolds)
+- Fixed incompatibility with ``typing_extensions`` v4.1+ on Python 3.10 (PR by David C.)
+- Fixed checking of ``Tuple[()]`` on Python 3.11 and ``tuple[()]`` on Python 3.9+
+- Fixed integers 0 and 1 passing for ``Literal[False]`` and ``Literal[True]``,
+ respectively
+- Fixed type checking of annotated variable positional and keyword arguments (``*args``
+ and ``**kwargs``)
+- Fixed checks against ``unittest.Mock`` and derivatives being done in the wrong place
+
+**2.13.3** (2021-12-10)
+
+- Fixed ``TypeError`` when using typeguard within ``exec()`` (where ``__module__`` is ``None``)
+ (PR by Andy Jones)
+- Fixed ``TypedDict`` causing ``TypeError: TypedDict does not support instance and class checks``
+ on Python 3.8 with standard library (not ``typing_extensions``) typed dicts
+
+**2.13.2** (2021-11-23)
+
+- Fixed ``typing_extensions`` being imported unconditionally on Python < 3.9
+ (bug introduced in 2.13.1)
+
+**2.13.1** (2021-11-23)
+
+- Fixed ``@typechecked`` replacing abstract properties with regular properties
+- Fixed any generic type subclassing ``Dict`` being mistakenly checked as ``TypedDict`` on
+ Python 3.10
+
+**2.13.0** (2021-10-11)
+
+- Added support for returning ``NotImplemented`` from binary magic methods (``__eq__()`` et al)
+- Added support for checking union types (e.g. ``Type[Union[X, Y]]``)
+- Fixed error message when a check against a ``Literal`` fails in a union on Python 3.10
+- Fixed ``NewType`` not being checked on Python 3.10
+- Fixed unwarranted warning when ``@typechecked`` is applied to a class that contains unannotated
+ properties
+- Fixed ``TypeError`` in the async generator wrapper due to changes in ``__aiter__()`` protocol
+- Fixed broken ``TypeVar`` checks – variance is now (correctly) disregarded, and only bound types
+ and constraints are checked against (but type variable resolution is not done)
+
+**2.12.1** (2021-06-04)
+
+- Fixed ``AttributeError`` when ``__code__`` is missing from the checked callable (PR by epenet)
+
+**2.12.0** (2021-04-01)
+
+- Added ``@typeguard_ignore`` decorator to exclude specific functions and classes from
+ runtime type checking (PR by Claudio Jolowicz)
+
+**2.11.1** (2021-02-16)
+
+- Fixed compatibility with Python 3.10
+
+**2.11.0** (2021-02-13)
+
+- Added support for type checking class properties (PR by Ethan Pronovost)
+- Fixed static type checking of ``@typechecked`` decorators (PR by Kenny Stauffer)
+- Fixed wrong error message when type check against a ``bytes`` declaration fails
+- Allowed ``memoryview`` objects to pass as ``bytes`` (like MyPy does)
+- Shortened tracebacks (PR by prescod)
+
+**2.10.0** (2020-10-17)
+
+- Added support for Python 3.9 (PR by Csergő Bálint)
+- Added support for nested ``Literal``
+- Added support for ``TypedDict`` inheritance (with some caveats; see the user guide on that for
+ details)
+- An appropriate ``TypeError`` is now raised when encountering an illegal ``Literal`` value
+- Fixed checking ``NoReturn`` on Python < 3.8 when ``typing_extensions`` was not installed
+- Fixed import hook matching unwanted modules (PR by Wouter Bolsterlee)
+- Install the pytest plugin earlier in the test run to support more use cases
+ (PR by Wouter Bolsterlee)
+
+**2.9.1** (2020-06-07)
+
+- Fixed ``ImportError`` on Python < 3.8 when ``typing_extensions`` was not installed
+
+**2.9.0** (2020-06-06)
+
+- Upped the minimum Python version from 3.5.2 to 3.5.3
+- Added support for ``typing.NoReturn``
+- Added full support for ``typing_extensions`` (now equivalent to support of the ``typing`` module)
+- Added the option of supplying ``check_type()`` with globals/locals for correct resolution of
+ forward references
+- Fixed erroneous ``TypeError`` when trying to check against non-runtime ``typing.Protocol``
+ (skips the check for now until a proper compatibility check has been implemented)
+- Fixed forward references in ``TypedDict`` not being resolved
+- Fixed checking against recursive types
+
+**2.8.0** (2020-06-02)
+
+- Added support for the ``Mock`` and ``MagicMock`` types (PR by prescod)
+- Added support for ``typing_extensions.Literal`` (PR by Ryan Rowe)
+- Fixed unintended wrapping of untyped generators (PR by prescod)
+- Fixed checking against bound type variables with ``check_type()`` without a call memo
+- Fixed error message when checking against a ``Union`` containing a ``Literal``
+
+**2.7.1** (2019-12-27)
+
+- Fixed ``@typechecked`` returning ``None`` when called with ``always=True`` and Python runs in
+ optimized mode
+- Fixed performance regression introduced in v2.7.0 (the ``getattr_static()`` call was causing a 3x
+ slowdown)
+
+**2.7.0** (2019-12-10)
+
+- Added support for ``typing.Protocol`` subclasses
+- Added support for ``typing.AbstractSet``
+- Fixed the handling of ``total=False`` in ``TypedDict``
+- Fixed no error reported on unknown keys with ``TypedDict``
+- Removed support of default values in ``TypedDict``, as they are not supported in the spec
+
+**2.6.1** (2019-11-17)
+
+- Fixed import errors when using the import hook and trying to import a module that has both a
+ module docstring and ``__future__`` imports in it
+- Fixed ``AttributeError`` when using ``@typechecked`` on a metaclass
+- Fixed ``@typechecked`` compatibility with built-in function wrappers
+- Fixed type checking generator wrappers not being recognized as generators
+- Fixed resolution of forward references in certain cases (inner classes, function-local classes)
+- Fixed ``AttributeError`` when a class has contains a variable that is an instance of a class
+ that has a ``__call__()`` method
+- Fixed class methods and static methods being wrapped incorrectly when ``@typechecked`` is applied
+ to the class
+- Fixed ``AttributeError`` when ``@typechecked`` is applied to a function that has been decorated
+ with a decorator that does not properly wrap the original (PR by Joel Beach)
+- Fixed collections with mixed value (or key) types raising ``TypeError`` on Python 3.7+ when
+ matched against unparametrized annotations from the ``typing`` module
+- Fixed inadvertent ``TypeError`` when checking against a type variable that has constraints or
+ a bound type expressed as a forward reference
+
+**2.6.0** (2019-11-06)
+
+- Added a :pep:`302` import hook for annotating functions and classes with ``@typechecked``
+- Added a pytest plugin that activates the import hook
+- Added support for ``typing.TypedDict``
+- Deprecated ``TypeChecker`` (will be removed in v3.0)
+
+**2.5.1** (2019-09-26)
+
+- Fixed incompatibility between annotated ``Iterable``, ``Iterator``, ``AsyncIterable`` or
+ ``AsyncIterator`` return types and generator/async generator functions
+- Fixed ``TypeError`` being wrapped inside another TypeError (PR by russok)
+
+**2.5.0** (2019-08-26)
+
+- Added yield type checking via ``TypeChecker`` for regular generators
+- Added yield, send and return type checking via ``@typechecked`` for regular and async generators
+- Silenced ``TypeChecker`` warnings about async generators
+- Fixed bogus ``TypeError`` on ``Type[Any]``
+- Fixed bogus ``TypeChecker`` warnings when an exception is raised from a type checked function
+- Accept a ``bytearray`` where ``bytes`` are expected, as per `python/typing#552`_
+- Added policies for dealing with unmatched forward references
+- Added support for using ``@typechecked`` as a class decorator
+- Added ``check_return_type()`` to accompany ``check_argument_types()``
+- Added Sphinx documentation
+
+.. _python/typing#552: https://github.com/python/typing/issues/552
+
+**2.4.1** (2019-07-15)
+
+- Fixed broken packaging configuration
+
+**2.4.0** (2019-07-14)
+
+- Added :pep:`561` support
+- Added support for empty tuples (``Tuple[()]``)
+- Added support for ``typing.Literal``
+- Make getting the caller frame faster (PR by Nick Sweeting)
+
+**2.3.1** (2019-04-12)
+
+- Fixed thread safety issue with the type hints cache (PR by Kelsey Francis)
+
+**2.3.0** (2019-03-27)
+
+- Added support for ``typing.IO`` and derivatives
+- Fixed return type checking for coroutine functions
+- Dropped support for Python 3.4
+
+**2.2.2** (2018-08-13)
+
+- Fixed false positive when checking a callable against the plain ``typing.Callable`` on Python 3.7
+
+**2.2.1** (2018-08-12)
+
+- Argument type annotations are no longer unioned with the types of their default values, except in
+ the case of ``None`` as the default value (although PEP 484 still recommends against this)
+- Fixed some generic types (``typing.Collection`` among others) producing false negatives on
+ Python 3.7
+- Shortened unnecessarily long tracebacks by raising a new ``TypeError`` based on the old one
+- Allowed type checking against arbitrary types by removing the requirement to supply a call memo
+ to ``check_type()``
+- Fixed ``AttributeError`` when running with the pydev debugger extension installed
+- Fixed getting type names on ``typing.*`` on Python 3.7 (fix by Dale Jung)
+
+**2.2.0** (2018-07-08)
+
+- Fixed compatibility with Python 3.7
+- Removed support for Python 3.3
+- Added support for ``typing.NewType`` (contributed by reinhrst)
+
+**2.1.4** (2018-01-07)
+
+- Removed support for backports.typing, as it has been removed from PyPI
+- Fixed checking of the numeric tower (complex -> float -> int) according to PEP 484
+
+**2.1.3** (2017-03-13)
+
+- Fixed type checks against generic classes
+
+**2.1.2** (2017-03-12)
+
+- Fixed leak of function objects (should've used a ``WeakValueDictionary`` instead of
+ ``WeakKeyDictionary``)
+- Fixed obscure failure of TypeChecker when it's unable to find the function object
+- Fixed parametrized ``Type`` not working with type variables
+- Fixed type checks against variable positional and keyword arguments
+
+**2.1.1** (2016-12-20)
+
+- Fixed formatting of README.rst so it renders properly on PyPI
+
+**2.1.0** (2016-12-17)
+
+- Added support for ``typings.Type`` (available in Python 3.5.2+)
+- Added a third, ``sys.setprofile()`` based type checking approach (``typeguard.TypeChecker``)
+- Changed certain type error messages to display "function" instead of the function's qualified
+ name
+
+**2.0.2** (2016-12-17)
+
+- More Python 3.6 compatibility fixes (along with a broader test suite)
+
+**2.0.1** (2016-12-10)
+
+- Fixed additional Python 3.6 compatibility issues
+
+**2.0.0** (2016-12-10)
+
+- **BACKWARD INCOMPATIBLE** Dropped Python 3.2 support
+- Fixed incompatibility with Python 3.6
+- Use ``inspect.signature()`` in place of ``inspect.getfullargspec``
+- Added support for ``typing.NamedTuple``
+
+**1.2.3** (2016-09-13)
+
+- Fixed ``@typechecked`` skipping the check of return value type when the type annotation was
+ ``None``
+
+**1.2.2** (2016-08-23)
+
+- Fixed checking of homogenous Tuple declarations (``Tuple[bool, ...]``)
+
+**1.2.1** (2016-06-29)
+
+- Use ``backports.typing`` when possible to get new features on older Pythons
+- Fixed incompatibility with Python 3.5.2
+
+**1.2.0** (2016-05-21)
+
+- Fixed argument counting when a class is checked against a Callable specification
+- Fixed argument counting when a functools.partial object is checked against a Callable
+ specification
+- Added checks against mandatory keyword-only arguments when checking against a Callable
+ specification
+
+**1.1.3** (2016-05-09)
+
+- Gracefully exit if ``check_type_arguments`` can't find a reference to the current function
+
+**1.1.2** (2016-05-08)
+
+- Fixed TypeError when checking a builtin function against a parametrized Callable
+
+**1.1.1** (2016-01-03)
+
+- Fixed improper argument counting with bound methods when typechecking callables
+
+**1.1.0** (2016-01-02)
+
+- Eliminated the need to pass a reference to the currently executing function to
+ ``check_argument_types()``
+
+**1.0.2** (2016-01-02)
+
+- Fixed types of default argument values not being considered as valid for the argument
+
+**1.0.1** (2016-01-01)
+
+- Fixed type hints retrieval being done for the wrong callable in cases where the callable was
+ wrapped with one or more decorators
+
+**1.0.0** (2015-12-28)
+
+- Initial release
diff --git a/pyproject.toml b/pyproject.toml
index 95b48ac..7c89494 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,2 +1,113 @@
[build-system]
-requires = ["setuptools >= 36.2.7", "wheel", "setuptools_scm >= 1.7.0"]
+requires = [
+ "setuptools >= 64",
+ "setuptools_scm[toml] >= 6.4"
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "typeguard"
+description = "Run-time type checker for Python"
+readme = "README.rst"
+authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}]
+license = {text = "MIT"}
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+requires-python = ">= 3.9"
+dependencies = [
+ "importlib_metadata >= 3.6; python_version < '3.10'",
+ "typing_extensions >= 4.10.0",
+]
+dynamic = ["version"]
+
+[project.urls]
+Documentation = "https://typeguard.readthedocs.io/en/latest/"
+"Change log" = "https://typeguard.readthedocs.io/en/latest/versionhistory.html"
+"Source code" = "https://github.com/agronholm/typeguard"
+"Issue tracker" = "https://github.com/agronholm/typeguard/issues"
+
+[project.optional-dependencies]
+test = [
+ "coverage[toml] >= 7",
+ "pytest >= 7",
+ 'mypy >= 1.2.0; python_implementation != "PyPy"',
+]
+doc = [
+ "packaging",
+ "Sphinx >= 7",
+ "sphinx-autodoc-typehints >= 1.2.0",
+ "sphinx-rtd-theme >= 1.3.0",
+]
+
+[project.entry-points]
+pytest11 = {typeguard = "typeguard._pytest_plugin"}
+
+[tool.setuptools.package-data]
+typeguard = ["py.typed"]
+
+[tool.setuptools_scm]
+version_scheme = "post-release"
+local_scheme = "dirty-tag"
+
+[tool.pytest.ini_options]
+addopts = "--tb=short"
+testpaths = "tests"
+xfail_strict = true
+filterwarnings = ["error"]
+
+[tool.coverage.run]
+source = ["typeguard"]
+
+[tool.coverage.report]
+show_missing = true
+exclude_lines = [
+ "pragma: no cover",
+ "if TYPE_CHECKING:"
+]
+
+[tool.ruff]
+src = ["src"]
+
+[tool.ruff.lint]
+extend-select = [
+ "B0", # flake8-bugbear
+ "I", # isort
+ "PGH", # pygrep-hooks
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+]
+ignore = [
+ "S307",
+ "B008",
+ "UP006",
+ "UP035",
+]
+
+[tool.mypy]
+python_version = "3.11"
+strict = true
+pretty = true
+
+[tool.tox]
+env_list = ["py39", "py310", "py311", "py312", "py313"]
+skip_missing_interpreters = true
+
+[tool.tox.env_run_base]
+commands = [["coverage", "run", "-m", "pytest", { replace = "posargs", extend = true }]]
+package = "editable"
+extras = ["test"]
+
+[tool.tox.env.docs]
+depends = []
+extras = ["doc"]
+commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx"]]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index e502980..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,47 +0,0 @@
-[metadata]
-name = typeguard
-description = Run-time type checker for Python
-long_description = file: README.rst
-author = Alex Grönholm
-author_email = alex.gronholm@nextday.fi
-project_urls =
- Source code = https://github.com/agronholm/typeguard
- Issue tracker = https://github.com/agronholm/typeguard/issues
-license = MIT
-license_file = LICENSE
-classifiers =
- Development Status :: 5 - Production/Stable
- Intended Audience :: Developers
- License :: OSI Approved :: MIT License
- Programming Language :: Python
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.4
- Programming Language :: Python :: 3.5
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
-
-[options]
-py_modules = typeguard
-python_requires = >= 3.4
-zip_safe = True
-install_requires =
- typing >= 3.5; python_version == "3.4"
-
-[options.extras_require]
-testing =
- pytest
- pytest-cov
-
-[tool:pytest]
-addopts = -rsx --tb=short --cov
-testpaths = tests
-
-[coverage:run]
-source = typeguard
-
-[coverage:report]
-show_missing = true
-
-[flake8]
-max-line-length = 99
-ignore = E251
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 3e51f8b..0000000
--- a/setup.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from setuptools import setup
-
-setup(
- use_scm_version={
- 'version_scheme': 'post-release',
- 'local_scheme': 'dirty-tag'
- },
- setup_requires=[
- 'setuptools_scm >= 1.7.0'
- ]
-)
diff --git a/src/typeguard/__init__.py b/src/typeguard/__init__.py
new file mode 100644
index 0000000..6781cad
--- /dev/null
+++ b/src/typeguard/__init__.py
@@ -0,0 +1,48 @@
+import os
+from typing import Any
+
+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
+
+# 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__
+
+
+config: TypeCheckConfiguration
+
+
+def __getattr__(name: str) -> Any:
+ if name == "config":
+ from ._config import global_config
+
+ return global_config
+
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+# Automatically load checker lookup functions unless explicitly disabled
+if "TYPEGUARD_DISABLE_PLUGIN_AUTOLOAD" not in os.environ:
+ load_plugins()
diff --git a/src/typeguard/_checkers.py b/src/typeguard/_checkers.py
new file mode 100644
index 0000000..5e34036
--- /dev/null
+++ b/src/typeguard/_checkers.py
@@ -0,0 +1,1075 @@
+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
+ elif not issubclass(value, expected_class): # type: ignore[arg-type]
+ 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 " f"({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_sig = inspect.signature(getattr(subject, attrname))
+ 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 and below
+ 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}: " f"{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/src/typeguard/_config.py b/src/typeguard/_config.py
new file mode 100644
index 0000000..36efad5
--- /dev/null
+++ b/src/typeguard/_config.py
@@ -0,0 +1,108 @@
+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[, ...]`` (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
+
+ Requires Python 3.9 or newer.
+
+ 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/src/typeguard/_decorators.py b/src/typeguard/_decorators.py
new file mode 100644
index 0000000..a6c20cb
--- /dev/null
+++ b/src/typeguard/_decorators.py
@@ -0,0 +1,233 @@
+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:
+ target_name = target_path[0]
+ for const in new_code.co_consts:
+ if isinstance(const, CodeType):
+ if const.co_name == target_name:
+ if const.co_firstlineno == firstlineno:
+ return const
+ elif len(target_path) > 1:
+ target_code = find_target_function(
+ const, target_path[1:], firstlineno
+ )
+ if target_code:
+ 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 == "":
+ 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 != ""]
+ 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.__annotations__ = f.__annotations__
+ new_function.__doc__ = f.__doc__
+ new_function.__defaults__ = f.__defaults__
+ new_function.__kwdefaults__ = f.__kwdefaults__
+ 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 `,
+ :func:`@staticmethod `, and :class:`@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/src/typeguard/_exceptions.py b/src/typeguard/_exceptions.py
new file mode 100644
index 0000000..625437a
--- /dev/null
+++ b/src/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/src/typeguard/_functions.py b/src/typeguard/_functions.py
new file mode 100644
index 0000000..ca21c14
--- /dev/null
+++ b/src/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/src/typeguard/_importhook.py b/src/typeguard/_importhook.py
new file mode 100644
index 0000000..0d1c627
--- /dev/null
+++ b/src/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] = "",
+ ) -> 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 ` 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/src/typeguard/_memo.py b/src/typeguard/_memo.py
new file mode 100644
index 0000000..1d0d80c
--- /dev/null
+++ b/src/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/src/typeguard/_pytest_plugin.py b/src/typeguard/_pytest_plugin.py
new file mode 100644
index 0000000..7b2f494
--- /dev/null
+++ b/src/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/src/typeguard/_suppression.py b/src/typeguard/_suppression.py
new file mode 100644
index 0000000..bbbfbfb
--- /dev/null
+++ b/src/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/src/typeguard/_transformer.py b/src/typeguard/_transformer.py
new file mode 100644
index 0000000..25696a5
--- /dev/null
+++ b/src/typeguard/_transformer.py
@@ -0,0 +1,1214 @@
+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, "")
+
+ 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 e
+ # 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
+
+ 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)
+ annotation = self._memo.variable_annotations.get(exp.id)
+ if annotation:
+ 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,
+ Constant(node.target.id),
+ annotation,
+ 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/src/typeguard/_union_transformer.py b/src/typeguard/_union_transformer.py
new file mode 100644
index 0000000..1c296d3
--- /dev/null
+++ b/src/typeguard/_union_transformer.py
@@ -0,0 +1,43 @@
+"""
+Transforms lazily evaluated PEP 604 unions into typing.Unions, for compatibility with
+Python versions older than 3.10.
+"""
+
+from __future__ import annotations
+
+from ast import (
+ BinOp,
+ BitOr,
+ Load,
+ Name,
+ NodeTransformer,
+ Subscript,
+ Tuple,
+ fix_missing_locations,
+ parse,
+)
+from types import CodeType
+from typing import Any
+
+
+class UnionTransformer(NodeTransformer):
+ def __init__(self, union_name: Name | None = None):
+ self.union_name = union_name or Name(id="Union", ctx=Load())
+
+ def visit_BinOp(self, node: BinOp) -> Any:
+ self.generic_visit(node)
+ if isinstance(node.op, BitOr):
+ return Subscript(
+ value=self.union_name,
+ slice=Tuple(elts=[node.left, node.right], ctx=Load()),
+ ctx=Load(),
+ )
+
+ return node
+
+
+def compile_type_hint(hint: str) -> CodeType:
+ parsed = parse(hint, "", "eval")
+ UnionTransformer().visit(parsed)
+ fix_missing_locations(parsed)
+ return compile(parsed, "", "eval", flags=0)
diff --git a/src/typeguard/_utils.py b/src/typeguard/_utils.py
new file mode 100644
index 0000000..e8f9b03
--- /dev/null
+++ b/src/typeguard/_utils.py
@@ -0,0 +1,171 @@
+from __future__ import annotations
+
+import inspect
+import sys
+from importlib import import_module
+from inspect import currentframe
+from types import CodeType, FrameType, FunctionType
+from typing import TYPE_CHECKING, Any, Callable, ForwardRef, Union, cast, final
+from weakref import WeakValueDictionary
+
+if TYPE_CHECKING:
+ from ._memo import TypeCheckMemo
+
+if sys.version_info >= (3, 13):
+ from typing import get_args, get_origin
+
+ def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any:
+ return forwardref._evaluate(
+ memo.globals, memo.locals, type_params=(), recursive_guard=frozenset()
+ )
+
+elif sys.version_info >= (3, 10):
+ from typing import get_args, get_origin
+
+ def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any:
+ return forwardref._evaluate(
+ memo.globals, memo.locals, recursive_guard=frozenset()
+ )
+
+else:
+ from typing_extensions import get_args, get_origin
+
+ evaluate_extra_args: tuple[frozenset[Any], ...] = (
+ (frozenset(),) if sys.version_info >= (3, 9) else ()
+ )
+
+ def evaluate_forwardref(forwardref: ForwardRef, memo: TypeCheckMemo) -> Any:
+ from ._union_transformer import compile_type_hint
+
+ if not forwardref.__forward_evaluated__:
+ forwardref.__forward_code__ = compile_type_hint(forwardref.__forward_arg__)
+
+ try:
+ return forwardref._evaluate(memo.globals, memo.locals, *evaluate_extra_args)
+ 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, *evaluate_extra_args
+ )
+
+ raise
+
+
+_functions_map: WeakValueDictionary[CodeType, FunctionType] = WeakValueDictionary()
+
+
+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}]"
+
+ 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()
diff --git a/src/typeguard/py.typed b/src/typeguard/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..b48bd69
--- /dev/null
+++ b/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/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..ef8731f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,45 @@
+import random
+import re
+import string
+import sys
+import typing
+from itertools import count
+from pathlib import Path
+
+import pytest
+import typing_extensions
+
+version_re = re.compile(r"_py(\d)(\d)\.py$")
+pytest_plugins = ["pytester"]
+
+
+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
+
+
+@pytest.fixture
+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
+
+
+@pytest.fixture(
+ params=[
+ pytest.param(typing, id="typing"),
+ pytest.param(typing_extensions, id="typing_extensions"),
+ ]
+)
+def typing_provider(request):
+ return request.param
diff --git a/tests/dummymodule.py b/tests/dummymodule.py
new file mode 100644
index 0000000..d53c972
--- /dev/null
+++ b/tests/dummymodule.py
@@ -0,0 +1,345 @@
+"""Module docstring."""
+
+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 (
+ CollectionCheckStrategy,
+ ForwardRefPolicy,
+ typechecked,
+ typeguard_ignore,
+)
+
+if sys.version_info >= (3, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+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"
+
+
+@typeguard_ignore
+def non_typeguard_checked_func(x: int, y: str) -> 6:
+ return "foo"
+
+
+class Metaclass(type):
+ pass
+
+
+@typechecked
+class DummyClass(metaclass=Metaclass):
+ def type_checked_method(self, x: int, y: int) -> int:
+ return x * y
+
+ @classmethod
+ def type_checked_classmethod(cls, x: int, y: int) -> int:
+ return x * y
+
+ @staticmethod
+ def type_checked_staticmethod(x: int, y: int) -> int:
+ return x * y
+
+ @classmethod
+ def undocumented_classmethod(cls, x, y):
+ pass
+
+ @staticmethod
+ def undocumented_staticmethod(x, y):
+ pass
+
+ @property
+ def unannotated_property(self):
+ return None
+
+
+def outer():
+ @typechecked
+ class Inner:
+ def get_self(self) -> "Inner":
+ return self
+
+ def create_inner() -> "Inner":
+ return Inner()
+
+ return create_inner
+
+
+@typechecked
+class Outer:
+ class Inner:
+ pass
+
+ def create_inner(self) -> "Inner":
+ return Outer.Inner()
+
+ @classmethod
+ def create_inner_classmethod(cls) -> "Inner":
+ return Outer.Inner()
+
+ @staticmethod
+ 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(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/tests/mypy/negative.py b/tests/mypy/negative.py
new file mode 100644
index 0000000..ec93022
--- /dev/null
+++ b/tests/mypy/negative.py
@@ -0,0 +1,56 @@
+from typeguard import typechecked, typeguard_ignore
+
+
+@typechecked
+def foo(x: int) -> int:
+ return x + 1
+
+
+@typechecked
+def bar(x: int) -> 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) # noqa: E501 # error: Incompatible return value type (got "str", expected "int") [return-value]
+
+
+@typechecked
+def returns_str() -> 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 True # noqa: E501 # error: Incompatible return value type (got "bool", expected "str") [return-value]
+
+
+@typechecked
+def ret_type() -> 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" [arg-type]
+_ = foo("typeguard") # noqa: E501 # error: Argument 1 to "foo" has incompatible type "str"; expected "int" [arg-type]
+
+
+@typechecked
+class MyClass:
+ def __init__(self, x: int = 0) -> None:
+ self.x = x
+
+ def add(self, y: int) -> int:
+ return self.x + y
+
+
+def get_value(c: MyClass) -> int:
+ return c.x
+
+
+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" [arg-type]
+_ = MyClass(returns_str()) # noqa: E501 # error: Argument 1 to "MyClass" has incompatible type "str"; expected "int" [arg-type]
diff --git a/tests/mypy/positive.py b/tests/mypy/positive.py
new file mode 100644
index 0000000..dc8a350
--- /dev/null
+++ b/tests/mypy/positive.py
@@ -0,0 +1,55 @@
+from typing import Callable
+
+from typeguard import typechecked
+
+
+@typechecked
+def foo(x: str) -> str:
+ return "hello " + x
+
+
+def takes_callable(f: Callable[[str], str]) -> str:
+ return f("typeguard")
+
+
+takes_callable(foo)
+
+
+@typechecked
+def has_valid_arguments(x: int, y: str) -> None:
+ pass
+
+
+def has_valid_return_type(y: str) -> str:
+ return y
+
+
+@typechecked
+class MyClass:
+ def __init__(self, x: int) -> None:
+ self.x = x
+
+ def add(self, y: int) -> int:
+ return self.x + y
+
+
+def get_value(c: MyClass) -> int:
+ return c.x
+
+
+@typechecked
+def get_value_checked(c: MyClass) -> int:
+ return c.x
+
+
+def create_myclass(x: int) -> MyClass:
+ return MyClass(x)
+
+
+@typechecked
+def create_myclass_checked(x: int) -> MyClass:
+ return MyClass(x)
+
+
+get_value(create_myclass(3))
+get_value_checked(create_myclass_checked(1))
diff --git a/tests/mypy/test_type_annotations.py b/tests/mypy/test_type_annotations.py
new file mode 100644
index 0000000..aacee7f
--- /dev/null
+++ b/tests/mypy/test_type_annotations.py
@@ -0,0 +1,113 @@
+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",
+ )
+]
+
+
+def get_mypy_cmd(filename: str) -> List[str]:
+ return ["mypy", "--strict", filename]
+
+
+def get_negative_mypy_output() -> str:
+ """
+ Get the output from running mypy on the negative examples file.
+ """
+ process = subprocess.run(
+ get_mypy_cmd(NEGATIVE_FILE), stdout=subprocess.PIPE, check=False
+ )
+ output = process.stdout.decode()
+ assert output
+ return output
+
+
+def get_expected_errors() -> Dict[int, str]:
+ """
+ Extract the expected errors from comments in the negative examples file.
+ """
+ with open(NEGATIVE_FILE) as f:
+ lines = f.readlines()
+
+ expected = {}
+
+ for idx, line in enumerate(lines):
+ line = line.rstrip()
+ if "# error" in line:
+ expected[idx + 1] = line[line.index("# error") + 2 :]
+
+ # Sanity check. Should update if negative.py changes.
+ assert len(expected) == 9
+ return expected
+
+
+def get_mypy_errors() -> Dict[int, str]:
+ """
+ Extract the errors from running mypy on the negative examples file.
+ """
+ mypy_output = get_negative_mypy_output()
+
+ 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 :]
+
+ return got
+
+
+@pytest.fixture
+def chdir_local() -> None:
+ """
+ Change to the local directory. This is so that mypy treats imports from
+ typeguard as external imports instead of source code (which is handled
+ differently by mypy).
+ """
+ os.chdir(os.path.dirname(__file__))
+
+
+@pytest.mark.usefixtures("chdir_local")
+def test_positive() -> None:
+ """
+ Run mypy on the positive test file. There should be no errors.
+ """
+ subprocess.check_call(get_mypy_cmd(POSITIVE_FILE))
+
+
+@pytest.mark.usefixtures("chdir_local")
+def test_negative() -> None:
+ """
+ Run mypy on the negative test file. This should fail. The errors from mypy
+ should match the comments in the file.
+ """
+ got_errors = get_mypy_errors()
+ expected_errors = get_expected_errors()
+
+ if set(got_errors) != set(expected_errors):
+ raise RuntimeError(
+ f"Expected error lines {set(expected_errors)} does not "
+ + f"match mypy error lines {set(got_errors)}."
+ )
+
+ mismatches = [
+ (idx, expected_errors[idx], got_errors[idx])
+ for idx in expected_errors
+ if expected_errors[idx] != got_errors[idx]
+ ]
+ 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/tests/test_checkers.py b/tests/test_checkers.py
new file mode 100644
index 0000000..1ba0407
--- /dev/null
+++ b/tests/test_checkers.py
@@ -0,0 +1,1505 @@
+import collections.abc
+import sys
+import types
+from contextlib import nullcontext
+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")
+
+
+@pytest.mark.skipif(
+ 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"
+ )
+
+
+@pytest.mark.parametrize(
+ "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):
+ with pytest.raises(
+ TypeCheckError, match=r"class str is not an instance of \w+\.UnionType$"
+ ):
+ 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"
+ )
+
+ @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"
+ )
+
+
+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"
+ )
+
+
+@pytest.mark.parametrize(
+ "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/tests/test_importhook.py b/tests/test_importhook.py
new file mode 100644
index 0000000..39d8968
--- /dev/null
+++ b/tests/test_importhook.py
@@ -0,0 +1,69 @@
+import sys
+import warnings
+from importlib import import_module
+from importlib.util import cache_from_source
+from pathlib import Path
+
+import pytest
+
+from typeguard import TypeCheckError, TypeguardFinder, install_import_hook
+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"
+cached_module_path = Path(
+ cache_from_source(str(dummy_module_path), optimization=OPTIMIZATION)
+)
+
+
+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 warnings.catch_warnings():
+ warnings.filterwarnings("error", module="typeguard")
+ module = import_module("dummymodule")
+ return module
+ finally:
+ sys.path.remove(str(this_dir))
+
+
+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():
+ """
+ The path finder only matches configured (sub)packages.
+ """
+ packages = ["ham", "spam.eggs"]
+ dummy_original_pathfinder = None
+ finder = TypeguardFinder(packages, dummy_original_pathfinder)
+
+ assert finder.should_instrument("ham")
+ assert finder.should_instrument("ham.eggs")
+ assert finder.should_instrument("spam.eggs")
+
+ assert not finder.should_instrument("spam")
+ assert not finder.should_instrument("ha")
+ assert not finder.should_instrument("spam_eggs")
+
+
+@pytest.mark.skipif(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)
+ assert f"Source code of {path_str!r} after instrumentation:" in err
+ assert "class DummyClass" in err
diff --git a/tests/test_instrumentation.py b/tests/test_instrumentation.py
new file mode 100644
index 0000000..74bab3e
--- /dev/null
+++ b/tests/test_instrumentation.py
@@ -0,0 +1,344 @@
+import asyncio
+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, config, 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"
+cached_module_path = Path(
+ cache_from_source(str(dummy_module_path), optimization=OPTIMIZATION)
+)
+
+# 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
+
+
+@pytest.fixture(scope="module", params=["typechecked", "importhook"])
+def method(request: FixtureRequest) -> str:
+ return request.param
+
+
+@pytest.fixture(scope="module")
+def dummymodule(method: str):
+ config.debug_instrumentation = True
+ sys.path.insert(0, str(this_dir))
+ try:
+ sys.modules.pop("dummymodule", None)
+ if cached_module_path.exists():
+ cached_module_path.unlink()
+
+ if method == "typechecked":
+ return import_module("dummymodule")
+
+ with install_import_hook(["dummymodule"]):
+ with warnings.catch_warnings():
+ warnings.filterwarnings("error", module="typeguard")
+ module = import_module("dummymodule")
+ return module
+ finally:
+ sys.path.remove(str(this_dir))
+
+
+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')
+
+
+@pytest.mark.xfail(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"
+
+
+@pytest.mark.xfail(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"
+
+
+@pytest.mark.xfail(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..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_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):
+ 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)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
new file mode 100644
index 0000000..f01a074
--- /dev/null
+++ b/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/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py
new file mode 100644
index 0000000..0c5b04d
--- /dev/null
+++ b/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
+
+
+@pytest.fixture
+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/tests/test_suppression.py b/tests/test_suppression.py
new file mode 100644
index 0000000..47c433c
--- /dev/null
+++ b/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/tests/test_transformer.py b/tests/test_transformer.py
new file mode 100644
index 0000000..2e18a5a
--- /dev/null
+++ b/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()
+ )
+
+
+@pytest.mark.parametrize(
+ "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()
+ )
+
+
+@pytest.mark.parametrize(
+ "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..foo', {'x': (x, int)}, memo_)
+ return check_return_type_('outer..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..foo', {'x': (x, int)}, memo)
+ return check_return_type('wrapper..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..Foo.Bar.method', \
+{'x': (x, int)}, memo)
+ return check_return_type(\
+'wrapper..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/tests/test_typechecked.py b/tests/test_typechecked.py
new file mode 100644
index 0000000..d56f3ae
--- /dev/null
+++ b/tests/test_typechecked.py
@@ -0,0 +1,694 @@
+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"\.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\.\.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"\.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\.\.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\.\.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..foo"
+ assert foo.__doc__ == "This is a docstring."
+ assert foo.__defaults__ == ("foo",)
+
+
+@pytest.mark.skipif(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..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..foo', \
+{'a': (a, str)}, memo)
+ return check_return_type('test_debug_instrumentation..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
+
+
+@pytest.mark.parametrize(
+ "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")
diff --git a/tests/test_typeguard.py b/tests/test_typeguard.py
deleted file mode 100644
index 52834eb..0000000
--- a/tests/test_typeguard.py
+++ /dev/null
@@ -1,874 +0,0 @@
-import sys
-from concurrent.futures import ThreadPoolExecutor
-from functools import wraps, partial
-from io import StringIO
-from typing import (
- Any, Callable, Dict, List, Set, Tuple, Union, TypeVar, Sequence, NamedTuple, Iterable,
- Container, Generic)
-
-import pytest
-
-from typeguard import (
- typechecked, check_argument_types, qualified_name, TypeChecker, TypeWarning, function_name,
- check_type)
-
-try:
- from typing import Type
-except ImportError:
- Type = List # don't worry, Type is not actually used if this happens!
-
-try:
- from typing import Collection
-except ImportError:
- Collection = None
-
-
-class Parent:
- pass
-
-
-class Child(Parent):
- def method(self, a: int):
- pass
-
-
-@pytest.mark.parametrize('inputval, expected', [
- (qualified_name, 'function'),
- (Child(), '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_no_memo_fail():
- pytest.raises(TypeError, check_type, 'foo', ['a'], List[int]).\
- match('type of foo\[0\] must be int; got str instead')
-
-
-class TestCheckArgumentTypes:
- def test_any_type(self):
- def foo(a: Any):
- assert check_argument_types()
-
- foo('aa')
-
- 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_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_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):
- Employee = NamedTuple('Employee', [('name', str), ('id', int)])
-
- def foo(bar: Employee):
- assert check_argument_types()
-
- foo(Employee('bob', 1))
-
- def test_namedtuple_type_mismatch(self):
- Employee = NamedTuple('Employee', [('name', str), ('id', int)])
-
- 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 '
- '(test_typeguard\.)?Employee; got tuple instead')
-
- def test_namedtuple_wrong_field_type(self):
- Employee = NamedTuple('Employee', [('name', str), ('id', int)])
-
- 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):
- T = TypeVar('T', int, str)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- foo(*values)
-
- def test_typevar_constraints_fail_typing_type(self):
- T = TypeVar('T', int, Collection)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- with pytest.raises(TypeError):
- foo('aa', 'bb')
-
- def test_typevar_constraints_fail(self):
- T = TypeVar('T', int, str)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- exc = pytest.raises(TypeError, foo, 2.5, 'aa')
- assert str(exc.value) == ('type of argument "a" must be one of (int, str); got float '
- 'instead')
-
- def test_typevar_bound(self):
- T = TypeVar('T', bound=Parent)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- foo(Child(), Child())
-
- def test_typevar_bound_fail(self):
- T = TypeVar('T', bound=Child)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- exc = pytest.raises(TypeError, foo, Parent(), Parent())
- assert str(exc.value) == ('type of argument "a" must be test_typeguard.Child or one of '
- 'its subclasses; got test_typeguard.Parent instead')
-
- def test_typevar_invariant_fail(self):
- T = TypeVar('T', int, str)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- exc = pytest.raises(TypeError, foo, 2, 3.6)
- assert str(exc.value) == 'type of argument "b" must be exactly int; got float instead'
-
- def test_typevar_covariant(self):
- T = TypeVar('T', covariant=True)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- foo(Parent(), Child())
-
- def test_typevar_covariant_fail(self):
- T = TypeVar('T', covariant=True)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- exc = pytest.raises(TypeError, foo, Child(), Parent())
- assert str(exc.value) == ('type of argument "b" must be test_typeguard.Child or one of '
- 'its subclasses; got test_typeguard.Parent instead')
-
- def test_typevar_contravariant(self):
- T = TypeVar('T', contravariant=True)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- foo(Child(), Parent())
-
- def test_typevar_contravariant_fail(self):
- T = TypeVar('T', contravariant=True)
-
- def foo(a: T, b: T):
- assert check_argument_types()
-
- exc = pytest.raises(TypeError, foo, Parent(), Child())
- assert str(exc.value) == ('type of argument "b" must be test_typeguard.Parent or one of '
- 'its superclasses; got test_typeguard.Child 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 test_typeguard.Child; got test_typeguard.Parent 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 test_typeguard.Child; '
- 'got 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_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('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('type of argument "kwargs"\[\'b\'\] must be int; got str instead')
-
- def test_generic(self):
- T_Foo = TypeVar('T_Foo')
-
- class FooGeneric(Generic[T_Foo]):
- pass
-
- def foo(a: FooGeneric[str]):
- assert check_argument_types()
-
- foo(FooGeneric[str]())
-
- def test_newtype(self):
- try:
- from typing import NewType
- except ImportError:
- pytest.skip('Skipping newtype test since no NewType in current typing library')
-
- myint = NewType("myint", int)
-
- 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')
-
-
-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'
-
- exc = pytest.raises(TypeError, foo, 4, 2)
- assert str(exc.value) == 'type of the return value must be exactly int; 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'
-
- @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', [
- 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', [
- Set[int],
- Set,
- set
- ], ids=['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))
-
- @pytest.mark.skipif(Type is List, reason='typing.Type could not be imported')
- @pytest.mark.parametrize('typehint', [
- Type[Parent],
- Type[TypeVar('UnboundType')],
- Type[TypeVar('BoundType', bound=Parent)],
- 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)
-
-
-class TestTypeChecker:
- @pytest.fixture
- def executor(self):
- executor = ThreadPoolExecutor(1)
- yield executor
- executor.shutdown()
-
- @pytest.fixture
- def checker(self):
- return TypeChecker(__name__)
-
- 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 TypeChecker(__name__), pytest.warns(TypeWarning) as record:
- foo('x')
- with TypeChecker(__name__):
- 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
diff --git a/tests/test_union_transformer.py b/tests/test_union_transformer.py
new file mode 100644
index 0000000..e6dcd25
--- /dev/null
+++ b/tests/test_union_transformer.py
@@ -0,0 +1,44 @@
+import typing
+from typing import Callable, Union
+
+import pytest
+from typing_extensions import Literal
+
+from typeguard._union_transformer import compile_type_hint
+
+eval_globals = {
+ "Callable": Callable,
+ "Literal": Literal,
+ "typing": typing,
+ "Union": Union,
+}
+
+
+@pytest.mark.parametrize(
+ "inputval, expected",
+ [
+ ["str | int", "Union[str, int]"],
+ ["str | int | bytes", "Union[str, int, bytes]"],
+ ["str | Union[int | bytes, set]", "Union[str, int, bytes, set]"],
+ ["str | int | Callable[..., bytes]", "Union[str, int, Callable[..., bytes]]"],
+ ["str | int | Callable[[], bytes]", "Union[str, int, Callable[[], bytes]]"],
+ [
+ "str | int | Callable[[], bytes | set]",
+ "Union[str, int, Callable[[], Union[bytes, set]]]",
+ ],
+ ["str | int | Literal['foo']", "Union[str, int, Literal['foo']]"],
+ ["str | int | Literal[-1]", "Union[str, int, Literal[-1]]"],
+ ["str | int | Literal[-1]", "Union[str, int, Literal[-1]]"],
+ [
+ 'str | int | Literal["It\'s a string \'\\""]',
+ "Union[str, int, Literal['It\\'s a string \\'\"']]",
+ ],
+ ],
+)
+def test_union_transformer(inputval: str, expected: str) -> None:
+ code = compile_type_hint(inputval)
+ evaluated = eval(code, eval_globals)
+ evaluated_repr = repr(evaluated)
+ evaluated_repr = evaluated_repr.replace("typing.", "")
+ evaluated_repr = evaluated_repr.replace("typing_extensions.", "")
+ assert evaluated_repr == expected
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..e57a842
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,22 @@
+import pytest
+
+from typeguard._utils import function_name, qualified_name
+
+from . import Child
+
+
+@pytest.mark.parametrize(
+ "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/tests/test_warn_on_error.py b/tests/test_warn_on_error.py
new file mode 100644
index 0000000..184b93b
--- /dev/null
+++ b/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/tox.ini b/tox.ini
deleted file mode 100644
index 2b54c9b..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,16 +0,0 @@
-[tox]
-minversion = 2.5.0
-envlist = pypy3, py34, py35, py36, py37, flake8
-skip_missing_interpreters = true
-
-[testenv]
-extras = testing
-commands = python -m pytest {posargs}
-
-[testenv:pypy3]
-ignore_outcome = true
-
-[testenv:flake8]
-deps = flake8
-commands = flake8 typeguard.py tests
-skip_install = true
diff --git a/typeguard.py b/typeguard.py
deleted file mode 100644
index cef1252..0000000
--- a/typeguard.py
+++ /dev/null
@@ -1,657 +0,0 @@
-__all__ = ('typechecked', 'check_argument_types', 'check_type', 'TypeWarning', 'TypeChecker')
-
-import collections.abc
-import gc
-import inspect
-import sys
-import threading
-from collections import OrderedDict
-from functools import wraps, partial
-from inspect import Parameter, isclass, isfunction
-from traceback import extract_stack, print_stack
-from types import CodeType, FunctionType # noqa
-from typing import (Callable, Any, Union, Dict, List, TypeVar, Tuple, Set, Sequence,
- get_type_hints, TextIO, Optional)
-from warnings import warn
-from weakref import WeakKeyDictionary, WeakValueDictionary
-
-try:
- from typing import Type
-except ImportError:
- Type = None
-
-_type_hints_map = WeakKeyDictionary() # type: Dict[FunctionType, Dict[str, Any]]
-_functions_map = WeakValueDictionary() # type: Dict[CodeType, FunctionType]
-
-
-class _CallMemo:
- __slots__ = ('func', 'func_name', 'signature', 'typevars', 'arguments', 'type_hints')
-
- def __init__(self, func: Callable, frame=None, args: tuple = None,
- kwargs: Dict[str, Any] = None):
- self.func = func
- self.func_name = function_name(func)
- self.signature = inspect.signature(func)
- self.typevars = {} # type: Dict[Any, type]
-
- if args is not None and kwargs is not None:
- self.arguments = self.signature.bind(*args, **kwargs).arguments
- else:
- assert frame, 'frame must be specified if args or kwargs is None'
- self.arguments = frame.f_locals.copy()
-
- self.type_hints = _type_hints_map.get(func)
- if self.type_hints is None:
- hints = get_type_hints(func)
- self.type_hints = _type_hints_map[func] = OrderedDict()
- for name, parameter in self.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']
-
-
-def get_type_name(type_):
- # typing.* types don't have a __name__ on Python 3.7+
- return getattr(type_, '__name__', None) or type_._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: FunctionType) -> 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.
-
- """
- module = func.__module__
- qualname = func.__qualname__
- return qualname if module == 'builtins' else '{}.{}'.format(module, qualname)
-
-
-def check_callable(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
- if not callable(value):
- raise TypeError('{} must be a callable'.format(argname))
-
- if expected_type.__args__:
- 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: Optional[_CallMemo]) -> None:
- if not isinstance(value, dict):
- raise TypeError('type of {} must be a dict; got {} instead'.
- format(argname, qualified_name(value)))
-
- key_type, value_type = getattr(expected_type, '__args__', expected_type.__parameters__)
- 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_list(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
- if not isinstance(value, list):
- raise TypeError('type of {} must be a list; got {} instead'.
- format(argname, qualified_name(value)))
-
- value_type = getattr(expected_type, '__args__', expected_type.__parameters__)[0]
- if value_type:
- for i, v in enumerate(value):
- check_type('{}[{}]'.format(argname, i), v, value_type, memo)
-
-
-def check_sequence(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
- if not isinstance(value, collections.Sequence):
- raise TypeError('type of {} must be a sequence; got {} instead'.
- format(argname, qualified_name(value)))
-
- value_type = getattr(expected_type, '__args__', expected_type.__parameters__)[0]
- if value_type:
- for i, v in enumerate(value):
- check_type('{}[{}]'.format(argname, i), v, value_type, memo)
-
-
-def check_set(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
- if not isinstance(value, collections.Set):
- raise TypeError('type of {} must be a set; got {} instead'.
- format(argname, qualified_name(value)))
-
- value_type = getattr(expected_type, '__args__', expected_type.__parameters__)[0]
- if value_type:
- for v in value:
- check_type('elements of {}'.format(argname), v, value_type, memo)
-
-
-def check_tuple(argname: str, value, expected_type, memo: Optional[_CallMemo]) -> None:
- # Specialized check for NamedTuples
- if hasattr(expected_type, '_field_types'):
- 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)))
-
- for name, field_type in expected_type._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)
- 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: Optional[_CallMemo]) -> 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: Optional[_CallMemo]) -> 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
-
- expected_class = expected_type.__args__[0] if expected_type.__args__ else None
- if expected_class:
- if isinstance(expected_class, TypeVar):
- check_typevar(argname, value, expected_class, memo, True)
- 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: Optional[_CallMemo],
- subclass_check: bool = False) -> None:
- if memo is None:
- raise TypeError('encountered a TypeVar but a call memo was not provided')
-
- bound_type = memo.typevars.get(typevar, typevar.__bound__)
- value_type = value if subclass_check else type(value)
- subject = argname if subclass_check else 'type of ' + argname
- if bound_type is None:
- # The type variable hasn't been bound yet -- check that the given value matches the
- # constraints of the type variable, if any
- if typevar.__constraints__ and value_type not in typevar.__constraints__:
- typelist = ', '.join(get_type_name(t) for t in typevar.__constraints__
- if t is not object)
- raise TypeError('{} must be one of ({}); got {} instead'.
- format(subject, typelist, qualified_name(value_type)))
- elif typevar.__covariant__ or typevar.__bound__:
- 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.__contravariant__:
- if not issubclass(bound_type, value_type):
- raise TypeError(
- '{} must be {} or one of its superclasses; got {} instead'.
- format(subject, qualified_name(bound_type), qualified_name(value_type)))
- else: # invariant
- if value_type is not bound_type:
- raise TypeError(
- '{} must be exactly {}; got {} instead'.
- format(subject, qualified_name(bound_type), qualified_name(value_type)))
-
- if typevar not in memo.typevars:
- # Bind the type variable to a concrete type
- memo.typevars[typevar] = value_type
-
-
-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__)))
-
-
-# Equality checks are applied to these
-origin_type_checkers = {
- 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,
- set: check_set,
- Set: check_set,
- tuple: check_tuple,
- Tuple: check_tuple,
- type: check_class,
- Union: check_union
-}
-_subclass_check_unions = hasattr(Union, '__union_set_params__')
-if Type is not None:
- origin_type_checkers[Type] = check_class
-
-
-def check_type(argname: str, value, expected_type, memo: Optional[_CallMemo] = 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
-
- """
- if expected_type is Any:
- return
-
- if expected_type is None:
- # Only happens on < 3.6
- expected_type = type(None)
-
- 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, Callable) and hasattr(expected_type, '__args__'):
- # Needed on Python 3.5.0 to 3.5.2
- check_callable(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)
- else:
- expected_type = (getattr(expected_type, '__extra__', None) or origin_type or
- expected_type)
- if 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 (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, should check against supertype (recursively)
- return check_type(argname, value, expected_type.__supertype__, memo)
-
-
-def check_return_type(retval, memo: Optional[_CallMemo]) -> bool:
- if 'return' in memo.type_hints:
- try:
- check_type('the return value', retval, memo.type_hints['return'], memo)
- except TypeError as exc: # suppress unnecessarily long tracebacks
- raise TypeError(exc) 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:
- frame = inspect.currentframe().f_back
- try:
- func = find_function(frame)
- except LookupError:
- return True # This can happen with the Pydev/PyCharm debugger extension installed
-
- memo = _CallMemo(func, frame)
-
- 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) from None
-
- return True
-
-
-def typechecked(func: Callable = None, *, always: bool = False):
- """
- 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``.
-
- :param func: the function to enable type checking for
- :param always: ``True`` to enable type checks even in optimized mode
-
- """
- if not __debug__ and not always: # pragma: no cover
- return func
-
- if func is None:
- return partial(typechecked, always=always)
-
- if not getattr(func, '__annotations__', None):
- warn('no type annotations present -- not typechecking {}'.format(function_name(func)))
- return func
-
- @wraps(func)
- def wrapper(*args, **kwargs):
- memo = _CallMemo(func, args=args, kwargs=kwargs)
- check_argument_types(memo)
- retval = func(*args, **kwargs)
- check_return_type(retval, memo)
- return retval
-
- return wrapper
-
-
-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 :cls:`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: 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 ``sys.setprofile()``.
-
- :param all_threads: ``True`` to check types in all threads created while the checker is
- running, ``False`` to only check in the current one
- """
-
- def __init__(self, packages: Union[str, Sequence[str]], *, all_threads: bool = True):
- assert check_argument_types()
- self.all_threads = all_threads
- 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
- 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)
- 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)
-
- memo = self._call_memos.pop(frame, None)
- if memo is not None:
- try:
- check_return_type(arg, memo)
- except TypeError as exc:
- warn(TypeWarning(memo, event, frame, exc))
- elif self._previous_profiler is not None:
- self._previous_profiler(frame, event, arg)