From ded1a04f0c990c1e901a9d0734a63337a76f3e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 23 Mar 2024 23:31:03 +0200 Subject: [PATCH] Added support for specifying options for the pytest plugin via pytest config files Closes #440. --- docs/userguide.rst | 19 +++++++++- docs/versionhistory.rst | 2 ++ src/typeguard/_pytest_plugin.py | 48 +++++++++++++++++++------ tests/conftest.py | 1 + tests/test_pytest_plugin.py | 63 +++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 tests/test_pytest_plugin.py diff --git a/docs/userguide.rst b/docs/userguide.rst index 22244626..669502be 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -157,7 +157,24 @@ previous section). To use it, run ``pytest`` with the appropriate pytest --typeguard-packages=foo.bar,xyz -There is currently no support for specifying a customized module finder. +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 ----------------------------- diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 87b23035..b51145b1 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -6,6 +6,8 @@ This library adheres to **UNRELEASED** +- Added support for specifying options for the pytest plugin via pytest config files + (`#440 `_) - Avoid creating reference cycles when type checking unions - Fixed ``Optional[...]`` being removed from the AST if it was located within a subscript (`#442 `_) diff --git a/src/typeguard/_pytest_plugin.py b/src/typeguard/_pytest_plugin.py index b52a41d9..7bca9c26 100644 --- a/src/typeguard/_pytest_plugin.py +++ b/src/typeguard/_pytest_plugin.py @@ -2,6 +2,7 @@ import sys import warnings +from typing import Any, Literal from pytest import Config, Parser @@ -12,6 +13,17 @@ 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", @@ -19,11 +31,15 @@ def pytest_addoption(parser: Parser) -> None: 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", @@ -33,6 +49,8 @@ def pytest_addoption(parser: Parser) -> None: "handle a TypeCheckError" ), ) + add_ini_option("string") + group.addoption( "--typeguard-forward-ref-policy", action="store", @@ -42,21 +60,31 @@ def pytest_addoption(parser: Parser) -> None: "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: - packages_option = config.getoption("typeguard_packages") - if packages_option: - if packages_option == ":all:": - packages: list[str] | None = 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: - packages = [pkg.strip() for pkg in packages_option.split(",")] already_imported_packages = sorted( package for package in packages if package in sys.modules ) @@ -70,11 +98,11 @@ def pytest_configure(config: Config) -> None: install_import_hook(packages=packages) - debug_option = config.getoption("typeguard_debug_instrumentation") + debug_option = getoption("typeguard-debug-instrumentation") if debug_option: global_config.debug_instrumentation = True - fail_callback_option = config.getoption("typeguard_typecheck_fail_callback") + fail_callback_option = getoption("typeguard-typecheck-fail-callback") if fail_callback_option: callback = resolve_reference(fail_callback_option) if not callable(callback): @@ -85,14 +113,12 @@ def pytest_configure(config: Config) -> None: global_config.typecheck_fail_callback = callback - forward_ref_policy_option = config.getoption("typeguard_forward_ref_policy") + 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 = config.getoption( - "typeguard_collection_check_strategy" - ) + collection_check_strategy_option = getoption("typeguard-collection-check-strategy") if collection_check_strategy_option: collection_check_strategy = CollectionCheckStrategy.__members__[ collection_check_strategy_option diff --git a/tests/conftest.py b/tests/conftest.py index ff270068..823b36d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import typing_extensions version_re = re.compile(r"_py(\d)(\d)\.py$") +pytest_plugins = ["pytester"] def pytest_ignore_collect(path, config): diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py new file mode 100644 index 00000000..f7e884ba --- /dev/null +++ b/tests/test_pytest_plugin.py @@ -0,0 +1,63 @@ +from textwrap import dedent + +from pytest import Pytester + +from typeguard import CollectionCheckStrategy, ForwardRefPolicy, config + + +def test_config_options(pytester: Pytester) -> 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) -> None: + pytester.makepyfile( + mypackage=( + dedent( + """ + def failcallback(): + pass + """ + ) + ) + ) + + pytester.plugins = ["typeguard"] + pytester.syspathinsert() + pytestconfig = pytester.parseconfigure( + "--typeguard-packages=mypackage,otherpackage" + ) + 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