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 AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ Omri Golan
Ondřej Súkup
Oscar Benjamin
Parth Patel
Parzival235
Patrick Hayes
Patrick Lannigan
Paul Müller
Expand Down
2 changes: 2 additions & 0 deletions changelog/14859.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Pytest's uses of ``pprint`` now consistently expand nested data structures on
all supported Python versions.
5 changes: 3 additions & 2 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

from itertools import islice
import pprint
import reprlib

from _pytest._io.pprint import PrettyPrinter


def _try_repr_or_str(obj: object) -> str:
try:
Expand Down Expand Up @@ -112,7 +113,7 @@ def safeformat(obj: object) -> str:
with a short exception info.
"""
try:
return pprint.pformat(obj)
return PrettyPrinter().pformat(obj)
except Exception as exc:
return _format_repr_exception(exc, obj)

Expand Down
9 changes: 6 additions & 3 deletions src/_pytest/approx.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from decimal import Decimal
import math
from numbers import Complex
import pprint
import sys
from typing import Any
from typing import Generic
Expand All @@ -23,6 +22,8 @@
from typing import TypeGuard
from typing import TypeVar

from _pytest._io.pprint import PrettyPrinter


if TYPE_CHECKING:
from numpy import ndarray
Expand Down Expand Up @@ -262,7 +263,9 @@ def __init__(
for key, value in expected.items():
if isinstance(value, type(expected)):
msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
raise TypeError(msg.format(key, value, pprint.pformat(expected)))
raise TypeError(
msg.format(key, value, PrettyPrinter().pformat(expected))
)

super().__init__(expected, rel=rel, abs=abs, nan_ok=nan_ok)

Expand Down Expand Up @@ -360,7 +363,7 @@ def __init__(
for index, x in enumerate(expected):
if isinstance(x, type(expected)):
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
raise TypeError(msg.format(x, index, pprint.pformat(expected)))
raise TypeError(msg.format(x, index, PrettyPrinter().pformat(expected)))

super().__init__(expected, rel=rel, abs=abs, nan_ok=nan_ok)

Expand Down
7 changes: 4 additions & 3 deletions src/_pytest/assertion/_compare_any.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from collections.abc import Iterator
import dataclasses
import pprint

from _pytest._io.pprint import PrettyPrinter
from _pytest.assertion._compare_mapping import _compare_eq_mapping
from _pytest.assertion._compare_sequence import _compare_eq_iterable
from _pytest.assertion._compare_sequence import _compare_eq_sequence
Expand Down Expand Up @@ -105,6 +105,7 @@ def _compare_eq_cls(
assert False

indent = " "
pp = PrettyPrinter()
same = []
diff = []
for field in fields_to_check:
Expand All @@ -119,10 +120,10 @@ def _compare_eq_cls(
yield f"Omitting {len(same)} identical items, use -vv to show"
elif same:
yield "Matching attributes:"
yield from highlighter(pprint.pformat(same)).splitlines()
yield from (highlighter(line) for line in pp.pformat_lines(same))
if diff:
yield "Differing attributes:"
yield from highlighter(pprint.pformat(diff)).splitlines()
yield from (highlighter(line) for line in pp.pformat_lines(diff))
for field in diff:
field_left = getattr(left, field)
field_right = getattr(right, field)
Expand Down
18 changes: 10 additions & 8 deletions src/_pytest/assertion/_compare_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from collections.abc import Iterator
from collections.abc import Mapping
import heapq
import pprint

from _pytest._io.pprint import _safe_key
from _pytest._io.pprint import PrettyPrinter
from _pytest._io.saferepr import saferepr
from _pytest.assertion._typing import _HighlightFunc
from _pytest.assertion._typing import NO_TRUNCATION_BUDGET
Expand All @@ -23,12 +23,13 @@ def _compare_eq_mapping(
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)
same = {k: left[k] for k in common if left[k] == right[k]}
same = {k: left[k] for k in sorted(common, key=_safe_key) if left[k] == right[k]}
pp = PrettyPrinter()
if same and verbose < 2:
yield f"Omitting {len(same)} identical items, use -vv to show"
elif same:
yield "Common items:"
yield from highlighter(pprint.pformat(same)).splitlines()
yield from (highlighter(line) for line in pp.pformat_lines(same))
diff = {k for k in common if left[k] != right[k]}
if diff:
yield "Differing items:"
Expand Down Expand Up @@ -61,12 +62,13 @@ def _format_extra_items(
"""Render the "X contains N more items" subdict."""
max_lines = truncation_budget.max_lines
if max_lines == 0 or len(keys) <= max_lines:
# If no need to truncate, let pprint handle it.
yield from highlighter(
pprint.pformat({k: mapping[k] for k in keys})
).splitlines()
# If no need to truncate, let the pretty printer handle it.
lines = PrettyPrinter().pformat_lines(
{k: mapping[k] for k in sorted(keys, key=_safe_key)}
)
yield from (highlighter(line) for line in lines)
else:
# To avoid spending effort on formatting entries that would be truncated,
# only format the needed entries, keeping the sorting that pprint would use.
# only format the needed entries, keeping deterministic key sorting.
for k in heapq.nsmallest(max_lines, keys, key=_safe_key):
yield highlighter(saferepr({k: mapping[k]}))
6 changes: 3 additions & 3 deletions src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest._io.pprint import PrettyPrinter
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
Expand Down Expand Up @@ -614,8 +615,6 @@ def cacheshow(config: Config, session: Session) -> int:
:param session: pytest session object.
:returns: Exit code (0 for success).
"""
from pprint import pformat

