From eb6ad08e5de1b63c1a46e2fce36be3ded899bead Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Fri, 17 Nov 2023 18:40:29 +0000 Subject: [PATCH 1/7] Vendor in the pprint module to allow further modifications We already have the AlwaysDispatchingPrettyPrinter override of the default pretty printer. In order to make more in depth changes, we need to copy the upstream version in, as it doesn't lend itself well to being extended. This does a verbatime copy, adding provenance information at the top. --- src/_pytest/_io/pprint.py | 664 ++++++++++++++++++++++++++++++++++++ src/_pytest/_io/saferepr.py | 6 +- 2 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 src/_pytest/_io/pprint.py diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py new file mode 100644 index 000000000..3875f7131 --- /dev/null +++ b/src/_pytest/_io/pprint.py @@ -0,0 +1,664 @@ +# This module was imported from the cpython standard library +# (https://github.com/python/cpython/) at commit +# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). +# +# fmt: off +# flake8: noqa +# type: ignore + +# +# Author: Fred L. Drake, Jr. +# fdrake@acm.org +# +# This is a simple little module I wrote to make life easier. I didn't +# see anything quite like it in the library, though I may have overlooked +# something. I wrote this when I was trying to read some heavily nested +# tuples with fairly non-descriptive content. This is modeled very much +# after Lisp/Scheme - style pretty-printing of lists. If you find it +# useful, thank small children who sleep at night. + +"""Support to pretty-print lists, tuples, & dictionaries recursively. + +Very simple, but useful, especially in debugging data structures. + +Classes +------- + +PrettyPrinter() + Handle pretty-printing operations onto a stream using a configured + set of formatting parameters.htop + +Functions +--------- + +pformat() + Format a Python object into a pretty-printed representation. + +pprint() + Pretty-print a Python object to a stream [default is sys.stdout]. + +saferepr() + Generate a 'standard' repr()-like value, but protect against recursive + data structures. + +""" + +import collections as _collections +import dataclasses as _dataclasses +import re +import sys as _sys +import types as _types +from io import StringIO as _StringIO + +__all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", + "PrettyPrinter", "pp"] + + +def pprint(object, stream=None, indent=1, width=80, depth=None, *, + compact=False, sort_dicts=True, underscore_numbers=False): + """Pretty-print a Python object to a stream [default is sys.stdout].""" + printer = PrettyPrinter( + stream=stream, indent=indent, width=width, depth=depth, + compact=compact, sort_dicts=sort_dicts, + underscore_numbers=underscore_numbers) + printer.pprint(object) + +def pformat(object, indent=1, width=80, depth=None, *, + compact=False, sort_dicts=True, underscore_numbers=False): + """Format a Python object into a pretty-printed representation.""" + return PrettyPrinter(indent=indent, width=width, depth=depth, + compact=compact, sort_dicts=sort_dicts, + underscore_numbers=underscore_numbers).pformat(object) + +def pp(object, *args, sort_dicts=False, **kwargs): + """Pretty-print a Python object""" + pprint(object, *args, sort_dicts=sort_dicts, **kwargs) + +def saferepr(object): + """Version of repr() which can handle recursive data structures.""" + return PrettyPrinter()._safe_repr(object, {}, None, 0)[0] + +def isreadable(object): + """Determine if saferepr(object) is readable by eval().""" + return PrettyPrinter()._safe_repr(object, {}, None, 0)[1] + +def isrecursive(object): + """Determine if object requires a recursive representation.""" + return PrettyPrinter()._safe_repr(object, {}, None, 0)[2] + +class _safe_key: + """Helper function for key functions when sorting unorderable objects. + + The wrapped-object will fallback to a Py2.x style comparison for + unorderable types (sorting first comparing the type name and then by + the obj ids). Does not work recursively, so dict.items() must have + _safe_key applied to both the key and the value. + + """ + + __slots__ = ['obj'] + + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + try: + return self.obj < other.obj + except TypeError: + return ((str(type(self.obj)), id(self.obj)) < \ + (str(type(other.obj)), id(other.obj))) + +def _safe_tuple(t): + "Helper function for comparing 2-tuples" + return _safe_key(t[0]), _safe_key(t[1]) + +class PrettyPrinter: + def __init__(self, indent=1, width=80, depth=None, stream=None, *, + compact=False, sort_dicts=True, underscore_numbers=False): + """Handle pretty printing operations onto a stream using a set of + configured parameters. + + indent + Number of spaces to indent for each level of nesting. + + width + Attempted maximum number of columns in the output. + + depth + The maximum depth to print out nested structures. + + stream + The desired output stream. If omitted (or false), the standard + output stream available at construction will be used. + + compact + If true, several items will be combined in one line. + + sort_dicts + If true, dict keys are sorted. + + """ + indent = int(indent) + width = int(width) + if indent < 0: + raise ValueError('indent must be >= 0') + if depth is not None and depth <= 0: + raise ValueError('depth must be > 0') + if not width: + raise ValueError('width must be != 0') + self._depth = depth + self._indent_per_level = indent + self._width = width + if stream is not None: + self._stream = stream + else: + self._stream = _sys.stdout + self._compact = bool(compact) + self._sort_dicts = sort_dicts + self._underscore_numbers = underscore_numbers + + def pprint(self, object): + if self._stream is not None: + self._format(object, self._stream, 0, 0, {}, 0) + self._stream.write("\n") + + def pformat(self, object): + sio = _StringIO() + self._format(object, sio, 0, 0, {}, 0) + return sio.getvalue() + + def isrecursive(self, object): + return self.format(object, {}, 0, 0)[2] + + def isreadable(self, object): + s, readable, recursive = self.format(object, {}, 0, 0) + return readable and not recursive + + def _format(self, object, stream, indent, allowance, context, level): + objid = id(object) + if objid in context: + stream.write(_recursion(object)) + self._recursive = True + self._readable = False + return + rep = self._repr(object, context, level) + max_width = self._width - indent - allowance + if len(rep) > max_width: + p = self._dispatch.get(type(object).__repr__, None) + if p is not None: + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + return + elif (_dataclasses.is_dataclass(object) and + not isinstance(object, type) and + object.__dataclass_params__.repr and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") and + "__create_fn__" in object.__repr__.__wrapped__.__qualname__): + context[objid] = 1 + self._pprint_dataclass(object, stream, indent, allowance, context, level + 1) + del context[objid] + return + stream.write(rep) + + def _pprint_dataclass(self, object, stream, indent, allowance, context, level): + cls_name = object.__class__.__name__ + indent += len(cls_name) + 1 + items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr] + stream.write(cls_name + '(') + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(')') + + _dispatch = {} + + def _pprint_dict(self, object, stream, indent, allowance, context, level): + write = stream.write + write('{') + if self._indent_per_level > 1: + write((self._indent_per_level - 1) * ' ') + length = len(object) + if length: + if self._sort_dicts: + items = sorted(object.items(), key=_safe_tuple) + else: + items = object.items() + self._format_dict_items(items, stream, indent, allowance + 1, + context, level) + write('}') + + _dispatch[dict.__repr__] = _pprint_dict + + def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + '(') + self._format(list(object.items()), stream, + indent + len(cls.__name__) + 1, allowance + 1, + context, level) + stream.write(')') + + _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + + def _pprint_list(self, object, stream, indent, allowance, context, level): + stream.write('[') + self._format_items(object, stream, indent, allowance + 1, + context, level) + stream.write(']') + + _dispatch[list.__repr__] = _pprint_list + + def _pprint_tuple(self, object, stream, indent, allowance, context, level): + stream.write('(') + endchar = ',)' if len(object) == 1 else ')' + self._format_items(object, stream, indent, allowance + len(endchar), + context, level) + stream.write(endchar) + + _dispatch[tuple.__repr__] = _pprint_tuple + + def _pprint_set(self, object, stream, indent, allowance, context, level): + if not len(object): + stream.write(repr(object)) + return + typ = object.__class__ + if typ is set: + stream.write('{') + endchar = '}' + else: + stream.write(typ.__name__ + '({') + endchar = '})' + indent += len(typ.__name__) + 1 + object = sorted(object, key=_safe_key) + self._format_items(object, stream, indent, allowance + len(endchar), + context, level) + stream.write(endchar) + + _dispatch[set.__repr__] = _pprint_set + _dispatch[frozenset.__repr__] = _pprint_set + + def _pprint_str(self, object, stream, indent, allowance, context, level): + write = stream.write + if not len(object): + write(repr(object)) + return + chunks = [] + lines = object.splitlines(True) + if level == 1: + indent += 1 + allowance += 1 + max_width1 = max_width = self._width - indent + for i, line in enumerate(lines): + rep = repr(line) + if i == len(lines) - 1: + max_width1 -= allowance + if len(rep) <= max_width1: + chunks.append(rep) + else: + # A list of alternating (non-space, space) strings + parts = re.findall(r'\S*\s*', line) + assert parts + assert not parts[-1] + parts.pop() # drop empty last part + max_width2 = max_width + current = '' + for j, part in enumerate(parts): + candidate = current + part + if j == len(parts) - 1 and i == len(lines) - 1: + max_width2 -= allowance + if len(repr(candidate)) > max_width2: + if current: + chunks.append(repr(current)) + current = part + else: + current = candidate + if current: + chunks.append(repr(current)) + if len(chunks) == 1: + write(rep) + return + if level == 1: + write('(') + for i, rep in enumerate(chunks): + if i > 0: + write('\n' + ' '*indent) + write(rep) + if level == 1: + write(')') + + _dispatch[str.__repr__] = _pprint_str + + def _pprint_bytes(self, object, stream, indent, allowance, context, level): + write = stream.write + if len(object) <= 4: + write(repr(object)) + return + parens = level == 1 + if parens: + indent += 1 + allowance += 1 + write('(') + delim = '' + for rep in _wrap_bytes_repr(object, self._width - indent, allowance): + write(delim) + write(rep) + if not delim: + delim = '\n' + ' '*indent + if parens: + write(')') + + _dispatch[bytes.__repr__] = _pprint_bytes + + def _pprint_bytearray(self, object, stream, indent, allowance, context, level): + write = stream.write + write('bytearray(') + self._pprint_bytes(bytes(object), stream, indent + 10, + allowance + 1, context, level + 1) + write(')') + + _dispatch[bytearray.__repr__] = _pprint_bytearray + + def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): + stream.write('mappingproxy(') + self._format(object.copy(), stream, indent + 13, allowance + 1, + context, level) + stream.write(')') + + _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy + + def _pprint_simplenamespace(self, object, stream, indent, allowance, context, level): + if type(object) is _types.SimpleNamespace: + # The SimpleNamespace repr is "namespace" instead of the class + # name, so we do the same here. For subclasses; use the class name. + cls_name = 'namespace' + else: + cls_name = object.__class__.__name__ + indent += len(cls_name) + 1 + items = object.__dict__.items() + stream.write(cls_name + '(') + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(')') + + _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace + + def _format_dict_items(self, items, stream, indent, allowance, context, + level): + write = stream.write + indent += self._indent_per_level + delimnl = ',\n' + ' ' * indent + last_index = len(items) - 1 + for i, (key, ent) in enumerate(items): + last = i == last_index + rep = self._repr(key, context, level) + write(rep) + write(': ') + self._format(ent, stream, indent + len(rep) + 2, + allowance if last else 1, + context, level) + if not last: + write(delimnl) + + def _format_namespace_items(self, items, stream, indent, allowance, context, level): + write = stream.write + delimnl = ',\n' + ' ' * indent + last_index = len(items) - 1 + for i, (key, ent) in enumerate(items): + last = i == last_index + write(key) + write('=') + if id(ent) in context: + # Special-case representation of recursion to match standard + # recursive dataclass repr. + write("...") + else: + self._format(ent, stream, indent + len(key) + 1, + allowance if last else 1, + context, level) + if not last: + write(delimnl) + + def _format_items(self, items, stream, indent, allowance, context, level): + write = stream.write + indent += self._indent_per_level + if self._indent_per_level > 1: + write((self._indent_per_level - 1) * ' ') + delimnl = ',\n' + ' ' * indent + delim = '' + width = max_width = self._width - indent + 1 + it = iter(items) + try: + next_ent = next(it) + except StopIteration: + return + last = False + while not last: + ent = next_ent + try: + next_ent = next(it) + except StopIteration: + last = True + max_width -= allowance + width -= allowance + if self._compact: + rep = self._repr(ent, context, level) + w = len(rep) + 2 + if width < w: + width = max_width + if delim: + delim = delimnl + if width >= w: + width -= w + write(delim) + delim = ', ' + write(rep) + continue + write(delim) + delim = delimnl + self._format(ent, stream, indent, + allowance if last else 1, + context, level) + + def _repr(self, object, context, level): + repr, readable, recursive = self.format(object, context.copy(), + self._depth, level) + if not readable: + self._readable = False + if recursive: + self._recursive = True + return repr + + def format(self, object, context, maxlevels, level): + """Format object for a specific context, returning a string + and flags indicating whether the representation is 'readable' + and whether the object represents a recursive construct. + """ + return self._safe_repr(object, context, maxlevels, level) + + def _pprint_default_dict(self, object, stream, indent, allowance, context, level): + if not len(object): + stream.write(repr(object)) + return + rdf = self._repr(object.default_factory, context, level) + cls = object.__class__ + indent += len(cls.__name__) + 1 + stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent)) + self._pprint_dict(object, stream, indent, allowance + 1, context, level) + stream.write(')') + + _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict + + def _pprint_counter(self, object, stream, indent, allowance, context, level): + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + '({') + if self._indent_per_level > 1: + stream.write((self._indent_per_level - 1) * ' ') + items = object.most_common() + self._format_dict_items(items, stream, + indent + len(cls.__name__) + 1, allowance + 2, + context, level) + stream.write('})') + + _dispatch[_collections.Counter.__repr__] = _pprint_counter + + def _pprint_chain_map(self, object, stream, indent, allowance, context, level): + if not len(object.maps): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + '(') + indent += len(cls.__name__) + 1 + for i, m in enumerate(object.maps): + if i == len(object.maps) - 1: + self._format(m, stream, indent, allowance + 1, context, level) + stream.write(')') + else: + self._format(m, stream, indent, 1, context, level) + stream.write(',\n' + ' ' * indent) + + _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map + + def _pprint_deque(self, object, stream, indent, allowance, context, level): + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + '(') + indent += len(cls.__name__) + 1 + stream.write('[') + if object.maxlen is None: + self._format_items(object, stream, indent, allowance + 2, + context, level) + stream.write('])') + else: + self._format_items(object, stream, indent, 2, + context, level) + rml = self._repr(object.maxlen, context, level) + stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml)) + + _dispatch[_collections.deque.__repr__] = _pprint_deque + + def _pprint_user_dict(self, object, stream, indent, allowance, context, level): + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict + + def _pprint_user_list(self, object, stream, indent, allowance, context, level): + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserList.__repr__] = _pprint_user_list + + def _pprint_user_string(self, object, stream, indent, allowance, context, level): + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserString.__repr__] = _pprint_user_string + + def _safe_repr(self, object, context, maxlevels, level): + # Return triple (repr_string, isreadable, isrecursive). + typ = type(object) + if typ in _builtin_scalars: + return repr(object), True, False + + r = getattr(typ, "__repr__", None) + + if issubclass(typ, int) and r is int.__repr__: + if self._underscore_numbers: + return f"{object:_d}", True, False + else: + return repr(object), True, False + + if issubclass(typ, dict) and r is dict.__repr__: + if not object: + return "{}", True, False + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}", False, objid in context + if objid in context: + return _recursion(object), False, True + context[objid] = 1 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + if self._sort_dicts: + items = sorted(object.items(), key=_safe_tuple) + else: + items = object.items() + for k, v in items: + krepr, kreadable, krecur = self.format( + k, context, maxlevels, level) + vrepr, vreadable, vrecur = self.format( + v, context, maxlevels, level) + append("%s: %s" % (krepr, vrepr)) + readable = readable and kreadable and vreadable + if krecur or vrecur: + recursive = True + del context[objid] + return "{%s}" % ", ".join(components), readable, recursive + + if (issubclass(typ, list) and r is list.__repr__) or \ + (issubclass(typ, tuple) and r is tuple.__repr__): + if issubclass(typ, list): + if not object: + return "[]", True, False + format = "[%s]" + elif len(object) == 1: + format = "(%s,)" + else: + if not object: + return "()", True, False + format = "(%s)" + objid = id(object) + if maxlevels and level >= maxlevels: + return format % "...", False, objid in context + if objid in context: + return _recursion(object), False, True + context[objid] = 1 + readable = True + recursive = False + components = [] + append = components.append + level += 1 + for o in object: + orepr, oreadable, orecur = self.format( + o, context, maxlevels, level) + append(orepr) + if not oreadable: + readable = False + if orecur: + recursive = True + del context[objid] + return format % ", ".join(components), readable, recursive + + rep = repr(object) + return rep, (rep and not rep.startswith('<')), False + +_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, + bool, type(None)}) + +def _recursion(object): + return ("<Recursion on %s with id=%s>" + % (type(object).__name__, id(object))) + + +def _wrap_bytes_repr(object, width, allowance): + current = b'' + last = len(object) // 4 * 4 + for i in range(0, len(object), 4): + part = object[i: i+4] + candidate = current + part + if i == last: + width -= allowance + if len(repr(candidate)) > width: + if current: + yield repr(current) + current = part + else: + current = candidate + if current: + yield repr(current) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index c70187223..50d6a303a 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -5,6 +5,8 @@ from typing import Dict from typing import IO from typing import Optional +from .pprint import PrettyPrinter # type: ignore + def _try_repr_or_str(obj: object) -> str: try: @@ -134,7 +136,7 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: return _format_repr_exception(exc, obj) -class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): +class AlwaysDispatchingPrettyPrinter(PrettyPrinter): """PrettyPrinter that always dispatches (regardless of width).""" def _format( @@ -175,6 +177,6 @@ def _pformat_dispatch( *, compact: bool = False, ) -> str: - return AlwaysDispatchingPrettyPrinter( + return AlwaysDispatchingPrettyPrinter( # type: ignore indent=indent, width=width, depth=depth, compact=compact ).pformat(object) From 23226683449ef07931e9aa5caa380ef03192db09 Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Fri, 17 Nov 2023 18:54:47 +0000 Subject: [PATCH 2/7] Remove unneeded pprint interfaces There are parts of the original pprint module that we won't need, let's limit the surface and remove the unnecessary code --- src/_pytest/_io/pprint.py | 79 +-------------------------------------- 1 file changed, 2 insertions(+), 77 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 3875f7131..c13ae99ae 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -6,9 +6,8 @@ # flake8: noqa # type: ignore -# -# Author: Fred L. Drake, Jr. -# fdrake@acm.org +# Original Author: Fred L. Drake, Jr. +# fdrake@acm.org # # This is a simple little module I wrote to make life easier. I didn't # see anything quite like it in the library, though I may have overlooked @@ -16,33 +15,6 @@ # tuples with fairly non-descriptive content. This is modeled very much # after Lisp/Scheme - style pretty-printing of lists. If you find it # useful, thank small children who sleep at night. - -"""Support to pretty-print lists, tuples, & dictionaries recursively. - -Very simple, but useful, especially in debugging data structures. - -Classes -------- - -PrettyPrinter() - Handle pretty-printing operations onto a stream using a configured - set of formatting parameters.htop - -Functions ---------- - -pformat() - Format a Python object into a pretty-printed representation. - -pprint() - Pretty-print a Python object to a stream [default is sys.stdout]. - -saferepr() - Generate a 'standard' repr()-like value, but protect against recursive - data structures. - -""" - import collections as _collections import dataclasses as _dataclasses import re @@ -50,41 +22,6 @@ import sys as _sys import types as _types from io import StringIO as _StringIO -__all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", - "PrettyPrinter", "pp"] - - -def pprint(object, stream=None, indent=1, width=80, depth=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): - """Pretty-print a Python object to a stream [default is sys.stdout].""" - printer = PrettyPrinter( - stream=stream, indent=indent, width=width, depth=depth, - compact=compact, sort_dicts=sort_dicts, - underscore_numbers=underscore_numbers) - printer.pprint(object) - -def pformat(object, indent=1, width=80, depth=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): - """Format a Python object into a pretty-printed representation.""" - return PrettyPrinter(indent=indent, width=width, depth=depth, - compact=compact, sort_dicts=sort_dicts, - underscore_numbers=underscore_numbers).pformat(object) - -def pp(object, *args, sort_dicts=False, **kwargs): - """Pretty-print a Python object""" - pprint(object, *args, sort_dicts=sort_dicts, **kwargs) - -def saferepr(object): - """Version of repr() which can handle recursive data structures.""" - return PrettyPrinter()._safe_repr(object, {}, None, 0)[0] - -def isreadable(object): - """Determine if saferepr(object) is readable by eval().""" - return PrettyPrinter()._safe_repr(object, {}, None, 0)[1] - -def isrecursive(object): - """Determine if object requires a recursive representation.""" - return PrettyPrinter()._safe_repr(object, {}, None, 0)[2] class _safe_key: """Helper function for key functions when sorting unorderable objects. @@ -157,23 +94,11 @@ class PrettyPrinter: self._sort_dicts = sort_dicts self._underscore_numbers = underscore_numbers - def pprint(self, object): - if self._stream is not None: - self._format(object, self._stream, 0, 0, {}, 0) - self._stream.write("\n") - def pformat(self, object): sio = _StringIO() self._format(object, sio, 0, 0, {}, 0) return sio.getvalue() - def isrecursive(self, object): - return self.format(object, {}, 0, 0)[2] - - def isreadable(self, object): - s, readable, recursive = self.format(object, {}, 0, 0) - return readable and not recursive - def _format(self, object, stream, indent, allowance, context, level): objid = id(object) if objid in context: From 66f2f20effb987e24cbf4dce2c86437bc813e57f Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Fri, 17 Nov 2023 19:05:20 +0000 Subject: [PATCH 3/7] Run pyupgrade on the pprint module --- src/_pytest/_io/pprint.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index c13ae99ae..66afca587 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -5,7 +5,8 @@ # fmt: off # flake8: noqa # type: ignore - +# +# # Original Author: Fred L. Drake, Jr. # fdrake@acm.org # @@ -408,7 +409,7 @@ class PrettyPrinter: rdf = self._repr(object.default_factory, context, level) cls = object.__class__ indent += len(cls.__name__) + 1 - stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent)) + stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}") self._pprint_dict(object, stream, indent, allowance + 1, context, level) stream.write(')') @@ -463,7 +464,7 @@ class PrettyPrinter: self._format_items(object, stream, indent, 2, context, level) rml = self._repr(object.maxlen, context, level) - stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml)) + stream.write(f"],\n{' ' * indent}maxlen={rml})") _dispatch[_collections.deque.__repr__] = _pprint_deque @@ -519,7 +520,7 @@ class PrettyPrinter: k, context, maxlevels, level) vrepr, vreadable, vrecur = self.format( v, context, maxlevels, level) - append("%s: %s" % (krepr, vrepr)) + append(f"{krepr}: {vrepr}") readable = readable and kreadable and vreadable if krecur or vrecur: recursive = True From 5fae5ef73e85ac6d753aa6c9d72424734cccaf3b Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Fri, 17 Nov 2023 19:06:51 +0000 Subject: [PATCH 4/7] Apply project-wide formatting standard to the pprint module (black) --- src/_pytest/_io/pprint.py | 268 +++++++++++++++++++++----------------- 1 file changed, 151 insertions(+), 117 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 66afca587..3bb4a2c7d 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -2,7 +2,6 @@ # (https://github.com/python/cpython/) at commit # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). # -# fmt: off # flake8: noqa # type: ignore # @@ -34,7 +33,7 @@ class _safe_key: """ - __slots__ = ['obj'] + __slots__ = ["obj"] def __init__(self, obj): self.obj = obj @@ -43,16 +42,29 @@ class _safe_key: try: return self.obj < other.obj except TypeError: - return ((str(type(self.obj)), id(self.obj)) < \ - (str(type(other.obj)), id(other.obj))) + return (str(type(self.obj)), id(self.obj)) < ( + str(type(other.obj)), + id(other.obj), + ) + def _safe_tuple(t): "Helper function for comparing 2-tuples" return _safe_key(t[0]), _safe_key(t[1]) + class PrettyPrinter: - def __init__(self, indent=1, width=80, depth=None, stream=None, *, - compact=False, sort_dicts=True, underscore_numbers=False): + def __init__( + self, + indent=1, + width=80, + depth=None, + stream=None, + *, + compact=False, + sort_dicts=True, + underscore_numbers=False, + ): """Handle pretty printing operations onto a stream using a set of configured parameters. @@ -79,11 +91,11 @@ class PrettyPrinter: indent = int(indent) width = int(width) if indent < 0: - raise ValueError('indent must be >= 0') + raise ValueError("indent must be >= 0") if depth is not None and depth <= 0: - raise ValueError('depth must be > 0') + raise ValueError("depth must be > 0") if not width: - raise ValueError('width must be != 0') + raise ValueError("width must be != 0") self._depth = depth self._indent_per_level = indent self._width = width @@ -116,14 +128,19 @@ class PrettyPrinter: p(self, object, stream, indent, allowance, context, level + 1) del context[objid] return - elif (_dataclasses.is_dataclass(object) and - not isinstance(object, type) and - object.__dataclass_params__.repr and - # Check dataclass has generated repr method. - hasattr(object.__repr__, "__wrapped__") and - "__create_fn__" in object.__repr__.__wrapped__.__qualname__): + elif ( + _dataclasses.is_dataclass(object) + and not isinstance(object, type) + and object.__dataclass_params__.repr + and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") + and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ + ): context[objid] = 1 - self._pprint_dataclass(object, stream, indent, allowance, context, level + 1) + self._pprint_dataclass( + object, stream, indent, allowance, context, level + 1 + ) del context[objid] return stream.write(rep) @@ -131,27 +148,32 @@ class PrettyPrinter: def _pprint_dataclass(self, object, stream, indent, allowance, context, level): cls_name = object.__class__.__name__ indent += len(cls_name) + 1 - items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr] - stream.write(cls_name + '(') + items = [ + (f.name, getattr(object, f.name)) + for f in _dataclasses.fields(object) + if f.repr + ] + stream.write(cls_name + "(") self._format_namespace_items(items, stream, indent, allowance, context, level) - stream.write(')') + stream.write(")") _dispatch = {} def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write - write('{') + write("{") if self._indent_per_level > 1: - write((self._indent_per_level - 1) * ' ') + write((self._indent_per_level - 1) * " ") length = len(object) if length: if self._sort_dicts: items = sorted(object.items(), key=_safe_tuple) else: items = object.items() - self._format_dict_items(items, stream, indent, allowance + 1, - context, level) - write('}') + self._format_dict_items( + items, stream, indent, allowance + 1, context, level + ) + write("}") _dispatch[dict.__repr__] = _pprint_dict @@ -160,27 +182,32 @@ class PrettyPrinter: stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '(') - self._format(list(object.items()), stream, - indent + len(cls.__name__) + 1, allowance + 1, - context, level) - stream.write(')') + stream.write(cls.__name__ + "(") + self._format( + list(object.items()), + stream, + indent + len(cls.__name__) + 1, + allowance + 1, + context, + level, + ) + stream.write(")") _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict def _pprint_list(self, object, stream, indent, allowance, context, level): - stream.write('[') - self._format_items(object, stream, indent, allowance + 1, - context, level) - stream.write(']') + stream.write("[") + self._format_items(object, stream, indent, allowance + 1, context, level) + stream.write("]") _dispatch[list.__repr__] = _pprint_list def _pprint_tuple(self, object, stream, indent, allowance, context, level): - stream.write('(') - endchar = ',)' if len(object) == 1 else ')' - self._format_items(object, stream, indent, allowance + len(endchar), - context, level) + stream.write("(") + endchar = ",)" if len(object) == 1 else ")" + self._format_items( + object, stream, indent, allowance + len(endchar), context, level + ) stream.write(endchar) _dispatch[tuple.__repr__] = _pprint_tuple @@ -191,15 +218,16 @@ class PrettyPrinter: return typ = object.__class__ if typ is set: - stream.write('{') - endchar = '}' + stream.write("{") + endchar = "}" else: - stream.write(typ.__name__ + '({') - endchar = '})' + stream.write(typ.__name__ + "({") + endchar = "})" indent += len(typ.__name__) + 1 object = sorted(object, key=_safe_key) - self._format_items(object, stream, indent, allowance + len(endchar), - context, level) + self._format_items( + object, stream, indent, allowance + len(endchar), context, level + ) stream.write(endchar) _dispatch[set.__repr__] = _pprint_set @@ -224,12 +252,12 @@ class PrettyPrinter: chunks.append(rep) else: # A list of alternating (non-space, space) strings - parts = re.findall(r'\S*\s*', line) + parts = re.findall(r"\S*\s*", line) assert parts assert not parts[-1] parts.pop() # drop empty last part max_width2 = max_width - current = '' + current = "" for j, part in enumerate(parts): candidate = current + part if j == len(parts) - 1 and i == len(lines) - 1: @@ -246,13 +274,13 @@ class PrettyPrinter: write(rep) return if level == 1: - write('(') + write("(") for i, rep in enumerate(chunks): if i > 0: - write('\n' + ' '*indent) + write("\n" + " " * indent) write(rep) if level == 1: - write(')') + write(")") _dispatch[str.__repr__] = _pprint_str @@ -265,83 +293,94 @@ class PrettyPrinter: if parens: indent += 1 allowance += 1 - write('(') - delim = '' + write("(") + delim = "" for rep in _wrap_bytes_repr(object, self._width - indent, allowance): write(delim) write(rep) if not delim: - delim = '\n' + ' '*indent + delim = "\n" + " " * indent if parens: - write(')') + write(")") _dispatch[bytes.__repr__] = _pprint_bytes def _pprint_bytearray(self, object, stream, indent, allowance, context, level): write = stream.write - write('bytearray(') - self._pprint_bytes(bytes(object), stream, indent + 10, - allowance + 1, context, level + 1) - write(')') + write("bytearray(") + self._pprint_bytes( + bytes(object), stream, indent + 10, allowance + 1, context, level + 1 + ) + write(")") _dispatch[bytearray.__repr__] = _pprint_bytearray def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): - stream.write('mappingproxy(') - self._format(object.copy(), stream, indent + 13, allowance + 1, - context, level) - stream.write(')') + stream.write("mappingproxy(") + self._format(object.copy(), stream, indent + 13, allowance + 1, context, level) + stream.write(")") _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy - def _pprint_simplenamespace(self, object, stream, indent, allowance, context, level): + def _pprint_simplenamespace( + self, object, stream, indent, allowance, context, level + ): if type(object) is _types.SimpleNamespace: # The SimpleNamespace repr is "namespace" instead of the class # name, so we do the same here. For subclasses; use the class name. - cls_name = 'namespace' + cls_name = "namespace" else: cls_name = object.__class__.__name__ indent += len(cls_name) + 1 items = object.__dict__.items() - stream.write(cls_name + '(') + stream.write(cls_name + "(") self._format_namespace_items(items, stream, indent, allowance, context, level) - stream.write(')') + stream.write(")") _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace - def _format_dict_items(self, items, stream, indent, allowance, context, - level): + def _format_dict_items(self, items, stream, indent, allowance, context, level): write = stream.write indent += self._indent_per_level - delimnl = ',\n' + ' ' * indent + delimnl = ",\n" + " " * indent last_index = len(items) - 1 for i, (key, ent) in enumerate(items): last = i == last_index rep = self._repr(key, context, level) write(rep) - write(': ') - self._format(ent, stream, indent + len(rep) + 2, - allowance if last else 1, - context, level) + write(": ") + self._format( + ent, + stream, + indent + len(rep) + 2, + allowance if last else 1, + context, + level, + ) if not last: write(delimnl) def _format_namespace_items(self, items, stream, indent, allowance, context, level): write = stream.write - delimnl = ',\n' + ' ' * indent + delimnl = ",\n" + " " * indent last_index = len(items) - 1 for i, (key, ent) in enumerate(items): last = i == last_index write(key) - write('=') + write("=") if id(ent) in context: # Special-case representation of recursion to match standard # recursive dataclass repr. write("...") else: - self._format(ent, stream, indent + len(key) + 1, - allowance if last else 1, - context, level) + self._format( + ent, + stream, + indent + len(key) + 1, + allowance if last else 1, + context, + level, + ) if not last: write(delimnl) @@ -349,9 +388,9 @@ class PrettyPrinter: write = stream.write indent += self._indent_per_level if self._indent_per_level > 1: - write((self._indent_per_level - 1) * ' ') - delimnl = ',\n' + ' ' * indent - delim = '' + write((self._indent_per_level - 1) * " ") + delimnl = ",\n" + " " * indent + delim = "" width = max_width = self._width - indent + 1 it = iter(items) try: @@ -377,18 +416,17 @@ class PrettyPrinter: if width >= w: width -= w write(delim) - delim = ', ' + delim = ", " write(rep) continue write(delim) delim = delimnl - self._format(ent, stream, indent, - allowance if last else 1, - context, level) + self._format(ent, stream, indent, allowance if last else 1, context, level) def _repr(self, object, context, level): - repr, readable, recursive = self.format(object, context.copy(), - self._depth, level) + repr, readable, recursive = self.format( + object, context.copy(), self._depth, level + ) if not readable: self._readable = False if recursive: @@ -411,7 +449,7 @@ class PrettyPrinter: indent += len(cls.__name__) + 1 stream.write(f"{cls.__name__}({rdf},\n{' ' * indent}") self._pprint_dict(object, stream, indent, allowance + 1, context, level) - stream.write(')') + stream.write(")") _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict @@ -420,14 +458,14 @@ class PrettyPrinter: stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '({') + stream.write(cls.__name__ + "({") if self._indent_per_level > 1: - stream.write((self._indent_per_level - 1) * ' ') + stream.write((self._indent_per_level - 1) * " ") items = object.most_common() - self._format_dict_items(items, stream, - indent + len(cls.__name__) + 1, allowance + 2, - context, level) - stream.write('})') + self._format_dict_items( + items, stream, indent + len(cls.__name__) + 1, allowance + 2, context, level + ) + stream.write("})") _dispatch[_collections.Counter.__repr__] = _pprint_counter @@ -436,15 +474,15 @@ class PrettyPrinter: stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '(') + stream.write(cls.__name__ + "(") indent += len(cls.__name__) + 1 for i, m in enumerate(object.maps): if i == len(object.maps) - 1: self._format(m, stream, indent, allowance + 1, context, level) - stream.write(')') + stream.write(")") else: self._format(m, stream, indent, 1, context, level) - stream.write(',\n' + ' ' * indent) + stream.write(",\n" + " " * indent) _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map @@ -453,16 +491,14 @@ class PrettyPrinter: stream.write(repr(object)) return cls = object.__class__ - stream.write(cls.__name__ + '(') + stream.write(cls.__name__ + "(") indent += len(cls.__name__) + 1 - stream.write('[') + stream.write("[") if object.maxlen is None: - self._format_items(object, stream, indent, allowance + 2, - context, level) - stream.write('])') + self._format_items(object, stream, indent, allowance + 2, context, level) + stream.write("])") else: - self._format_items(object, stream, indent, 2, - context, level) + self._format_items(object, stream, indent, 2, context, level) rml = self._repr(object.maxlen, context, level) stream.write(f"],\n{' ' * indent}maxlen={rml})") @@ -516,10 +552,8 @@ class PrettyPrinter: else: items = object.items() for k, v in items: - krepr, kreadable, krecur = self.format( - k, context, maxlevels, level) - vrepr, vreadable, vrecur = self.format( - v, context, maxlevels, level) + krepr, kreadable, krecur = self.format(k, context, maxlevels, level) + vrepr, vreadable, vrecur = self.format(v, context, maxlevels, level) append(f"{krepr}: {vrepr}") readable = readable and kreadable and vreadable if krecur or vrecur: @@ -527,8 +561,9 @@ class PrettyPrinter: del context[objid] return "{%s}" % ", ".join(components), readable, recursive - if (issubclass(typ, list) and r is list.__repr__) or \ - (issubclass(typ, tuple) and r is tuple.__repr__): + if (issubclass(typ, list) and r is list.__repr__) or ( + issubclass(typ, tuple) and r is tuple.__repr__ + ): if issubclass(typ, list): if not object: return "[]", True, False @@ -551,8 +586,7 @@ class PrettyPrinter: append = components.append level += 1 for o in object: - orepr, oreadable, orecur = self.format( - o, context, maxlevels, level) + orepr, oreadable, orecur = self.format(o, context, maxlevels, level) append(orepr) if not oreadable: readable = False @@ -562,21 +596,21 @@ class PrettyPrinter: return format % ", ".join(components), readable, recursive rep = repr(object) - return rep, (rep and not rep.startswith('<')), False + return rep, (rep and not rep.startswith("<")), False + + +_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)}) -_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, - bool, type(None)}) def _recursion(object): - return ("<Recursion on %s with id=%s>" - % (type(object).__name__, id(object))) + return f"<Recursion on {type(object).__name__} with id={id(object)}>" def _wrap_bytes_repr(object, width, allowance): - current = b'' + current = b"" last = len(object) // 4 * 4 for i in range(0, len(object), 4): - part = object[i: i+4] + part = object[i : i + 4] candidate = current + part if i == last: width -= allowance From 295312000338670c0d83dfb75ce16a34be851eb2 Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Fri, 17 Nov 2023 19:20:55 +0000 Subject: [PATCH 5/7] Fix typing information for the pprint module There is more type information that could be added. We can add those later to make it easier, this is jsut the minimum to allow linting to pass --- src/_pytest/_io/pprint.py | 19 ++++++++++++------- src/_pytest/_io/saferepr.py | 10 ++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 3bb4a2c7d..04f7edbbe 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -2,9 +2,6 @@ # (https://github.com/python/cpython/) at commit # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). # -# flake8: noqa -# type: ignore -# # # Original Author: Fred L. Drake, Jr. # fdrake@acm.org @@ -21,6 +18,11 @@ import re import sys as _sys import types as _types from io import StringIO as _StringIO +from typing import Any +from typing import Callable +from typing import Dict +from typing import IO +from typing import List class _safe_key: @@ -49,7 +51,7 @@ class _safe_key: def _safe_tuple(t): - "Helper function for comparing 2-tuples" + """Helper function for comparing 2-tuples""" return _safe_key(t[0]), _safe_key(t[1]) @@ -107,7 +109,7 @@ class PrettyPrinter: self._sort_dicts = sort_dicts self._underscore_numbers = underscore_numbers - def pformat(self, object): + def pformat(self, object: Any) -> str: sio = _StringIO() self._format(object, sio, 0, 0, {}, 0) return sio.getvalue() @@ -157,7 +159,10 @@ class PrettyPrinter: self._format_namespace_items(items, stream, indent, allowance, context, level) stream.write(")") - _dispatch = {} + _dispatch: Dict[ + Callable[..., str], + Callable[["PrettyPrinter", Any, IO[str], int, int, Dict[int, int], int], str], + ] = {} def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write @@ -544,7 +549,7 @@ class PrettyPrinter: context[objid] = 1 readable = True recursive = False - components = [] + components: List[str] = [] append = components.append level += 1 if self._sort_dicts: diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 50d6a303a..ba8ea4302 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -5,7 +5,7 @@ from typing import Dict from typing import IO from typing import Optional -from .pprint import PrettyPrinter # type: ignore +from .pprint import PrettyPrinter def _try_repr_or_str(obj: object) -> str: @@ -148,13 +148,11 @@ class AlwaysDispatchingPrettyPrinter(PrettyPrinter): context: Dict[int, Any], level: int, ) -> None: - # Type ignored because _dispatch is private. - p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined] + p = self._dispatch.get(type(object).__repr__, None) objid = id(object) if objid in context or p is None: - # Type ignored because _format is private. - super()._format( # type: ignore[misc] + super()._format( object, stream, indent, @@ -177,6 +175,6 @@ def _pformat_dispatch( *, compact: bool = False, ) -> str: - return AlwaysDispatchingPrettyPrinter( # type: ignore + return AlwaysDispatchingPrettyPrinter( indent=indent, width=width, depth=depth, compact=compact ).pformat(object) From 19934b2b0cc83eef0830b9e2adb6c5301a7201fc Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Sat, 18 Nov 2023 09:07:00 +0000 Subject: [PATCH 6/7] Merge the AlwaysDispathPrettyPrinter into the now vendored PrettyPrinter We don't need to keep the separation anymore, and this will make it easier to extend --- src/_pytest/_io/pprint.py | 47 ++++++++++++++++----------------- src/_pytest/_io/saferepr.py | 49 ----------------------------------- src/_pytest/assertion/util.py | 7 ++--- testing/io/test_pprint.py | 8 ++++++ testing/io/test_saferepr.py | 7 ----- 5 files changed, 34 insertions(+), 84 deletions(-) create mode 100644 testing/io/test_pprint.py diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 04f7edbbe..9923d0a62 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -121,31 +121,28 @@ class PrettyPrinter: self._recursive = True self._readable = False return - rep = self._repr(object, context, level) - max_width = self._width - indent - allowance - if len(rep) > max_width: - p = self._dispatch.get(type(object).__repr__, None) - if p is not None: - context[objid] = 1 - p(self, object, stream, indent, allowance, context, level + 1) - del context[objid] - return - elif ( - _dataclasses.is_dataclass(object) - and not isinstance(object, type) - and object.__dataclass_params__.repr - and - # Check dataclass has generated repr method. - hasattr(object.__repr__, "__wrapped__") - and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ - ): - context[objid] = 1 - self._pprint_dataclass( - object, stream, indent, allowance, context, level + 1 - ) - del context[objid] - return - stream.write(rep) + + p = self._dispatch.get(type(object).__repr__, None) + if p is not None: + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + elif ( + _dataclasses.is_dataclass(object) + and not isinstance(object, type) + and object.__dataclass_params__.repr + and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") + and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ + ): + context[objid] = 1 + self._pprint_dataclass( + object, stream, indent, allowance, context, level + 1 + ) + del context[objid] + else: + stream.write(self._repr(object, context, level)) def _pprint_dataclass(self, object, stream, indent, allowance, context, level): cls_name = object.__class__.__name__ diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index ba8ea4302..c51578ed4 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,12 +1,7 @@ import pprint import reprlib -from typing import Any -from typing import Dict -from typing import IO from typing import Optional -from .pprint import PrettyPrinter - def _try_repr_or_str(obj: object) -> str: try: @@ -134,47 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: return repr(obj) except Exception as exc: return _format_repr_exception(exc, obj) - - -class AlwaysDispatchingPrettyPrinter(PrettyPrinter): - """PrettyPrinter that always dispatches (regardless of width).""" - - def _format( - self, - object: object, - stream: IO[str], - indent: int, - allowance: int, - context: Dict[int, Any], - level: int, - ) -> None: - p = self._dispatch.get(type(object).__repr__, None) - - objid = id(object) - if objid in context or p is None: - super()._format( - object, - stream, - indent, - allowance, - context, - level, - ) - return - - context[objid] = 1 - p(self, object, stream, indent, allowance, context, level + 1) - del context[objid] - - -def _pformat_dispatch( - object: object, - indent: int = 1, - width: int = 80, - depth: Optional[int] = None, - *, - compact: bool = False, -) -> str: - return AlwaysDispatchingPrettyPrinter( - indent=indent, width=width, depth=depth, compact=compact - ).pformat(object) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 65abe8d23..4d9fd114b 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -16,7 +16,7 @@ from unicodedata import normalize import _pytest._code from _pytest import outcomes -from _pytest._io.saferepr import _pformat_dispatch +from _pytest._io.pprint import PrettyPrinter from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr_unlimited from _pytest.config import Config @@ -348,8 +348,9 @@ def _compare_eq_iterable( lines_left = len(left_formatting) lines_right = len(right_formatting) if lines_left != lines_right: - left_formatting = _pformat_dispatch(left).splitlines() - right_formatting = _pformat_dispatch(right).splitlines() + printer = PrettyPrinter() + left_formatting = printer.pformat(left).splitlines() + right_formatting = printer.pformat(right).splitlines() if lines_left > 1 or lines_right > 1: _surrounding_parens_on_own_lines(left_formatting) diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py new file mode 100644 index 000000000..900ccbbfa --- /dev/null +++ b/testing/io/test_pprint.py @@ -0,0 +1,8 @@ +from _pytest._io.pprint import PrettyPrinter + + +def test_pformat_dispatch(): + printer = PrettyPrinter(width=5) + assert printer.pformat("a") == "'a'" + assert printer.pformat("a" * 10) == "'aaaaaaaaaa'" + assert printer.pformat("foo bar") == "('foo '\n 'bar')" diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 24746bc22..d94faa4f1 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,5 +1,4 @@ import pytest -from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr_unlimited @@ -159,12 +158,6 @@ def test_unicode(): assert saferepr(val) == reprval -def test_pformat_dispatch(): - assert _pformat_dispatch("a") == "'a'" - assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'" - assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')" - - def test_broken_getattribute(): """saferepr() can create proper representations of classes with broken __getattribute__ (#7145) From 53d7b5ed3ef93780b866a9fea3d8afe48f36d319 Mon Sep 17 00:00:00 2001 From: Benjamin Schubert <bschubert15@bloomberg.net> Date: Sat, 18 Nov 2023 09:21:44 +0000 Subject: [PATCH 7/7] Add some tests for the pprint module --- testing/io/test_pprint.py | 332 +++++++++++++++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 5 deletions(-) diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py index 900ccbbfa..8c15740bd 100644 --- a/testing/io/test_pprint.py +++ b/testing/io/test_pprint.py @@ -1,8 +1,330 @@ +import textwrap +from collections import ChainMap +from collections import Counter +from collections import defaultdict +from collections import deque +from collections import OrderedDict +from dataclasses import dataclass +from types import MappingProxyType +from types import SimpleNamespace +from typing import Any + +import pytest from _pytest._io.pprint import PrettyPrinter -def test_pformat_dispatch(): - printer = PrettyPrinter(width=5) - assert printer.pformat("a") == "'a'" - assert printer.pformat("a" * 10) == "'aaaaaaaaaa'" - assert printer.pformat("foo bar") == "('foo '\n 'bar')" +@dataclass +class EmptyDataclass: + pass + + +@dataclass +class DataclassWithOneItem: + foo: str + + +@dataclass +class DataclassWithTwoItems: + foo: str + bar: str + + +@pytest.mark.parametrize( + ("data", "expected"), + ( + pytest.param( + EmptyDataclass(), + "EmptyDataclass()", + id="dataclass-empty", + ), + pytest.param( + DataclassWithOneItem(foo="bar"), + """ + DataclassWithOneItem(foo='bar') + """, + id="dataclass-one-item", + ), + pytest.param( + DataclassWithTwoItems(foo="foo", bar="bar"), + """ + DataclassWithTwoItems(foo='foo', + bar='bar') + """, + id="dataclass-two-items", + ), + pytest.param( + {}, + "{}", + id="dict-empty", + ), + pytest.param( + {"one": 1}, + """ + {'one': 1} + """, + id="dict-one-item", + ), + pytest.param( + {"one": 1, "two": 2}, + """ + {'one': 1, + 'two': 2} + """, + id="dict-two-items", + ), + pytest.param(OrderedDict(), "OrderedDict()", id="ordereddict-empty"), + pytest.param( + OrderedDict({"one": 1}), + """ + OrderedDict([('one', + 1)]) + """, + id="ordereddict-one-item", + ), + pytest.param( + OrderedDict({"one": 1, "two": 2}), + """ + OrderedDict([('one', + 1), + ('two', + 2)]) + """, + id="ordereddict-two-items", + ), + pytest.param( + [], + "[]", + id="list-empty", + ), + pytest.param( + [1], + """ + [1] + """, + id="list-one-item", + ), + pytest.param( + [1, 2], + """ + [1, + 2] + """, + id="list-two-items", + ), + pytest.param( + tuple(), + "()", + id="tuple-empty", + ), + pytest.param( + (1,), + """ + (1,) + """, + id="tuple-one-item", + ), + pytest.param( + (1, 2), + """ + (1, + 2) + """, + id="tuple-two-items", + ), + pytest.param( + set(), + "set()", + id="set-empty", + ), + pytest.param( + {1}, + """ + {1} + """, + id="set-one-item", + ), + pytest.param( + {1, 2}, + """ + {1, + 2} + """, + id="set-two-items", + ), + pytest.param( + MappingProxyType({}), + "mappingproxy({})", + id="mappingproxy-empty", + ), + pytest.param( + MappingProxyType({"one": 1}), + """ + mappingproxy({'one': 1}) + """, + id="mappingproxy-one-item", + ), + pytest.param( + MappingProxyType({"one": 1, "two": 2}), + """ + mappingproxy({'one': 1, + 'two': 2}) + """, + id="mappingproxy-two-items", + ), + pytest.param( + SimpleNamespace(), + "namespace()", + id="simplenamespace-empty", + ), + pytest.param( + SimpleNamespace(one=1), + """ + namespace(one=1) + """, + id="simplenamespace-one-item", + ), + pytest.param( + SimpleNamespace(one=1, two=2), + """ + namespace(one=1, + two=2) + """, + id="simplenamespace-two-items", + ), + pytest.param( + defaultdict(str), "defaultdict(<class 'str'>, {})", id="defaultdict-empty" + ), + pytest.param( + defaultdict(str, {"one": "1"}), + """ + defaultdict(<class 'str'>, + {'one': '1'}) + """, + id="defaultdict-one-item", + ), + pytest.param( + defaultdict(str, {"one": "1", "two": "2"}), + """ + defaultdict(<class 'str'>, + {'one': '1', + 'two': '2'}) + """, + id="defaultdict-two-items", + ), + pytest.param( + Counter(), + "Counter()", + id="counter-empty", + ), + pytest.param( + Counter("1"), + """ + Counter({'1': 1}) + """, + id="counter-one-item", + ), + pytest.param( + Counter("121"), + """ + Counter({'1': 2, + '2': 1}) + """, + id="counter-two-items", + ), + pytest.param(ChainMap(), "ChainMap({})", id="chainmap-empty"), + pytest.param( + ChainMap({"one": 1, "two": 2}), + """ + ChainMap({'one': 1, + 'two': 2}) + """, + id="chainmap-one-item", + ), + pytest.param( + ChainMap({"one": 1}, {"two": 2}), + """ + ChainMap({'one': 1}, + {'two': 2}) + """, + id="chainmap-two-items", + ), + pytest.param( + deque(), + "deque([])", + id="deque-empty", + ), + pytest.param( + deque([1]), + """ + deque([1]) + """, + id="deque-one-item", + ), + pytest.param( + deque([1, 2]), + """ + deque([1, + 2]) + """, + id="deque-two-items", + ), + pytest.param( + deque([1, 2], maxlen=3), + """ + deque([1, + 2], + maxlen=3) + """, + id="deque-maxlen", + ), + pytest.param( + { + "chainmap": ChainMap({"one": 1}, {"two": 2}), + "counter": Counter("122"), + "dataclass": DataclassWithTwoItems(foo="foo", bar="bar"), + "defaultdict": defaultdict(str, {"one": "1", "two": "2"}), + "deque": deque([1, 2], maxlen=3), + "dict": {"one": 1, "two": 2}, + "list": [1, 2], + "mappingproxy": MappingProxyType({"one": 1, "two": 2}), + "ordereddict": OrderedDict({"one": 1, "two": 2}), + "set": {1, 2}, + "simplenamespace": SimpleNamespace(one=1, two=2), + "tuple": (1, 2), + }, + """ + {'chainmap': ChainMap({'one': 1}, + {'two': 2}), + 'counter': Counter({'2': 2, + '1': 1}), + 'dataclass': DataclassWithTwoItems(foo='foo', + bar='bar'), + 'defaultdict': defaultdict(<class 'str'>, + {'one': '1', + 'two': '2'}), + 'deque': deque([1, + 2], + maxlen=3), + 'dict': {'one': 1, + 'two': 2}, + 'list': [1, + 2], + 'mappingproxy': mappingproxy({'one': 1, + 'two': 2}), + 'ordereddict': OrderedDict([('one', + 1), + ('two', + 2)]), + 'set': {1, + 2}, + 'simplenamespace': namespace(one=1, + two=2), + 'tuple': (1, + 2)} + """, + id="deep-example", + ), + ), +) +def test_consistent_pretty_printer(data: Any, expected: str) -> None: + assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip()