diff --git a/changelog/14675.bugfix.rst b/changelog/14675.bugfix.rst new file mode 100644 index 00000000000..234e7be6183 --- /dev/null +++ b/changelog/14675.bugfix.rst @@ -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. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 4f2e0b70850..912e937ceef 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -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. @@ -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. @@ -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): diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index ad2454ab820..4813bb3214b 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -49,13 +49,17 @@ def pytest_addoption(parser: Parser) -> None: "Make sure to delete any previously generated pyc cache files.", ) + # ``int | str`` (not plain ``int``) for backward compatibility: INI files + # and ``-o`` overrides provide the value as a string. 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"), ) diff --git a/src/_pytest/assertion/_typing.py b/src/_pytest/assertion/_typing.py index d9f0ad8e78c..689bd867ae3 100644 --- a/src/_pytest/assertion/_typing.py +++ b/src/_pytest/assertion/_typing.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import ClassVar from typing import Literal from typing import Protocol @@ -17,8 +18,12 @@ 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 # Reusable "no cap" budget, used as a default argument (B008). diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 4627b7c6fe1..cb9fe79186b 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -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}" @@ -61,23 +60,36 @@ 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) + # are set to 0 explicitly. + verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + if verbose >= 2 or running_on_ci(): + return False, NO_TRUNCATION_BUDGET - max_chars = config.getini("truncation_limit_chars") - max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS) + budget = _budget_from_config(config) + should_truncate = budget.max_lines > 0 or budget.max_chars > 0 + return should_truncate, budget - verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) - should_truncate = verbose < 2 and not running_on_ci() - should_truncate = should_truncate and (max_lines > 0 or max_chars > 0) +def _budget_from_config(config: Config) -> TruncationBudget: + """Build a budget from the ``truncation_limit_*`` ini options. - return should_truncate, TruncationBudget(max_lines=max_lines, max_chars=max_chars) + ``getini`` returns ``None`` when an 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 TruncationBudget( + max_lines=( + TruncationBudget.DEFAULT_MAX_LINES if max_lines is None else int(max_lines) + ), + max_chars=( + TruncationBudget.DEFAULT_MAX_CHARS if max_chars is None else int(max_chars) + ), + ) def _truncate_explanation( diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 609c4b1a62f..97093118f2f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -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``."""