Skip to content

Commit 9dc1fc4

Browse files
authored
Add verbosity_assertions and config.get_verbosity
Fixes #11387
1 parent 80442ae commit 9dc1fc4

File tree

12 files changed

+299
-13
lines changed

12 files changed

+299
-13
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ Ondřej Súkup
293293
Oscar Benjamin
294294
Parth Patel
295295
Patrick Hayes
296+
Patrick Lannigan
296297
Paul Müller
297298
Paul Reece
298299
Pauli Virtanen

changelog/11387.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.
2+
3+
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
4+
5+
For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.

doc/en/how-to/output.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,20 @@ situations, for example you are shown even fixtures that start with ``_`` if you
286286
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
287287
however some plugins might make use of higher verbosity.
288288

289+
.. _`pytest.fine_grained_verbosity`:
290+
291+
Fine-grained verbosity
292+
~~~~~~~~~~~~~~~~~~~~~~
293+
294+
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
295+
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
296+
297+
:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
298+
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
299+
the file is shown by a single character in the output.
300+
301+
(Note: currently this is the only option available, but more might be added in the future).
302+
289303
.. _`pytest.detailed_failed_tests_usage`:
290304

291305
Producing a detailed summary report

doc/en/reference/reference.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,19 @@ passed multiple times. The expected format is ``name=value``. For example::
18221822
clean_db
18231823
18241824
1825+
.. confval:: verbosity_assertions
1826+
1827+
Set a verbosity level specifically for assertion related output, overriding the application wide level.
1828+
1829+
.. code-block:: ini
1830+
1831+
[pytest]
1832+
verbosity_assertions = 2
1833+
1834+
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
1835+
"auto" can be used to explicitly use the global verbosity level.
1836+
1837+
18251838
.. confval:: xfail_strict
18261839

18271840
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the

src/_pytest/assertion/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
4242
help="Enables the pytest_assertion_pass hook. "
4343
"Make sure to delete any previously generated pyc cache files.",
4444
)
45+
Config._add_verbosity_ini(
46+
parser,
47+
Config.VERBOSITY_ASSERTIONS,
48+
help=(
49+
"Specify a verbosity level for assertions, overriding the main level. "
50+
"Higher levels will provide more detailed explanation when an assertion fails."
51+
),
52+
)
4553

4654

4755
def register_assert_rewrite(*names: str) -> None:

src/_pytest/assertion/rewrite.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,10 @@ def _saferepr(obj: object) -> str:
426426

427427
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
428428
"""Get `maxsize` configuration for saferepr based on the given config object."""
429-
verbosity = config.getoption("verbose") if config is not None else 0
429+
if config is None:
430+
verbosity = 0
431+
else:
432+
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
430433
if verbosity >= 2:
431434
return None
432435
if verbosity >= 1:

