[Python-checkins] gh-92734: Add indentation feature to reprlib.Repr (GH-92735)

rhettinger webhook-mailer at python.org
Thu Sep 8 14:51:54 EDT 2022


https://github.com/python/cpython/commit/c06c001b30849d8826132c288426f35403f8a47d
commit: c06c001b30849d8826132c288426f35403f8a47d
branch: main
author: finefoot <33361833+finefoot at users.noreply.github.com>
committer: rhettinger <rhettinger at users.noreply.github.com>
date: 2022-09-08T13:51:44-05:00
summary:

gh-92734: Add indentation feature to reprlib.Repr (GH-92735)

files:
A Misc/NEWS.d/next/Library/2022-05-12-15-19-00.gh-issue-92734.d0wjDt.rst
M Doc/library/reprlib.rst
M Lib/reprlib.py
M Lib/test/test_reprlib.py

diff --git a/Doc/library/reprlib.rst b/Doc/library/reprlib.rst
index bb94b6089ad3..5ebb0a7780c3 100644
--- a/Doc/library/reprlib.rst
+++ b/Doc/library/reprlib.rst
@@ -19,7 +19,7 @@ This module provides a class, an instance, and a function:
 
 .. class:: Repr(*, maxlevel=6, maxtuple=6, maxlist=6, maxarray=5, maxdict=4, \
                 maxset=6, maxfrozenset=6, maxdeque=6, maxstring=30, maxlong=40, \
-                maxother=30, fillvalue="...")
+                maxother=30, fillvalue="...", indent=None)
 
    Class which provides formatting services useful in implementing functions
    similar to the built-in :func:`repr`; size limits for  different object types
@@ -142,6 +142,66 @@ which format specific object types.
    similar manner as :attr:`maxstring`.  The default is ``20``.
 
 
+.. attribute:: Repr.indent
+
+   If this attribute is set to ``None`` (the default), the output is formatted
+   with no line breaks or indentation, like the standard :func:`repr`.
+   For example:
+
+   .. code-block:: pycon
+
+      >>> example = [
+              1, 'spam', {'a': 2, 'b': 'spam eggs', 'c': {3: 4.5, 6: []}}, 'ham']
+      >>> import reprlib
+      >>> aRepr = reprlib.Repr()
+      >>> print(aRepr.repr(example))
+      [1, 'spam', {'a': 2, 'b': 'spam eggs', 'c': {3: 4.5, 6: []}}, 'ham']
+
+   If :attr:`~Repr.indent` is set to a string, each recursion level
+   is placed on its own line, indented by that string:
+
+   .. code-block:: pycon
+
+      >>> aRepr.indent = '-->'
+      >>> print(aRepr.repr(example))
+      [
+      -->1,
+      -->'spam',
+      -->{
+      -->-->'a': 2,
+      -->-->'b': 'spam eggs',
+      -->-->'c': {
+      -->-->-->3: 4.5,
+      -->-->-->6: [],
+      -->-->},
+      -->},
+      -->'ham',
+      ]
+
+   Setting :attr:`~Repr.indent` to a positive integer value behaves as if it
+   was set to a string with that number of spaces:
+
+   .. code-block:: pycon
+
+      >>> aRepr.indent = 4
+      >>> print(aRepr.repr(example))
+      [
+          1,
+          'spam',
+          {
+              'a': 2,
+              'b': 'spam eggs',
+              'c': {
+                  3: 4.5,
+                  6: [],
+              },
+          },
+          'ham',
+      ]
+
+   .. versionadded:: 3.12
+
+
 .. method:: Repr.repr(obj)
 
    The equivalent to the built-in :func:`repr` that uses the formatting imposed by
diff --git a/Lib/reprlib.py b/Lib/reprlib.py
index c33b4da166fa..a92b3e3dbb61 100644
--- a/Lib/reprlib.py
+++ b/Lib/reprlib.py
@@ -38,7 +38,7 @@ class Repr:
     def __init__(
         self, *, maxlevel=6, maxtuple=6, maxlist=6, maxarray=5, maxdict=4,
         maxset=6, maxfrozenset=6, maxdeque=6, maxstring=30, maxlong=40,
-        maxother=30, fillvalue='...',
+        maxother=30, fillvalue='...', indent=None,
     ):
         self.maxlevel = maxlevel
         self.maxtuple = maxtuple
