diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 2066dbb5762158..6fd4a671d4b02a 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -401,13 +401,14 @@ Object Protocol Pretty print *o* with *printer*, and return the result as a string. This is the equivalent of the Python expression ``f'{o!p:{printer}}'``, or - ``f'{o!p}'`` when *printer* is ``NULL``. + ``f'{o!p}'`` when *printer* is ``NULL`` or ``None``. *printer* is called with *o*, and must return an iterable of strings, one for each line of output. The lines are joined with ``'\n'``, with no - trailing newline. If *printer* is ``NULL``, a default printer is used. It - calls *o*'s ``__pprint__()`` method if there is one, and interprets the - description that it returns. Otherwise it uses :c:func:`PyObject_Repr`. + trailing newline. If *printer* is ``NULL`` or ``None``, a default printer + is used. It calls *o*'s ``__pprint__()`` method if there is one, and + interprets the description that it returns. Otherwise it uses + :c:func:`PyObject_Repr`. Raise :exc:`TypeError` if *printer* returns a :class:`str` rather than an iterable of lines, or if any line is not a :class:`str`. diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index f287ecf4b054a9..5e96514e07df66 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -34,6 +34,7 @@ from abc import ABCMeta, abstractmethod import sys +lazy from _pprint import Layout as _Layout GenericAlias = type(list[int]) EllipsisType = type(...) @@ -840,8 +841,8 @@ def __repr__(self): return '{0.__class__.__name__}({0._mapping!r})'.format(self) def __pprint__(self): - from pprint import _mapping_view_pprint - return _mapping_view_pprint(self) + yield _Layout(open=f'{type(self).__name__}(', close=')', hug=True) + yield self._mapping __class_getitem__ = classmethod(GenericAlias) diff --git a/Lib/_pprint_protocol.py b/Lib/_pprint_protocol.py new file mode 100644 index 00000000000000..bef564045008a3 --- /dev/null +++ b/Lib/_pprint_protocol.py @@ -0,0 +1,807 @@ +"""The pretty-print protocol of PEP 813. + +The pprint module re-exports these names with lazy imports, so that importing +pprint alone does not pay for defining the printer. +""" + +import dataclasses as _dataclass +import enum as _enum +import sys as _sys +from collections.abc import Iterable as _Iterable, Mapping as _Mapping, Set as _Set +from itertools import chain as _chain +lazy import re as _re + +from _pprint import ( + VALUES, KWARGS, ITEMS, DELEGATE, Layout, BreakableString, + BreakableBytes, FormattableNumber, +) + + +_EMPTY = "" +_NL = "\n" +_SPACE = " " +_MISSING = sentinel("_MISSING") +LEAF = sentinel("LEAF", repr="pprint.LEAF") +type DescriptionType = _Iterable[object] + + +@_dataclass.dataclass(frozen=True) +class Keyword: + """A keyword item, printed as name=value.""" + + name: str + value: object + default: object = _MISSING + + +@_dataclass.dataclass(frozen=True) +class Entry: + """A key/value item, printed as key: value.""" + + key: object + value: object + + + +class _ComparisonError: + """Stand in for a keyword value whose comparison to its default failed.""" + + def __init__(self, error): + self._error = error + + def __repr__(self): + return f"" + + +def _find_pprint(cls): + """Return the __pprint__ method that applies to instances of cls, or None.""" + mro = cls.__mro__ + pprint_owner = next((base for base in mro if "__pprint__" in vars(base)), None) + if pprint_owner is None: + return None + repr_owner = next(base for base in mro if "__repr__" in vars(base)) + if mro.index(pprint_owner) <= mro.index(repr_owner): + return vars(pprint_owner)["__pprint__"] + return None + + +def _apply_default(keyword): + """Hide a keyword if its value equals its declared default.""" + if keyword.default is _MISSING: + return keyword + try: + same = bool(keyword.value == keyword.default) + except Exception as error: + return Keyword(keyword.name, _ComparisonError(error)) + return None if same else keyword + + +def decompose(obj): + """Read one level of an object's pretty-print description stream.""" + method = _find_pprint(type(obj)) + if method is None: + return LEAF, None + stream = iter(method(obj)) + + first = next(stream, _MISSING) + if first is DELEGATE: + value = next(stream, _MISSING) + return (DELEGATE, value) if value is not _MISSING else (LEAF, None) + + if isinstance(first, Layout): + layout = first + else: + layout = Layout() + if first is not _MISSING: + stream = _chain((first,), stream) + + items = [] + section = VALUES + for item in stream: + if item is VALUES or item is KWARGS or item is ITEMS: + section = item + continue + + match item: + case tuple((str() as name, value)) if section is KWARGS: + item = Keyword(name, value) + case tuple((str() as name, value, default)) if section is KWARGS: + item = Keyword(name, value, default) + case tuple((key, value)) if section is ITEMS: + item = Entry(key, value) + + if isinstance(item, Keyword): + item = _apply_default(item) + if item is None: + continue + items.append(item) + + name = type(obj).__name__ if layout.name is None else layout.name + if layout.open is None: + keyed = len(items) > 0 and all(isinstance(item, Entry) for item in items) + if keyed: + open_, close = f"{name}({{", "})" + else: + open_, close = f"{name}(", ")" + else: + open_ = layout.open + close = _EMPTY if layout.close is None else layout.close + + if layout.close is not None: + close = layout.close + + filled = Layout( + hints=layout.hints, + name=name, + open=open_, + close=close, + sep=layout.sep, + empty=open_ + close if layout.empty is None else layout.empty, + single_close=close if layout.single_close is None else layout.single_close, + recursion=layout.recursion, + sortable=layout.sortable, + hug=layout.hug, + ) + return filled, items + + +@_dataclass.dataclass(frozen=True) +class _ValueDocument: + value: object + + +@_dataclass.dataclass(frozen=True) +class _LazyDocument: + value: object + ancestors: tuple + + +@_dataclass.dataclass(frozen=True) +class _TextDocument: + text: str + source: object = None + + +@_dataclass.dataclass(frozen=True) +class _EntryDocument: + key: object + value: object + key_source: object + + +@_dataclass.dataclass(frozen=True) +class _KeywordDocument: + name: str + value: object + source: object + + +@_dataclass.dataclass(frozen=True) +class _ContainerDocument: + source: object + layout: Layout + items: tuple + + +def _build_document(obj, ancestors=()): + """Build a printer-independent document node for *obj*. + + Nested objects' descriptions are read when layout conversion reaches them. + """ + ancestor_ids = {id(ancestor) for ancestor in ancestors} + + header, items = decompose(obj) + if header is LEAF: + return _ValueDocument(obj) + if header is DELEGATE: + if id(obj) in ancestor_ids: + return _TextDocument("...", obj) + return _build_document(items, ancestors + (obj,)) + + if id(obj) in ancestor_ids: + return _TextDocument(header.recursion, obj) + ancestors = ancestors + (obj,) + documents = [] + for item in items: + match item: + case Entry(key=key, value=value): + documents.append( + _EntryDocument( + _LazyDocument(key, ancestors), + _LazyDocument(value, ancestors), + key, + ) + ) + case Keyword(name=name, value=value): + documents.append( + _KeywordDocument(name, _LazyDocument(value, ancestors), item) + ) + case _: + documents.append(_LazyDocument(item, ancestors)) + return _ContainerDocument(obj, header, tuple(documents)) + +# End of object descriptions and structural documents. +######################################################################## + + +######################################################################## +# Printer formatting and rendering. +# +# A layout document is a tree of text and layout instructions. It represents +# the output before line breaks are chosen; the renderer turns it into lines. +@_dataclass.dataclass(frozen=True) +class _Text: + text: str + + +@_dataclass.dataclass(frozen=True) +class _Line: + """Flat text when a group fits, or a line break otherwise.""" + + flat: str = _SPACE + + +@_dataclass.dataclass(frozen=True) +class _Concat: + parts: tuple + + +@_dataclass.dataclass(frozen=True) +class _Nest: + width: int + doc: object + + +@_dataclass.dataclass(frozen=True) +class _Align: + doc: object + + +@_dataclass.dataclass(frozen=True) +class _Group: + doc: object + + +@_dataclass.dataclass(frozen=True) +class _IfBroken: + broken: str + flat: str = _EMPTY + + +@_dataclass.dataclass(frozen=True) +class _KeyValue: + key: object + value: object + hang: int | None + + +class _Marker: + """A marker rendered by a function from the printer's marker table.""" + + __slots__ = ("value", "render", "top_level", "_flat") + + def __init__(self, value, render, top_level): + self.value = value + self.render = render + self.top_level = top_level + self._flat = None + + def lines(self, width, last_width): + return list(self.render( + self.value, + width=width, + last_width=last_width, + top_level=self.top_level, + )) + + def flat(self): + if self._flat is None: + self._flat = self.lines(_sys.maxsize, _sys.maxsize) + return self._flat + + +_SOFTLINE = _Line(_EMPTY) +_COLON_SPACE = _Text(": ") + + +def _join(separator, docs): + parts = [] + for index, doc in enumerate(docs): + if index: + parts.append(separator) + parts.append(doc) + return _Concat(tuple(parts)) + + +class Expand(_enum.Enum): + """Policy for laying out containers that do not fit on one line.""" + + NEVER = _enum.auto() + FIT = _enum.auto() + ALWAYS = _enum.auto() + + def __repr__(self): + return f"{type(self).__name__}.{self.name}" + + +@_dataclass.dataclass(frozen=True) +class _FormatOptions: + indent: int + width: int + depth: int | None + compact: bool + sort_dicts: bool + expand: Expand + number_format: str | None + markers: _Mapping[type, object] + + +class Printer: + """Default printer. Calling an instance yields the formatted lines.""" + + def __init__( + self, + indent=4, + width=78, + depth=None, + *, + compact=False, + expand=Expand.FIT, + sort_dicts=True, + number_format=None, + ): + 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') + markers = { + BreakableString: self._render_literal, + BreakableBytes: self._render_literal, + FormattableNumber: self._render_number, + } + self._options = _FormatOptions( + indent, width, depth, compact, sort_dicts, expand, number_format, + markers, + ) + + def __call__(self, obj): + """Yield the formatted lines for *obj*.""" + # Build the complete structural document before applying printer settings. + document = _build_document(obj) + layout_document = _layout_document(document, 0, self._options) + yield from _render_lines(layout_document, self._options.width) + + def _render_literal(self, marker, *, width, last_width, top_level): + value = marker.value + rep = repr(value) + if len(rep) <= last_width or len(value) == 0: + return [rep] + allowance = width - last_width + if top_level: + pad = self._options.indent if self._options.expand is not Expand.NEVER else 1 + width -= pad + allowance += 1 + else: + pad = 0 + wrap = _wrap_bytes if isinstance(value, bytes) else _wrap_string + chunks = wrap(value, width, allowance) + if len(chunks) <= 1: + return [rep] + if not top_level: + return chunks + spaces = _SPACE * pad + if self._options.expand is not Expand.NEVER: + return ["(", *(spaces + chunk for chunk in chunks), ")"] + return [ + "(" + chunks[0], + *(spaces + chunk for chunk in chunks[1:-1]), + spaces + chunks[-1] + ")", + ] + + def _render_number(self, marker, *, width, last_width, top_level): + number_format = self._options.number_format + if number_format is None: + return [repr(marker.value)] + try: + return [format(marker.value, number_format)] + except Exception: + return [repr(marker.value)] + + +def _wrap_string(text, max_width, allowance): + """Split a string's repr into chunks, as PrettyPrinter does.""" + chunks = [] + lines = text.splitlines(True) + max_width1 = max_width + 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) + continue + # A list of alternating (non-space, space) strings, ending with an + # empty string. + parts = _re.findall(r"\S*\s*", line) + parts.pop() + max_width2 = max_width + current = _EMPTY + 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)) + return chunks + + +def _wrap_bytes(data, max_width, allowance): + """Split a bytes value's repr into chunks, as PrettyPrinter does.""" + chunks = [] + current = b"" + last = len(data) // 4 * 4 + for i in range(0, len(data), 4): + part = data[i:i + 4] + candidate = current + part + if i == last: + max_width -= allowance + if len(repr(candidate)) > max_width: + if current: + chunks.append(repr(current)) + current = part + else: + current = candidate + if current: + chunks.append(repr(current)) + return chunks + + +def _find_marker(value, markers): + """Return the function in *markers* for *value*'s class, or None.""" + for cls in type(value).__mro__: + render = markers.get(cls) + if render is not None: + return render + return None + + +def _layout_document(document, level, options): + """Apply printer options to a structural document and return layout instructions.""" + match document: + case _LazyDocument(value=value, ancestors=ancestors): + return _layout_document( + _build_document(value, ancestors), level, options + ) + case _TextDocument(text=text): + return _Text(text) + case _ValueDocument(value=value): + render = _find_marker(value, options.markers) + if render is None: + return _Text(repr(value)) + return _Marker(value, render, level == 0) + case _ContainerDocument(layout=header, items=items): + if not items: + return _Text(header.empty) + if options.depth is not None and level >= options.depth: + return _Text(f"{header.open}...{header.close}") + + if header.sortable and options.sort_dicts: + items = _sorted_documents(items, level, options) + hugged = None + if header.hug and options.expand is not Expand.NEVER: + items = tuple(_resolve_item(item) for item in items) + hugged = _hugged_index(items) + parts = [_item_document(item, level + 1, options) for item in items] + if hugged is not None: + return _hugged_document(header, parts) + positional = not any( + isinstance(item, (_EntryDocument, _KeywordDocument)) for item in items + ) + return _container_document(header, parts, positional, level, options) + case _: + raise TypeError(f"unexpected document node: {document!r}") + + +def _document_source(document): + match document: + case _ValueDocument(value=value): + return value + case _ContainerDocument(source=value): + return value + case _LazyDocument(value=value): + return value + case _EntryDocument(key_source=value): + return value + case _KeywordDocument(source=value): + return value + case _TextDocument(source=value): + return value + case _: + raise TypeError(f"unexpected structural document: {document!r}") + + +def _document_sort_key(item): + """Return an entry's key, or the item itself, for sorting.""" + return item.key if isinstance(item, _EntryDocument) else item + + +def _sorted_documents(items, level, options): + """Sort an object's items, keeping keyword items after the others.""" + keywords = sorted( + (item for item in items if isinstance(item, _KeywordDocument)), + key=lambda item: item.name, + ) + others = [item for item in items if not isinstance(item, _KeywordDocument)] + return _sorted_values(others, level, options) + keywords + + +def _sorted_values(items, level, options): + if not any( + isinstance(_document_source(_document_sort_key(item)), _Set) for item in items + ): + try: + return sorted( + items, + key=lambda item: _document_source(_document_sort_key(item)), + ) + except TypeError: + pass + return sorted( + items, + key=lambda item: _flat_document(_document_sort_key(item), level + 1, options), + ) + + +def _flat_document(document, level, options): + layout_document = _layout_document(document, level, options) + return _NL.join(_render_lines(_Group(layout_document), _sys.maxsize)) + + +def _item_document(item, level, options): + match item: + case _EntryDocument(key=key, value=value): + hang = options.indent if options.expand is Expand.NEVER else None + return _KeyValue( + _layout_document(key, level, options), + _layout_document(value, level, options), + hang, + ) + case _KeywordDocument(name=name, value=value): + return _Concat( + (_Text(f"{name}="), _layout_document(value, level, options)) + ) + case _: + return _layout_document(item, level, options) + + +def _resolve_item(item): + """Build the documents of an item's lazy parts.""" + match item: + case _LazyDocument(value=value, ancestors=ancestors): + return _build_document(value, ancestors) + case _KeywordDocument(value=_LazyDocument(value=value, ancestors=ancestors)): + return _KeywordDocument( + item.name, _build_document(value, ancestors), item.source + ) + case _: + return item + + +def _hugged_index(items): + """Return the index of the only item that is a non-empty container, or None.""" + found = None + for index, item in enumerate(items): + document = item.value if isinstance(item, _KeywordDocument) else item + if isinstance(document, _ContainerDocument) and len(document.items) > 0: + if found is not None: + return None + found = index + return found + + +def _hugged_document(header, parts): + """Keep every item on the opening and closing lines. + + Only the one container among the items breaks across lines, so its + brackets hug the object's own, as in ``defaultdict(, {``. + """ + close = header.single_close if len(parts) == 1 else header.close + separator = _Text(header.sep + _SPACE) + return _Group( + _Concat((_Text(header.open), _join(separator, parts), _Text(close))) + ) + + +def _container_document(header, parts, positional, level, options): + close = header.single_close if len(parts) == 1 else header.close + separator = _Text(header.sep) + + if options.expand is Expand.NEVER: + if options.compact and positional: + body = _join(_Concat((separator, _Group(_Line()))), parts) + return _Concat((_Text(header.open), _Align(body), _Text(close))) + body = _join(_Concat((separator, _Line())), parts) + return _Group(_Concat((_Text(header.open), _Align(body), _Text(close)))) + + body = _join(_Concat((separator, _Line())), parts) + doc = _Concat( + ( + _Text(header.open), + _Nest(options.indent, _Concat((_SOFTLINE, body, _IfBroken(header.sep)))), + _SOFTLINE, + _IfBroken(header.close, flat=close), + ) + ) + if options.expand is Expand.ALWAYS and level > 0: + return doc + return _Group(doc) + + +def _render_lines(doc, width): + out = [] + column = 0 + # Each pending item is (indent, flat, node): nodes come from the layout + # document, while indent and flat carry the rendering context. Compound + # nodes add their children to this stack as they are traversed. The stack + # can grow to O(N), where N is the number of layout nodes: a deep document + # leaves pending nodes at each level, while a wide _Concat adds many at once. + stack = [(0, False, doc)] + while stack: + indent, flat, node = stack.pop() + match node: + case _Text(text=text): + parts = text.split(_NL) + for part in parts[:-1]: + out.append(part) + yield _EMPTY.join(out) + out.clear() + out.append(parts[-1]) + column = len(parts[-1]) if len(parts) > 1 else column + len(text) + case _Line(flat=flat_text): + if flat: + out.append(flat_text) + column += len(flat_text) + else: + yield _EMPTY.join(out) + out.clear() + out.append(_SPACE * indent) + column = indent + case _Concat(parts=parts): + stack.extend((indent, flat, part) for part in reversed(parts)) + case _Nest(width=extra, doc=inner): + stack.append((indent + extra, flat, inner)) + case _Align(doc=inner): + stack.append((column, flat, inner)) + case _Group(doc=inner): + fits = flat or _fits(width - column, (indent, True, inner), stack) + stack.append((indent, fits, inner)) + case _IfBroken(broken=broken, flat=flat_text): + text = flat_text if flat else broken + out.append(text) + column += len(text) + case _Marker(): + if flat: + lines = node.flat() + else: + room = width - column + lines = node.lines(room, room - _trailing_width(stack)) + start = column + for index, line in enumerate(lines): + if index > 0: + yield _EMPTY.join(out) + out.clear() + line = _SPACE * start + line + column = 0 + out.append(line) + column += len(line) + case _KeyValue(key=key, value=value, hang=hang): + if flat or hang is None: + stack.append((indent, flat, value)) + stack.append((indent, flat, _COLON_SPACE)) + stack.append((indent, flat, key)) + elif isinstance(key, _Text) or _fits( + width - column, (indent, True, _Concat((key, _Text(":")))), [] + ): + stack.append((indent, False, value)) + stack.append((indent, False, _COLON_SPACE)) + stack.append((indent, True, key)) + else: + hanging = column + hang + stack.append((hanging, False, value)) + stack.append((hanging, False, _Line())) + stack.append((indent, False, _Text(":"))) + stack.append((indent, False, key)) + yield _EMPTY.join(out) + + +def _fits(remaining, first, rest): + local = [first] + index = len(rest) + while remaining >= 0: + if local: + indent, flat, node = local.pop() + elif index: + index -= 1 + indent, flat, node = rest[index] + else: + return True + + match node: + case _Text(text=text): + remaining -= len(text) + case _Line(flat=flat_text): + if flat: + remaining -= len(flat_text) + else: + return True + case _Concat(parts=parts): + local.extend((indent, flat, part) for part in reversed(parts)) + case _Nest(doc=inner) | _Align(doc=inner) | _Group(doc=inner): + local.append((indent, flat, inner)) + case _IfBroken(broken=broken, flat=flat_text): + remaining -= len(flat_text if flat else broken) + case _KeyValue(key=key, value=value): + local.append((indent, flat, _Concat((key, _COLON_SPACE, value)))) + case _Marker(): + if flat: + lines = node.flat() + if len(lines) != 1: + return False + else: + lines = node.lines(remaining, remaining) + remaining -= len(lines[0]) + if len(lines) > 1: + # The rest of the marker goes on later lines. + return remaining >= 0 + return False + + +def _trailing_width(stack): + """Return the width of the text that follows on the current line. + + The scan stops at the first possible line break, including any group, + since whether a group breaks is not known yet. This is the allowance + that PrettyPrinter leaves for closing brackets and separators. + """ + total = 0 + local = [] + index = len(stack) + while True: + if local: + indent, flat, node = local.pop() + elif index: + index -= 1 + indent, flat, node = stack[index] + else: + return total + + match node: + case _Text(text=text): + if _NL in text: + return total + text.index(_NL) + total += len(text) + case _Line(flat=flat_text): + if not flat: + return total + total += len(flat_text) + case _Concat(parts=parts): + local.extend((indent, flat, part) for part in reversed(parts)) + case _Nest(doc=inner) | _Align(doc=inner): + local.append((indent, flat, inner)) + case _IfBroken(broken=broken, flat=flat_text): + total += len(flat_text if flat else broken) + case _: + return total + +# End of printer formatting and rendering. +######################################################################## diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index e8a4a28b2607c3..84c8e01732d1ba 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -34,6 +34,10 @@ lazy from copy import copy as _copy lazy from heapq import nlargest as _nlargest +lazy from _pprint import DELEGATE as _DELEGATE +lazy from _pprint import ITEMS as _ITEMS +lazy from _pprint import KWARGS as _KWARGS +lazy from _pprint import Layout as _Layout from itertools import chain as _chain from itertools import repeat as _repeat from itertools import starmap as _starmap @@ -283,8 +287,9 @@ def __repr__(self): return '%s(%r)' % (self.__class__.__name__, dict(self.items())) def __pprint__(self): - from pprint import _ordered_dict_pprint - return _ordered_dict_pprint(self) + yield _Layout(empty=f'{type(self).__name__}()') + yield _ITEMS + yield from self.items() def __reduce__(self): 'Return state information for pickling' @@ -478,9 +483,8 @@ def __repr__(self): return self.__class__.__name__ + repr_fmt % self def __pprint__(self): - from pprint import KWARGS, Layout - yield Layout(name=self.__class__.__name__) - yield KWARGS + yield _Layout(name=self.__class__.__name__) + yield _KWARGS yield from zip(self._fields, self) def _asdict(self): @@ -771,8 +775,10 @@ def __repr__(self): return f'{self.__class__.__name__}({d!r})' def __pprint__(self): - from pprint import _counter_pprint - return _counter_pprint(self) + name = type(self).__name__ + yield _Layout(open=f'{name}({{', close='})', empty=f'{name}()') + yield _ITEMS + yield from self.most_common() # Multiset-style mathematical operations discussed in: # Knuth TAOCP Volume II section 4.6.3 exercise 19 @@ -1111,8 +1117,8 @@ def __repr__(self): return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})' def __pprint__(self): - from pprint import _chainmap_pprint - return _chainmap_pprint(self) + yield _Layout(open=f'{type(self).__name__}(', close=')', empty=repr(self)) + yield from self.maps @classmethod def fromkeys(cls, iterable, value=None, /): @@ -1237,8 +1243,8 @@ def __repr__(self): return repr(self.data) def __pprint__(self): - from pprint import _user_container_pprint - return _user_container_pprint(self) + yield _DELEGATE + yield self.data def __or__(self, other): if isinstance(other, UserDict): @@ -1324,8 +1330,8 @@ def __repr__(self): return repr(self.data) def __pprint__(self): - from pprint import _user_container_pprint - return _user_container_pprint(self) + yield _DELEGATE + yield self.data def __lt__(self, other): return self.data < self.__cast(other) @@ -1460,8 +1466,8 @@ def __repr__(self): return repr(self.data) def __pprint__(self): - from pprint import _user_container_pprint - return _user_container_pprint(self) + yield _DELEGATE + yield self.data def __int__(self): return int(self.data) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index bac30d6bdaeac5..2f5946cd5fb3d1 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -7,6 +7,7 @@ from reprlib import recursive_repr lazy import copy lazy import re +lazy from _pprint import KWARGS, Layout __all__ = ['dataclass', @@ -922,8 +923,6 @@ def _set_new_attribute(cls, name, value): def _dataclass_pprint(obj, field_names): - from pprint import KWARGS, Layout - yield Layout(name=obj.__class__.__qualname__) yield KWARGS for name in field_names: diff --git a/Lib/pprint.py b/Lib/pprint.py index a8755cce7c4e3d..4fac0e619d035d 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -38,37 +38,25 @@ import sys as _sys import types as _types from io import StringIO as _StringIO - -# The pretty-print protocol of PEP 813. -_PROTOCOL_NAMES = ( - "VALUES", "KWARGS", "ITEMS", "DELEGATE", "LEAF", "Layout", "Keyword", - "Entry", "BreakableString", "BreakableBytes", "FormattableNumber", - "DescriptionType", "decompose", "Printer", "Expand", +lazy import dataclasses as _dataclass +lazy import re as _re + +# The pretty-print protocol of PEP 813. The names implemented in Python are +# imported lazily, so that importing pprint alone does not pay for them. +from _pprint import ( + VALUES, KWARGS, ITEMS, DELEGATE, Layout, BreakableString, + BreakableBytes, FormattableNumber, +) +lazy from _pprint_protocol import ( + LEAF, DescriptionType, Entry, Expand, Keyword, Printer, decompose, ) __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr", - "PrettyPrinter", "pp", *_PROTOCOL_NAMES] - - -_PROTOCOL_IMPLEMENTATION_NAMES = frozenset({ - *_PROTOCOL_NAMES, - "_ordered_dict_pprint", "_mapping_view_pprint", "_counter_pprint", - "_chainmap_pprint", "_user_container_pprint", -}) - - -def __getattr__(name): - if name in _PROTOCOL_IMPLEMENTATION_NAMES: - _load_protocol() - try: - return globals()[name] - except KeyError: - pass - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def __dir__(): - return sorted({*globals(), *_PROTOCOL_NAMES}) + "PrettyPrinter", "pp", + # New in PEP 813. + "BreakableBytes", "BreakableString", "DELEGATE", "DescriptionType", + "Entry", "Expand", "FormattableNumber", "ITEMS", "KWARGS", "Keyword", + "LEAF", "Layout", "Printer", "VALUES", "decompose"] def pprint(object, stream=None, indent=1, width=80, depth=None, *, @@ -230,15 +218,12 @@ def _format(self, object, stream, indent, allowance, context, level): max_width = self._width - indent - prefix_len - allowance if len(rep) > max_width: p = self._dispatch.get(type(object).__repr__, None) - # Lazy import to improve module import time - from dataclasses import is_dataclass - if p is not None: context[objid] = 1 p(self, object, stream, indent, allowance, context, level + 1) del context[objid] return - elif (is_dataclass(object) and + elif (_dataclass.is_dataclass(object) and not isinstance(object, type) and object.__dataclass_params__.repr and # Check dataclass has generated repr method. @@ -289,15 +274,12 @@ def _write_indent_padding(self, write): write((self._indent_per_level - 1) * " ") def _pprint_dataclass(self, object, stream, indent, allowance, context, level): - # Lazy import to improve module import time - from dataclasses import fields as dataclass_fields - cls_name = object.__class__.__name__ if self._expand: indent += self._indent_per_level else: indent += len(cls_name) + 1 - items = [(f.name, getattr(object, f.name)) for f in dataclass_fields(object) if f.repr] + items = [(f.name, getattr(object, f.name)) for f in _dataclass.fields(object) if f.repr] stream.write(self._format_block_start(cls_name + '(', indent)) self._format_namespace_items(items, stream, indent, allowance, context, level) stream.write(self._format_block_end(')', indent - self._indent_per_level)) @@ -472,11 +454,8 @@ def _pprint_str(self, object, stream, indent, allowance, context, level): if len(rep) <= max_width1: chunks.append(rep) else: - # Lazy import to improve module import time - import re - # 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 @@ -998,829 +977,3 @@ def _wrap_bytes_repr(object, width, allowance): current = candidate if current: yield repr(current) - - -# PEP 813 protocol definitions are initialized on demand so importing pprint -# alone keeps the traditional lightweight behavior. -def _load_protocol(): - if "Printer" in globals(): - return - import enum - from collections.abc import Iterable, Mapping, Set - from dataclasses import dataclass - from itertools import chain - from _pprint import ( - VALUES, KWARGS, ITEMS, DELEGATE, Layout, BreakableString, - BreakableBytes, FormattableNumber, _LeafMarker, - ) - - - _EMPTY = "" - _NL = "\n" - _SPACE = " " - _MISSING = sentinel("_MISSING") - LEAF = sentinel("LEAF", repr="pprint.LEAF") - type DescriptionType = Iterable[object] - - @dataclass(frozen=True) - class Keyword: - """A keyword item, printed as name=value.""" - - name: str - value: object - default: object = _MISSING - - - @dataclass(frozen=True) - class Entry: - """A key/value item, printed as key: value.""" - - key: object - value: object - - - - class _ComparisonError: - """Stand in for a keyword value whose comparison to its default failed.""" - - def __init__(self, error): - self._error = error - - def __repr__(self): - return f"" - - - def _find_pprint(cls): - """Return the __pprint__ method that applies to instances of cls, or None.""" - mro = cls.__mro__ - pprint_owner = next((base for base in mro if "__pprint__" in vars(base)), None) - if pprint_owner is None: - return None - repr_owner = next(base for base in mro if "__repr__" in vars(base)) - if mro.index(pprint_owner) <= mro.index(repr_owner): - return vars(pprint_owner)["__pprint__"] - return None - - - def _apply_default(keyword): - """Hide a keyword if its value equals its declared default.""" - if keyword.default is _MISSING: - return keyword - try: - same = bool(keyword.value == keyword.default) - except Exception as error: - return Keyword(keyword.name, _ComparisonError(error)) - return None if same else keyword - - - def decompose(obj): - """Read one level of an object's pretty-print description stream.""" - method = _find_pprint(type(obj)) - if method is None: - return LEAF, None - stream = iter(method(obj)) - - first = next(stream, _MISSING) - if first is DELEGATE: - value = next(stream, _MISSING) - return (DELEGATE, value) if value is not _MISSING else (LEAF, None) - - if isinstance(first, Layout): - layout = first - else: - layout = Layout() - if first is not _MISSING: - stream = chain((first,), stream) - - items = [] - section = VALUES - for item in stream: - if item is VALUES or item is KWARGS or item is ITEMS: - section = item - continue - - match item: - case tuple((str() as name, value)) if section is KWARGS: - item = Keyword(name, value) - case tuple((str() as name, value, default)) if section is KWARGS: - item = Keyword(name, value, default) - case tuple((key, value)) if section is ITEMS: - item = Entry(key, value) - - if isinstance(item, Keyword): - item = _apply_default(item) - if item is None: - continue - items.append(item) - - name = type(obj).__name__ if layout.name is None else layout.name - if layout.open is None: - keyed = len(items) > 0 and all(isinstance(item, Entry) for item in items) - if keyed: - open_, close = f"{name}({{", "})" - else: - open_, close = f"{name}(", ")" - else: - open_ = layout.open - close = _EMPTY if layout.close is None else layout.close - - if layout.close is not None: - close = layout.close - - filled = Layout( - hints=layout.hints, - name=name, - open=open_, - close=close, - sep=layout.sep, - empty=open_ + close if layout.empty is None else layout.empty, - single_close=close if layout.single_close is None else layout.single_close, - recursion=layout.recursion, - sortable=layout.sortable, - hug=layout.hug, - ) - return filled, items - - - @dataclass(frozen=True) - class _ValueDocument: - value: object - - - @dataclass(frozen=True) - class _LazyDocument: - value: object - ancestors: tuple - - - @dataclass(frozen=True) - class _TextDocument: - text: str - source: object = None - - - @dataclass(frozen=True) - class _EntryDocument: - key: object - value: object - key_source: object - - - @dataclass(frozen=True) - class _KeywordDocument: - name: str - value: object - source: object - - - @dataclass(frozen=True) - class _ContainerDocument: - source: object - layout: Layout - items: tuple - - - def _build_document(obj, ancestors=()): - """Build a printer-independent document node for *obj*. - - Nested objects' descriptions are read when layout conversion reaches them. - """ - ancestor_ids = {id(ancestor) for ancestor in ancestors} - - header, items = decompose(obj) - if header is LEAF: - return _ValueDocument(obj) - if header is DELEGATE: - if id(obj) in ancestor_ids: - return _TextDocument("...", obj) - return _build_document(items, ancestors + (obj,)) - - if id(obj) in ancestor_ids: - return _TextDocument(header.recursion, obj) - ancestors = ancestors + (obj,) - documents = [] - for item in items: - match item: - case Entry(key=key, value=value): - documents.append( - _EntryDocument( - _LazyDocument(key, ancestors), - _LazyDocument(value, ancestors), - key, - ) - ) - case Keyword(name=name, value=value): - documents.append( - _KeywordDocument(name, _LazyDocument(value, ancestors), item) - ) - case _: - documents.append(_LazyDocument(item, ancestors)) - return _ContainerDocument(obj, header, tuple(documents)) - - # End of object descriptions and structural documents. - ######################################################################## - - - ######################################################################## - # Printer formatting and rendering. - # - # A layout document is a tree of text and layout instructions. It represents - # the output before line breaks are chosen; the renderer turns it into lines. - @dataclass(frozen=True) - class _Text: - text: str - - - @dataclass(frozen=True) - class _Line: - """Flat text when a group fits, or a line break otherwise.""" - - flat: str = _SPACE - - - @dataclass(frozen=True) - class _Concat: - parts: tuple - - - @dataclass(frozen=True) - class _Nest: - width: int - doc: object - - - @dataclass(frozen=True) - class _Align: - doc: object - - - @dataclass(frozen=True) - class _Group: - doc: object - - - @dataclass(frozen=True) - class _IfBroken: - broken: str - flat: str = _EMPTY - - - @dataclass(frozen=True) - class _KeyValue: - key: object - value: object - hang: int | None - - - class _Marker: - """A marker rendered by a function from the printer's marker table.""" - - __slots__ = ("value", "render", "top_level", "_flat") - - def __init__(self, value, render, top_level): - self.value = value - self.render = render - self.top_level = top_level - self._flat = None - - def lines(self, width, last_width): - return list(self.render( - self.value, - width=width, - last_width=last_width, - top_level=self.top_level, - )) - - def flat(self): - if self._flat is None: - self._flat = self.lines(_sys.maxsize, _sys.maxsize) - return self._flat - - - _SOFTLINE = _Line(_EMPTY) - _COLON_SPACE = _Text(": ") - - - def _join(separator, docs): - parts = [] - for index, doc in enumerate(docs): - if index: - parts.append(separator) - parts.append(doc) - return _Concat(tuple(parts)) - - - class Expand(enum.Enum): - """Policy for laying out containers that do not fit on one line.""" - - NEVER = enum.auto() - FIT = enum.auto() - ALWAYS = enum.auto() - - def __repr__(self): - return f"{type(self).__name__}.{self.name}" - - - @dataclass(frozen=True) - class _FormatOptions: - indent: int - width: int - depth: int | None - compact: bool - sort_dicts: bool - expand: Expand - number_format: str | None - markers: Mapping[type, object] - - - class Printer: - """Default printer. Calling an instance yields the formatted lines.""" - - def __init__( - self, - indent=4, - width=78, - depth=None, - *, - compact=False, - expand=Expand.FIT, - sort_dicts=True, - number_format=None, - ): - 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') - markers = { - BreakableString: self._render_literal, - BreakableBytes: self._render_literal, - FormattableNumber: self._render_number, - } - self._options = _FormatOptions( - indent, width, depth, compact, sort_dicts, expand, number_format, - markers, - ) - - def __call__(self, obj): - """Yield the formatted lines for *obj*.""" - # Build the complete structural document before applying printer settings. - document = _build_document(obj) - layout_document = _layout_document(document, 0, self._options) - yield from _render_lines(layout_document, self._options.width) - - def _render_literal(self, marker, *, width, last_width, top_level): - value = marker.value - rep = repr(value) - if len(rep) <= last_width or len(value) == 0: - return [rep] - allowance = width - last_width - if top_level: - pad = self._options.indent if self._options.expand is not Expand.NEVER else 1 - width -= pad - allowance += 1 - else: - pad = 0 - wrap = _wrap_bytes if isinstance(value, bytes) else _wrap_string - chunks = wrap(value, width, allowance) - if len(chunks) <= 1: - return [rep] - if not top_level: - return chunks - spaces = _SPACE * pad - if self._options.expand is not Expand.NEVER: - return ["(", *(spaces + chunk for chunk in chunks), ")"] - return [ - "(" + chunks[0], - *(spaces + chunk for chunk in chunks[1:-1]), - spaces + chunks[-1] + ")", - ] - - def _render_number(self, marker, *, width, last_width, top_level): - number_format = self._options.number_format - if number_format is None: - return [repr(marker.value)] - try: - return [format(marker.value, number_format)] - except Exception: - return [repr(marker.value)] - - - def _wrap_string(text, max_width, allowance): - """Split a string's repr into chunks, as PrettyPrinter does.""" - import re - - chunks = [] - lines = text.splitlines(True) - max_width1 = max_width - 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) - continue - # A list of alternating (non-space, space) strings, ending with an - # empty string. - parts = re.findall(r"\S*\s*", line) - parts.pop() - max_width2 = max_width - current = _EMPTY - 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)) - return chunks - - - def _wrap_bytes(data, max_width, allowance): - """Split a bytes value's repr into chunks, as PrettyPrinter does.""" - chunks = [] - current = b"" - last = len(data) // 4 * 4 - for i in range(0, len(data), 4): - part = data[i:i + 4] - candidate = current + part - if i == last: - max_width -= allowance - if len(repr(candidate)) > max_width: - if current: - chunks.append(repr(current)) - current = part - else: - current = candidate - if current: - chunks.append(repr(current)) - return chunks - - - def _find_marker(value, markers): - """Return the function in *markers* for *value*'s class, or None.""" - for cls in type(value).__mro__: - render = markers.get(cls) - if render is not None: - return render - return None - - - def _layout_document(document, level, options): - """Apply printer options to a structural document and return layout instructions.""" - match document: - case _LazyDocument(value=value, ancestors=ancestors): - return _layout_document( - _build_document(value, ancestors), level, options - ) - case _TextDocument(text=text): - return _Text(text) - case _ValueDocument(value=value): - render = _find_marker(value, options.markers) - if render is None: - return _Text(repr(value)) - return _Marker(value, render, level == 0) - case _ContainerDocument(layout=header, items=items): - if not items: - return _Text(header.empty) - if options.depth is not None and level >= options.depth: - return _Text(f"{header.open}...{header.close}") - - if header.sortable and options.sort_dicts: - items = _sorted_documents(items, level, options) - hugged = None - if header.hug and options.expand is not Expand.NEVER: - items = tuple(_resolve_item(item) for item in items) - hugged = _hugged_index(items) - parts = [_item_document(item, level + 1, options) for item in items] - if hugged is not None: - return _hugged_document(header, parts) - positional = not any( - isinstance(item, (_EntryDocument, _KeywordDocument)) for item in items - ) - return _container_document(header, parts, positional, level, options) - case _: - raise TypeError(f"unexpected document node: {document!r}") - - - def _document_source(document): - match document: - case _ValueDocument(value=value): - return value - case _ContainerDocument(source=value): - return value - case _LazyDocument(value=value): - return value - case _EntryDocument(key_source=value): - return value - case _KeywordDocument(source=value): - return value - case _TextDocument(source=value): - return value - case _: - raise TypeError(f"unexpected structural document: {document!r}") - - - def _document_sort_key(item): - """Return an entry's key, or the item itself, for sorting.""" - return item.key if isinstance(item, _EntryDocument) else item - - - def _sorted_documents(items, level, options): - if not any( - isinstance(_document_source(_document_sort_key(item)), Set) for item in items - ): - try: - return sorted( - items, - key=lambda item: _document_source(_document_sort_key(item)), - ) - except TypeError: - pass - return sorted( - items, - key=lambda item: _flat_document(_document_sort_key(item), level + 1, options), - ) - - - def _flat_document(document, level, options): - layout_document = _layout_document(document, level, options) - return _NL.join(_render_lines(_Group(layout_document), _sys.maxsize)) - - - def _item_document(item, level, options): - match item: - case _EntryDocument(key=key, value=value): - hang = options.indent if options.expand is Expand.NEVER else None - return _KeyValue( - _layout_document(key, level, options), - _layout_document(value, level, options), - hang, - ) - case _KeywordDocument(name=name, value=value): - return _Concat( - (_Text(f"{name}="), _layout_document(value, level, options)) - ) - case _: - return _layout_document(item, level, options) - - - def _resolve_item(item): - """Build the documents of an item's lazy parts.""" - match item: - case _LazyDocument(value=value, ancestors=ancestors): - return _build_document(value, ancestors) - case _KeywordDocument(value=_LazyDocument(value=value, ancestors=ancestors)): - return _KeywordDocument( - item.name, _build_document(value, ancestors), item.source - ) - case _: - return item - - - def _hugged_index(items): - """Return the index of the only item that is a non-empty container, or None.""" - found = None - for index, item in enumerate(items): - document = item.value if isinstance(item, _KeywordDocument) else item - if isinstance(document, _ContainerDocument) and len(document.items) > 0: - if found is not None: - return None - found = index - return found - - - def _hugged_document(header, parts): - """Keep every item on the opening and closing lines. - - Only the one container among the items breaks across lines, so its - brackets hug the object's own, as in ``defaultdict(, {``. - """ - close = header.single_close if len(parts) == 1 else header.close - separator = _Text(header.sep + _SPACE) - return _Group( - _Concat((_Text(header.open), _join(separator, parts), _Text(close))) - ) - - - def _container_document(header, parts, positional, level, options): - close = header.single_close if len(parts) == 1 else header.close - separator = _Text(header.sep) - - if options.expand is Expand.NEVER: - if options.compact and positional: - body = _join(_Concat((separator, _Group(_Line()))), parts) - return _Concat((_Text(header.open), _Align(body), _Text(close))) - body = _join(_Concat((separator, _Line())), parts) - return _Group(_Concat((_Text(header.open), _Align(body), _Text(close)))) - - body = _join(_Concat((separator, _Line())), parts) - doc = _Concat( - ( - _Text(header.open), - _Nest(options.indent, _Concat((_SOFTLINE, body, _IfBroken(header.sep)))), - _SOFTLINE, - _IfBroken(header.close, flat=close), - ) - ) - if options.expand is Expand.ALWAYS and level > 0: - return doc - return _Group(doc) - - - def _render_lines(doc, width): - out = [] - column = 0 - # Each pending item is (indent, flat, node): nodes come from the layout - # document, while indent and flat carry the rendering context. Compound - # nodes add their children to this stack as they are traversed. The stack - # can grow to O(N), where N is the number of layout nodes: a deep document - # leaves pending nodes at each level, while a wide _Concat adds many at once. - stack = [(0, False, doc)] - while stack: - indent, flat, node = stack.pop() - match node: - case _Text(text=text): - parts = text.split(_NL) - for part in parts[:-1]: - out.append(part) - yield _EMPTY.join(out) - out.clear() - out.append(parts[-1]) - column = len(parts[-1]) if len(parts) > 1 else column + len(text) - case _Line(flat=flat_text): - if flat: - out.append(flat_text) - column += len(flat_text) - else: - yield _EMPTY.join(out) - out.clear() - out.append(_SPACE * indent) - column = indent - case _Concat(parts=parts): - stack.extend((indent, flat, part) for part in reversed(parts)) - case _Nest(width=extra, doc=inner): - stack.append((indent + extra, flat, inner)) - case _Align(doc=inner): - stack.append((column, flat, inner)) - case _Group(doc=inner): - fits = flat or _fits(width - column, (indent, True, inner), stack) - stack.append((indent, fits, inner)) - case _IfBroken(broken=broken, flat=flat_text): - text = flat_text if flat else broken - out.append(text) - column += len(text) - case _Marker(): - if flat: - lines = node.flat() - else: - room = width - column - lines = node.lines(room, room - _trailing_width(stack)) - start = column - for index, line in enumerate(lines): - if index > 0: - yield _EMPTY.join(out) - out.clear() - line = _SPACE * start + line - column = 0 - out.append(line) - column += len(line) - case _KeyValue(key=key, value=value, hang=hang): - if flat or hang is None: - stack.append((indent, flat, value)) - stack.append((indent, flat, _COLON_SPACE)) - stack.append((indent, flat, key)) - elif isinstance(key, _Text) or _fits( - width - column, (indent, True, _Concat((key, _Text(":")))), [] - ): - stack.append((indent, False, value)) - stack.append((indent, False, _COLON_SPACE)) - stack.append((indent, True, key)) - else: - hanging = column + hang - stack.append((hanging, False, value)) - stack.append((hanging, False, _Line())) - stack.append((indent, False, _Text(":"))) - stack.append((indent, False, key)) - yield _EMPTY.join(out) - - - def _fits(remaining, first, rest): - local = [first] - index = len(rest) - while remaining >= 0: - if local: - indent, flat, node = local.pop() - elif index: - index -= 1 - indent, flat, node = rest[index] - else: - return True - - match node: - case _Text(text=text): - remaining -= len(text) - case _Line(flat=flat_text): - if flat: - remaining -= len(flat_text) - else: - return True - case _Concat(parts=parts): - local.extend((indent, flat, part) for part in reversed(parts)) - case _Nest(doc=inner) | _Align(doc=inner) | _Group(doc=inner): - local.append((indent, flat, inner)) - case _IfBroken(broken=broken, flat=flat_text): - remaining -= len(flat_text if flat else broken) - case _KeyValue(key=key, value=value): - local.append((indent, flat, _Concat((key, _COLON_SPACE, value)))) - case _Marker(): - if flat: - lines = node.flat() - if len(lines) != 1: - return False - else: - lines = node.lines(remaining, remaining) - remaining -= len(lines[0]) - if len(lines) > 1: - # The rest of the marker goes on later lines. - return remaining >= 0 - return False - - - def _trailing_width(stack): - """Return the width of the text that follows on the current line. - - The scan stops at the first possible line break, including any group, - since whether a group breaks is not known yet. This is the allowance - that PrettyPrinter leaves for closing brackets and separators. - """ - total = 0 - local = [] - index = len(stack) - while True: - if local: - indent, flat, node = local.pop() - elif index: - index -= 1 - indent, flat, node = stack[index] - else: - return total - - match node: - case _Text(text=text): - if _NL in text: - return total + text.index(_NL) - total += len(text) - case _Line(flat=flat_text): - if not flat: - return total - total += len(flat_text) - case _Concat(parts=parts): - local.extend((indent, flat, part) for part in reversed(parts)) - case _Nest(doc=inner) | _Align(doc=inner): - local.append((indent, flat, inner)) - case _IfBroken(broken=broken, flat=flat_text): - total += len(flat_text if flat else broken) - case _: - return total - - # End of printer formatting and rendering. - ######################################################################## - - - - def _ordered_dict_pprint(obj): - yield Layout(empty=f"{type(obj).__name__}()") - yield ITEMS - yield from obj.items() - - - def _mapping_view_pprint(obj): - yield Layout(open=f"{type(obj).__name__}(", close=")", hug=True) - yield obj._mapping - - - def _counter_pprint(obj): - name = type(obj).__name__ - yield Layout(open=f"{name}({{", close="})", empty=f"{name}()") - yield ITEMS - yield from obj.most_common() - - - def _chainmap_pprint(obj): - yield Layout(open=f"{type(obj).__name__}(", close=")", empty=repr(obj)) - yield from obj.maps - - - def _user_container_pprint(obj): - yield DELEGATE - yield obj.data - globals().update(locals()) diff --git a/Lib/string/templatelib.py b/Lib/string/templatelib.py index 19fa6c44fe4611..92d89fbb216c73 100644 --- a/Lib/string/templatelib.py +++ b/Lib/string/templatelib.py @@ -16,8 +16,6 @@ def convert(obj, /, conversion, printer=None): if conversion == 'a': return ascii(obj) if conversion == 'p': - if printer is None: - return f'{obj!p}' return f'{obj!p:{printer}}' raise ValueError(f'invalid conversion specifier: {conversion}') diff --git a/Lib/test/test__pprint.py b/Lib/test/test__pprint.py index 25f8d7eb2eda0a..86b4713b7b2348 100644 --- a/Lib/test/test__pprint.py +++ b/Lib/test/test__pprint.py @@ -27,6 +27,32 @@ def pformat(obj, **options): class PprintProtocolTests(unittest.TestCase): + def test_sort_keywords_by_name(self): + class Example: + def __pprint__(self): + yield Layout(sortable=True) + yield KWARGS + yield "b", 1 + yield "a", 10 + yield "c", None + + self.assertEqual(pformat(Example()), "Example(a=10, b=1, c=None)") + self.assertEqual( + pformat(Example(), sort_dicts=False), + "Example(b=1, a=10, c=None)", + ) + + def test_sort_keywords_after_other_items(self): + class Example: + def __pprint__(self): + yield Layout(sortable=True) + yield pprint.Keyword("b", 0) + yield "z" + yield pprint.Keyword("a", 10) + yield "y" + + self.assertEqual(pformat(Example()), "Example('y', 'z', a=10, b=0)") + def test_invalid_printer_options(self): for options in ({"indent": -1}, {"width": 0}, {"depth": 0}, {"depth": -1}): with self.subTest(options=options): diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 7fc7d5c2485ac7..6058a8800383c2 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1489,6 +1489,8 @@ def newline_printer(obj): f'{1!p:{int_lines_printer}}' # Lines are joined as they are, even if they contain newlines. self.assertEqual(f'{1!p:{newline_printer}}', 'a\nb\nc') + # A printer of None means the default printer. + self.assertEqual(f'{[1, 2]!p:{None}}', f'{[1, 2]!p}') # Conversions can have trailing whitespace after them since it # does not provide any significance diff --git a/Objects/object.c b/Objects/object.c index c0602e7969c368..a5334a1994b325 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -844,7 +844,7 @@ PyObject * PyObject_Pretty(PyObject *v, PyObject *printer) { PyObject *default_printer = NULL; - if (printer == NULL) { + if (printer == NULL || printer == Py_None) { PyObject *module = PyImport_ImportModule("pprint"); if (module == NULL) { return NULL; diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 565be27b7cebda..fb38e5910901d1 100644 --- a/Python/stdlib_module_names.h +++ b/Python/stdlib_module_names.h @@ -63,6 +63,8 @@ static const char* _Py_stdlib_module_names[] = { "_pickle", "_posixshmem", "_posixsubprocess", +"_pprint", +"_pprint_protocol", "_py_abc", "_py_warnings", "_pybuiltins",