diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 65b8ffdb23111d..45ba7f18649e94 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1598,17 +1598,23 @@ are always available. They are listed here in alphabetical order. supported. -.. function:: print(*objects, sep=' ', end='\n', file=None, flush=False) +.. function:: print(*objects, sep=' ', end='\n', file=None, flush=False, pretty=None) - Print *objects* to the text stream *file*, separated by *sep* and followed - by *end*. *sep*, *end*, *file*, and *flush*, if present, must be given as keyword - arguments. + Print *objects* to the text stream *file*, separated by *sep* and followed by + *end*. *sep*, *end*, *file*, *flush*, and *pretty*, if present, must be + given as keyword arguments. + + When *pretty* is ``None``, all non-keyword arguments are converted to + strings like :func:`str` does and written to the stream, separated by *sep* + and followed by *end*. Both *sep* and *end* must be strings; they can also + be ``None``, which means to use the default values. If no *objects* are + given, :func:`print` will just write *end*. - All non-keyword arguments are converted to strings like :func:`str` does and - written to the stream, separated by *sep* and followed by *end*. Both *sep* - and *end* must be strings; they can also be ``None``, which means to use the - default values. If no *objects* are given, :func:`print` will just write - *end*. + When *pretty* is given, it signals that the objects should be "pretty + printed". *pretty* can be ``True`` or an object implementing the + :meth:`pprint.PrettyPrinter.pformat` API which takes an object and returns a + formatted representation of the object. When *pretty* is ``True``, then it + calls ``PrettyPrinter.pformat()`` explicitly. The *file* argument must be an object with a ``write(string)`` method; if it is not present or ``None``, :data:`sys.stdout` will be used. Since printed @@ -1622,6 +1628,9 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.3 Added the *flush* keyword argument. + .. versionchanged:: 3.15 + Added the *pretty* keyword argument. + .. class:: property(fget=None, fset=None, fdel=None, doc=None) diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index be942949d3ebfe..8cd547b2f749ee 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -28,6 +28,9 @@ adjustable by the *width* parameter defaulting to 80 characters. .. versionchanged:: 3.10 Added support for pretty-printing :class:`dataclasses.dataclass`. +.. versionchanged:: 3.15 + Added support for the :ref:`__pprint__ ` protocol. + .. _pprint-functions: Functions @@ -253,6 +256,34 @@ are converted to strings. The default implementation uses the internals of the calls. The fourth argument, *level*, gives the current level; recursive calls should be passed a value less than that of the current call. +.. _dunder-pprint: + +The "__pprint__" protocol +------------------------- + +Pretty printing uses an object's ``__repr__`` by default. For custom pretty printing, objects can +implement a ``__pprint__()`` function to customize how their representations will be printed. If this method +exists, it is called instead of ``__repr__``. The method is called with a single argument, the object to be +pretty printed. + +The method is expected to return or yield a sequence of values, which are used to construct a pretty +representation of the object. These values are wrapped in standard class "chrome", such as the class name. +The printed representation will usually look like a class constructor, with positional, keyword, and default +arguments. The values can be any of the following formats: + +* A single value, representing a positional argument. The value itself is used. +* A 2-tuple of ``(name, value)`` representing a keyword argument. A representation of + ``name=value`` is used. +* A 3-tuple of ``(name, value, default_value)`` representing a keyword argument with a default + value. If ``value`` equals ``default_value``, then this tuple is skipped, otherwise + ``name=value`` is used. + +.. note:: + + This protocol is compatible with the `Rich library's pretty printing protocol + `_. + +See the :ref:`pprint-protocol-example` for how this can be used in practice. .. _pprint-example: @@ -418,3 +449,38 @@ cannot be split, the specified width will be exceeded:: 'requires_python': None, 'summary': 'A sample Python project', 'version': '1.2.0'} + +.. _pprint-protocol-example: + +Pretty Print Protocol Example +----------------------------- + +Let's start with a simple class that defines a ``__pprint__()`` method: + +.. code-block:: python + + class Bass: + def __init__(self, strings: int, pickups: str, active: bool=False): + self._strings = strings + self._pickups = pickups + self._active = active + + def __pprint__(self): + yield self._strings + yield 'pickups', self._pickups + yield 'active', self._active, False + + precision = Bass(4, 'split coil P', active=False) + stingray = Bass(5, 'humbucker', active=True) + +The ``__pprint__()`` method yields three values, which correspond to the ``__init__()`` arguments, +showing by example each of the three different allowed formats. Here is what the output looks like: + +.. code-block:: pycon + + >>> pprint.pprint(precision) + Bass(4, pickups='split coil P') + >>> pprint.pprint(stingray) + Bass(5, pickups='humbucker', active=True) + +Note that you'd get exactly the same output if you used ``print(..., pretty=True)``. diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 64e3438f9157fe..7e2473cea72867 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1993,6 +1993,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posix)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(prec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(preserve_exc)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pretty)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(priority)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 78ed30dd7f62a2..88d51b1112d958 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -716,6 +716,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(posix) STRUCT_FOR_ID(prec) STRUCT_FOR_ID(preserve_exc) + STRUCT_FOR_ID(pretty) STRUCT_FOR_ID(print_file_and_line) STRUCT_FOR_ID(priority) STRUCT_FOR_ID(progress) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index d4b7b090f93f31..5144fa86ba0762 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1991,6 +1991,7 @@ extern "C" { INIT_ID(posix), \ INIT_ID(prec), \ INIT_ID(preserve_exc), \ + INIT_ID(pretty), \ INIT_ID(print_file_and_line), \ INIT_ID(priority), \ INIT_ID(progress), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index d843674f180902..944d65b0ac0953 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2644,6 +2644,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(pretty); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(print_file_and_line); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..54aa98a6b7de4f 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -609,12 +609,49 @@ def _pprint_user_string(self, object, stream, indent, allowance, context, level) _dispatch[_collections.UserString.__repr__] = _pprint_user_string + def _format_pprint(self, object, method, context, maxlevels, level): + """Format an object using its __pprint__ method. + + The __pprint__ method should be a generator yielding values: + - yield value -> positional arg + - yield (name, value) -> keyword arg, always shown + - yield (name, value, default) -> keyword arg, shown if value != default + """ + cls_name = type(object).__name__ + parts = [] + readable = True + + for item in method(object): + match item: + case (name, value, default): + # Keyword argument w/default. Show only if value != default. + if value != default: + formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1) + parts.append(f"{name}={formatted}") + readable = readable and is_readable + case (name, value): + # Keyword argument. Always show. + formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1) + parts.append(f"{name}={formatted}") + readable = readable and is_readable + case _: + # Positional argument. + formatted, is_readable, _ = self.format(item, context, maxlevels, level + 1) + parts.append(formatted) + readable = readable and is_readable + + rep = f"{cls_name}({', '.join(parts)})" + return rep, readable, False + def _safe_repr(self, object, context, maxlevels, level): # Return triple (repr_string, isreadable, isrecursive). typ = type(object) if typ in _builtin_scalars: return repr(object), True, False + if (p := getattr(typ, "__pprint__", None)): + return self._format_pprint(object, p, context, maxlevels, level) + r = getattr(typ, "__repr__", None) if issubclass(typ, int) and r is int.__repr__: diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 41c337ade7eca1..e6ac761df28fa6 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -134,6 +134,7 @@ def __ne__(self, other): def __hash__(self): return self._hash + class QueryTestCase(unittest.TestCase): def setUp(self): @@ -1472,6 +1473,99 @@ def test_user_string(self): 'jumped over a ' 'lazy dog'}""") + def test_custom_pprinter(self): + # Test __pprint__ with positional and keyword argument. + class CustomPrintable: + def __init__(self, name="my pprint", value=42, is_custom=True): + self.name = name + self.value = value + + def __pprint__(self): + yield self.name + yield "value", self.value + + stream = io.StringIO() + pprint.pprint(CustomPrintable(), stream=stream) + self.assertEqual(stream.getvalue(), "CustomPrintable('my pprint', value=42)\n") + + def test_pprint_protocol_positional(self): + # Test __pprint__ with positional arguments only + class Point: + def __init__(self, x, y): + self.x = x + self.y = y + def __pprint__(self): + yield self.x + yield self.y + + stream = io.StringIO() + pprint.pprint(Point(1, 2), stream=stream) + self.assertEqual(stream.getvalue(), "Point(1, 2)\n") + + def test_pprint_protocol_keyword(self): + # Test __pprint__ with keyword arguments + class Config: + def __init__(self, host, port): + self.host = host + self.port = port + def __pprint__(self): + yield ("host", self.host) + yield ("port", self.port) + + stream = io.StringIO() + pprint.pprint(Config("localhost", 8080), stream=stream) + self.assertEqual(stream.getvalue(), "Config(host='localhost', port=8080)\n") + + def test_pprint_protocol_default(self): + # Test __pprint__ with default values (3-tuple form) + class Bass: + def __init__(self, strings: int, pickups: str, active: bool=False): + self._strings = strings + self._pickups = pickups + self._active = active + + def __pprint__(self): + yield self._strings + yield 'pickups', self._pickups + yield 'active', self._active, False + + # Defaults should be hidden if the value is equal to the default. + stream = io.StringIO() + pprint.pprint(Bass(4, 'split coil P'), stream=stream) + self.assertEqual(stream.getvalue(), "Bass(4, pickups='split coil P')\n") + # Show the argument if the value is not equal to the default. + stream = io.StringIO() + pprint.pprint(Bass(5, 'humbucker', active=True), stream=stream) + self.assertEqual(stream.getvalue(), "Bass(5, pickups='humbucker', active=True)\n") + + def test_pprint_protocol_nested(self): + # Test __pprint__ with nested objects. + class Container: + def __init__(self, items): + self.items = items + def __pprint__(self): + yield "items", self.items + + stream = io.StringIO() + c = Container([1, 2, 3]) + pprint.pprint(c, stream=stream) + self.assertEqual(stream.getvalue(), "Container(items=[1, 2, 3])\n") + # Nested in a list + stream = io.StringIO() + pprint.pprint([c], stream=stream) + self.assertEqual(stream.getvalue(), "[Container(items=[1, 2, 3])]\n") + + def test_pprint_protocol_isreadable(self): + # Test that isreadable works correctly with __pprint__ + class Readable: + def __pprint__(self): + yield 42 + class Unreadable: + def __pprint__(self): + yield open # built-in function, not readable + self.assertTrue(pprint.isreadable(Readable())) + self.assertFalse(pprint.isreadable(Unreadable())) + class DottedPrettyPrinter(pprint.PrettyPrinter): diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index 12256b3b562637..cd3139837d4dee 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -1,6 +1,7 @@ import unittest import sys from io import StringIO +from pprint import PrettyPrinter from test import support @@ -200,5 +201,44 @@ def test_string_in_loop_on_same_line(self): str(context.exception)) +class PPrintable: + def __pprint__(self): + yield 'I feel pretty' + + +class PrettySmart(PrettyPrinter): + def pformat(self, obj): + if isinstance(obj, str): + return obj + return super().pformat(obj) + + +class TestPrettyPrinting(unittest.TestCase): + """Test the optional `pretty` keyword argument.""" + + def setUp(self): + self.file = StringIO() + + def test_default_pretty(self): + print('one', 2, file=self.file, pretty=None) + self.assertEqual(self.file.getvalue(), 'one 2\n') + + def test_default_pretty_printer(self): + print('one', 2, file=self.file, pretty=True) + self.assertEqual(self.file.getvalue(), "'one' 2\n") + + def test_pprint_magic(self): + print('one', PPrintable(), 2, file=self.file, pretty=True) + self.assertEqual(self.file.getvalue(), "'one' PPrintable('I feel pretty') 2\n") + + def test_custom_pprinter(self): + print('one', PPrintable(), 2, file=self.file, pretty=PrettySmart()) + self.assertEqual(self.file.getvalue(), "one PPrintable('I feel pretty') 2\n") + + def test_bad_pprinter(self): + with self.assertRaises(AttributeError): + print('one', PPrintable(), 2, file=self.file, pretty=object()) + + if __name__ == "__main__": unittest.main() diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 493a6e0413d8eb..8f518c9737366a 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2282,6 +2282,8 @@ print as builtin_print a file-like object (stream); defaults to the current sys.stdout. flush: bool = False whether to forcibly flush the stream. + pretty: object = None + a pretty-printing object, None, or True. Prints the values to a stream, or to sys.stdout by default. @@ -2290,10 +2292,11 @@ Prints the values to a stream, or to sys.stdout by default. static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush) -/*[clinic end generated code: output=38d8def56c837bcc input=ff35cb3d59ee8115]*/ + PyObject *file, int flush, PyObject *pretty) +/*[clinic end generated code: output=2c26c52acf1807b9 input=e5c1e64da822042c]*/ { int i, err; + PyObject *printer = NULL; if (file == Py_None) { file = PySys_GetAttr(&_Py_ID(stdout)); @@ -2331,6 +2334,30 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } + if (pretty == Py_True) { + /* Use default `pprint.PrettyPrinter` */ + PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); + + if (!printer_factory) { + Py_DECREF(file); + return NULL; + } + printer = PyObject_CallNoArgs(printer_factory); + Py_DECREF(printer_factory); + + if (!printer) { + Py_DECREF(file); + return NULL; + } + } + else if (pretty == Py_None) { + /* Don't use a pretty printer */ + } + else { + /* Use the given object as the pretty printer */ + printer = pretty; + Py_INCREF(printer); + } for (i = 0; i < objects_length; i++) { if (i > 0) { @@ -2342,12 +2369,28 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); + return NULL; + } + } + + if (printer) { + PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); + + if (!prettified) { + Py_DECREF(file); + Py_DECREF(printer); return NULL; } + err = PyFile_WriteObject(prettified, file, Py_PRINT_RAW); + Py_XDECREF(prettified); + } + else { + err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); } - err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } @@ -2360,16 +2403,19 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } if (flush) { if (_PyFile_Flush(file) < 0) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } Py_DECREF(file); + Py_XDECREF(printer); Py_RETURN_NONE; } diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index c8c141f863d26a..89b517ece4cd08 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -1023,7 +1023,8 @@ builtin_pow(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } PyDoc_STRVAR(builtin_print__doc__, -"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False)\n" +"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False,\n" +" pretty=None)\n" "--\n" "\n" "Prints the values to a stream, or to sys.stdout by default.\n" @@ -1035,7 +1036,9 @@ PyDoc_STRVAR(builtin_print__doc__, " file\n" " a file-like object (stream); defaults to the current sys.stdout.\n" " flush\n" -" whether to forcibly flush the stream."); +" whether to forcibly flush the stream.\n" +" pretty\n" +" a pretty-printing object, None, or True."); #define BUILTIN_PRINT_METHODDEF \ {"print", _PyCFunction_CAST(builtin_print), METH_FASTCALL|METH_KEYWORDS, builtin_print__doc__}, @@ -1043,7 +1046,7 @@ PyDoc_STRVAR(builtin_print__doc__, static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush); + PyObject *file, int flush, PyObject *pretty); static PyObject * builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -1051,7 +1054,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 4 + #define NUM_KEYWORDS 5 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -1060,7 +1063,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), }, + .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), &_Py_ID(pretty), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1069,14 +1072,14 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"sep", "end", "file", "flush", NULL}; + static const char * const _keywords[] = {"sep", "end", "file", "flush", "pretty", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "print", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[4]; + PyObject *argsbuf[5]; PyObject * const *fastargs; Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; PyObject * const *objects; @@ -1085,6 +1088,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *end = Py_None; PyObject *file = Py_None; int flush = 0; + PyObject *pretty = Py_None; fastargs = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); @@ -1112,14 +1116,20 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec goto skip_optional_kwonly; } } - flush = PyObject_IsTrue(fastargs[3]); - if (flush < 0) { - goto exit; + if (fastargs[3]) { + flush = PyObject_IsTrue(fastargs[3]); + if (flush < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } } + pretty = fastargs[4]; skip_optional_kwonly: objects = args; objects_length = nargs; - return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush); + return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush, pretty); exit: return return_value; @@ -1380,4 +1390,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=1c3327da8885bb8e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=2ac0e5b1a8cd3b53 input=a9049054013a1b77]*/