Skip to content

Commit 9a4135e

Browse files
authored
bpo-36817: Add f-string debugging using '='. (GH-13123)
If a "=" is specified a the end of an f-string expression, the f-string will evaluate to the text of the expression, followed by '=', followed by the repr of the value of the expression.
1 parent 65d98d0 commit 9a4135e

File tree

11 files changed

+286
-49
lines changed

11 files changed

+286
-49
lines changed

Diff for: Doc/whatsnew/3.8.rst

+14
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ extensions compiled in release mode and for C extensions compiled with the
148148
stable ABI.
149149
(Contributed by Victor Stinner in :issue:`36722`.)
150150

151+
f-strings now support = for quick and easy debugging
152+
-----------------------------------------------------
153+
154+
Add ``=`` specifier to f-strings. ``f'{expr=}'`` expands
155+
to the text of the expression, an equal sign, then the repr of the
156+
evaluated expression. So::
157+
158+
x = 3
159+
print(f'{x*9 + 15=}')
160+
161+
Would print ``x*9 + 15=42``.
162+
163+
(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.)
164+
151165

152166
Other Language Changes
153167
======================

Diff for: Include/Python-ast.h

+4-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Lib/test/test_fstring.py

+109
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# -*- coding: utf-8 -*-
2+
# There are tests here with unicode string literals and
3+
# identifiers. There's a code in ast.c that was added because of a
4+
# failure with a non-ascii-only expression. So, I have tests for
5+
# that. There are workarounds that would let me run tests for that
6+
# code without unicode identifiers and strings, but just using them
7+
# directly seems like the easiest and therefore safest thing to do.
8+
# Unicode identifiers in tests is allowed by PEP 3131.
9+
110
import ast
211
import types
312
import decimal
@@ -878,6 +887,12 @@ def test_not_equal(self):
878887
self.assertEqual(f'{3!=4!s}', 'True')
879888
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
880889

890+
def test_equal_equal(self):
891+
# Because an expression ending in = has special meaning,
892+
# there's a special test for ==. Make sure it works.
893+
894+
self.assertEqual(f'{0==1}', 'False')
895+
881896
def test_conversions(self):
882897
self.assertEqual(f'{3.14:10.10}', ' 3.14')
883898
self.assertEqual(f'{3.14!s:10.10}', '3.14 ')
@@ -1049,6 +1064,100 @@ def test_backslash_char(self):
10491064
self.assertEqual(eval('f"\\\n"'), '')
10501065
self.assertEqual(eval('f"\\\r"'), '')
10511066

