Add OuputVerbosity and use it in assertions

This commit is contained in:
Patrick Lannigan 2023-09-23 17:18:43 -04:00
parent b73b4c464c
commit c93bc1e0ca
No known key found for this signature in database
GPG Key ID: BBF5D9DED1E4AAF9
11 changed files with 178 additions and 9 deletions

View File

@ -291,6 +291,7 @@ Ondřej Súkup
Oscar Benjamin Oscar Benjamin
Parth Patel Parth Patel
Patrick Hayes Patrick Hayes
Patrick Lannigan
Paul Müller Paul Müller
Paul Reece Paul Reece
Pauli Virtanen Pauli Virtanen

View File

@ -0,0 +1 @@
:confval:`verbosity_assertions` option added to be able to control assertion output independent of the application wide verbosity level.

View File

@ -270,6 +270,16 @@ situations, for example you are shown even fixtures that start with ``_`` if you
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment, Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
however some plugins might make use of higher verbosity. however some plugins might make use of higher verbosity.
Fine gain verbosity
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
``verbosity_assertions``: Controls how verbose the assertion output should be when pytest is executed. A value of ``2``
would have the same output as the previous example, but each test inside the file is shown by a single character in the
output.
.. _`pytest.detailed_failed_tests_usage`: .. _`pytest.detailed_failed_tests_usage`:
Producing a detailed summary report Producing a detailed summary report

View File

@ -1819,6 +1819,19 @@ passed multiple times. The expected format is ``name=value``. For example::
clean_db clean_db
.. confval:: verbosity_assertions
Set a verbosity level specifically for assertion related output, overriding the application wide level.
.. code-block:: ini
[pytest]
verbosity_assertions = 2
Defaults to application wide verbosity level.
.. confval:: xfail_strict .. confval:: xfail_strict
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the

View File

@ -12,6 +12,7 @@ from _pytest.assertion import util
from _pytest.assertion.rewrite import assertstate_key from _pytest.assertion.rewrite import assertstate_key
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config import OutputVerbosity
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.nodes import Item from _pytest.nodes import Item
@ -42,6 +43,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Enables the pytest_assertion_pass hook. " help="Enables the pytest_assertion_pass hook. "
"Make sure to delete any previously generated pyc cache files.", "Make sure to delete any previously generated pyc cache files.",
) )
OutputVerbosity.add_ini(
parser,
"assertions",
help=(
"Specify a verbosity level for assertions, overriding the main level. "
"Higher levels will provide more a more detailed explanation when an assertion fails."
),
)
def register_assert_rewrite(*names: str) -> None: def register_assert_rewrite(*names: str) -> None:

View File

@ -1,7 +1,7 @@
"""Utilities for truncating assertion output. """Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode or running on CI. terminal lines, unless running with a verbosity level of at least 2 or running on CI.
""" """
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -26,7 +26,7 @@ def truncate_if_required(
def _should_truncate_item(item: Item) -> bool: def _should_truncate_item(item: Item) -> bool:
"""Whether or not this test item is eligible for truncation.""" """Whether or not this test item is eligible for truncation."""
verbose = item.config.option.verbose verbose = item.config.output_verbosity.verbosity_for("assertions")
return verbose < 2 and not util.running_on_ci() return verbose < 2 and not util.running_on_ci()

View File

@ -161,7 +161,7 @@ def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]: ) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands.""" """Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose") verbose = config.output_verbosity.verbosity_for("assertions")
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier. # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246. # See issue #3246.

View File

@ -69,7 +69,7 @@ from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from .argparsing import Argument from .argparsing import Argument, Parser
_PluggyPlugin = object _PluggyPlugin = object
@ -1020,6 +1020,7 @@ class Config:
) )
self.args_source = Config.ArgsSource.ARGS self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = [] self.args: List[str] = []
self.output_verbosity = OutputVerbosity(self)
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
@ -1662,6 +1663,54 @@ class Config:
) )
class OutputVerbosity:
DEFAULT = "auto"
_option_name_fmt = "verbosity_{}"
def __init__(self, config: Config) -> None:
self._config = config
@property
def verbose(self) -> int:
"""Application wide verbosity level."""
return cast(int, self._config.option.verbose)
def verbosity_for(self, output_type: str) -> int:
"""Return verbosity level for the given output type.
:param output_type: Name of the output type.
If the level is not configured, the value of ``config.option.verbose``.
"""
level = self._config.getini(OutputVerbosity._ini_name(output_type))
if level == OutputVerbosity.DEFAULT:
return self.verbose
return int(level)
@staticmethod
def _ini_name(output_type: str) -> str:
return f"verbosity_{output_type}"
@staticmethod
def add_ini(parser: "Parser", output_type: str, help: str) -> None:
"""Add a output verbosity configuration option for the given output type.
:param parser: Parser for command line arguments and ini-file values.
:param output_type: Name of the output type.
:param help: Description of the output this type controls.
The value should be retrieved via a call to
:py:func:`config.output_verbosity.verbosity_for(name) <pytest.OutputVerbosity.verbosity_for>`.
"""
parser.addini(
OutputVerbosity._ini_name(output_type),
help=help,
type="string",
default=OutputVerbosity.DEFAULT,
)
def _assertion_supported() -> bool: def _assertion_supported() -> bool:
try: try:
assert False assert False

