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

This commit is contained in:
finefoot 2022-09-08 20:51:44 +02:00 committed by GitHub
parent aa3b4cf779
commit c06c001b30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 421 additions and 5 deletions

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -0,0 +1 @@
Allow multi-element reprs emitted by :mod:`reprlib` to be pretty-printed using configurable indentation.