]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112632: Add optional keyword-only argument `expand` to `pprint` (#136964)
authorSemyon Moroz <donbarbos@proton.me>
Tue, 7 Apr 2026 04:42:54 +0000 (08:42 +0400)
committerGitHub <noreply@github.com>
Tue, 7 Apr 2026 04:42:54 +0000 (07:42 +0300)
Co-authored-by: stodoran <stefan.todoran@uipath.com>
Co-authored-by: StefanTodoran <stefan.alex4@gmail.com>
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Doc/library/pprint.rst
Doc/whatsnew/3.15.rst
Lib/pprint.py
Lib/test/test_pprint.py
Misc/NEWS.d/next/Library/2025-02-07-00-48-07.gh-issue-112632.95MM0C.rst [new file with mode: 0644]

index 350831d6ad3c1b54f20af709d14294cd52816545..77ed44c2bb7c0c968bcfbc95321fe0f5aca087dc 100644 (file)
@@ -31,7 +31,8 @@ Functions
 ---------
 
 .. function:: pp(object, stream=None, indent=1, width=80, depth=None, *, \
-                     compact=False, sort_dicts=False, underscore_numbers=False)
+                 compact=False, expand=False, sort_dicts=False, \
+                 underscore_numbers=False)
 
    Prints the formatted representation of *object*, followed by a newline.
    This function may be used in the interactive interpreter
@@ -69,6 +70,13 @@ Functions
       each item of a sequence will be formatted on a separate line,
       otherwise as many items as will fit within the *width*
       will be formatted on each output line.
+      Incompatible with *expand*.
+
+   :param bool expand:
+      If ``True``,
+      opening parentheses and brackets will be followed by a newline and the
+      following content will be indented by one level, similar to
+      pretty-printed JSON. Incompatible with *compact*.
 
    :param bool sort_dicts:
       If ``True``, dictionaries will be formatted with
@@ -95,7 +103,8 @@ Functions
 
 
 .. function:: pprint(object, stream=None, indent=1, width=80, depth=None, *, \
-                     compact=False, sort_dicts=True, underscore_numbers=False)
+                     compact=False, expand=False, sort_dicts=True, \
+                     underscore_numbers=False)
 
    Alias for :func:`~pprint.pp` with *sort_dicts* set to ``True`` by default,
    which would automatically sort the dictionaries' keys,
@@ -103,10 +112,11 @@ Functions
 
 
 .. function:: pformat(object, indent=1, width=80, depth=None, *, \
-                      compact=False, sort_dicts=True, underscore_numbers=False)
+                      compact=False, expand=False, sort_dicts=True, \
+                      underscore_numbers=False)
 
    Return the formatted representation of *object* as a string.  *indent*,
-   *width*, *depth*, *compact*, *sort_dicts* and *underscore_numbers* are
+   *width*, *depth*, *compact*, *expand*, *sort_dicts* and *underscore_numbers* are
    passed to the :class:`PrettyPrinter` constructor as formatting parameters
    and their meanings are as described in the documentation above.
 
@@ -150,7 +160,8 @@ PrettyPrinter Objects
 .. index:: single: ...; placeholder
 
 .. class:: PrettyPrinter(indent=1, width=80, depth=None, stream=None, *, \
-                         compact=False, sort_dicts=True, underscore_numbers=False)
+                         compact=False, expand=False, sort_dicts=True, \
+                         underscore_numbers=False)
 
    Construct a :class:`PrettyPrinter` instance.
 
@@ -174,6 +185,22 @@ PrettyPrinter Objects
      'knights', 'ni'],
     'spam', 'eggs', 'lumberjack', 'knights',
     'ni']
+   >>> pp = pprint.PrettyPrinter(width=41, expand=True, indent=3)
+   >>> pp.pprint(stuff)
+   [
+      [
+         'spam',
+         'eggs',
+         'lumberjack',
+         'knights',
+         'ni',
+      ],
+      'spam',
+      'eggs',
+      'lumberjack',
+      'knights',
+      'ni',
+   ]
    >>> tup = ('spam', ('eggs', ('lumberjack', ('knights', ('ni', ('dead',
    ... ('parrot', ('fresh fruit',))))))))
    >>> pp = pprint.PrettyPrinter(depth=6)
@@ -193,6 +220,9 @@ PrettyPrinter Objects
    .. versionchanged:: 3.11
       No longer attempts to write to :data:`!sys.stdout` if it is ``None``.
 
+   .. versionchanged:: next
+      Added the *expand* parameter.
+
 
 :class:`PrettyPrinter` instances have the following methods:
 
@@ -415,3 +445,72 @@ cannot be split, the specified width will be exceeded::
     'requires_python': None,
     'summary': 'A sample Python project',
     'version': '1.2.0'}
