Skip to content

Commit e70cde9

Browse files
committed
Update repr output of t-strings to resemble to t-string's source, but include the interpolated values.
1 parent ae95a48 commit e70cde9

10 files changed

Lines changed: 253 additions & 62 deletions

File tree

Doc/library/string.templatelib.rst

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ To write a t-string, use a ``'t'`` prefix instead of an ``'f'``, like so:
3333
3434
>>> pi = 3.14
3535
>>> t't-strings are new in Python {pi!s}!'
36-
Template(
37-
strings=('t-strings are new in Python ', '!'),
38-
interpolations=(Interpolation(3.14, 'pi', 's', ''),)
39-
)
36+
<Template t't-strings are new in Python {pi!s=3.14}!' at 0x...>
4037
4138
Types
4239
-----
@@ -118,7 +115,7 @@ Types
118115
>>> cheese = 'Camembert'
119116
>>> template = t'Ah! We do have {cheese}.'
120117
>>> template.interpolations
121-
(Interpolation('Camembert', 'cheese', None, ''),)
118+
(Interpolation('Camembert', 'cheese'),)
122119

123120
The ``interpolations`` tuple may be empty and always contains one fewer
124121
values than the ``strings`` tuple:
@@ -153,7 +150,7 @@ Types
153150
... 'Ah! We do have ', Interpolation(cheese, 'cheese'), '.'
154151
... )
155152
>>> list(template)
156-
['Ah! We do have ', Interpolation('Camembert', 'cheese', None, ''), '.']
153+
['Ah! We do have ', Interpolation('Camembert', 'cheese'), '.']
157154

158155
If multiple strings are passed consecutively, they will be concatenated
159156
into a single value in the :attr:`~Template.strings` attribute. For example,
@@ -184,7 +181,7 @@ Types
184181

185182
>>> cheese = 'Camembert'
186183
>>> list(t'Ah! We do have {cheese}.')
187-
['Ah! We do have ', Interpolation('Camembert', 'cheese', None, ''), '.']
184+
['Ah! We do have ', Interpolation('Camembert', 'cheese'), '.']
188185

189186
.. caution::
190187

@@ -194,8 +191,8 @@ Types
194191
>>> cheese = 'Camembert'
195192
>>> list(t'Ah! {response}{cheese}.') # doctest: +NORMALIZE_WHITESPACE
196193
['Ah! ',
197-
Interpolation('We do have ', 'response', None, ''),
198-
Interpolation('Camembert', 'cheese', None, ''),
194+
Interpolation('We do have ', 'response'),
195+
Interpolation('Camembert', 'cheese'),
199196
'.']
200197

201198
.. describe:: template + other
@@ -206,7 +203,7 @@ Types
206203

207204
>>> cheese = 'Camembert'
208205
>>> list(t'Ah! ' + t'We do have {cheese}.')
209-
['Ah! We do have ', Interpolation('Camembert', 'cheese', None, ''), '.']
206+
['Ah! We do have ', Interpolation('Camembert', 'cheese'), '.']
210207

211208
Concatenating a :class:`!Template` and a ``str`` is **not** supported.
212209
This is because it is unclear whether the string should be treated as
@@ -224,7 +221,7 @@ Types
224221
>>> cheese = 'Camembert'
225222
>>> template += Template(Interpolation(cheese, 'cheese'))
226223
>>> list(template)
227-
['Ah! We do have ', Interpolation('Camembert', 'cheese', None, '')]
224+
['Ah! We do have ', Interpolation('Camembert', 'cheese')]
228225

229226

230227
.. class:: Interpolation

Doc/reference/expressions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ Also, template string literals may only be combined with other template
318318
string literals::
319319

320320
>>> t"Hello" t"{name}!"
321-
Template(strings=('Hello', '!'), interpolations=(...))
321+
<Template t'Hello{name='Blaise'}!' at 0x...>
322322

323323
Formally:
324324

Include/internal/pycore_interpolation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ PyAPI_FUNC(PyObject *) _PyInterpolation_Build(PyObject *value, PyObject *str,
1818

1919
extern PyStatus _PyInterpolation_InitTypes(PyInterpreterState *interp);
2020
extern PyObject *_PyInterpolation_GetValueRef(PyObject *interpolation);
21+
extern int _PyInterpolation_WriteSource(PyUnicodeWriter *writer,
22+
PyObject *interpolation);
2123

2224
#ifdef __cplusplus
2325
}

