/
niceSOFT
/
python3-pytest
Обзор
Документация
Войти
/
niceSOFT
/
python3-pytest
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/_pytest/assertion/truncate.py
159 строк
6 KB
Pierre Sassoulas
Allow int for ``truncation_limit_lines`` and ``truncation_limit_chars`` in TOML (#14692)
31 июл 2026, 10:56
Не верифицирован
31 июл 2026, 10:56
f306da7
Код
Авторство
О чём код?
"""Utilities for truncating assertion output. Current default behaviour is to truncate assertion explanations at terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. """ from __future__ import annotations 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 USAGE_MSG = "use '-vv' to show" TRUNCATION_MSG = f"...Full output truncated, {USAGE_MSG}" # Truncating appends a footer: ``...`` on the last kept line, a blank line, # then ``TRUNCATION_MSG``. A body may exceed the raw budget by the footer's # cost before truncating, so a body that nearly fits is not cut just to # make room for the footer. TRUNCATION_FOOTER_LINES = 2 # blank separator + message line TRUNCATION_FOOTER_CHARS = len("...") + len(TRUNCATION_MSG) def materialize_with_truncation(lines: Iterable[str], config: Config) -> list[str]: """Materialise a streaming explanation, applying truncation lazily. Pulls from ``lines`` only until the truncation threshold is reached; the rest of the iterator is dropped without being consumed. """ should_truncate, budget = _get_truncation_parameters(config) if not should_truncate: return list(lines) tolerable_max_chars = budget.max_chars + TRUNCATION_FOOTER_CHARS # Pull one line past the point where ``_truncate_explanation`` keeps the # body whole (``max_lines + TRUNCATION_FOOTER_LINES``) so it can detect the # overflow, without us materialising more than we need. line_cap = ( budget.max_lines + TRUNCATION_FOOTER_LINES + 1 if budget.max_lines > 0 else None ) buffered: list[str] = [] char_count = 0 for line in lines: buffered.append(line) char_count += len(line) if line_cap is not None and len(buffered) >= line_cap: break if budget.max_chars > 0 and char_count > tolerable_max_chars: break else: # Iterator exhausted within limits — nothing to truncate. return buffered return _truncate_explanation(buffered, budget) 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 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 # are set to 0 explicitly. verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) if verbose >= 2 or running_on_ci(): return False, NO_TRUNCATION_BUDGET budget = _budget_from_config(config) should_truncate = budget.max_lines > 0 or budget.max_chars > 0 return should_truncate, budget def _budget_from_config(config: Config) -> TruncationBudget: """Build a budget from the ``truncation_limit_*`` ini options. ``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( input_lines: list[str], budget: TruncationBudget, ) -> list[str]: """Truncate given list of strings that makes up the assertion explanation. Truncates to either ``budget.max_lines`` or ``budget.max_chars`` - whichever the input reaches first, taking the truncation explanation into account. The remaining lines will be replaced by a usage message. If max_chars=0, no truncation by character count is performed. If max_lines=0, no truncation by line count is performed. When this function is launched we know max_lines > 0 or max_chars > 0 because _get_truncation_parameters was called first. """ # ``max_chars`` bounds the body only; the footer slack is added on top # (see ``TRUNCATION_FOOTER_CHARS``). tolerable_max_chars = budget.max_chars + TRUNCATION_FOOTER_CHARS if ( budget.max_lines == 0 or len(input_lines) <= budget.max_lines + TRUNCATION_FOOTER_LINES ): if ( budget.max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars ): return input_lines truncated_explanation = input_lines else: # Truncate first to max_lines, and then truncate to max_chars if necessary truncated_explanation = input_lines[: budget.max_lines] # We reevaluate the need to truncate chars following removal of some lines if ( budget.max_chars > 0 and sum(len(e) for e in truncated_explanation) > tolerable_max_chars ): truncated_explanation = _truncate_by_char_count( truncated_explanation, budget.max_chars ) # Something was truncated, adding '...' at the end to show that truncated_explanation[-1] += "..." return [ *truncated_explanation, "", TRUNCATION_MSG, ] def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]: # Find point at which input length exceeds total allowed length iterated_char_count = 0 for iterated_index, input_line in enumerate(input_lines): if iterated_char_count + len(input_line) > max_chars: break iterated_char_count += len(input_line) # Create truncated explanation with modified final line truncated_result = input_lines[:iterated_index] final_line = input_lines[iterated_index] if final_line: final_line_truncate_point = max_chars - iterated_char_count final_line = final_line[:final_line_truncate_point] truncated_result.append(final_line) return truncated_result