Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OuputVerbosity and use it in assertions #11473

Merged
merged 30 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c93bc1e
Add OuputVerbosity and use it in assertions
plannigan Sep 23, 2023
64f7277
Add test cases to confirm implementation of mock_config()
plannigan Oct 1, 2023
12cb693
Fix typo
plannigan Oct 1, 2023
5b36f86
Merge branch 'main' into fine-grain-verbosity
plannigan Oct 11, 2023
cb9729b
Merge branch 'main' into fine-grain-verbosity
plannigan Oct 21, 2023
ec76cf5
Apply documentation suggestions
plannigan Oct 31, 2023
933742a
Adjust verbosity level retrival
plannigan Oct 31, 2023
8e38fc0
Expose VerbosityType
plannigan Oct 31, 2023
cfb1654
Assert isinstance intead of cast
plannigan Oct 31, 2023
4d7ceb8
Improve documentation of functionality
plannigan Nov 5, 2023
855f579
Merge branch 'main' into fine-grain-verbosity
plannigan Nov 5, 2023
d2529ed
Fix docs
nicoddemus Nov 7, 2023
c5cc08a
Improve docs
nicoddemus Nov 7, 2023
1e91814
Remove unused _option_name_fmt
nicoddemus Nov 7, 2023
b8714de
Remove OutputVerbosity.verbose
nicoddemus Nov 7, 2023
50c0b93
Additional design changes from review
plannigan Nov 10, 2023
192b62d
Merge branch 'main' into fine-grain-verbosity
plannigan Nov 10, 2023
1baca27
Update changelog/11387.feature.rst
nicoddemus Nov 11, 2023
21d8111
User fine grained verbosity for saferepr max size
plannigan Nov 11, 2023
675584f
Add end to end test of assertion fine grained verbosity
plannigan Nov 11, 2023
9580f4b
Merge branch 'main' into fine-grain-verbosity
plannigan Nov 11, 2023
b47b203
More explict names
plannigan Nov 13, 2023
91bfcab
Test case for unknown verbosity type
plannigan Nov 13, 2023
5d2183d
Improve documentation of get_verbosity() and configuraiton
plannigan Nov 13, 2023
6ea1fe5
Clean up end to end test
plannigan Nov 13, 2023
8fdfc1e
Merge branch 'main' into fine-grain-verbosity
plannigan Nov 13, 2023
e663889
Mark constant as final
plannigan Nov 18, 2023
d943866
Check parser for ini name instead of manually listing known types
plannigan Nov 18, 2023
b91a467
Merge branch 'main' into fine-grain-verbosity
plannigan Nov 18, 2023
59e39c8
Restore whitespace changes
plannigan Nov 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ Ondřej Súkup
Oscar Benjamin
Parth Patel
Patrick Hayes
Patrick Lannigan
Paul Müller
Paul Reece
Pauli Virtanen
Expand Down
5 changes: 5 additions & 0 deletions changelog/11387.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.

See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.

For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.
14 changes: 14 additions & 0 deletions doc/en/how-to/output.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ 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,
however some plugins might make use of higher verbosity.

.. _`pytest.fine_grained_verbosity`:

Fine-grained verbosity
~~~~~~~~~~~~~~~~~~~~~~

In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only have one aspect at the moment, it seems premature to write the prose in a general/list format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention is to implement some of the other verbosity types I identified once this PR is merged. A different suggestion was to add a note that only one is currently supported. That seems sufficient to me if "life happens" and it takes me some time to get to adding other types.

This is done by setting a verbosity level in the configuration file for the specific aspect of the output.

:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
``pytest --no-header`` with 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.

(Note: currently this is the only option available, but more might be added in the future).

plannigan marked this conversation as resolved.
Show resolved Hide resolved
.. _`pytest.detailed_failed_tests_usage`:

Producing a detailed summary report
Expand Down
13 changes: 13 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,19 @@ passed multiple times. The expected format is ``name=value``. For example::
clean_db


