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 1 commit
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 @@ -291,6 +291,7 @@ Ondřej Súkup
Oscar Benjamin
Parth Patel
Patrick Hayes
Patrick Lannigan
Paul Müller
Paul Reece
Pauli Virtanen
Expand Down
1 change: 1 addition & 0 deletions changelog/11387.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:confval:`verbosity_assertions` option added to be able to control assertion output independent of the application wide verbosity level.
plannigan marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions doc/en/how-to/output.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
however some plugins might make use of higher verbosity.

Fine gain verbosity
plannigan marked this conversation as resolved.
Show resolved Hide resolved
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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.

``verbosity_assertions``: Controls how verbose the assertion output should be when pytest is executed. A value of ``2``
plannigan marked this conversation as resolved.
Show resolved Hide resolved
would have the same output as the previous example, but each test inside the file is shown by a single character in the
plannigan marked this conversation as resolved.
Show resolved Hide resolved
output.

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 @@ -1819,6 +1819,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.


.. confval:: xfail_strict

If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
Expand Down
9 changes: 9 additions & 0 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from _pytest.assertion.rewrite import assertstate_key
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config import OutputVerbosity
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item

Expand Down Expand Up @@ -42,6 +43,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Enables the pytest_assertion_pass hook. "
"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."
plannigan marked this conversation as resolved.
Show resolved Hide resolved
),
)


def register_assert_rewrite(*names: str) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/assertion/truncate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""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 a verbosity level of at least 2 or running on CI.
plannigan marked this conversation as resolved.
Show resolved Hide resolved
"""
from typing import List
from typing import Optional
Expand All @@ -26,7 +26,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.output_verbosity.verbosity_for("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 @@ -161,7 +161,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.output_verbosity.verbosity_for("assertions")

# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
Expand Down
51 changes: 50 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,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 @@ -1020,6 +1020,7 @@ def __init__(
)
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
self.output_verbosity = OutputVerbosity(self)

if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
Expand Down Expand Up @@ -1662,6 +1663,54 @@ def _warn_about_skipped_plugins(self) -> None:
)


class OutputVerbosity:
Copy link
Member

Choose a reason for hiding this comment

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

Why OutputVerbosity and not just Verbosity? I can't think of a verbosity that is not related to output, so it seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm open tweaking the name. I generally default to more explicit names.

DEFAULT = "auto"
_option_name_fmt = "verbosity_{}"

def __init__(self, config: Config) -> None:
self._config = config

@property
def verbose(self) -> int:
plannigan marked this conversation as resolved.
Show resolved Hide resolved
"""Application wide verbosity level."""
return cast(int, self._config.option.verbose)
plannigan marked this conversation as resolved.
Show resolved Hide resolved

def verbosity_for(self, output_type: str) -> int:
Copy link
Member

@nicoddemus nicoddemus Oct 23, 2023

Choose a reason for hiding this comment

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

I think if one passes an invalid output_type at the moment, we will get an error (due to self._config.getini).

We probably want to have this classs accessible to plugins and users, via config.output_verbosity, so we need to think about its API carefully as we will need to support this officially moving on.

Some points:

  1. In tandem with pytest's direction regarding type safety, I think we should use an Enum with the output types currently available, something along the lines of:

    class VerbosityType(Enum):
        """..."""     
    
        Global = "global"
        Assertions = "assertions"

    Instead of a simple output_type: str parameter/value.

  2. The class name is OutputVerbosity and the public methods are verbosity and verbosity_for, which read a bit redundant: confg.output_verbosity.verbosity, confg.output_verbosity.get_verbosity_for. I suggest we use simply get, defaulting to the Global verbosity:

    def get(self, type: VerbosityType = VerbosityType.Global) -> int:
        ...

    Which reads well I think:

    config.output_verbosity.get()
    config.output_verbosity.get(VerbosityType.Assertions)
  3. We need to properly document both OutputVerbosity and VerbosityType in the reference docs (including some usage examples, but possibly we can write those in the class docstring). We also need to expose VerbosityType in the pytest namespace.

  4. Given OutputVerbosity is public, we should make OutputVerbosity.addini private, as it is intended for internal use only.

I'm not married to the names above, so suggestions and bikeshedding are welcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I opted for str because I was thinking that plugins might want also register types, like any other config setting. Which would mean that it wouldn't be possible to statically define all of the types. However, reflecting on that, I'm not sure that level of extensibility is necessary. It is likely better for pytest to say "here are N buckets that a plugin can utilize" and that way users of multiple plugins that leverage this functionality will see consistent behavior without needing to enable M settings.
  2. That is fair. It also works nicely with the enum suggestion.
  3. Sure
  4. I had it as public for the reasons described in 1, but it makes sense to make it private based on that change.

plannigan marked this conversation as resolved.
Show resolved Hide resolved
"""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:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since many things will add these type of settings, I thought it would be helpful to have this function to ensure it is dong consistently. I'm not completely in love with this level of indirection. But it is necessary since config settings are added to the Parser before the Config object exists.

Copy link
Member

Choose a reason for hiding this comment

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

Good idea. 👍

"""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:
try:
assert False
Expand Down
2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from _pytest.config import hookimpl
from _pytest.config import hookspec
from _pytest.config import main
from _pytest.config import OutputVerbosity
from _pytest.config import PytestPluginManager
from _pytest.config import UsageError
from _pytest.config.argparsing import OptionGroup
Expand Down Expand Up @@ -126,6 +127,7 @@
"Module",
"MonkeyPatch",
"OptionGroup",
"OutputVerbosity",
"Package",
"param",
"Parser",
Expand Down
21 changes: 16 additions & 5 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@
from _pytest.pytester import Pytester


def mock_config(verbose=0):
class Config:
def getoption(self, name):
if name == "verbose":
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 OutputVerbosity:
@property
def verbose(self) -> int:
return verbose

Check warning on line 24 in testing/test_assertion.py

View check run for this annotation

Codecov / codecov/patch

testing/test_assertion.py#L24

Added line #L24 was not covered by tests

def verbosity_for(self, output_type: str) -> int:
if output_type == "assertions":
if assertion_override is not None:
return assertion_override

Check warning on line 29 in testing/test_assertion.py

View check run for this annotation

Codecov / codecov/patch

testing/test_assertion.py#L29

Added line #L29 was not covered by tests
return verbose
raise KeyError("Not mocked out: %s" % name)

raise KeyError("Not mocked out: %s" % output_type)

Check warning on line 32 in testing/test_assertion.py

View check run for this annotation

Codecov / codecov/patch

testing/test_assertion.py#L32

Added line #L32 was not covered by tests

class Config:
def __init__(self) -> None:
self.output_verbosity = OutputVerbosity()

return Config()

Expand Down
73 changes: 73 additions & 0 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import ExitCode
from _pytest.config import OutputVerbosity
from _pytest.config import parse_warning_filter
from _pytest.config.argparsing import Parser
from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor
Expand Down Expand Up @@ -2181,3 +2183,74 @@ def test_debug_help(self, pytester: Pytester) -> None:
"*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
)
Loading