Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 45 additions & 86 deletions Lib/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,9 @@ def decompose(obj):
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,
Expand Down Expand Up @@ -1269,20 +1272,6 @@ class _KeyValue:
hang: int | None


@dataclass(frozen=True)
class _Breakable:
"""A str or bytes literal that the renderer may split across lines.

The split depends on the column where the literal starts, which is only
known while rendering.
"""

value: object
top: bool
expand: bool
indent: int


class _Marker:
"""A marker rendered by a function from the printer's marker table."""

Expand Down Expand Up @@ -1358,6 +1347,14 @@ def __init__(
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,
Expand Down Expand Up @@ -1468,46 +1465,13 @@ def _wrap_bytes(data, max_width, allowance):
return chunks


def _break_literal(node, column, width, allowance):
"""Return the output lines for a literal that starts at *column*.

Every line after the first includes its leading spaces.
"""
rep = repr(node.value)
if column + len(rep) + allowance <= width or len(node.value) == 0:
return [rep]
if node.top:
# The literal is the whole object, so it is parenthesized.
indent = column + (node.indent if node.expand else 1)
allowance += 1
else:
indent = column
if isinstance(node.value, bytes):
chunks = _wrap_bytes(node.value, width - indent, allowance)
else:
chunks = _wrap_string(node.value, width - indent, allowance)
if len(chunks) <= 1:
return [rep]

pad = _SPACE * indent
if not node.top:
return [chunks[0], *(pad + chunk for chunk in chunks[1:])]
if node.expand:
return ["(", *(pad + chunk for chunk in chunks), _SPACE * column + ")"]
return [
"(" + chunks[0],
*(pad + chunk for chunk in chunks[1:-1]),
pad + chunks[-1] + ")",
]


def _format_number(value, number_format):
if number_format is None:
return repr(value)
try:
return format(value, number_format)
except Exception:
return repr(value)
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):
Expand All @@ -1519,17 +1483,11 @@ def _layout_document(document, level, options):
)
case _TextDocument(text=text):
return _Text(text)
case _BreakableStringDocument(value=value) | _BreakableBytesDocument(value=value):
return _Breakable(
value,
level == 0,
options.expand is not Expand.NEVER,
options.indent,
)
case _FormattableNumberDocument(value=value):
return _Text(_format_number(value, options.number_format))
case _ValueDocument(value=value):
return _Text(repr(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)
Expand Down Expand Up @@ -1557,12 +1515,6 @@ def _document_source(document):
match document:
case _ValueDocument(value=value):
return value
case _BreakableStringDocument(value=value):
return value
case _BreakableBytesDocument(value=value):
return value
case _FormattableNumberDocument(value=value):
return value
case _ContainerDocument(source=value):
return value
case _LazyDocument(value=value):
Expand Down Expand Up @@ -1726,20 +1678,21 @@ def _render_lines(doc, width):
text = flat_text if flat else broken
out.append(text)
column += len(text)
case _Breakable(value=value):
case _Marker():
if flat:
lines = [repr(value)]
lines = node.flat()
else:
lines = _break_literal(
node, column, width, _trailing_width(stack)
)
out.append(lines[0])
column += len(lines[0])
for line in lines[1:]:
yield _EMPTY.join(out)
out.clear()
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)
column += len(line)
case _KeyValue(key=key, value=value, hang=hang):
if flat or hang is None:
stack.append((indent, flat, value))
Expand Down Expand Up @@ -1788,11 +1741,17 @@ def _fits(remaining, first, rest):
remaining -= len(flat_text if flat else broken)
case _KeyValue(key=key, value=value):
local.append((indent, flat, _Concat((key, _COLON_SPACE, value))))
case _Breakable(value=value):
if not flat:
# It can break, so the rest goes on later lines.
return True
remaining -= len(repr(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


Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test__pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,32 @@ def pformat(obj, **options):


class PprintProtocolTests(unittest.TestCase):
def test_invalid_printer_options(self):
for options in ({"indent": -1}, {"width": 0}, {"depth": 0}, {"depth": -1}):
with self.subTest(options=options):
with self.assertRaises(ValueError):
Printer(**options)

def test_printer_normalizes_width_and_indent(self):
self.assertEqual(
pformat([1, 2], width="5", indent="2"),
"[\n 1,\n 2,\n]",
)

def test_close_without_open(self):
class Custom:
def __pprint__(self):
yield Layout(close="]")
yield 1

layout, items = decompose(Custom())
self.assertEqual(layout.open, "Custom(")
self.assertEqual(layout.close, "]")
self.assertEqual(layout.empty, "Custom(]")
self.assertEqual(layout.single_close, "]")
self.assertEqual(items, [1])
self.assertEqual(pformat(Custom()), "Custom(1]")

def test_namedtuple_descriptions(self):
Point = collections.namedtuple("Point", "x y")

Expand Down
10 changes: 5 additions & 5 deletions Modules/_pprint.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,14 +347,14 @@ leaf_marker_repr(PyObject *op)
if (value == NULL) {
return NULL;
}
PyObject *qualname = PyObject_GetAttrString((PyObject *)Py_TYPE(op),
"__qualname__");
if (qualname == NULL) {
PyObject *name = PyObject_GetAttrString(
(PyObject *)Py_TYPE(op), "__name__");
if (name == NULL) {
Py_DECREF(value);
return NULL;
}
PyObject *result = PyUnicode_FromFormat("%U(%R)", qualname, value);
Py_DECREF(qualname);
PyObject *result = PyUnicode_FromFormat("%U(%R)", name, value);
Py_DECREF(name);
Py_DECREF(value);
return result;
}
Expand Down
Loading