.. confval:: verbosity_assertions
plannigan marked this conversation as resolved.
Show resolved Hide resolved

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 (via the ``-v`` command-line option). A special value of
"auto" can be used to explicitly use the global verbosity level.


.. confval:: xfail_strict

If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Enables the pytest_assertion_pass hook. "
"Make sure to delete any previously generated pyc cache files.",
)
Config._add_verbosity_ini(
parser,
Config.VERBOSITY_ASSERTIONS,
help=(
"Specify a verbosity level for assertions, overriding the main level. "
"Higher levels will provide more detailed explanation when an assertion fails."
),
)


def register_assert_rewrite(*names: str) -> None:
Expand Down
5 changes: 4 additions & 1 deletion src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,10 @@ def _saferepr(obj: object) -> str:

def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
"""Get `maxsize` configuration for saferepr based on the given config object."""
verbosity = config.getoption("verbose") if config is not None else 0
if config is None:
verbosity = 0
else:
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
if verbosity >= 2:
return None
if verbosity >= 1:
Expand Down
5 changes: 3 additions & 2 deletions src/_pytest/assertion/truncate.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Utilities for truncating assertion output.

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 an assertions verbosity level of at least 2 or running on CI.
"""
from typing import List
from typing import Optional

from _pytest.assertion import util
from _pytest.config import Config
from _pytest.nodes import Item


plannigan marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -26,7 +27,7 @@ def truncate_if_required(

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


Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/assertion/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)

# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
Expand Down
75 changes: 74 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import Callable
from typing import cast
from typing import Dict
from typing import Final
from typing import final
from typing import Generator
from typing import IO
Expand Down Expand Up @@ -69,7 +70,7 @@
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
from .argparsing import Argument, Parser


_PluggyPlugin = object
Expand Down Expand Up @@ -1650,6 +1651,78 @@
"""Deprecated, use getoption(skip=True) instead."""
return self.getoption(name, skip=True)

#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
VERBOSITY_ASSERTIONS: Final = "assertions"
_VERBOSITY_INI_DEFAULT: Final = "auto"

def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
plannigan marked this conversation as resolved.
Show resolved Hide resolved
r"""Retrieve the verbosity level for a fine-grained verbosity type.

:param verbosity_type: Verbosity type to get level for. If a level is
configured for the given type, that value will be returned. If the
given type is not a known verbosity type, the global verbosity
level will be returned. If the given type is None (default), the
global verbosity level will be returned.

To configure a level for a fine-grained verbosity type, the
configuration file should have a setting for the configuration name
and a numeric value for the verbosity level. A special value of "auto"
can be used to explicitly use the global verbosity level.

Example:

.. code-block:: ini

# content of pytest.ini
[pytest]
verbosity_assertions = 2

.. code-block:: console

pytest -v

.. code-block:: python

print(config.get_verbosity()) # 1
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
"""
global_level = self.option.verbose
assert isinstance(global_level, int)
if verbosity_type is None:
return global_level

Check warning on line 1692 in src/_pytest/config/__init__.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/config/__init__.py#L1692

Added line #L1692 was not covered by tests
plannigan marked this conversation as resolved.
Show resolved Hide resolved

ini_name = Config._verbosity_ini_name(verbosity_type)
if ini_name not in self._parser._inidict:
return global_level

level = self.getini(ini_name)
if level == Config._VERBOSITY_INI_DEFAULT:
bluetech marked this conversation as resolved.
Show resolved Hide resolved
return global_level

return int(level)

@staticmethod
bluetech marked this conversation as resolved.
Show resolved Hide resolved
def _verbosity_ini_name(verbosity_type: str) -> str:
return f"verbosity_{verbosity_type}"

@staticmethod
def _add_verbosity_ini(parser: "Parser", verbosity_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 verbosity_type: Fine-grained verbosity category.
:param help: Description of the output this type controls.

The value should be retrieved via a call to
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
"""
parser.addini(
Config._verbosity_ini_name(verbosity_type),
help=help,
type="string",
default=Config._VERBOSITY_INI_DEFAULT,
)