Lib/pprint.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,8 +785,11 @@ def _pprint_interpolation(self, object, stream, indent, allowance, context, leve
785785
stream.write(")")
786786

787787
t = t"{0}"
788-
_dispatch[type(t).__repr__] = _pprint_template
789-
_dispatch[type(t.interpolations[0]).__repr__] = _pprint_interpolation
788+
_template = type(t)
789+
_dispatch[_template.__repr__] = _pprint_template
790+
791+
_interpolation = type(t.interpolations[0])
792+
_dispatch[_interpolation.__repr__] = _pprint_interpolation
790793
del t
791794

792795
def _safe_repr(self, object, context, maxlevels, level):
@@ -915,6 +918,42 @@ def _safe_repr(self, object, context, maxlevels, level):
915918
del context[objid]
916919
return typ.__name__ + '([%s])' % ", ".join(components), readable, recursive
917920

921+
if (issubclass(typ, self._template) and r is self._template.__repr__) or \
922+
(issubclass(typ, self._interpolation) and r is self._interpolation.__repr__):
923+
# Don't use the repr of templates and interpolations, since the repr
924+
# of a template resembles its source code (and includes the id of
925+
# the template). Use constructor syntax instead, which is what
926+
# _pprint_template and _pprint_interpolation produce as well.
927+
objid = id(object)
928+
if maxlevels and level >= maxlevels:
929+
return f"{typ.__name__}(...)", False, objid in context
930+
if objid in context:
931+
return _recursion(object), False, True
932+
if typ is self._template:
933+
items = object
934+
else:
935+
items = (object.value, object.expression)
936+
if object.conversion is not None or object.format_spec:
937+
items += (object.conversion,)
938+
if object.format_spec:
939+
items += (object.format_spec,)
940+
context[objid] = 1
941+
readable = True
942+
recursive = False
943+
components = []
944+
append = components.append
945+
level += 1
946+
for item in items:
947+
irepr, ireadable, irecur = self.format(
948+
item, context, maxlevels, level)
949+
append(irepr)
950+
readable = readable and ireadable
951+
if irecur:
952+
recursive = True
953+
del context[objid]
954+
rep = "%s(%s)" % (typ.__name__, ", ".join(components))
955+
return rep, readable, recursive
956+
918957
rep = repr(object)
919958
return rep, (rep and not rep.startswith('<')), False
920959

Lib/test/test_pprint.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1517,14 +1517,27 @@ def test_user_string(self):
15171517

15181518
def test_template(self):
15191519
d = t""
1520+
# Templates are always printed using constructor syntax, i.e. the
1521+
# "<Template ...>" format of repr() is never used
15201522
self.assertEqual(pprint.pformat(d), "Template()")
1521-
self.assertEqual(pprint.pformat(d), repr(d))
1523+
self.assertNotEqual(pprint.pformat(d), repr(d))
15221524
self.assertEqual(pprint.pformat(d, width=1), "Template()")
15231525
name = "World"
15241526
d = t"Hello {name}"
15251527
self.assertEqual(pprint.pformat(d),
15261528
"""\
15271529
Template('Hello ', Interpolation('World', 'name'))""")
1530+
# This is also true for templates nested inside other objects and for
1531+
# saferepr()
1532+
self.assertEqual(pprint.pformat({'greeting': d}),
1533+
"""\
1534+
{'greeting': Template('Hello ', Interpolation('World', 'name'))}""")
1535+
self.assertEqual(pprint.saferepr(d),
1536+
"""\
1537+
Template('Hello ', Interpolation('World', 'name'))""")
1538+
self.assertEqual(pprint.saferepr(d.interpolations[0]),
1539+
"""\
1540+
Interpolation('World', 'name')""")
15281541
d = t"Hello {name!r}"
15291542
self.assertEqual(pprint.pformat(d),
15301543
"""\

Lib/test/test_string/test_templatelib.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pickle
2+
import re
23
import unittest
34
from collections.abc import Iterator, Iterable
45
from string.templatelib import Template, Interpolation, convert
@@ -101,32 +102,45 @@ def test_template_values(self):
101102
t = t'Hello, {name}, {age} from {country}'
102103
self.assertEqual(t.values, ("Lys", 0, "GR"))
103104

105+
def check_repr(self, template, source):
106+
"""
107+
Check that the repr of 'template' consists of the class name, 'source'
108+
(the source-like part of the repr, including the 't' prefix and the
109+
quotes) and the id of the template.
110+
"""
111+
self.assertRegex(
112+
repr(template),
113+
rf'^<Template {re.escape(source)} at 0x[0-9A-Fa-f]+>$')
114+
104115
def test_repr(self):
105-
self.assertEqual(repr(t''), 'Template()')
106-
self.assertEqual(repr(t'foo'), "Template('foo')")
116+
self.check_repr(t'', """t''""")
117+
self.check_repr(t'foo', """t'foo'""")
107118

108119
# Test various combination for present/absent conversion and format_spec
109120
x = 42
110-
self.assertEqual(
111-
repr(t'{x}'),
112-
"Template(Interpolation(42, 'x'))")
113-
self.assertEqual(
114-
repr(t'{x!r}'),
115-
"Template(Interpolation(42, 'x', 'r'))")
116-
self.assertEqual(
117-
repr(t'{x:02}'),
118-
"Template(Interpolation(42, 'x', None, '02'))")
119-
self.assertEqual(
120-
repr(t'a{x!r:02}b'),
121-
"Template('a', Interpolation(42, 'x', 'r', '02'), 'b')")
121+
self.check_repr(t'{x}', """t'{x=42}'""")
122+
self.check_repr(t'{x!r}', """t'{x!r=42}'""")
123+
self.check_repr(t'{x:02}', """t'{x:02=42}'""")
124+
self.check_repr(t'a{x!r:02}b', """t'a{x!r:02=42}b'""")
125+
126+
# Braces in the literal parts have to be doubled
127+
self.check_repr(t'{{a}}{x}', """t'{{a}}{x=42}'""")
128+
129+
# The literal parts are escaped like in a string repr, and the quote
130+
# is chosen the same way
131+
self.check_repr(t'a\n{x}', """t'a\\n{x=42}'""")
132+
self.check_repr(t'"a"{x}', """t'"a"{x=42}'""")
133+
self.check_repr(t"'a'{x}", '''t"'a'{x=42}"''')
134+
self.check_repr(t'\'a\'"b"{x}', """t'\\'a\\'"b"{x=42}'""")
122135

123136
# Test a "recursive" template
124137
x = []
125138
t = t'a{x}b'
126139
x.append(t)
127-
self.assertEqual(
140+
self.assertRegex(
128141
repr(t),
129-
"Template('a', Interpolation([Template(...)], 'x'), 'b')")
142+
r"^<Template t'a\{x=\[<Template \.\.\. at 0x[0-9A-Fa-f]+>\]\}b' "
143+
r"at 0x[0-9A-Fa-f]+>$")
130144

131145
def test_pickle_template(self):
132146
user = 'test'

Lib/test/test_tstring.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ class TestTString(unittest.TestCase, TStringBaseCase):
77
def test_string_representation(self):
88
# Test __repr__
99
t = t"Hello"
10-
self.assertEqual(repr(t), "Template(strings=('Hello',), interpolations=())")
10+
self.assertRegex(repr(t), r"^<Template t'Hello' at 0x[0-9A-Fa-f]+>$")
1111

1212
name = "Python"
1313
t = t"Hello, {name}"
14-
self.assertEqual(repr(t),
15-
"Template(strings=('Hello, ', ''), "
16-
"interpolations=(Interpolation('Python', 'name', None, ''),))"
14+
self.assertRegex(repr(t),
15+
r"^<Template t'Hello, \{name='Python'\}' at 0x[0-9A-Fa-f]+>$"
1716
)
1817

1918
def test_interpolation_basics(self):
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
Change :func:`repr` output of :class:`string.templatelib.Template` objects
2-
to list the parts of the template in their original source order.
1+
Change :func:`repr` output of :class:`string.templatelib.Template` objects to
2+
resemble the source code the template was created from, additionally including
3+
the value of each interpolation.

Objects/interpolationobject.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,46 @@ interpolation_repr(PyObject *op)
139139
}
140140
}
141141

142+
/* Write a representation of the interpolation that resembles the source code
143+
it was created from, i.e. the expression, the conversion and the format spec
144+
(if they are non-default), followed by "=" and the repr of the value, all
145+
enclosed in braces. This is used by the repr of Template. */
146+
int
147+
_PyInterpolation_WriteSource(PyUnicodeWriter *writer, PyObject *op)
148+
{
149+
interpolationobject *self = interpolationobject_CAST(op);
150+
151+
if (PyUnicodeWriter_WriteChar(writer, '{') < 0) {
152+
return -1;
153+
}
154+
if (PyUnicodeWriter_WriteStr(writer, self->expression) < 0) {
155+
return -1;
156+
}
157+
if (self->conversion != Py_None) {
158+
if (PyUnicodeWriter_WriteChar(writer, '!') < 0) {
159+
return -1;
160+
}
161+
if (PyUnicodeWriter_WriteStr(writer, self->conversion) < 0) {
162+
return -1;
163+
}
164+
}
165+
if (PyUnicode_GET_LENGTH(self->format_spec) > 0) {
166+
if (PyUnicodeWriter_WriteChar(writer, ':') < 0) {
167+
return -1;
168+
}
169+
if (PyUnicodeWriter_WriteStr(writer, self->format_spec) < 0) {
170+
return -1;
171+
}
172+
}
173+
if (PyUnicodeWriter_WriteChar(writer, '=') < 0) {
174+
return -1;
175+
}
176+
if (PyUnicodeWriter_WriteRepr(writer, self->value) < 0) {
177+
return -1;
178+
}
179+
return PyUnicodeWriter_WriteChar(writer, '}');
180+
}
181+
142182
static PyMemberDef interpolation_members[] = {
143183
{"value", Py_T_OBJECT_EX, offsetof(interpolationobject, value), Py_READONLY, "Value"},
144184
{"expression", Py_T_OBJECT_EX, offsetof(interpolationobject, expression), Py_READONLY, "Expression"},

0 commit comments

Comments
 (0)