@@ -52,6 +52,7 @@ def __init__(
         self.maxlong = maxlong
         self.maxother = maxother
         self.fillvalue = fillvalue
+        self.indent = indent
 
     def repr(self, x):
         return self.repr1(x, self.maxlevel)
@@ -66,6 +67,26 @@ def repr1(self, x, level):
         else:
             return self.repr_instance(x, level)
 
+    def _join(self, pieces, level):
+        if self.indent is None:
+            return ', '.join(pieces)
+        if not pieces:
+            return ''
+        indent = self.indent
+        if isinstance(indent, int):
+            if indent < 0:
+                raise ValueError(
+                    f'Repr.indent cannot be negative int (was {indent!r})'
+                )
+            indent *= ' '
+        try:
+            sep = ',\n' + (self.maxlevel - level + 1) * indent
+        except TypeError as error:
+            raise TypeError(
+                f'Repr.indent must be a str, int or None, not {type(indent)}'
+            ) from error
+        return sep.join(('', *pieces, ''))[1:-len(indent) or None]
+
     def _repr_iterable(self, x, level, left, right, maxiter, trail=''):
         n = len(x)
         if level <= 0 and n:
@@ -76,8 +97,8 @@ def _repr_iterable(self, x, level, left, right, maxiter, trail=''):
             pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]
             if n > maxiter:
                 pieces.append(self.fillvalue)
-            s = ', '.join(pieces)
-            if n == 1 and trail:
+            s = self._join(pieces, level)
+            if n == 1 and trail and self.indent is None:
                 right = trail + right
         return '%s%s%s' % (left, s, right)
 
@@ -124,7 +145,7 @@ def repr_dict(self, x, level):
             pieces.append('%s: %s' % (keyrepr, valrepr))
         if n > self.maxdict:
             pieces.append(self.fillvalue)
-        s = ', '.join(pieces)
+        s = self._join(pieces, level)
         return '{%s}' % (s,)
 
     def repr_str(self, x, level):
diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py
index 5119511f5d68..e7216d427200 100644
--- a/Lib/test/test_reprlib.py
+++ b/Lib/test/test_reprlib.py
@@ -9,6 +9,7 @@
 import importlib
 import importlib.util
 import unittest
+import textwrap
 
 from test.support import verbose
 from test.support.os_helper import create_empty_file
@@ -39,6 +40,7 @@ def test_init_kwargs(self):
             "maxlong": 110,
             "maxother": 111,
             "fillvalue": "x" * 112,
+            "indent": "x" * 113,
         }
         r1 = Repr()
         for attr, val in example_kwargs.items():
@@ -246,6 +248,338 @@ def test_unsortable(self):
         r(y)
         r(z)
 