src/_pytest/assertion/truncate.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Utilities for truncating assertion output.
22
33
Current default behaviour is to truncate assertion explanations at
4-
~8 terminal lines, unless running in "-vv" mode or running on CI.
4+
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
55
"""
66
from typing import List
77
from typing import Optional
88

99
from _pytest.assertion import util
10+
from _pytest.config import Config
1011
from _pytest.nodes import Item
1112

1213

@@ -26,7 +27,7 @@ def truncate_if_required(
2627

2728
def _should_truncate_item(item: Item) -> bool:
2829
"""Whether or not this test item is eligible for truncation."""
29-
verbose = item.config.option.verbose
30+
verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
3031
return verbose < 2 and not util.running_on_ci()
3132

3233

src/_pytest/assertion/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def assertrepr_compare(
168168
config, op: str, left: Any, right: Any, use_ascii: bool = False
169169
) -> Optional[List[str]]:
170170
"""Return specialised explanations for some operators/operands."""
171-
verbose = config.getoption("verbose")
171+
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
172172

173173
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
174174
# See issue #3246.

src/_pytest/config/__init__.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from typing import Callable
2323
from typing import cast
2424
from typing import Dict
25+
from typing import Final
2526
from typing import final
2627
from typing import Generator
2728
from typing import IO
@@ -69,7 +70,7 @@
6970
if TYPE_CHECKING:
7071
from _pytest._code.code import _TracebackStyle
7172
from _pytest.terminal import TerminalReporter
72-
from .argparsing import Argument
73+
from .argparsing import Argument, Parser
7374

7475

7576
_PluggyPlugin = object
@@ -1650,6 +1651,78 @@ def getvalueorskip(self, name: str, path=None):
16501651
"""Deprecated, use getoption(skip=True) instead."""
16511652
return self.getoption(name, skip=True)
16521653

1654+
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
1655+
VERBOSITY_ASSERTIONS: Final = "assertions"
1656+
_VERBOSITY_INI_DEFAULT: Final = "auto"
1657+
1658+
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
1659+
r"""Retrieve the verbosity level for a fine-grained verbosity type.
1660+
1661+
:param verbosity_type: Verbosity type to get level for. If a level is
1662+
configured for the given type, that value will be returned. If the
1663+
given type is not a known verbosity type, the global verbosity
1664+
level will be returned. If the given type is None (default), the
1665+
global verbosity level will be returned.
1666+
1667+
To configure a level for a fine-grained verbosity type, the
1668+
configuration file should have a setting for the configuration name
1669+
and a numeric value for the verbosity level. A special value of "auto"
1670+
can be used to explicitly use the global verbosity level.
1671+
1672+
Example:
1673+
1674+
.. code-block:: ini
1675+
1676+
# content of pytest.ini
1677+
[pytest]
1678+
verbosity_assertions = 2
1679+
1680+
.. code-block:: console
1681+
1682+
pytest -v
1683+
1684+
.. code-block:: python
1685+
1686+
print(config.get_verbosity()) # 1
1687+
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
1688+
"""
1689+
global_level = self.option.verbose
1690+
assert isinstance(global_level, int)
1691+
if verbosity_type is None:
1692+
return global_level
1693+
1694+
ini_name = Config._verbosity_ini_name(verbosity_type)
1695+
if ini_name not in self._parser._inidict:
1696+
return global_level
1697+
1698+
level = self.getini(ini_name)
1699+
if level == Config._VERBOSITY_INI_DEFAULT:
1700+
return global_level
1701+
1702+
return int(level)
1703+
1704+
@staticmethod
1705+
def _verbosity_ini_name(verbosity_type: str) -> str:
1706+
return f"verbosity_{verbosity_type}"
1707+
1708+
@staticmethod
1709+
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
1710+
"""Add a output verbosity configuration option for the given output type.
1711+
1712+
:param parser: Parser for command line arguments and ini-file values.
1713+
:param verbosity_type: Fine-grained verbosity category.
1714+
:param help: Description of the output this type controls.
1715+
1716+
The value should be retrieved via a call to
1717+
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
1718+
"""
1719+
parser.addini(
1720+
Config._verbosity_ini_name(verbosity_type),
1721+
help=help,
1722+
type="string",
1723+
default=Config._VERBOSITY_INI_DEFAULT,
1724+
)
1725+
16531726
def _warn_about_missing_assertion(self, mode: str) -> None:
16541727
if not _assertion_supported():
16551728
if mode == "plain":

testing/test_assertion.py

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,68 @@
1313
from _pytest import outcomes
1414
from _pytest.assertion import truncate
1515
from _pytest.assertion import util
16+
from _pytest.config import Config as _Config
1617
from _pytest.monkeypatch import MonkeyPatch
1718
from _pytest.pytester import Pytester
1819

1920

20-
def mock_config(verbose=0):
21+
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
2122
class TerminalWriter:
2223
def _highlight(self, source, lexer):
2324
return source
2425

2526
class Config:
26-
def getoption(self, name):
27-
if name == "verbose":
28-
return verbose
29-
raise KeyError("Not mocked out: %s" % name)
30-
3127
def get_terminal_writer(self):
3228
return TerminalWriter()
3329

30+
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
31+
if verbosity_type is None:
32+
return verbose
33+
if verbosity_type == _Config.VERBOSITY_ASSERTIONS:
34+
if assertion_override is not None:
35+
return assertion_override
36+
return verbose
37+
38+
raise KeyError(f"Not mocked out: {verbosity_type}")
39+
3440
return Config()
3541

3642

43+
class TestMockConfig:
44+
SOME_VERBOSITY_LEVEL = 3
45+
SOME_OTHER_VERBOSITY_LEVEL = 10
46+
47+
def test_verbose_exposes_value(self):
48+
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
49+
50+
assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL
51+
52+
def test_get_assertion_override_not_set_verbose_value(self):
53+
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
54+
55+
assert (
56+
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
57+
== TestMockConfig.SOME_VERBOSITY_LEVEL
58+
)
59+
60+
def test_get_assertion_override_set_custom_value(self):
61+
config = mock_config(
62+
verbose=TestMockConfig.SOME_VERBOSITY_LEVEL,
63+
assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL,
64+
)
65+
66+
assert (
67+
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
68+
== TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL
69+
)
70+
71+
def test_get_unsupported_type_error(self):
72+
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)
73+
74+
with pytest.raises(KeyError):
75+
config.get_verbosity("--- NOT A VERBOSITY LEVEL ---")
76+
77+
3778
class TestImportHookInstallation:
3879
@pytest.mark.parametrize("initial_conftest", [True, False])
3980
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
@@ -1836,3 +1877,54 @@ def test_comparisons_handle_colors(
18361877
)
18371878

18381879
result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)
1880+
1881+
1882+
def test_fine_grained_assertion_verbosity(pytester: Pytester):
1883+
long_text = "Lorem ipsum dolor sit amet " * 10
1884+
p = pytester.makepyfile(
1885+
f"""
1886+
def test_ok():
1887+
pass
1888+
1889+
1890+
def test_words_fail():
1891+
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
1892+
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
1893+
assert fruits1 == fruits2
1894+
1895+
1896+
def test_numbers_fail():
1897+
number_to_text1 = {{str(x): x for x in range(5)}}
1898+
number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}}
1899+
assert number_to_text1 == number_to_text2
1900+
1901+
1902+
def test_long_text_fail():
1903+
long_text = "{long_text}"
1904+
assert "hello world" in long_text
1905+
"""
1906+
)
1907+
pytester.makeini(
1908+
"""
1909+
[pytest]
1910+
verbosity_assertions = 2
1911+
"""
1912+
)
1913+
result = pytester.runpytest(p)
1914+
1915+
result.stdout.fnmatch_lines(
1916+
[
1917+
f"{p.name} .FFF [100%]",
1918+
"E At index 2 diff: 'grapes' != 'orange'",
1919+
"E Full diff:",
1920+
"E - ['banana', 'apple', 'orange', 'melon', 'kiwi']",
1921+
"E ? ^ ^^",
1922+
"E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']",
1923+
"E ? ^ ^ +",
1924+
"E Full diff:",
1925+
"E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}",
1926+
"E ? - - - - - - - -",
1927+
"E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}",
1928+
f"E AssertionError: assert 'hello world' in '{long_text}'",
1929+
]
1930+
)

0 commit comments

Comments
 (0)