assert config.cache is not None

tw = TerminalWriter()
Expand All @@ -638,6 +637,7 @@ def globfiles(base: Path) -> Iterable[Path]:
yield x

dummy = object()
pp = PrettyPrinter()
basedir = config.cache._cachedir
vdir = basedir / Cache._CACHE_PREFIX_VALUES
tw.sep("-", f"cache values for {glob!r}")
Expand All @@ -648,7 +648,7 @@ def globfiles(base: Path) -> Iterable[Path]:
tw.line(f"{key} contains unreadable content, will be ignored")
else:
tw.line(f"{key} contains:")
for line in pformat(val).splitlines():
for line in pp.pformat_lines(val):
tw.line(" " + line)

ddir = basedir / Cache._CACHE_PREFIX_DIRS
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/recwarn.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from collections.abc import Callable
from collections.abc import Generator
from collections.abc import Iterator
from pprint import pformat
import re
from types import TracebackType
from typing import Any
Expand All @@ -24,6 +23,7 @@

import warnings

from _pytest._io.pprint import PrettyPrinter
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.outcomes import Exit
Expand Down Expand Up @@ -317,7 +317,7 @@ def __exit__(
return

def found_str() -> str:
return pformat([record.message for record in self], indent=2)
return PrettyPrinter(indent=2).pformat([record.message for record in self])

try:
if not any(issubclass(w.category, self.expected_warning) for w in self):
Expand Down
15 changes: 14 additions & 1 deletion testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,20 @@ def test_repr_local_truncated(self) -> None:
full_reprlocals = q.repr_locals(loc)
assert full_reprlocals is not None
assert full_reprlocals.lines
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
assert full_reprlocals.lines[0] == (
"l = [\n"
" 0,\n"
" 1,\n"
" 2,\n"
" 3,\n"
" 4,\n"
" 5,\n"
" 6,\n"
" 7,\n"
" 8,\n"
" 9,\n"
"]"
)

def test_repr_args_not_truncated(self, importasmod) -> None:
mod = importasmod(
Expand Down
14 changes: 14 additions & 0 deletions testing/io/test_saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import SafeRepr
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
Expand All @@ -13,6 +14,19 @@ def test_simple_repr():
assert saferepr(None) == "None"


def test_safeformat_expands_nested_collections() -> None:
assert safeformat({"outer": [1, {"inner": 2}]}) == (
"{\n"
" 'outer': [\n"
" 1,\n"
" {\n"
" 'inner': 2,\n"
" },\n"
" ],\n"
"}"
)


def test_maxsize():
s = saferepr("x" * 50, maxsize=25)
assert len(s) == 25
Expand Down
14 changes: 14 additions & 0 deletions testing/python/approx.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,20 @@ def test_expected_value_type_error(self, x, name):
):
approx(x)

def test_nested_mapping_error_uses_expanded_format(self) -> None:
with pytest.raises(TypeError) as excinfo:
approx({"outer": {"inner": 1}})

assert str(excinfo.value) == (
"pytest.approx() does not support nested dictionaries: "
"key='outer' value={'inner': 1}\n"
" full mapping={\n"
" 'outer': {\n"
" 'inner': 1,\n"
" },\n"
"}"
)

@pytest.mark.parametrize(
"x",
[
Expand Down
Loading