From 5f6e2dc2c55710bcf01603de1659b990c999cbcb Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 23 Oct 2026 14:43:47 -0700 Subject: [PATCH 1/2] PEP 813: sort keyword items last, and treat a None printer as the default Sorting a sortable layout that contains keyword items failed with TypeError. Keyword items are now sorted by name after the other items, so the output stays in call order, as in Example('y', 'z', a=10, b=0). PyObject_Pretty() now treats a printer of None like NULL and uses the default printer. So f'{obj!p:{None}}' matches f'{obj!p}' and string.templatelib.convert(obj, 'p', None), where None already meant that no printer was given. Co-Authored-By: Claude Opus 5.5 --- Doc/c-api/object.rst | 9 +++++---- Lib/pprint.py | 10 ++++++++++ Lib/test/test__pprint.py | 26 ++++++++++++++++++++++++++ Lib/test/test_fstring.py | 2 ++ Objects/object.c | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 2066dbb57621583..6fd4a671d4b02af 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/pprint.py b/Lib/pprint.py index a8755cce7c4e3dd..52193925345a795 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -1535,6 +1535,16 @@ def _document_sort_key(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 ): diff --git a/Lib/test/test__pprint.py b/Lib/test/test__pprint.py index 25f8d7eb2eda0a2..86b4713b7b2348d 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 7fc7d5c2485ac76..6058a8800383c22 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 c0602e7969c368d..a5334a1994b3256 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; From 3c770fbad81abe323f3a8e3e56e0f160774a032c Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 23 Oct 2026 15:14:56 -0700 Subject: [PATCH 2/2] Move the PEP 813 protocol into _pprint_protocol and break the import cycles The protocol code lived inside _load_protocol(), which a module __getattr__ ran on first use and then copied into the module globals. It now lives at module level in a new private module, _pprint_protocol, and pprint re-exports its names with lazy imports. Importing pprint alone still does not import dataclasses or the protocol module. The __pprint__() methods in collections, _collections_abc and dataclasses imported helpers from pprint inside the function to avoid import cycles. The helpers are now inlined into those methods, and the names they need come from the C _pprint module with lazy imports, so there is no cycle even when lazy imports are forced eager. pprint's own function-local imports of dataclasses and re are now lazy imports at the top of the module. string.templatelib.convert() no longer special-cases a printer of None, since PyObject_Pretty() handles it. Regenerate stdlib_module_names.h, which also adds the missing _pprint entry. Co-Authored-By: Claude Opus 5.5 --- Lib/_collections_abc.py | 5 +- Lib/_pprint_protocol.py | 807 +++++++++++++++++++++++++++++++ Lib/collections/__init__.py | 36 +- Lib/dataclasses.py | 3 +- Lib/pprint.py | 895 +---------------------------------- Lib/string/templatelib.py | 2 - Python/stdlib_module_names.h | 2 + 7 files changed, 853 insertions(+), 897 deletions(-) create mode 100644 Lib/_pprint_protocol.py diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index f287ecf4b054a9c..5e96514e07df667 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 000000000000000..bef564045008a3a --- /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 e8a4a28b2607c31..84c8e01732d1bab 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 bac30d6bdaeac51..2f5946cd5fb3d18 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 52193925345a795..4fac0e619d035dd 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,839 +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): - """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. - ######################################################################## - - - - 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 19fa6c44fe4611f..92d89fbb216c73a 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/Python/stdlib_module_names.h b/Python/stdlib_module_names.h index 565be27b7cebda5..fb38e5910901d19 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",