Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 changelog/14675.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :confval:`truncation_limit_lines` and :confval:`truncation_limit_chars` configuration options now accept integer values in TOML configuration files, while still accepting strings for backward compatibility.
8 changes: 4 additions & 4 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2664,7 +2664,7 @@ passed multiple times. The expected format is ``name=value``. For example::


.. confval:: truncation_limit_chars
:type: ``int``
:type: ``int | str``
:default: ``640``

Controls maximum number of characters to truncate assertion message contents.
Expand Down Expand Up @@ -2693,7 +2693,7 @@ passed multiple times. The expected format is ``name=value``. For example::


.. confval:: truncation_limit_lines
:type: ``int``
:type: ``int | str``
:default: ``8``

Controls maximum number of lines to truncate assertion message contents.
Expand Down Expand Up @@ -3720,10 +3720,10 @@ All the command-line flags can also be obtained by running ``pytest --help``::
enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files.
truncation_limit_lines (string):
truncation_limit_lines (int | string):
Set threshold of LINES after which truncation will
take effect
truncation_limit_chars (string):
truncation_limit_chars (int | string):
Set threshold of CHARS after which truncation will
take effect
assertion_text_diff_style (string):
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/assertion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ def pytest_addoption(parser: Parser) -> None:

parser.addini(
"truncation_limit_lines",
type=int | str,
default=None,
help="Set threshold of LINES after which truncation will take effect",
)
parser.addini(
"truncation_limit_chars",
type=int | str,
default=None,
help=("Set threshold of CHARS after which truncation will take effect"),
)
Expand Down
31 changes: 29 additions & 2 deletions src/_pytest/assertion/_typing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import ClassVar
from typing import Literal
from typing import Protocol
from typing import TYPE_CHECKING


if TYPE_CHECKING:
from _pytest.config import Config


_AssertionTextDiffStyle = Literal["ndiff", "block"]
Expand All @@ -17,8 +23,29 @@ class TruncationBudget:
dimension; ``0`` leaves it unbounded (the limit is disabled).
"""

max_lines: int
max_chars: int
#: Default limits applied when the corresponding ini option is left unset.
DEFAULT_MAX_LINES: ClassVar[int] = 8
DEFAULT_MAX_CHARS: ClassVar[int] = DEFAULT_MAX_LINES * 80

max_lines: int = DEFAULT_MAX_LINES
max_chars: int = DEFAULT_MAX_CHARS

@classmethod
def from_config(cls, config: Config) -> TruncationBudget:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since this introduces a cyclic dependency, maybe the previous location is better.

"""Build a budget from the ``truncation_limit_*`` ini options.

Both options are registered with ``type=int | str`` for

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this comment belongs on the addini, not here. Can also be shorter 😉

backward compatibility, so :meth:`~_pytest.config.Config.getini` may
return an ``int`` (native TOML value) or a ``str`` (INI files, ``-o``
overrides); it returns ``None`` when the option is unset, which falls
back to the default limit.
"""
max_lines = config.getini("truncation_limit_lines")
max_chars = config.getini("truncation_limit_chars")
return cls(
max_lines=cls.DEFAULT_MAX_LINES if max_lines is None else int(max_lines),
max_chars=cls.DEFAULT_MAX_CHARS if max_chars is None else int(max_chars),
)


# Reusable "no cap" budget, used as a default argument (B008).
Expand Down
22 changes: 8 additions & 14 deletions src/_pytest/assertion/truncate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@

from collections.abc import Iterable

from _pytest.assertion._typing import NO_TRUNCATION_BUDGET
from _pytest.assertion._typing import TruncationBudget
from _pytest.compat import running_on_ci
from _pytest.config import Config


DEFAULT_MAX_LINES = 8
DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
USAGE_MSG = "use '-vv' to show"
TRUNCATION_MSG = f"...Full output truncated, {USAGE_MSG}"

Expand Down Expand Up @@ -61,23 +60,18 @@ def materialize_with_truncation(lines: Iterable[str], config: Config) -> list[st

def _get_truncation_parameters(config: Config) -> tuple[bool, TruncationBudget]:
"""Return the truncation parameters from the given config, as (should truncate, budget)."""
# We do not need to truncate if one of conditions is met:
# We do not need to truncate if one of these conditions is met:
# 1. Verbosity level is 2 or more;
# 2. Test is being run in CI environment;
# 3. Both truncation_limit_lines and truncation_limit_chars
# .ini parameters are set to 0 explicitly.
max_lines = config.getini("truncation_limit_lines")
max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES)

max_chars = config.getini("truncation_limit_chars")
max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS)

# are set to 0 explicitly.
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
if verbose >= 2 or running_on_ci():
return False, NO_TRUNCATION_BUDGET

should_truncate = verbose < 2 and not running_on_ci()
should_truncate = should_truncate and (max_lines > 0 or max_chars > 0)

return should_truncate, TruncationBudget(max_lines=max_lines, max_chars=max_chars)
budget = TruncationBudget.from_config(config)
should_truncate = budget.max_lines > 0 or budget.max_chars > 0
return should_truncate, budget


def _truncate_explanation(
Expand Down
62 changes: 62 additions & 0 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,68 @@ def test():
]
)

@pytest.mark.parametrize(
"config",
[
# Native [tool.pytest] uses TOML types, so an int is accepted (#14675).
pytest.param(
"""
[tool.pytest]
truncation_limit_lines = 3
truncation_limit_chars = 0
""",
id="native-toml-int",
),
# A string in native [tool.pytest] must keep working (backward compat).
pytest.param(
"""
[tool.pytest]
truncation_limit_lines = "3"
truncation_limit_chars = "0"
""",
id="native-toml-string",
),
# [tool.pytest.ini_options] keeps the string-based INI behaviour.
pytest.param(
"""
[tool.pytest.ini_options]
truncation_limit_lines = "3"
truncation_limit_chars = "0"
""",
id="ini-options-string",
),
# ... and also accepts a bare int, coerced like the INI format does.
pytest.param(
"""
[tool.pytest.ini_options]
truncation_limit_lines = 3
truncation_limit_chars = 0
""",
id="ini-options-int",
),
],
)
def test_truncation_limits_accept_int_and_string(
self, monkeypatch, pytester: Pytester, config: str
) -> None:
"""Truncation limits accept both int and string values in TOML (#14675)."""
pytester.makepyfile(
"""\
string_a = "123456789\\n23456789\\n3"
string_b = "123456789\\n23456789\\n4"

def test():
assert string_a == string_b
"""
)
monkeypatch.delenv("CI", raising=False)
pytester.makepyprojecttoml(config)

result = pytester.runpytest()

result.stdout.no_fnmatch_line("*TypeError*")
result.stdout.fnmatch_lines(["*Full output truncated, use '-vv' to show*"])


class TestMaterializeWithTruncation:
"""Tests for ``truncate.materialize_with_truncation``."""
Expand Down