+
+Lastly, we can format like pretty-printed JSON with the *expand* parameter.
+Best results are achieved with a higher *indent* value::
+
+   >>> pprint.pp(project_info, indent=4, expand=True)
+   {
+      'author': 'The Python Packaging Authority',
+      'author_email': 'pypa-dev@googlegroups.com',
+      'bugtrack_url': None,
+      'classifiers': [
+         'Development Status :: 3 - Alpha',
+         'Intended Audience :: Developers',
+         'License :: OSI Approved :: MIT License',
+         'Programming Language :: Python :: 2',
+         'Programming Language :: Python :: 2.6',
+         'Programming Language :: Python :: 2.7',
+         'Programming Language :: Python :: 3',
+         'Programming Language :: Python :: 3.2',
+         'Programming Language :: Python :: 3.3',
+         'Programming Language :: Python :: 3.4',
+         'Topic :: Software Development :: Build Tools',
+      ],
+      'description': 'A sample Python project\n'
+      '=======================\n'
+      '\n'
+      'This is the description file for the project.\n'
+      '\n'
+      'The file should use UTF-8 encoding and be written using ReStructured '
+      'Text. It\n'
+      'will be used to generate the project webpage on PyPI, and should be '
+      'written for\n'
+      'that purpose.\n'
+      '\n'
+      'Typical contents for this file would include an overview of the project, '
+      'basic\n'
+      'usage examples, etc. Generally, including the project changelog in here '
+      'is not\n'
+      'a good idea, although a simple "What\'s New" section for the most recent '
+      'version\n'
+      'may be appropriate.',
+      'description_content_type': None,
+      'docs_url': None,
+      'download_url': 'UNKNOWN',
+      'downloads': {'last_day': -1, 'last_month': -1, 'last_week': -1},
+      'dynamic': None,
+      'home_page': 'https://github.com/pypa/sampleproject',
+      'keywords': 'sample setuptools development',
+      'license': 'MIT',
+      'license_expression': None,
+      'license_files': None,
+      'maintainer': None,
+      'maintainer_email': None,
+      'name': 'sampleproject',
+      'package_url': 'https://pypi.org/project/sampleproject/',
+      'platform': 'UNKNOWN',
+      'project_url': 'https://pypi.org/project/sampleproject/',
+      'project_urls': {
+         'Download': 'UNKNOWN',
+         'Homepage': 'https://github.com/pypa/sampleproject',
+      },
+      'provides_extra': None,
+      'release_url': 'https://pypi.org/project/sampleproject/1.2.0/',
+      'requires_dist': None,
+      'requires_python': None,
+      'summary': 'A sample Python project',
+      'version': '1.2.0',
+      'yanked': False,
+      'yanked_reason': None,
+   }
index d1d4b92bcf4e973b4843913c303348ff47f3f53a..68e2911deeb4f1baa2a4c8d5fbc6443b4fb8d47b 100644 (file)
@@ -1507,6 +1507,16 @@ platform
   (Contributed by Alexey Makridenko in :gh:`133604`.)
 
 
+pprint
+------
+
+* Add an *expand* keyword argument for :func:`pprint.pprint`,
+  :func:`pprint.pformat`, :func:`pprint.pp`. If true, the output will be
+  formatted similar to pretty-printed :func:`json.dumps` when
+  *indent* is supplied.
+  (Contributed by Stefan Todoran and Semyon Moroz in :gh:`112632`.)
+
+
 sre_*
 -----
 
index a0e484b1c097c3aa5b4cb72e9e39e87ef0110afd..f197d7d17cdb9666c5806ec1d988749bdfb236bf 100644 (file)
@@ -44,20 +44,22 @@ __all__ = ["pprint","pformat","isreadable","isrecursive","saferepr",
 
 
 def pprint(object, stream=None, indent=1, width=80, depth=None, *,
-           compact=False, sort_dicts=True, underscore_numbers=False):
+           compact=False, expand=False, sort_dicts=True,
+           underscore_numbers=False):
     """Pretty-print a Python object to a stream [default is sys.stdout]."""
     printer = PrettyPrinter(
         stream=stream, indent=indent, width=width, depth=depth,
-        compact=compact, sort_dicts=sort_dicts,
+        compact=compact, expand=expand, sort_dicts=sort_dicts,
         underscore_numbers=underscore_numbers)
     printer.pprint(object)
 
 
 def pformat(object, indent=1, width=80, depth=None, *,
-            compact=False, sort_dicts=True, underscore_numbers=False):
+            compact=False, expand=False, sort_dicts=True,
+            underscore_numbers=False):
     """Format a Python object into a pretty-printed representation."""
     return PrettyPrinter(indent=indent, width=width, depth=depth,
-                         compact=compact, sort_dicts=sort_dicts,
+                         compact=compact, expand=expand, sort_dicts=sort_dicts,
                          underscore_numbers=underscore_numbers).pformat(object)
 
 
