mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
gh-92734: Add indentation feature to reprlib.Repr (GH-92735)
This commit is contained in:
parent
aa3b4cf779
commit
c06c001b30
4 changed files with 421 additions and 5 deletions
|
@ -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, \
|
.. class:: Repr(*, maxlevel=6, maxtuple=6, maxlist=6, maxarray=5, maxdict=4, \
|
||||||
maxset=6, maxfrozenset=6, maxdeque=6, maxstring=30, maxlong=40, \
|
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
|
Class which provides formatting services useful in implementing functions
|
||||||
similar to the built-in :func:`repr`; size limits for different object types
|
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``.
|
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)
|
.. method:: Repr.repr(obj)
|
||||||
|
|
||||||
The equivalent to the built-in :func:`repr` that uses the formatting imposed by
|
The equivalent to the built-in :func:`repr` that uses the formatting imposed by
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Repr:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, maxlevel=6, maxtuple=6, maxlist=6, maxarray=5, maxdict=4,
|
self, *, maxlevel=6, maxtuple=6, maxlist=6, maxarray=5, maxdict=4,
|
||||||
maxset=6, maxfrozenset=6, maxdeque=6, maxstring=30, maxlong=40,
|
maxset=6, maxfrozenset=6, maxdeque=6, maxstring=30, maxlong=40,
|
||||||
maxother=30, fillvalue='...',
|
maxother=30, fillvalue='...', indent=None,
|
||||||
):
|
):
|
||||||
self.maxlevel = maxlevel
|
self.maxlevel = maxlevel
|
||||||
self.maxtuple = maxtuple
|
self.maxtuple = maxtuple
|
||||||
|
@ -52,6 +52,7 @@ class Repr:
|
||||||
self.maxlong = maxlong
|
self.maxlong = maxlong
|
||||||
self.maxother = maxother
|
self.maxother = maxother
|
||||||
self.fillvalue = fillvalue
|
self.fillvalue = fillvalue
|
||||||
|
self.indent = indent
|
||||||
|
|
||||||
def repr(self, x):
|
def repr(self, x):
|
||||||
return self.repr1(x, self.maxlevel)
|
return self.repr1(x, self.maxlevel)
|
||||||
|
@ -66,6 +67,26 @@ class Repr:
|
||||||
else:
|
else:
|
||||||
return self.repr_instance(x, level)
|
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=''):
|
def _repr_iterable(self, x, level, left, right, maxiter, trail=''):
|
||||||
n = len(x)
|
n = len(x)
|
||||||
if level <= 0 and n:
|
if level <= 0 and n:
|
||||||
|
@ -76,8 +97,8 @@ class Repr:
|
||||||
pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]
|
pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)]
|
||||||
if n > maxiter:
|
if n > maxiter:
|
||||||
pieces.append(self.fillvalue)
|
pieces.append(self.fillvalue)
|
||||||
s = ', '.join(pieces)
|
s = self._join(pieces, level)
|
||||||
if n == 1 and trail:
|
if n == 1 and trail and self.indent is None:
|
||||||
right = trail + right
|
right = trail + right
|
||||||
return '%s%s%s' % (left, s, right)
|
return '%s%s%s' % (left, s, right)
|
||||||
|
|
||||||
|
@ -124,7 +145,7 @@ class Repr:
|
||||||
pieces.append('%s: %s' % (keyrepr, valrepr))
|
pieces.append('%s: %s' % (keyrepr, valrepr))
|
||||||
if n > self.maxdict:
|
if n > self.maxdict:
|
||||||
pieces.append(self.fillvalue)
|
pieces.append(self.fillvalue)
|
||||||
s = ', '.join(pieces)
|
s = self._join(pieces, level)
|
||||||
return '{%s}' % (s,)
|
return '{%s}' % (s,)
|
||||||
|
|
||||||
def repr_str(self, x, level):
|
def repr_str(self, x, level):
|
||||||
|
|
|
@ -9,6 +9,7 @@ import shutil
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import unittest
|
import unittest
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from test.support import verbose
|
from test.support import verbose
|
||||||
from test.support.os_helper import create_empty_file
|
from test.support.os_helper import create_empty_file
|
||||||
|
@ -39,6 +40,7 @@ class ReprTests(unittest.TestCase):
|
||||||
"maxlong": 110,
|
"maxlong": 110,
|
||||||
"maxother": 111,
|
"maxother": 111,
|
||||||
"fillvalue": "x" * 112,
|
"fillvalue": "x" * 112,
|
||||||
|
"indent": "x" * 113,
|
||||||
}
|
}
|
||||||
r1 = Repr()
|
r1 = Repr()
|
||||||
for attr, val in example_kwargs.items():
|
for attr, val in example_kwargs.items():
|
||||||
|
@ -246,6 +248,338 @@ class ReprTests(unittest.TestCase):
|
||||||
r(y)
|
r(y)
|
||||||
r(z)
|
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):
|
def write_file(path, text):
|
||||||
with open(path, 'w', encoding='ASCII') as fp:
|
with open(path, 'w', encoding='ASCII') as fp:
|
||||||
fp.write(text)
|
fp.write(text)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Allow multi-element reprs emitted by :mod:`reprlib` to be pretty-printed using configurable indentation.
|
Loading…
Add table
Add a link
Reference in a new issue