+    def test_valid_indent(self):
+        test_cases = [
+            {
+                'object': (),
+                'tests': (
+                    (dict(indent=None), '()'),
+                    (dict(indent=False), '()'),
+                    (dict(indent=True), '()'),
+                    (dict(indent=0), '()'),
+                    (dict(indent=1), '()'),
+                    (dict(indent=4), '()'),
+                    (dict(indent=4, maxlevel=2), '()'),
+                    (dict(indent=''), '()'),
+                    (dict(indent='-->'), '()'),
+                    (dict(indent='....'), '()'),
+                ),
+            },
+            {
+                'object': '',
+                'tests': (
+                    (dict(indent=None), "''"),
+                    (dict(indent=False), "''"),
+                    (dict(indent=True), "''"),
+                    (dict(indent=0), "''"),
+                    (dict(indent=1), "''"),
+                    (dict(indent=4), "''"),
+                    (dict(indent=4, maxlevel=2), "''"),
+                    (dict(indent=''), "''"),
+                    (dict(indent='-->'), "''"),
+                    (dict(indent='....'), "''"),
+                ),
+            },
+            {
+                'object': [1, 'spam', {'eggs': True, 'ham': []}],
+                'tests': (
+                    (dict(indent=None), '''\
+                        [1, 'spam', {'eggs': True, 'ham': []}]'''),
+                    (dict(indent=False), '''\
+                        [
+                        1,
+                        'spam',
+                        {
+                        'eggs': True,
+                        'ham': [],
+                        },
+                        ]'''),
+                    (dict(indent=True), '''\
+                        [
+                         1,
+                         'spam',
+                         {
+                          'eggs': True,
+                          'ham': [],
+                         },
+                        ]'''),
+                    (dict(indent=0), '''\
+                        [
+                        1,
+                        'spam',
+                        {
+                        'eggs': True,
+                        'ham': [],
+                        },
+                        ]'''),
+                    (dict(indent=1), '''\
+                        [
+                         1,
+                         'spam',
+                         {
+                          'eggs': True,
+                          'ham': [],
+                         },
+                        ]'''),
+                    (dict(indent=4), '''\
+                        [
+                            1,
+                            'spam',
+                            {
+                                'eggs': True,
+                                'ham': [],
+                            },
+                        ]'''),
+                    (dict(indent=4, maxlevel=2), '''\
+                        [
+                            1,
+                            'spam',
+                            {
+                                'eggs': True,
+                                'ham': [],
+                            },
+                        ]'''),
+                    (dict(indent=''), '''\
+                        [
+                        1,
+                        'spam',
+                        {
+                        'eggs': True,
+                        'ham': [],
+                        },
+                        ]'''),
+                    (dict(indent='-->'), '''\
+                        [
+                        -->1,
+                        -->'spam',
+                        -->{
+                        -->-->'eggs': True,
+                        -->-->'ham': [],
+                        -->},
+                        ]'''),
+                    (dict(indent='....'), '''\
+                        [
+                        ....1,
+                        ....'spam',
+                        ....{
+                        ........'eggs': True,
+                        ........'ham': [],
+                        ....},
+                        ]'''),
+                ),
+            },
+            {
+                'object': {
+                    1: 'two',
+                    b'three': [
+                        (4.5, 6.7),
+                        [set((8, 9)), frozenset((10, 11))],
+                    ],
+                },
+                'tests': (
+                    (dict(indent=None), '''\
+                        {1: 'two', b'three': [(4.5, 6.7), [{8, 9}, frozenset({10, 11})]]}'''),
+                    (dict(indent=False), '''\
+                        {
+                        1: 'two',
+                        b'three': [
+                        (
+                        4.5,
+                        6.7,
+                        ),
+                        [
+                        {
+                        8,
+                        9,
+                        },
+                        frozenset({
+                        10,
+                        11,
+                        }),
+                        ],
+                        ],
+                        }'''),
+                    (dict(indent=True), '''\
+                        {
+                         1: 'two',
+                         b'three': [
+                          (
+                           4.5,
+                           6.7,
+                          ),
+                          [
+                           {
+                            8,
+                            9,
+                           },
+                           frozenset({
+                            10,
+                            11,
+                           }),
+                          ],
+                         ],
+                        }'''),
+                    (dict(indent=0), '''\
+                        {
+                        1: 'two',
+                        b'three': [
+                        (
+                        4.5,
+                        6.7,
+                        ),
+                        [
+                        {
+                        8,
+                        9,
+                        },
+                        frozenset({
+                        10,
+                        11,
+                        }),
+                        ],
+                        ],
+                        }'''),
+                    (dict(indent=1), '''\
+                        {
+                         1: 'two',
+                         b'three': [
+                          (
+                           4.5,
+                           6.7,
+                          ),
+                          [
+                           {
+                            8,
+                            9,
+                           },
+                           frozenset({
+                            10,
+                            11,
+                           }),
+                          ],
+                         ],
+                        }'''),
+                    (dict(indent=4), '''\
+                        {
+                            1: 'two',
+                            b'three': [
+                                (
+                                    4.5,
+                                    6.7,
+                                ),
+                                [
+                                    {
+                                        8,
+                                        9,
+                                    },
+                                    frozenset({
+                                        10,
+                                        11,
+                                    }),
+                                ],
+                            ],
+                        }'''),
+                    (dict(indent=4, maxlevel=2), '''\
+                        {
+                            1: 'two',
+                            b'three': [
+                                (...),
+                                [...],
+                            ],
+                        }'''),
+                    (dict(indent=''), '''\
+                        {
+                        1: 'two',
+                        b'three': [
+                        (
+                        4.5,
+                        6.7,
+                        ),
+                        [
+                        {
+                        8,
+                        9,
+                        },
+                        frozenset({
+                        10,
+                        11,
+                        }),
+                        ],
+                        ],
+                        }'''),
+                    (dict(indent='-->'), '''\
+                        {
+                        -->1: 'two',
+                        -->b'three': [
+                        -->-->(
+                        -->-->-->4.5,
+                        -->-->-->6.7,
+                        -->-->),
+                        -->-->[
+                        -->-->-->{
+                        -->-->-->-->8,
+                        -->-->-->-->9,
+                        -->-->-->},
+                        -->-->-->frozenset({
+                        -->-->-->-->10,
+                        -->-->-->-->11,
+                        -->-->-->}),
+                        -->-->],
+                        -->],
+                        }'''),
+                    (dict(indent='....'), '''\
+                        {
+                        ....1: 'two',
+                        ....b'three': [
+                        ........(
+                        ............4.5,
+                        ............6.7,
+                        ........),
+                        ........[
+                        ............{
+                        ................8,
+                        ................9,
+                        ............},
+                        ............frozenset({
+                        ................10,
+                        ................11,
+                        ............}),
+                        ........],
+                        ....],
+                        }'''),
+                ),
+            },
+        ]
+        for test_case in test_cases:
+            with self.subTest(test_object=test_case['object']):
+                for repr_settings, expected_repr in test_case['tests']:
+                    with self.subTest(repr_settings=repr_settings):
+                        r = Repr()
+                        for attribute, value in repr_settings.items():
+                            setattr(r, attribute, value)
+                        resulting_repr = r.repr(test_case['object'])
+                        expected_repr = textwrap.dedent(expected_repr)
+                        self.assertEqual(resulting_repr, expected_repr)
+
+    def test_invalid_indent(self):
+        test_object = [1, 'spam', {'eggs': True, 'ham': []}]
+        test_cases = [
+            (-1, (ValueError, '[Nn]egative|[Pp]ositive')),
+            (-4, (ValueError, '[Nn]egative|[Pp]ositive')),
+            ((), (TypeError, None)),
+            ([], (TypeError, None)),
+            ((4,), (TypeError, None)),
+            ([4,], (TypeError, None)),
+            (object(), (TypeError, None)),
+        ]
+        for indent, (expected_error, expected_msg) in test_cases:
+            with self.subTest(indent=indent):
+                r = Repr()
+                r.indent = indent
+                expected_msg = expected_msg or f'{type(indent)}'
+                with self.assertRaisesRegex(expected_error, expected_msg):
+                    r.repr(test_object)
+
 def write_file(path, text):
     with open(path, 'w', encoding='ASCII') as fp:
         fp.write(text)
diff --git a/Misc/NEWS.d/next/Library/2022-05-12-15-19-00.gh-issue-92734.d0wjDt.rst b/Misc/NEWS.d/next/Library/2022-05-12-15-19-00.gh-issue-92734.d0wjDt.rst
new file mode 100644
index 000000000000..a2fcd1ed3dc7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-05-12-15-19-00.gh-issue-92734.d0wjDt.rst
@@ -0,0 +1 @@
+Allow multi-element reprs emitted by :mod:`reprlib` to be pretty-printed using configurable indentation.



More information about the Python-checkins mailing list