@@ -111,7 +113,8 @@ def _safe_tuple(t):
 
 class PrettyPrinter:
     def __init__(self, indent=1, width=80, depth=None, stream=None, *,
-                 compact=False, sort_dicts=True, underscore_numbers=False):
+                 compact=False, expand=False, sort_dicts=True,
+                 underscore_numbers=False):
         """Handle pretty printing operations onto a stream using a set of
         configured parameters.
 
@@ -130,6 +133,12 @@ class PrettyPrinter:
 
         compact
             If true, several items will be combined in one line.
+            Incompatible with expand mode.
+
+        expand
+            If true, the output will be formatted similar to
+            pretty-printed json.dumps() when ``indent`` is supplied.
+            Incompatible with compact mode.
 
         sort_dicts
             If true, dict keys are sorted.
@@ -146,6 +155,8 @@ class PrettyPrinter:
             raise ValueError('depth must be > 0')
         if not width:
             raise ValueError('width must be != 0')
+        if compact and expand:
+            raise ValueError('compact and expand are incompatible')
         self._depth = depth
         self._indent_per_level = indent
         self._width = width
@@ -154,6 +165,7 @@ class PrettyPrinter:
         else:
             self._stream = _sys.stdout
         self._compact = bool(compact)
+        self._expand = bool(expand)
         self._sort_dicts = sort_dicts
         self._underscore_numbers = underscore_numbers
 
@@ -205,24 +217,48 @@ class PrettyPrinter:
                 return
         stream.write(rep)
 
+    def _format_block_start(self, start_str, indent):
+        if self._expand:
+            return f"{start_str}\n{' ' * indent}"
+        return start_str
+
+    def _format_block_end(self, end_str, indent):
+        if self._expand:
+            return f"\n{' ' * indent}{end_str}"
+        return end_str
+
+    def _child_indent(self, indent, prefix_len):
+        if self._expand:
+            return indent
+        return indent + prefix_len
+
+    def _write_indent_padding(self, write):
+        if self._expand:
+            if self._indent_per_level > 0:
+                write(self._indent_per_level * " ")
+        elif self._indent_per_level > 1:
+            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__
-        indent += len(cls_name) + 1
+        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]
-        stream.write(cls_name + '(')
+        stream.write(self._format_block_start(cls_name + '(', indent))
         self._format_namespace_items(items, stream, indent, allowance, context, level)
-        stream.write(')')
+        stream.write(self._format_block_end(')', indent - self._indent_per_level))
 
     _dispatch = {}
 
     def _pprint_dict(self, object, stream, indent, allowance, context, level):
         write = stream.write
-        write('{')
-        if self._indent_per_level > 1:
-            write((self._indent_per_level - 1) * ' ')
+        write(self._format_block_start('{', indent))
+        self._write_indent_padding(write)
         length = len(object)
         if length:
             if self._sort_dicts:
@@ -231,21 +267,33 @@ class PrettyPrinter:
                 items = object.items()
             self._format_dict_items(items, stream, indent, allowance + 1,
                                     context, level)
-        write('}')
+        write(self._format_block_end('}', indent))
 
     _dispatch[dict.__repr__] = _pprint_dict
 
     def _pprint_frozendict(self, object, stream, indent, allowance, context, level):
         write = stream.write
         cls = object.__class__
-        write(cls.__name__ + '(')
-        length = len(object)
-        if length:
-            self._pprint_dict(object, stream,
-                              indent + len(cls.__name__) + 1,
-                              allowance + 1,
-                              context, level)
-        write(')')
+        if not len(object):
+            write(repr(object))
+            return
+
+        write(self._format_block_start(cls.__name__ + "({", indent))
+        self._write_indent_padding(write)
+
+        if self._sort_dicts:
+            items = sorted(object.items(), key=_safe_tuple)
+        else:
+            items = object.items()
+        self._format_dict_items(
+            items,
+            stream,
+            self._child_indent(indent, len(cls.__name__) + 1),
+            allowance + 2,
+            context,
+            level,
+        )
+        write(self._format_block_end("})", indent))
 
     _dispatch[frozendict.__repr__] = _pprint_frozendict
 
@@ -255,9 +303,14 @@ class PrettyPrinter:
             return
         cls = object.__class__
         stream.write(cls.__name__ + '(')
-        self._format(list(object.items()), stream,
-                     indent + len(cls.__name__) + 1, allowance + 1,
-                     context, level)
+        self._format(
+            list(object.items()),
+            stream,
+            self._child_indent(indent, len(cls.__name__) + 1),
+            allowance + 1,
+            context,
+            level,
+        )
         stream.write(')')
 
     _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
@@ -268,19 +321,21 @@ class PrettyPrinter:
             key = _safe_tuple
         else:
             key = _safe_key
+
         write = stream.write
-        write(object.__class__.__name__ + '([')
-        if self._indent_per_level > 1:
-            write((self._indent_per_level - 1) * ' ')
-        length = len(object)
-        if length:
+        write(
+            self._format_block_start(object.__class__.__name__ + "([", indent)
+        )
+
+        if len(object):
             if self._sort_dicts:
                 entries = sorted(object, key=key)
             else:
                 entries = object
-            self._format_items(entries, stream, indent, allowance + 1,
-                               context, level)
-        write('])')
+            self._format_items(
+                entries, stream, indent, allowance + 2, context, level
+            )
+        write(self._format_block_end("])", indent))
 
     def _pprint_mapping_abc_view(self, object, stream, indent, allowance, context, level):
         """Pretty print mapping views from collections.abc."""
@@ -306,19 +361,22 @@ class PrettyPrinter:
                     _collections.abc.MappingView)}
 
     def _pprint_list(self, object, stream, indent, allowance, context, level):
-        stream.write('[')
+        stream.write(self._format_block_start('[', indent))
         self._format_items(object, stream, indent, allowance + 1,
                            context, level)
-        stream.write(']')
+        stream.write(self._format_block_end(']', indent))
 
     _dispatch[list.__repr__] = _pprint_list
 
     def _pprint_tuple(self, object, stream, indent, allowance, context, level):
-        stream.write('(')
-        endchar = ',)' if len(object) == 1 else ')'
+        stream.write(self._format_block_start('(', indent))
+        if len(object) == 1 and not self._expand:
+            endchar = ',)'
+        else:
+            endchar = ')'
         self._format_items(object, stream, indent, allowance + len(endchar),
                            context, level)
-        stream.write(endchar)
+        stream.write(self._format_block_end(endchar, indent))
 
     _dispatch[tuple.__repr__] = _pprint_tuple
 
@@ -328,16 +386,17 @@ class PrettyPrinter:
             return
         typ = object.__class__
         if typ is set:
-            stream.write('{')
+            stream.write(self._format_block_start('{', indent))
             endchar = '}'
         else:
-            stream.write(typ.__name__ + '({')
+            stream.write(self._format_block_start(typ.__name__ + '({', indent))
             endchar = '})'
-            indent += len(typ.__name__) + 1
+            if not self._expand:
+                indent += len(typ.__name__) + 1
         object = sorted(object, key=_safe_key)
         self._format_items(object, stream, indent, allowance + len(endchar),
                            context, level)
-        stream.write(endchar)
+        stream.write(self._format_block_end(endchar, indent))
 
     _dispatch[set.__repr__] = _pprint_set
     _dispatch[frozenset.__repr__] = _pprint_set
@@ -350,7 +409,10 @@ class PrettyPrinter:
         chunks = []
         lines = object.splitlines(True)
         if level == 1:
-            indent += 1
+            if self._expand:
+                indent += self._indent_per_level
+            else:
+                indent += 1
             allowance += 1
         max_width1 = max_width = self._width - indent
         for i, line in enumerate(lines):
@@ -386,13 +448,13 @@ class PrettyPrinter:
             write(rep)
             return
         if level == 1:
-            write('(')
+            write(self._format_block_start("(", indent))
         for i, rep in enumerate(chunks):
             if i > 0:
                 write('\n' + ' '*indent)
             write(rep)
         if level == 1:
-            write(')')
+            write(self._format_block_end(")", indent - self._indent_per_level))
 
     _dispatch[str.__repr__] = _pprint_str
 
@@ -403,9 +465,12 @@ class PrettyPrinter:
             return
         parens = level == 1
         if parens:
-            indent += 1
+            if self._expand:
+                indent += self._indent_per_level
+            else:
+                indent += 1
             allowance += 1
-            write('(')
+            write(self._format_block_start('(', indent))
         delim = ''
         for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
             write(delim)
@@ -413,23 +478,34 @@ class PrettyPrinter:
             if not delim:
                 delim = '\n' + ' '*indent
         if parens:
-            write(')')
+            write(self._format_block_end(')', indent - self._indent_per_level))
 
     _dispatch[bytes.__repr__] = _pprint_bytes
 
     def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
         write = stream.write
-        write('bytearray(')
-        self._pprint_bytes(bytes(object), stream, indent + 10,
+        write(self._format_block_start('bytearray(', indent))
+        if self._expand:
+            write(' ' * self._indent_per_level)
+            recursive_indent = indent + self._indent_per_level
+        else:
+            recursive_indent = indent + 10
+        self._pprint_bytes(bytes(object), stream, recursive_indent,
                            allowance + 1, context, level + 1)
-        write(')')
+        write(self._format_block_end(')', indent))
 
     _dispatch[bytearray.__repr__] = _pprint_bytearray
 
     def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
         stream.write('mappingproxy(')
-        self._format(object.copy(), stream, indent + 13, allowance + 1,
-                     context, level)
+        self._format(
+            object.copy(),
+            stream,
+            self._child_indent(indent, 13),
+            allowance + 1,
+            context,
+            level,
+        )
         stream.write(')')
 
     _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
@@ -441,11 +517,15 @@ class PrettyPrinter:
             cls_name = 'namespace'
         else:
             cls_name = object.__class__.__name__
-        indent += len(cls_name) + 1
+        if self._expand:
+            indent += self._indent_per_level
+        else:
+            indent += len(cls_name) + 1
         items = object.__dict__.items()
-        stream.write(cls_name + '(')
-        self._format_namespace_items(items, stream, indent, allowance, context, level)
-        stream.write(')')
+        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))
 
     _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
 
@@ -460,11 +540,18 @@ class PrettyPrinter:
             rep = self._repr(key, context, level)
             write(rep)
             write(': ')
-            self._format(ent, stream, indent + len(rep) + 2,
-                         allowance if last else 1,
-                         context, level)
+            self._format(
+                ent,
+                stream,
+                self._child_indent(indent, len(rep) + 2),
+                allowance if last else 1,
+                context,
+                level,
+            )
             if not last:
                 write(delimnl)
+            elif self._expand:
+                write(',')
 
     def _format_namespace_items(self, items, stream, indent, allowance, context, level):
         write = stream.write
@@ -479,17 +566,23 @@ class PrettyPrinter:
                 # recursive dataclass repr.
                 write("...")
             else:
-                self._format(ent, stream, indent + len(key) + 1,
-                             allowance if last else 1,
-                             context, level)
+                self._format(
+                    ent,
+                    stream,
+                    self._child_indent(indent, len(key) + 1),
+                    allowance if last else 1,
+                    context,
+                    level,
+                )
             if not last:
                 write(delimnl)
+            elif self._expand:
+                write(',')
 
     def _format_items(self, items, stream, indent, allowance, context, level):
         write = stream.write
         indent += self._indent_per_level
-        if self._indent_per_level > 1:
-            write((self._indent_per_level - 1) * ' ')
+        self._write_indent_padding(write)
         delimnl = ',\n' + ' ' * indent
         delim = ''
         width = max_width = self._width - indent + 1
@@ -525,6 +618,8 @@ class PrettyPrinter:
             self._format(ent, stream, indent,
                          allowance if last else 1,
                          context, level)
+            if last and self._expand:
+                write(',')
 
     def _repr(self, object, context, level):
         repr, readable, recursive = self.format(object, context.copy(),
@@ -548,9 +643,13 @@ class PrettyPrinter:
             return
         rdf = self._repr(object.default_factory, context, level)
         cls = object.__class__
-        indent += len(cls.__name__) + 1
-        stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent))
-        self._pprint_dict(object, stream, indent, allowance + 1, context, level)
+        if self._expand:
+            stream.write('%s(%s, ' % (cls.__name__, rdf))
+        else:
+            indent += len(cls.__name__) + 1
+            stream.write('%s(%s,\n%s' % (cls.__name__, rdf, ' ' * indent))
+        self._pprint_dict(object, stream, indent, allowance + 1, context,
+                          level)
         stream.write(')')
 
     _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
@@ -560,14 +659,18 @@ class PrettyPrinter:
             stream.write(repr(object))
             return
         cls = object.__class__
-        stream.write(cls.__name__ + '({')
-        if self._indent_per_level > 1:
-            stream.write((self._indent_per_level - 1) * ' ')
+        stream.write(self._format_block_start(cls.__name__ + '({', indent))
+        self._write_indent_padding(stream.write)
         items = object.most_common()
-        self._format_dict_items(items, stream,
-                                indent + len(cls.__name__) + 1, allowance + 2,
-                                context, level)
-        stream.write('})')
+        self._format_dict_items(
+            items,
+            stream,
+            self._child_indent(indent, len(cls.__name__) + 1),
+            allowance + 2,
+            context,
+            level,
+        )
+        stream.write(self._format_block_end('})', indent))
 
     _dispatch[_collections.Counter.__repr__] = _pprint_counter
 
@@ -576,12 +679,18 @@ class PrettyPrinter:
             stream.write(repr(object))
             return
         cls = object.__class__
-        stream.write(cls.__name__ + '(')
-        indent += len(cls.__name__) + 1
+        stream.write(self._format_block_start(cls.__name__ + '(',
+                                              indent + self._indent_per_level))
+        if self._expand:
+            indent += self._indent_per_level
+        else:
+            indent += len(cls.__name__) + 1
         for i, m in enumerate(object.maps):
             if i == len(object.maps) - 1:
                 self._format(m, stream, indent, allowance + 1, context, level)
-                stream.write(')')
+                if self._expand:
+                    stream.write(',')
+                stream.write(self._format_block_end(')', indent - self._indent_per_level))
             else:
                 self._format(m, stream, indent, 1, context, level)
                 stream.write(',\n' + ' ' * indent)
@@ -593,18 +702,21 @@ class PrettyPrinter:
             stream.write(repr(object))
             return
         cls = object.__class__
-        stream.write(cls.__name__ + '(')
-        indent += len(cls.__name__) + 1
-        stream.write('[')
+        stream.write(self._format_block_start(cls.__name__ + '([', indent))
+        if not self._expand:
+            indent += len(cls.__name__) + 1
         if object.maxlen is None:
             self._format_items(object, stream, indent, allowance + 2,
                                context, level)
-            stream.write('])')
+            stream.write(self._format_block_end('])', indent))
         else:
             self._format_items(object, stream, indent, 2,
                                context, level)
             rml = self._repr(object.maxlen, context, level)
-            stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml))
+            if self._expand:
+                stream.write('%s], maxlen=%s)' % ('\n' + ' ' * indent, rml))
+            else:
+                stream.write('],\n%smaxlen=%s)' % (' ' * indent, rml))
 
     _dispatch[_collections.deque.__repr__] = _pprint_deque
 
index f3860a5d511989d7acb5b09566407167be0a8a12..45e081c233f0b024cdf7dd541fe6229c66a18d6b 100644 (file)
@@ -164,6 +164,7 @@ class QueryTestCase(unittest.TestCase):
         self.assertRaises(ValueError, pprint.PrettyPrinter, depth=0)
         self.assertRaises(ValueError, pprint.PrettyPrinter, depth=-1)
         self.assertRaises(ValueError, pprint.PrettyPrinter, width=0)
+        self.assertRaises(ValueError, pprint.PrettyPrinter, compact=True, expand=True)
 
     def test_basic(self):
         # Verify .isrecursive() and .isreadable() w/o recursion
@@ -1298,6 +1299,12 @@ Counter({'s': 6,
          'e': 4,
          'n': 2,
          'l': 1})""")
