diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dcb50e31d9a68..6565525d7cd80e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,8 @@ repos: name: Run Ruff (lint) on Lib/test/ args: [--exit-non-zero-on-fix] files: ^Lib/test/ + # TODO: remove this exclude once !p f-string support is merged to main + exclude: ^Lib/test/test_print\.py$ - id: ruff-check name: Run Ruff (lint) on Tools/build/ args: [--exit-non-zero-on-fix, --config=Tools/build/.ruff.toml] 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 350831d6ad3c1b..4a4b1baba5bde6 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -25,6 +25,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 @@ -250,6 +253,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: @@ -415,3 +446,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/ceval.h b/Include/ceval.h index e9df8684996e23..af65feea546e4e 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -125,13 +125,14 @@ PyAPI_FUNC(void) PyEval_ReleaseThread(PyThreadState *tstate); } /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x3 +#define FVC_MASK 0x7 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 #define FVC_ASCII 0x3 -#define FVS_MASK 0x4 -#define FVS_HAVE_SPEC 0x4 +#define FVC_PRETTY 0x4 +#define FVS_MASK 0x8 +#define FVS_HAVE_SPEC 0x8 #ifndef Py_LIMITED_API # define Py_CPYTHON_CEVAL_H 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/Include/object.h b/Include/object.h index ad452be8405671..f958974cc4861d 100644 --- a/Include/object.h +++ b/Include/object.h @@ -466,6 +466,7 @@ PyAPI_FUNC(void) PyType_Modified(PyTypeObject *); PyAPI_FUNC(PyObject *) PyObject_Repr(PyObject *); PyAPI_FUNC(PyObject *) PyObject_Str(PyObject *); PyAPI_FUNC(PyObject *) PyObject_ASCII(PyObject *); +PyAPI_FUNC(PyObject *) PyObject_Pretty(PyObject *); PyAPI_FUNC(PyObject *) PyObject_Bytes(PyObject *); PyAPI_FUNC(PyObject *) PyObject_RichCompare(PyObject *, PyObject *, int); PyAPI_FUNC(int) PyObject_RichCompareBool(PyObject *, PyObject *, int); diff --git a/Lib/pprint.py b/Lib/pprint.py index e111bd59d4152c..ee99a7454e07ec 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -623,12 +623,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_fstring.py b/Lib/test/test_fstring.py index 05d0cbd2445c4c..983638aa880883 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1369,7 +1369,7 @@ def test_conversions(self): for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ': self.assertAllRaise(SyntaxError, "f-string: invalid conversion character %r: " - "expected 's', 'r', or 'a'" % conv_identifier, + "expected 's', 'r', 'a', or 'p'" % conv_identifier, ["f'{3!" + conv_identifier + "}'"]) for conv_non_identifier in '3', '!': @@ -1385,7 +1385,7 @@ def test_conversions(self): self.assertAllRaise(SyntaxError, "f-string: invalid conversion character 'ss': " - "expected 's', 'r', or 'a'", + "expected 's', 'r', 'a', or 'p'", ["f'{3!ss}'", "f'{3!ss:}'", "f'{3!ss:s}'", diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index f3860a5d511989..ffd6e3df7d1a63 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -141,6 +141,7 @@ def __ne__(self, other): def __hash__(self): return self._hash + class QueryTestCase(unittest.TestCase): def setUp(self): @@ -1508,6 +1509,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..3973019cca148d 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,47 @@ 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()) + + def test_fstring(self): + self.assertEqual(f'{PPrintable()!p}', "PPrintable('I feel pretty')") + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 74653c77c55de1..93a92578333bd6 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -216,7 +216,7 @@ def test_syntax_errors(self): ("t'{x!}'", "t-string: missing conversion character"), ("t'{x=!}'", "t-string: missing conversion character"), ("t'{x!z}'", "t-string: invalid conversion character 'z': " - "expected 's', 'r', or 'a'"), + "expected 's', 'r', 'a', or 'p'"), ("t'{lambda:1}'", "t-string: lambda expressions are not allowed " "without parentheses"), ("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"), diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst new file mode 100644 index 00000000000000..69cf4c61a2bd39 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst @@ -0,0 +1,2 @@ +`PEP 813 `_ (Pretty Print Protocol) +implementation. diff --git a/Objects/object.c b/Objects/object.c index ab73d2eb1c9c1f..71e0c56ce3ebb0 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -862,6 +862,25 @@ PyObject_ASCII(PyObject *v) return res; } +PyObject * +PyObject_Pretty(PyObject *v) +{ + /* Call `pprint.pformat` */ + PyObject *printer = PyImport_ImportModuleAttrString("pprint", "pformat"); + if (!printer) { + return NULL; + } + + PyObject *prettified = PyObject_CallOneArg(printer, v); + Py_DECREF(printer); + + if (!prettified) { + return NULL; + } + + return prettified; +} + PyObject * PyObject_Bytes(PyObject *v) { diff --git a/Objects/stringlib/unicode_format.h b/Objects/stringlib/unicode_format.h index ff32db65b11a0b..5a234b79838bfc 100644 --- a/Objects/stringlib/unicode_format.h +++ b/Objects/stringlib/unicode_format.h @@ -760,7 +760,7 @@ MarkupIterator_next(MarkupIterator *self, SubString *literal, } -/* do the !r or !s conversion on obj */ +/* do the !r, !s, !a, or !p conversion on obj */ static PyObject * do_conversion(PyObject *obj, Py_UCS4 conversion) { @@ -773,6 +773,8 @@ do_conversion(PyObject *obj, Py_UCS4 conversion) return PyObject_Str(obj); case 'a': return PyObject_ASCII(obj); + case 'p': + return PyObject_Pretty(obj); default: if (conversion > 32 && conversion < 127) { /* It's the ASCII subrange; casting to char is safe diff --git a/PC/python3dll.c b/PC/python3dll.c index b23bc2b8f4382f..4f29648c708418 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -482,6 +482,7 @@ EXPORT_FUNC(PyNumber_TrueDivide) EXPORT_FUNC(PyNumber_Xor) EXPORT_FUNC(PyObject_AsCharBuffer) EXPORT_FUNC(PyObject_ASCII) +EXPORT_FUNC(PyObject_Pretty) EXPORT_FUNC(PyObject_AsFileDescriptor) EXPORT_FUNC(PyObject_AsReadBuffer) EXPORT_FUNC(PyObject_AsWriteBuffer) diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 1f5b6220ba1baa..865ac2ebedec7e 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1001,9 +1001,9 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv) Py_UCS4 first = PyUnicode_READ_CHAR(conv->v.Name.id, 0); if (PyUnicode_GET_LENGTH(conv->v.Name.id) > 1 || - !(first == 's' || first == 'r' || first == 'a')) { + !(first == 's' || first == 'r' || first == 'a' || first == 'p')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv, - "%c-string: invalid conversion character %R: expected 's', 'r', or 'a'", + "%c-string: invalid conversion character %R: expected 's', 'r', 'a', or 'p'", TOK_GET_STRING_PREFIX(p->tok), conv->v.Name.id); return NULL; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 301125051f3b0e..9e3bd7b2d68f98 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2288,6 +2288,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. @@ -2296,10 +2298,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)); @@ -2337,6 +2340,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) { @@ -2348,12 +2375,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; } } @@ -2366,16 +2409,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/bytecodes.c b/Python/bytecodes.c index 01eaf4a59b645a..9d8e14eee08e69 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -5135,7 +5135,7 @@ dummy_func( inst(CONVERT_VALUE, (value -- result)) { conversion_func conv_fn; - assert(oparg >= FVC_STR && oparg <= FVC_ASCII); + assert(oparg >= FVC_STR && oparg <= FVC_PRETTY); conv_fn = _PyEval_ConversionFuncs[oparg]; PyObject *result_o = conv_fn(PyStackRef_AsPyObjectBorrow(value)); PyStackRef_CLOSE(value); diff --git a/Python/ceval.c b/Python/ceval.c index 2cd7c7bfd28d09..f4080248faf1ee 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -350,10 +350,11 @@ const binaryfunc _PyEval_BinaryOps[] = { [NB_SUBSCR] = PyObject_GetItem, }; -const conversion_func _PyEval_ConversionFuncs[4] = { +const conversion_func _PyEval_ConversionFuncs[5] = { [FVC_STR] = PyObject_Str, [FVC_REPR] = PyObject_Repr, - [FVC_ASCII] = PyObject_ASCII + [FVC_ASCII] = PyObject_ASCII, + [FVC_PRETTY] = PyObject_Pretty }; const _Py_SpecialMethod _Py_SpecialMethods[] = { 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]*/ diff --git a/Python/codegen.c b/Python/codegen.c index 42fccb07d31dba..72873f230259f3 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4243,6 +4243,7 @@ codegen_interpolation(compiler *c, expr_ty e) case 's': oparg |= FVC_STR << 2; break; case 'r': oparg |= FVC_REPR << 2; break; case 'a': oparg |= FVC_ASCII << 2; break; + case 'p': oparg |= FVC_PRETTY << 2; break; default: PyErr_Format(PyExc_SystemError, "Unrecognized conversion character %d", conversion); @@ -4270,6 +4271,7 @@ codegen_formatted_value(compiler *c, expr_ty e) case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; + case 'p': oparg = FVC_PRETTY; break; default: PyErr_Format(PyExc_SystemError, "Unrecognized conversion character %d", conversion); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index bc9ae7e0ab3be3..dd020cbb7d28b3 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -5265,7 +5265,7 @@ _PyStackRef result; value = stack_pointer[-1]; conversion_func conv_fn; - assert(oparg >= FVC_STR && oparg <= FVC_ASCII); + assert(oparg >= FVC_STR && oparg <= FVC_PRETTY); conv_fn = _PyEval_ConversionFuncs[oparg]; _PyFrame_SetStackPointer(frame, stack_pointer); PyObject *result_o = conv_fn(PyStackRef_AsPyObjectBorrow(value));