Skip to content
Open
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
9 changes: 9 additions & 0 deletions Doc/library/pprint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Functions
A file-like object to which the output will be written
by calling its :meth:`!write` method.
If ``None`` (the default), :data:`sys.stdout` is used.
Characters which cannot be encoded in the encoding of the stream are
escaped with backslashes, unless the stream itself handles them.
:type stream: :term:`file-like object` | None

:param int indent:
Expand Down Expand Up @@ -101,6 +103,10 @@ Functions

.. versionadded:: 3.8

.. versionchanged:: next
Unencodable characters are escaped instead of raising
:exc:`UnicodeEncodeError`.


.. function:: pprint(object, stream=None, indent=1, width=80, depth=None, *, \
compact=False, expand=False, sort_dicts=True, \
Expand Down Expand Up @@ -238,6 +244,9 @@ PrettyPrinter Objects
Print the formatted representation of *object* on the configured stream,
followed by a newline.

Characters which cannot be encoded in the encoding of the stream are
escaped with backslashes, unless the stream itself handles them.

The following methods provide the implementations for the corresponding
functions of the same names. Using these methods on an instance is slightly
more efficient since new :class:`PrettyPrinter` objects don't need to be
Expand Down
36 changes: 34 additions & 2 deletions Lib/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,37 @@ def _safe_tuple(t):
return _safe_key(t[0]), _safe_key(t[1])


class _EscapingWriter:
"""Wrapper which escapes characters unencodable in the stream encoding."""

def __init__(self, stream, encoding):
self._stream = stream
self._encoding = encoding

def write(self, text):
text = text.encode(self._encoding, 'backslashreplace')
return self._stream.write(text.decode(self._encoding))

def __getattr__(self, name):
return getattr(self._stream, name)


def _escape_unencodable(stream):
"""Return a stream which never fails on unencodable characters.

The output is intended to be read by humans, so it is better to escape
unencodable characters than to fail. Streams which do not encode the
written text, or which already handle unencodable characters, are
returned unchanged.
"""
encoding = getattr(stream, 'encoding', None)
if encoding is None:
return stream
if getattr(stream, 'errors', 'strict') != 'strict':
return stream
return _EscapingWriter(stream, encoding)


class PrettyPrinter:
def __init__(self, indent=1, width=80, depth=None, stream=None, *,
compact=False, expand=False, sort_dicts=True,
Expand Down Expand Up @@ -171,8 +202,9 @@ def __init__(self, indent=1, width=80, depth=None, stream=None, *,

def pprint(self, object):
if self._stream is not None:
self._format(object, self._stream, 0, 0, {}, 0)
self._stream.write("\n")
stream = _escape_unencodable(self._stream)
self._format(object, stream, 0, 0, {}, 0)
stream.write("\n")

def pformat(self, object):
sio = _StringIO()
Expand Down
46 changes: 46 additions & 0 deletions Lib/test/test_pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,52 @@ def test_str_wrap(self):
formatted = pprint.pformat([special] * 2, width=width)
self.assertEqual(eval(formatted), [special] * 2)

def test_unencodable(self):
# with encoding and buffer
with io.BytesIO() as bio, \
io.TextIOWrapper(bio, encoding='latin1',
newline='') as stream:
stream.write('\xab')
pprint.pprint('\xa3\u20ac', stream)
stream.flush()
self.assertEqual(bio.getvalue(), b"\xab'\xa3\\u20ac'\n")
stream.write('\xbb')
stream.flush()
self.assertEqual(bio.getvalue(), b"\xab'\xa3\\u20ac'\n\xbb")
# with encoding but without buffer
class MockWriter(list):
encoding = 'latin1'
errors = 'strict'
write = list.append
stream = MockWriter()
stream.write('\xab')
pprint.pprint('\xa3\u20ac', stream)
self.assertEqual(''.join(stream), "\xab'\xa3\\u20ac'\n")
# without encoding
with io.StringIO() as stream:
stream.write('\xab')
pprint.pprint('\xa3\u20ac', stream)
self.assertEqual(stream.getvalue(), "\xab'\xa3\u20ac'\n")
# the error handler of the stream is used if it is not strict
with io.BytesIO() as bio, \
io.TextIOWrapper(bio, encoding='latin1', errors='replace',
newline='') as stream:
pprint.pprint('\u20ac', stream)
stream.flush()
self.assertEqual(bio.getvalue(), b"'?'\n")

def test_unencodable_repr(self):
class Surrogate:
def __repr__(self):
return '\udcff'

with io.BytesIO() as bio, \
io.TextIOWrapper(bio, encoding='utf-8',
newline='') as stream:
pprint.pprint(Surrogate(), stream)
stream.flush()
self.assertEqual(bio.getvalue(), b"\\udcff\n")

def test_compact(self):
o = ([list(range(i * i)) for i in range(5)] +
[list(range(i)) for i in range(6)])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:func:`pprint.pp`, :func:`pprint.pprint` and
:meth:`pprint.PrettyPrinter.pprint` no longer fail with
:exc:`UnicodeEncodeError` if the output contains characters unencodable in the
encoding of the output stream. Such characters are now escaped with
backslashes.
Loading