View File

@ -15,6 +15,7 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config import hookspec from _pytest.config import hookspec
from _pytest.config import main from _pytest.config import main
from _pytest.config import OutputVerbosity
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.config.argparsing import OptionGroup from _pytest.config.argparsing import OptionGroup
@ -126,6 +127,7 @@ __all__ = [
"Module", "Module",
"MonkeyPatch", "MonkeyPatch",
"OptionGroup", "OptionGroup",
"OutputVerbosity",
"Package", "Package",
"param", "param",
"Parser", "Parser",

View File

@ -17,12 +17,23 @@ from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
def mock_config(verbose=0): def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
class Config: class OutputVerbosity:
def getoption(self, name): @property
if name == "verbose": def verbose(self) -> int:
return verbose return verbose
raise KeyError("Not mocked out: %s" % name)
def verbosity_for(self, output_type: str) -> int:
if output_type == "assertions":
if assertion_override is not None:
return assertion_override
return verbose
raise KeyError("Not mocked out: %s" % output_type)
class Config:
def __init__(self) -> None:
self.output_verbosity = OutputVerbosity()
return Config() return Config()

View File

@ -20,7 +20,9 @@ from _pytest.config import _strtobool
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import OutputVerbosity
from _pytest.config import parse_warning_filter from _pytest.config import parse_warning_filter
from _pytest.config.argparsing import Parser
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_common_ancestor
@ -2181,3 +2183,74 @@ class TestDebugOptions:
"*Default: pytestdebug.log.", "*Default: pytestdebug.log.",
] ]
) )
class TestOutputVerbosity:
SOME_OUTPUT_TYPE = "foo"
SOME_OUTPUT_VERBOSITY_LEVEL = 5
class VerbosityIni:
def pytest_addoption(self, parser: Parser) -> None:
OutputVerbosity.add_ini(
parser, TestOutputVerbosity.SOME_OUTPUT_TYPE, help="some help text"
)
def test_verbose_matches_option_verbose(
self, pytester: Pytester, tmp_path: Path
) -> None:
tmp_path.joinpath("pytest.ini").write_text(
textwrap.dedent(
"""\
[pytest]
addopts = --verbose
"""
),
encoding="utf-8",
)
config = pytester.parseconfig(tmp_path)
assert config.option.verbose == config.output_verbosity.verbose
def test_level_matches_verbost_when_not_specified(
self, pytester: Pytester, tmp_path: Path
) -> None:
tmp_path.joinpath("pytest.ini").write_text(
textwrap.dedent(
"""\
[pytest]
addopts = --verbose
"""
),
encoding="utf-8",
)
pytester.plugins = [TestOutputVerbosity.VerbosityIni()]
config = pytester.parseconfig(tmp_path)
assert (
config.output_verbosity.verbosity_for(TestOutputVerbosity.SOME_OUTPUT_TYPE)
== config.output_verbosity.verbose
)
def test_level_matches_specified_override(
self, pytester: Pytester, tmp_path: Path
) -> None:
tmp_path.joinpath("pytest.ini").write_text(
textwrap.dedent(
f"""\
[pytest]
addopts = --verbose
verbosity_{TestOutputVerbosity.SOME_OUTPUT_TYPE} = {TestOutputVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL}
"""
),
encoding="utf-8",
)
pytester.plugins = [TestOutputVerbosity.VerbosityIni()]
config = pytester.parseconfig(tmp_path)
assert (
config.output_verbosity.verbosity_for(TestOutputVerbosity.SOME_OUTPUT_TYPE)
== TestOutputVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL
)