Skip to content

Commit

Permalink
bpo-36817: Add f-string debugging using '='. (GH-13123)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ericvsmith authored May 8, 2019
1 parent 65d98d0 commit 9a4135e
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 49 deletions.
14 changes: 14 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ extensions compiled in release mode and for C extensions compiled with the
stable ABI.
(Contributed by Victor Stinner in :issue:`36722`.)

f-strings now support = for quick and easy debugging
-----------------------------------------------------

Add ``=`` specifier to f-strings. ``f'{expr=}'`` expands
to the text of the expression, an equal sign, then the repr of the
evaluated expression. So::

x = 3
print(f'{x*9 + 15=}')

Would print ``x*9 + 15=42``.

(Contributed by Eric V. Smith and Larry Hastings in :issue:`36817`.)


Other Language Changes
======================
Expand Down
7 changes: 4 additions & 3 deletions Include/Python-ast.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 109 additions & 0 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# -*- coding: utf-8 -*-
# There are tests here with unicode string literals and
# identifiers. There's a code in ast.c that was added because of a
# failure with a non-ascii-only expression. So, I have tests for
# that. There are workarounds that would let me run tests for that
# code without unicode identifiers and strings, but just using them
# directly seems like the easiest and therefore safest thing to do.
# Unicode identifiers in tests is allowed by PEP 3131.

import ast
import types
import decimal
Expand Down Expand Up @@ -878,6 +887,12 @@ def test_not_equal(self):
self.assertEqual(f'{3!=4!s}', 'True')
self.assertEqual(f'{3!=4!s:.3}', 'Tru')

def test_equal_equal(self):
# Because an expression ending in = has special meaning,
# there's a special test for ==. Make sure it works.

self.assertEqual(f'{0==1}', 'False')

def test_conversions(self):
self.assertEqual(f'{3.14:10.10}', ' 3.14')
self.assertEqual(f'{3.14!s:10.10}', '3.14 ')
Expand Down Expand Up @@ -1049,6 +1064,100 @@ def test_backslash_char(self):
self.assertEqual(eval('f"\\\n"'), '')
self.assertEqual(eval('f"\\\r"'), '')

def test_debug_conversion(self):
x = 'A string'
self.assertEqual(f'{x=}', 'x=' + repr(x))
self.assertEqual(f'{x =}', 'x =' + repr(x))
self.assertEqual(f'{x=!s}', 'x=' + str(x))
self.assertEqual(f'{x=!r}', 'x=' + repr(x))
self.assertEqual(f'{x=!a}', 'x=' + ascii(x))

x = 2.71828
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20'))
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))

x = 9
self.assertEqual(f'{3*x+15=}', '3*x+15=42')

# There is code in ast.c that deals with non-ascii expression values. So,
# use a unicode identifier to trigger that.
tenπ = 31.4
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')

# Also test with Unicode in non-identifiers.
self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'')

# Make sure nested fstrings still work.
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')

# Make sure text before and after an expression with = works
# correctly.
pi = 'π'
self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega")

# Check multi-line expressions.
self.assertEqual(f'''{
3
=}''', '\n3\n=3')

# Since = is handled specially, make sure all existing uses of
# it still work.

self.assertEqual(f'{0==1}', 'False')
self.assertEqual(f'{0!=1}', 'True')
self.assertEqual(f'{0<=1}', 'True')
self.assertEqual(f'{0>=1}', 'False')
self.assertEqual(f'{(x:="5")}', '5')
self.assertEqual(x, '5')
self.assertEqual(f'{(x:=5)}', '5')
self.assertEqual(x, 5)
self.assertEqual(f'{"="}', '=')

x = 20
# This isn't an assignment expression, it's 'x', with a format
# spec of '=10'. See test_walrus: you need to use parens.
self.assertEqual(f'{x:=10}', ' 20')

# Test named function parameters, to make sure '=' parsing works
# there.
def f(a):
nonlocal x
oldx = x
x = a
return oldx
x = 0
self.assertEqual(f'{f(a="3=")}', '0')
self.assertEqual(x, '3=')
self.assertEqual(f'{f(a=4)}', '3=')
self.assertEqual(x, 4)

# Make sure __format__ is being called.
class C:
def __format__(self, s):
return f'FORMAT-{s}'
def __repr__(self):
return 'REPR'

self.assertEqual(f'{C()=}', 'C()=REPR')
self.assertEqual(f'{C()=!r}', 'C()=REPR')
self.assertEqual(f'{C()=:}', 'C()=FORMAT-')
self.assertEqual(f'{C()=: }', 'C()=FORMAT- ')
self.assertEqual(f'{C()=:x}', 'C()=FORMAT-x')
self.assertEqual(f'{C()=!r:*^20}', 'C()=********REPR********')

def test_walrus(self):
x = 20
# This isn't an assignment expression, it's 'x', with a format
# spec of '=10'.
self.assertEqual(f'{x:=10}', ' 20')

# This is an assignment expression, which requires parens.
self.assertEqual(f'{(x:=10)}', '10')
self.assertEqual(x, 10)


if __name__ == '__main__':
unittest.main()
9 changes: 9 additions & 0 deletions Lib/test/test_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ def test_annotations(self):
eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'")
eq("f'{(lambda x: x)}'")
eq("f'{(None if a else lambda x: x)}'")
eq("f'{x}'")
eq("f'{x!r}'")
eq("f'{x!a}'")
eq("f'{x=!r}'")
eq("f'{x=:}'")
eq("f'{x=:.2f}'")
eq("f'{x=!r}'")
eq("f'{x=!a}'")
eq("f'{x=!s:*^20}'")
eq('(yield from outside_of_generator)')
eq('(yield)')
eq('(yield a + b)')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Add a ``=`` feature f-strings for debugging. This can precede ``!s``,
``!r``, or ``!a``. It produces the text of the expression, followed by
an equal sign, followed by the repr of the value of the expression. So
``f'{3*9+15=}'`` would be equal to the string ``'3*9+15=42'``. If
``=`` is specified, the default conversion is set to ``!r``, unless a
format spec is given, in which case the formatting behavior is
unchanged, and __format__ will be used.
2 changes: 1 addition & 1 deletion Parser/Python.asdl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ module Python
-- x < 4 < 3 and (x < 4) < 3
| Compare(expr left, cmpop* ops, expr* comparators)
| Call(expr func, expr* args, keyword* keywords)
| FormattedValue(expr value, int? conversion, expr? format_spec)
| FormattedValue(expr value, int? conversion, expr? format_spec, string? expr_text)
| JoinedStr(expr* values)
| Constant(constant value, string? kind)

Expand Down
35 changes: 29 additions & 6 deletions Python/Python-ast.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9a4135e

Please sign in to comment.