def _warn_about_missing_assertion(self, mode: str) -> None:
if not _assertion_supported():
if mode == "plain":
Expand Down
104 changes: 98 additions & 6 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,68 @@
from _pytest import outcomes
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.config import Config as _Config
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester


def mock_config(verbose=0):
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the move away from getoption(), this needed to get a bit more complicated. (In the long term, it would probably be beneficial move the code base to a consistent way to access the verbosity level.)

class TerminalWriter:
def _highlight(self, source, lexer):
return source

class Config:
def getoption(self, name):
if name == "verbose":
return verbose
raise KeyError("Not mocked out: %s" % name)

def get_terminal_writer(self):
return TerminalWriter()

def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
if verbosity_type is None:
return verbose
if verbosity_type == _Config.VERBOSITY_ASSERTIONS:
if assertion_override is not None:
return assertion_override
return verbose

raise KeyError(f"Not mocked out: {verbosity_type}")

return Config()


class TestMockConfig:
SOME_VERBOSITY_LEVEL = 3
SOME_OTHER_VERBOSITY_LEVEL = 10

def test_verbose_exposes_value(self):
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL

def test_get_assertion_override_not_set_verbose_value(self):
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

assert (
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
== TestMockConfig.SOME_VERBOSITY_LEVEL
)

def test_get_assertion_override_set_custom_value(self):
config = mock_config(
verbose=TestMockConfig.SOME_VERBOSITY_LEVEL,
assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL,
)

assert (
config.get_verbosity(_Config.VERBOSITY_ASSERTIONS)
== TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL
)

def test_get_unsupported_type_error(self):
config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL)

with pytest.raises(KeyError):
config.get_verbosity("--- NOT A VERBOSITY LEVEL ---")
bluetech marked this conversation as resolved.
Show resolved Hide resolved


class TestImportHookInstallation:
@pytest.mark.parametrize("initial_conftest", [True, False])
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
Expand Down Expand Up @@ -1836,3 +1877,54 @@ def test_comparisons_handle_colors(
)

result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False)


def test_fine_grained_assertion_verbosity(pytester: Pytester):
long_text = "Lorem ipsum dolor sit amet " * 10
p = pytester.makepyfile(
f"""
def test_ok():
pass


def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
assert fruits1 == fruits2


def test_numbers_fail():
number_to_text1 = {{str(x): x for x in range(5)}}
number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}}
assert number_to_text1 == number_to_text2


def test_long_text_fail():
long_text = "{long_text}"
assert "hello world" in long_text
"""
)
pytester.makeini(
"""
[pytest]
verbosity_assertions = 2
"""
)
result = pytester.runpytest(p)

result.stdout.fnmatch_lines(
[
f"{p.name} .FFF [100%]",
"E At index 2 diff: 'grapes' != 'orange'",
"E Full diff:",
"E - ['banana', 'apple', 'orange', 'melon', 'kiwi']",
"E ? ^ ^^",
"E + ['banana', 'apple', 'grapes', 'melon', 'kiwi']",
"E ? ^ ^ +",
"E Full diff:",
"E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}",
"E ? - - - - - - - -",
"E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}",
f"E AssertionError: assert 'hello world' in '{long_text}'",
]
)
6 changes: 4 additions & 2 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -2056,13 +2056,15 @@ class TestReprSizeVerbosity:
)
def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None:
class FakeConfig:
def getoption(self, name: str) -> int:
assert name == "verbose"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
return verbose

config = FakeConfig()
assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size

def test_get_maxsize_for_saferepr_no_config(self) -> None:
assert _get_maxsize_for_saferepr(None) == DEFAULT_REPR_MAX_SIZE

def create_test_file(self, pytester: Pytester, size: int) -> None:
pytester.makepyfile(
f"""
Expand Down
Loading