diff --git a/Lib/pprint.py b/Lib/pprint.py index 7355021998081dc..2a39e6b8568cab4 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -737,18 +737,13 @@ def _pprint_user_string(self, object, stream, indent, allowance, context, level) def _pprint_template(self, object, stream, indent, allowance, context, level): cls_name = object.__class__.__name__ - if self._expand: - indent += self._indent_per_level - else: - indent += len(cls_name) + 1 - items = ( - ("strings", object.strings), - ("interpolations", object.interpolations), - ) + if not self._expand: + indent += len(cls_name) + stream.write(self._format_block_start(cls_name + "(", indent)) - self._format_namespace_items( - items, stream, indent, allowance, context, level + self._format_items( + object, stream, indent, allowance, context, level ) stream.write( self._format_block_end(")", indent - self._indent_per_level) @@ -758,13 +753,15 @@ def _pprint_interpolation(self, object, stream, indent, allowance, context, leve cls_name = object.__class__.__name__ if self._expand: indent += self._indent_per_level + stream.write(self._format_block_start(cls_name + "(", indent)) items = ( ("value", object.value), ("expression", object.expression), - ("conversion", object.conversion), - ("format_spec", object.format_spec), ) - stream.write(self._format_block_start(cls_name + "(", indent)) + if object.conversion is not None or object.format_spec: + items += (("conversion", object.conversion),) + if object.format_spec: + items += (("format_spec", object.format_spec),) self._format_namespace_items( items, stream, indent, allowance, context, level ) @@ -773,13 +770,15 @@ def _pprint_interpolation(self, object, stream, indent, allowance, context, leve ) else: indent += len(cls_name) + stream.write(cls_name + "(") items = ( object.value, object.expression, - object.conversion, - object.format_spec, ) - stream.write(cls_name + "(") + if object.conversion is not None or object.format_spec: + items += (object.conversion,) + if object.format_spec: + items += (object.format_spec,) self._format_items( items, stream, indent, allowance, context, level ) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 041c2072b9e253a..df3eeba4ef14782 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1517,56 +1517,129 @@ def test_user_string(self): def test_template(self): d = t"" - self.assertEqual(pprint.pformat(d), - "Template(strings=('',), interpolations=())") + self.assertEqual(pprint.pformat(d), "Template()") self.assertEqual(pprint.pformat(d), repr(d)) - self.assertEqual(pprint.pformat(d, width=1), -"""\ -Template(strings=('',), - interpolations=())""") + self.assertEqual(pprint.pformat(d, width=1), "Template()") name = "World" d = t"Hello {name}" self.assertEqual(pprint.pformat(d), """\ -Template(strings=('Hello ', ''), - interpolations=(Interpolation('World', 'name', None, ''),))""") +Template('Hello ', Interpolation('World', 'name'))""") + d = t"Hello {name!r}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', 'r'))""") + d = t"Hello {name:0}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', None, '0'))""") + d = t"Hello {name!r:0}" + self.assertEqual(pprint.pformat(d), +"""\ +Template('Hello ', Interpolation('World', 'name', 'r', '0'))""") + d = t"Hello {name}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name'))""") + d = t"Hello {name!r}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + 'r'))""") + d = t"Hello {name:0}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + None, + '0'))""") + d = t"Hello {name!r:0}" + self.assertEqual(pprint.pformat(d, width=10), +"""\ +Template('Hello ', + Interpolation('World', + 'name', + 'r', + '0'))""") ver = {3.13: False, 3.14: True} d = t"Hello { {"name": "Python", "version": ver}!s:z}!" self.assertEqual(pprint.pformat(d, width=1), """\ -Template(strings=('Hello ', - '!'), - interpolations=(Interpolation({'name': 'Python', - 'version': {3.13: False, - 3.14: True}}, - ' ' - '{"name": ' - '"Python", ' - '"version": ' - 'ver}', - 's', - 'z'),))""") +Template('Hello ', + Interpolation({'name': 'Python', + 'version': {3.13: False, + 3.14: True}}, + ' ' + '{"name": ' + '"Python", ' + '"version": ' + 'ver}', + 's', + 'z'), + '!')""") def test_expand_template(self): d = t"" self.assertEqual( pprint.pformat(d, expand=True), - "Template(strings=('',), interpolations=())", + "Template()", ) name = "World" d = t"Hello {name}" self.assertEqual( - pprint.pformat(d, width=40, indent=4, expand=True), + pprint.pformat(d, width=30, indent=4, expand=True), """\ Template( - strings=('Hello ', ''), - interpolations=( - Interpolation( - value='World', - expression='name', - conversion=None, - format_spec='', - ), + 'Hello ', + Interpolation( + value='World', + expression='name', + ), +)""", + ) + d = t"Hello {name!r}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion='r', + ), +)""", + ) + d = t"Hello {name:0}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion=None, + format_spec='0', + ), +)""", + ) + d = t"Hello {name!r:0}" + self.assertEqual( + pprint.pformat(d, width=30, indent=4, expand=True), + """\ +Template( + 'Hello ', + Interpolation( + value='World', + expression='name', + conversion='r', + format_spec='0', ), )""", ) @@ -1576,22 +1649,18 @@ def test_expand_template(self): pprint.pformat(d, width=40, indent=4, expand=True), """\ Template( - strings=('Hello ', '!'), - interpolations=( - Interpolation( - value={ - 'name': 'Python', - 'version': { - 3.13: False, - 3.14: True, - }, - }, - expression=' {"name": "Python", ' - '"version": ver}', - conversion='s', - format_spec='z', - ), + 'Hello ', + Interpolation( + value={ + 'name': 'Python', + 'version': {3.13: False, 3.14: True}, + }, + expression=' {"name": "Python", ' + '"version": ver}', + conversion='s', + format_spec='z', ), + '!', )""", ) diff --git a/Lib/test/test_string/test_templatelib.py b/Lib/test/test_string/test_templatelib.py index 1c86717155fd5ab..45e1ff66a884a2d 100644 --- a/Lib/test/test_string/test_templatelib.py +++ b/Lib/test/test_string/test_templatelib.py @@ -101,6 +101,33 @@ def test_template_values(self): t = t'Hello, {name}, {age} from {country}' self.assertEqual(t.values, ("Lys", 0, "GR")) + def test_repr(self): + self.assertEqual(repr(t''), 'Template()') + self.assertEqual(repr(t'foo'), "Template('foo')") + + # Test various combination for present/absent conversion and format_spec + x = 42 + self.assertEqual( + repr(t'{x}'), + "Template(Interpolation(42, 'x'))") + self.assertEqual( + repr(t'{x!r}'), + "Template(Interpolation(42, 'x', 'r'))") + self.assertEqual( + repr(t'{x:02}'), + "Template(Interpolation(42, 'x', None, '02'))") + self.assertEqual( + repr(t'a{x!r:02}b'), + "Template('a', Interpolation(42, 'x', 'r', '02'), 'b')") + + # Test a "recursive" template + x = [] + t = t'a{x}b' + x.append(t) + self.assertEqual( + repr(t), + "Template('a', Interpolation([Template(...)], 'x'), 'b')") + def test_pickle_template(self): user = 'test' for template in ( diff --git a/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst b/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst new file mode 100644 index 000000000000000..6720b7fb8447ab2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-08-20-38-28.gh-issue-151099.6Opls1.rst @@ -0,0 +1,2 @@ +Change :func:`repr` output of :class:`string.templatelib.Template` objects +to list the parts of the template in their original source order. diff --git a/Objects/interpolationobject.c b/Objects/interpolationobject.c index e37724fb7852a27..9efb4f7da1333e9 100644 --- a/Objects/interpolationobject.c +++ b/Objects/interpolationobject.c @@ -112,9 +112,31 @@ static PyObject * interpolation_repr(PyObject *op) { interpolationobject *self = interpolationobject_CAST(op); - return PyUnicode_FromFormat("%s(%R, %R, %R, %R)", - _PyType_Name(Py_TYPE(self)), self->value, self->expression, - self->conversion, self->format_spec); + + /* Only emit trailing arguments that differ from their default values + (conversion=None and format_spec=""). We never use keyword arguments, so + if 'format_spec' is non-default, 'conversion' has to be emitted too even + when it still has its default value. */ + int show_format_spec = PyUnicode_GET_LENGTH(self->format_spec) > 0; + int show_conversion = show_format_spec || self->conversion != Py_None; + + if (show_format_spec) { + return PyUnicode_FromFormat("%s(%R, %R, %R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression, + self->conversion, self->format_spec); + } + else if (show_conversion) { + return PyUnicode_FromFormat("%s(%R, %R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression, + self->conversion); + } + else { + return PyUnicode_FromFormat("%s(%R, %R)", + _PyType_Name(Py_TYPE(self)), + self->value, self->expression); + } } static PyMemberDef interpolation_members[] = { diff --git a/Objects/templateobject.c b/Objects/templateobject.c index 1609e82b444516c..3613000a717dc8a 100644 --- a/Objects/templateobject.c +++ b/Objects/templateobject.c @@ -212,10 +212,73 @@ static PyObject * template_repr(PyObject *op) { templateobject *self = templateobject_CAST(op); - return PyUnicode_FromFormat("%s(strings=%R, interpolations=%R)", - _PyType_Name(Py_TYPE(self)), - self->strings, - self->interpolations); + + int res = Py_ReprEnter(op); + if (res != 0) { + if (res < 0) + return NULL; + else + return PyUnicode_FromFormat("%s(...)", _PyType_Name(Py_TYPE(self))); + } + + Py_ssize_t stringslen = PyTuple_GET_SIZE(self->strings); + Py_ssize_t interpolationslen = PyTuple_GET_SIZE(self->interpolations); + + PyUnicodeWriter *writer = PyUnicodeWriter_Create(10); + if (writer == NULL) { + return NULL; + } + + if (PyUnicodeWriter_WriteUTF8(writer, _PyType_Name(Py_TYPE(self)), -1) < 0) { + goto error; + } + if (PyUnicodeWriter_WriteChar(writer, '(') < 0) { + goto error; + } + + /* Render the strings and interpolations in the order they appeared in the + constructor call, i.e. interleaved and skipping empty strings. This + matches the order produced by templateiter_next. */ + int first = 1; + for (Py_ssize_t i = 0; i < stringslen; i++) { + PyObject *string = PyTuple_GET_ITEM(self->strings, i); + if (PyUnicode_GET_LENGTH(string) > 0) { + if (!first) { + if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) { + goto error; + } + } + if (PyUnicodeWriter_WriteRepr(writer, string) < 0) { + goto error; + } + first = 0; + } + if (i < interpolationslen) { + PyObject *interpolation = PyTuple_GET_ITEM(self->interpolations, i); + if (!first) { + if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) { + goto error; + } + } + if (PyUnicodeWriter_WriteRepr(writer, interpolation) < 0) { + goto error; + } + first = 0; + } + } + + if (PyUnicodeWriter_WriteChar(writer, ')') < 0) { + goto error; + } + + Py_ReprLeave(op); + + return PyUnicodeWriter_Finish(writer); + +error: + Py_ReprLeave(op); + PyUnicodeWriter_Discard(writer); + return NULL; } static PyObject *