+        self.assertEqual(pprint.pformat(d, indent=2, width=1),
+"""\
+Counter({ 's': 6,
+          'e': 4,
+          'n': 2,
+          'l': 1})""")
 
     def test_chainmap(self):
         d = collections.ChainMap()
@@ -1508,6 +1515,457 @@ ValuesView({'a': 6,
     'jumped over a '
     'lazy dog'}""")
 
+    def test_expand_dataclass(self):
+        @dataclasses.dataclass
+        class DummyDataclass:
+            foo: str
+            bar: float
+            baz: bool
+            qux: dict = dataclasses.field(default_factory=dict)
+            quux: list = dataclasses.field(default_factory=list)
+            corge: int = 1
+            garply: tuple = (1, 2, 3, 4)
+        dummy_dataclass = DummyDataclass(
+            foo="foo",
+            bar=1.2,
+            baz=False,
+            qux={"foo": "bar", "baz": 123},
+            quux=["foo", "bar", "baz"],
+            corge=7,
+            garply=(1, 2, 3, 4),
+        )
+        self.assertEqual(pprint.pformat(dummy_dataclass, width=40, indent=4,
+                                        expand=True),
+"""\
+DummyDataclass(
+    foo='foo',
+    bar=1.2,
+    baz=False,
+    qux={'baz': 123, 'foo': 'bar'},
+    quux=['foo', 'bar', 'baz'],
+    corge=7,
+    garply=(1, 2, 3, 4),
+)""")
+
+    def test_expand_dict(self):
+        dummy_dict = {
+            "foo": "bar",
+            "baz": 123,
+            "qux": {"foo": "bar", "baz": 123},
+            "quux": ["foo", "bar", "baz"],
+            "corge": 7,
+        }
+        self.assertEqual(pprint.pformat(dummy_dict, width=40, indent=4,
+                                        expand=True, sort_dicts=False),
+"""\
+{
+    'foo': 'bar',
+    'baz': 123,
+    'qux': {'foo': 'bar', 'baz': 123},
+    'quux': ['foo', 'bar', 'baz'],
+    'corge': 7,
+}""")
+
+    def test_expand_ordered_dict(self):
+        dummy_ordered_dict = collections.OrderedDict(
+            [
+                ("foo", 1),
+                ("bar", 12),
+                ("baz", 123),
+            ]
+        )
+        self.assertEqual(pprint.pformat(dummy_ordered_dict, width=20, indent=4,
+                                        expand=True),
+"""\
+OrderedDict([
+    ('foo', 1),
+    ('bar', 12),
+    ('baz', 123),
+])""")
+
+    def test_expand_list(self):
+        dummy_list = [
+            "foo",
+            "bar",
+            "baz",
+            "qux",
+        ]
+        self.assertEqual(pprint.pformat(dummy_list, width=20, indent=4,
+                                        expand=True),
+"""\
+[
+    'foo',
+    'bar',
+    'baz',
+    'qux',
+]""")
+
+    def test_expand_tuple(self):
+        dummy_tuple = (
+            "foo",
+            "bar",
+            "baz",
+            4,
+            5,
+            6,
+        )
+        self.assertEqual(pprint.pformat(dummy_tuple, width=20, indent=4,
+                                        expand=True),
+"""\
+(
+    'foo',
+    'bar',
+    'baz',
+    4,
+    5,
+    6,
+)""")
+
+    def test_expand_single_element_tuple(self):
+        self.assertEqual(
+            pprint.pformat((1,), width=1, indent=4, expand=True),
+            """\
+(
+    1,
+)""")
+
+    def test_expand_set(self):
+        dummy_set = {
+            "foo",
+            "bar",
+            "baz",
+            "qux",
+            (1, 2, 3),
+        }
+        self.assertEqual(pprint.pformat(dummy_set, width=20, indent=4,
+                                        expand=True),
+"""\
+{
+    'bar',
+    'baz',
+    'foo',
+    'qux',
+    (1, 2, 3),
+}""")
+
+    def test_expand_frozenset(self):
+        dummy_set = {
+            (1, 2, 3),
+        }
+        dummy_frozenset = frozenset(
+            {
+                "foo",
+                "bar",
+                "baz",
+                (1, 2, 3),
+                frozenset(dummy_set),
+            }
+        )
+        self.assertEqual(pprint.pformat(dummy_frozenset, width=40, indent=4,
+                                        expand=True),
+"""\
+frozenset({
+    frozenset({(1, 2, 3)}),
+    'bar',
+    'baz',
+    'foo',
+    (1, 2, 3),
+})""")
+
+    def test_expand_frozendict(self):
+        dummy_frozendict = frozendict(
+            {"foo": "bar", "baz": 123, "qux": [1, 2]}
+        )
+        self.assertEqual(
+            pprint.pformat(dummy_frozendict, width=20, indent=4, expand=True),
+            """\
+frozendict({
+    'baz': 123,
+    'foo': 'bar',
+    'qux': [1, 2],
+})""",
+        )
+
+    def test_expand_bytes(self):
+        dummy_bytes = b"Hello world! foo bar baz 123 456 789"
+        self.assertEqual(pprint.pformat(dummy_bytes, width=20, indent=4,
+                                        expand=True),
+"""\
+(
+    b'Hello world!'
+    b' foo bar baz'
+    b' 123 456 789'
+)""")
+
+    def test_expand_bytearray(self):
+        dummy_bytes = b"Hello world! foo bar baz 123 456 789"
+        dummy_byte_array = bytearray(dummy_bytes)
+        self.assertEqual(pprint.pformat(dummy_byte_array, width=40, indent=4,
+                                        expand=True),
+"""\
+bytearray(
+    b'Hello world! foo bar baz 123 456'
+    b' 789'
+)""")
+
+    def test_expand_mappingproxy(self):
+        dummy_dict = {
+            "foo": "bar",
+            "baz": 123,
+            "qux": {"foo": "bar", "baz": 123},
+            "quux": ["foo", "bar", "baz"],
+            "corge": 7,
+        }
+        dummy_mappingproxy = types.MappingProxyType(dummy_dict)
+        self.assertEqual(pprint.pformat(dummy_mappingproxy, width=40, indent=4,
+                                        expand=True),
+"""\
+mappingproxy({
+    'baz': 123,
+    'corge': 7,
+    'foo': 'bar',
+    'quux': ['foo', 'bar', 'baz'],
+    'qux': {'baz': 123, 'foo': 'bar'},
+})""")
+
+    def test_expand_namespace(self):
+        dummy_namespace = types.SimpleNamespace(
+            foo="bar",
+            bar=42,
+            baz=types.SimpleNamespace(
+                x=321,
+                y="string",
+                d={"foo": True, "bar": "baz"},
+            ),
+        )
+
+        self.assertEqual(pprint.pformat(dummy_namespace, width=40, indent=4,
+                                        expand=True),
+"""\
+namespace(
+    foo='bar',
+    bar=42,
+    baz=namespace(
+        x=321,
+        y='string',
+        d={'bar': 'baz', 'foo': True},
+    ),
+)""")
+
+    def test_expand_defaultdict(self):
+        dummy_defaultdict = collections.defaultdict(list)
+        dummy_defaultdict["foo"].append("bar")
+        dummy_defaultdict["foo"].append("baz")
+        dummy_defaultdict["foo"].append("qux")
+        dummy_defaultdict["bar"] = {"foo": "bar", "baz": None}
+        self.assertEqual(pprint.pformat(dummy_defaultdict, width=40, indent=4,
+                                        expand=True),
+"""\
+defaultdict(<class 'list'>, {
+    'bar': {'baz': None, 'foo': 'bar'},
+    'foo': ['bar', 'baz', 'qux'],
+})""")
+
+    def test_expand_counter(self):
+        dummy_counter = collections.Counter("abcdeabcdabcaba")
+        expected = """\
+Counter({
+    'a': 5,
+    'b': 4,
+    'c': 3,
+    'd': 2,
+    'e': 1,
+})"""
+        self.assertEqual(pprint.pformat(dummy_counter, width=40, indent=4,
+                                        expand=True), expected)
+
+        expected2 = """\
+Counter({
+  'a': 5,
+  'b': 4,
+  'c': 3,
+  'd': 2,
+  'e': 1,
+})"""
+        self.assertEqual(pprint.pformat(dummy_counter, width=20, indent=2,
+                                        expand=True), expected2)
+
+    def test_expand_chainmap(self):
+        dummy_dict = {
+            "foo": "bar",
+            "baz": 123,
+            "qux": {"foo": "bar", "baz": 123},
+            "quux": ["foo", "bar", "baz"],
+            "corge": 7,
+        }
+        dummy_chainmap = collections.ChainMap(
+            {"foo": "bar"},
+            {"baz": "qux"},
+            {"corge": dummy_dict},
+        )
+        dummy_chainmap.maps.append({"garply": "waldo"})
+        self.assertEqual(pprint.pformat(dummy_chainmap, width=40, indent=4,
+                                        expand=True),
+"""\
+ChainMap(
+    {'foo': 'bar'},
+    {'baz': 'qux'},
+    {
+        'corge': {
+            'baz': 123,
+            'corge': 7,
+            'foo': 'bar',
+            'quux': ['foo', 'bar', 'baz'],
+            'qux': {
+                'baz': 123,
+                'foo': 'bar',
+            },
+        },
+    },
+    {'garply': 'waldo'},
+)""")
+
+    def test_expand_deque(self):
+        dummy_dict = {
+            "foo": "bar",
+            "baz": 123,
+            "qux": {"foo": "bar", "baz": 123},
+            "quux": ["foo", "bar", "baz"],
+            "corge": 7,
+        }
+        dummy_list = [
+            "foo",
+            "bar",
+            "baz",
+        ]
+        dummy_set = {
+            (1, 2, 3),
+        }
+        dummy_deque = collections.deque(maxlen=10)
+        dummy_deque.append("foo")
+        dummy_deque.append(123)
+        dummy_deque.append(dummy_dict)
+        dummy_deque.extend(dummy_list)
+        dummy_deque.appendleft(dummy_set)
+        self.assertEqual(pprint.pformat(dummy_deque, width=40, indent=4,
+                                        expand=True),
+"""\
+deque([
+    {(1, 2, 3)},
+    'foo',
+    123,
+    {
+        'baz': 123,
+        'corge': 7,
+        'foo': 'bar',
+        'quux': ['foo', 'bar', 'baz'],
+        'qux': {'baz': 123, 'foo': 'bar'},
+    },
+    'foo',
+    'bar',
+    'baz',
+], maxlen=10)""")
+
+    def test_expand_userdict(self):
+        class DummyUserDict(collections.UserDict):
+            """A custom UserDict with some extra attributes"""
+
+            def __init__(self, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+                self.access_count = 0
+        dummy_userdict = DummyUserDict({ "foo": "bar", "baz": 123,
+                                        "qux": {"foo": "bar", "baz": 123},
+                                        "quux": ["foo", "bar", "baz"],
+                                        "corge": 7 })
+        dummy_userdict.access_count = 5
+
+        self.assertEqual(pprint.pformat(dummy_userdict, width=40, indent=4,
+                                        expand=True),
+"""\
+{
+    'baz': 123,
+    'corge': 7,
+    'foo': 'bar',
+    'quux': ['foo', 'bar', 'baz'],
+    'qux': {'baz': 123, 'foo': 'bar'},
+}""")
+
+    def test_expand_userlist(self):
+        class DummyUserList(collections.UserList):
+            """A custom UserList with some extra attributes"""
+
+            def __init__(self, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+                self.description = "foo"
+        dummy_userlist = DummyUserList(["first", 2, {"key": "value"},
+                                       [4, 5, 6]])
+
+        self.assertEqual(pprint.pformat(dummy_userlist, width=40, indent=4,
+                                        expand=True),
+"""\
+[
+    'first',
+    2,
+    {'key': 'value'},
+    [4, 5, 6],
+]""")
+
+    def test_expand_dict_keys(self):
+        d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5}
+        self.assertEqual(
+            pprint.pformat(d.keys(), width=20, indent=4, expand=True),
+            """\
+dict_keys([
+    'bar',
+    'baz',
+    'foo',
+    'quux',
+    'qux',
+])""",
+        )
+
+    def test_expand_dict_values(self):
+        d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5}
+        self.assertEqual(
+            pprint.pformat(d.values(), width=20, indent=4, expand=True),
+            """\
+dict_values([
+    1,
+    2,
+    3,
+    4,
+    5,
+])""",
+        )
+
+    def test_expand_dict_items(self):
+        d = {"foo": 1, "bar": 2, "baz": 3, "qux": 4, "quux": 5}
+        self.assertEqual(
+            pprint.pformat(d.items(), width=20, indent=4, expand=True),
+            """\
+dict_items([
+    ('bar', 2),
+    ('baz', 3),
+    ('foo', 1),
+    ('quux', 5),
+    ('qux', 4),
+])""",
+        )
+
+    def test_expand_str(self):
+        s = "The quick brown fox jumped over the lazy dog " * 3
+        self.assertEqual(
+            pprint.pformat(s, width=40, indent=4, expand=True),
+            """\
+(
+    'The quick brown fox jumped over '
+    'the lazy dog The quick brown fox '
+    'jumped over the lazy dog The '
+    'quick brown fox jumped over the '
+    'lazy dog '
+)""",
+        )
+
 
 class DottedPrettyPrinter(pprint.PrettyPrinter):
 
diff --git a/Misc/NEWS.d/next/Library/2025-02-07-00-48-07.gh-issue-112632.95MM0C.rst b/Misc/NEWS.d/next/Library/2025-02-07-00-48-07.gh-issue-112632.95MM0C.rst
new file mode 100644 (file)
index 0000000..0842c8e
--- /dev/null
@@ -0,0 +1,3 @@
+Add an *expand* keyword argument for :func:`pprint.pprint`,
+:func:`pprint.pformat`, :func:`pprint.pp` by passing on all *kwargs* and
+:class:`pprint.PrettyPrinter`. Contributed by Stefan Todoran and Semyon Moroz.