Skip to content

Commit

Permalink
Added support for specifying options for the pytest plugin via pytest…
Browse files Browse the repository at this point in the history
… config files

Closes #440.
  • Loading branch information
agronholm committed Mar 23, 2024
1 parent 3c8d46f commit ded1a04
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 12 deletions.
19 changes: 18 additions & 1 deletion docs/userguide.rst
Expand Up @@ -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
-----------------------------
Expand Down
2 changes: 2 additions & 0 deletions docs/versionhistory.rst
Expand Up @@ -6,6 +6,8 @@ This library adheres to

**UNRELEASED**

- Added support for specifying options for the pytest plugin via pytest config files
(`#440 <https://github.com/agronholm/typeguard/issues/440>`_)
- Avoid creating reference cycles when type checking unions
- Fixed ``Optional[...]`` being removed from the AST if it was located within a
subscript (`#442 <https://github.com/agronholm/typeguard/issues/442>`_)
Expand Down
48 changes: 37 additions & 11 deletions src/typeguard/_pytest_plugin.py
Expand Up @@ -2,6 +2,7 @@

import sys
import warnings
from typing import Any, Literal

from pytest import Config, Parser

Expand All @@ -12,18 +13,33 @@


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",
Expand All @@ -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",
Expand All @@ -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
)
Expand All @@ -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):
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Expand Up @@ -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):
Expand Down
63 changes: 63 additions & 0 deletions 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

0 comments on commit ded1a04

Please sign in to comment.