1067+
def test_debug_conversion(self):
1068+
x = 'A string'
1069+
self.assertEqual(f'{x=}', 'x=' + repr(x))
1070+
self.assertEqual(f'{x =}', 'x =' + repr(x))
1071+
self.assertEqual(f'{x=!s}', 'x=' + str(x))
1072+
self.assertEqual(f'{x=!r}', 'x=' + repr(x))
1073+
self.assertEqual(f'{x=!a}', 'x=' + ascii(x))
1074+
1075+
x = 2.71828
1076+
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
1077+
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
1078+
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20'))
1079+
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
1080+
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))
1081+
1082+
x = 9
1083+
self.assertEqual(f'{3*x+15=}', '3*x+15=42')
1084+
1085+
# There is code in ast.c that deals with non-ascii expression values. So,
1086+
# use a unicode identifier to trigger that.
1087+
tenπ = 31.4
1088+
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')
1089+
1090+
# Also test with Unicode in non-identifiers.
1091+
self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'')
1092+
1093+
# Make sure nested fstrings still work.
1094+
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')
1095+
1096+
# Make sure text before and after an expression with = works
1097+
# correctly.
1098+
pi = 'π'
1099+
self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega")
1100+
1101+
# Check multi-line expressions.
1102+
self.assertEqual(f'''{
1103+
3
1104+
=}''', '\n3\n=3')
1105+
1106+
# Since = is handled specially, make sure all existing uses of
1107+
# it still work.
1108+
1109+
self.assertEqual(f'{0==1}', 'False')
1110+
self.assertEqual(f'{0!=1}', 'True')
1111+
self.assertEqual(f'{0<=1}', 'True')
1112+
self.assertEqual(f'{0>=1}', 'False')
1113+
self.assertEqual(f'{(x:="5")}', '5')
1114+
self.assertEqual(x, '5')
1115+
self.assertEqual(f'{(x:=5)}', '5')
1116+
self.assertEqual(x, 5)
1117+
self.assertEqual(f'{"="}', '=')
1118+
1119+
x = 20
1120+
# This isn't an assignment expression, it's 'x', with a format
1121+
# spec of '=10'. See test_walrus: you need to use parens.
1122+
self.assertEqual(f'{x:=10}', ' 20')
1123+
1124+
# Test named function parameters, to make sure '=' parsing works
1125+
# there.
1126+
def f(a):
1127+
nonlocal x
1128+
oldx = x
1129+
x = a
1130+
return oldx
1131+
x = 0
1132+
self.assertEqual(f'{f(a="3=")}', '0')
1133+
self.assertEqual(x, '3=')
1134+
self.assertEqual(f'{f(a=4)}', '3=')
1135+
self.assertEqual(x, 4)
1136+
1137+
# Make sure __format__ is being called.
1138+
class C:
1139+
def __format__(self, s):
1140+
return f'FORMAT-{s}'
1141+
def __repr__(self):
1142+
return 'REPR'
1143+
1144+
self.assertEqual(f'{C()=}', 'C()=REPR')
1145+
self.assertEqual(f'{C()=!r}', 'C()=REPR')
1146+
self.assertEqual(f'{C()=:}', 'C()=FORMAT-')
1147+
self.assertEqual(f'{C()=: }', 'C()=FORMAT- ')
1148+
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
1149+
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')
1150+
1151+
def test_walrus(self):
1152+
x = 20
1153+
# This isn't an assignment expression, it's 'x', with a format
1154+
# spec of '=10'.
1155+
self.assertEqual(f'{x:=10}', ' 20')
1156+
1157+
# This is an assignment expression, which requires parens.
1158+
self.assertEqual(f'{(x:=10)}', '10')
1159+
self.assertEqual(x, 10)
1160+
10521161

10531162
if __name__ == '__main__':
10541163
unittest.main()

Diff for: Lib/test/test_future.py

+9
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ def test_annotations(self):
255255
eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'")
256256
eq("f'{(lambda x: x)}'")
257257
eq("f'{(None if a else lambda x: x)}'")
258+
eq("f'{x}'")
259+
eq("f'{x!r}'")
260+
eq("f'{x!a}'")
261+
eq("f'{x=!r}'")
262+
eq("f'{x=:}'")
263+
eq("f'{x=:.2f}'")
264+
eq("f'{x=!r}'")
265+
eq("f'{x=!a}'")
266+
eq("f'{x=!s:*^20}'")
258267
eq('(yield from outside_of_generator)')
259268
eq('(yield)')
260269
eq('(yield a + b)')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add a ``=`` feature f-strings for debugging. This can precede ``!s``,
2+
``!r``, or ``!a``. It produces the text of the expression, followed by
3+
an equal sign, followed by the repr of the value of the expression. So
4+
``f'{3*9+15=}'`` would be equal to the string ``'3*9+15=42'``. If
5+
``=`` is specified, the default conversion is set to ``!r``, unless a
6+
format spec is given, in which case the formatting behavior is
7+
unchanged, and __format__ will be used.

Diff for: Parser/Python.asdl

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ module Python
7676
-- x < 4 < 3 and (x < 4) < 3
7777
| Compare(expr left, cmpop* ops, expr* comparators)
7878
| Call(expr func, expr* args, keyword* keywords)
79-
| FormattedValue(expr value, int? conversion, expr? format_spec)
79+
| FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
8080
| JoinedStr(expr* values)
8181
| Constant(constant value, string? kind)
8282

Diff for: Python/Python-ast.c

+29-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)