summaryrefslogtreecommitdiff
path: root/tests/run/test_fstring.pyx
diff options
context:
space:
mode:
authorStefan Behnel <stefan_ml@behnel.de>2020-08-29 09:27:02 +0200
committerStefan Behnel <stefan_ml@behnel.de>2020-08-29 09:27:02 +0200
commitbe43235ba3f4fca32dceda46424b36afb3cb00c9 (patch)
tree4626fd9133d14e6e1481861e3ec70d9a2f46a9ea /tests/run/test_fstring.pyx
parent4d54aeff34753551cf0ac9977d50c292dbf9d5d5 (diff)
downloadcython-be43235ba3f4fca32dceda46424b36afb3cb00c9.tar.gz
Update CPython "test_fstring" copy to Py3.9.
Diffstat (limited to 'tests/run/test_fstring.pyx')
-rw-r--r--tests/run/test_fstring.pyx454
1 files changed, 430 insertions, 24 deletions
diff --git a/tests/run/test_fstring.pyx b/tests/run/test_fstring.pyx
index b8e93673a..f2e0825be 100644
--- a/tests/run/test_fstring.pyx
+++ b/tests/run/test_fstring.pyx
@@ -3,10 +3,10 @@
# tag: allow_unknown_names, f_strings, pep498
import ast
+import os
import types
import decimal
import unittest
-import contextlib
import sys
IS_PY2 = sys.version_info[0] < 3
@@ -63,9 +63,6 @@ class TestCase(CythonTest):
super(TestCase, self).assertEqual(first, second, msg)
def test__format__lookup(self):
- if IS_PY2:
- raise unittest.SkipTest("Py3-only")
-
# Make sure __format__ is looked up on the type, not the instance.
class X:
def __format__(self, spec):
@@ -116,14 +113,263 @@ f'{a * x()}'"""
# Make sure x was called.
self.assertTrue(x.called)
+ def __test_ast_line_numbers(self):
+ expr = """
+a = 10
+f'{a * x()}'"""
+ t = ast.parse(expr)
+ self.assertEqual(type(t), ast.Module)
+ self.assertEqual(len(t.body), 2)
+ # check `a = 10`
+ self.assertEqual(type(t.body[0]), ast.Assign)
+ self.assertEqual(t.body[0].lineno, 2)
+ # check `f'...'`
+ self.assertEqual(type(t.body[1]), ast.Expr)
+ self.assertEqual(type(t.body[1].value), ast.JoinedStr)
+ self.assertEqual(len(t.body[1].value.values), 1)
+ self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
+ self.assertEqual(t.body[1].lineno, 3)
+ self.assertEqual(t.body[1].value.lineno, 3)
+ self.assertEqual(t.body[1].value.values[0].lineno, 3)
+ # check the binop location
+ binop = t.body[1].value.values[0].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.Call)
+ self.assertEqual(binop.lineno, 3)
+ self.assertEqual(binop.left.lineno, 3)
+ self.assertEqual(binop.right.lineno, 3)
+ self.assertEqual(binop.col_offset, 3)
+ self.assertEqual(binop.left.col_offset, 3)
+ self.assertEqual(binop.right.col_offset, 7)
+
+ def __test_ast_line_numbers_multiple_formattedvalues(self):
+ expr = """
+f'no formatted values'
+f'eggs {a * x()} spam {b + y()}'"""
+ t = ast.parse(expr)
+ self.assertEqual(type(t), ast.Module)
+ self.assertEqual(len(t.body), 2)
+ # check `f'no formatted value'`
+ self.assertEqual(type(t.body[0]), ast.Expr)
+ self.assertEqual(type(t.body[0].value), ast.JoinedStr)
+ self.assertEqual(t.body[0].lineno, 2)
+ # check `f'...'`
+ self.assertEqual(type(t.body[1]), ast.Expr)
+ self.assertEqual(type(t.body[1].value), ast.JoinedStr)
+ self.assertEqual(len(t.body[1].value.values), 4)
+ self.assertEqual(type(t.body[1].value.values[0]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[0].value), str)
+ self.assertEqual(type(t.body[1].value.values[1]), ast.FormattedValue)
+ self.assertEqual(type(t.body[1].value.values[2]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[2].value), str)
+ self.assertEqual(type(t.body[1].value.values[3]), ast.FormattedValue)
+ self.assertEqual(t.body[1].lineno, 3)
+ self.assertEqual(t.body[1].value.lineno, 3)
+ self.assertEqual(t.body[1].value.values[0].lineno, 3)
+ self.assertEqual(t.body[1].value.values[1].lineno, 3)
+ self.assertEqual(t.body[1].value.values[2].lineno, 3)
+ self.assertEqual(t.body[1].value.values[3].lineno, 3)
+ # check the first binop location
+ binop1 = t.body[1].value.values[1].value
+ self.assertEqual(type(binop1), ast.BinOp)
+ self.assertEqual(type(binop1.left), ast.Name)
+ self.assertEqual(type(binop1.op), ast.Mult)
+ self.assertEqual(type(binop1.right), ast.Call)
+ self.assertEqual(binop1.lineno, 3)
+ self.assertEqual(binop1.left.lineno, 3)
+ self.assertEqual(binop1.right.lineno, 3)
+ self.assertEqual(binop1.col_offset, 8)
+ self.assertEqual(binop1.left.col_offset, 8)
+ self.assertEqual(binop1.right.col_offset, 12)
+ # check the second binop location
+ binop2 = t.body[1].value.values[3].value
+ self.assertEqual(type(binop2), ast.BinOp)
+ self.assertEqual(type(binop2.left), ast.Name)
+ self.assertEqual(type(binop2.op), ast.Add)
+ self.assertEqual(type(binop2.right), ast.Call)
+ self.assertEqual(binop2.lineno, 3)
+ self.assertEqual(binop2.left.lineno, 3)
+ self.assertEqual(binop2.right.lineno, 3)
+ self.assertEqual(binop2.col_offset, 23)
+ self.assertEqual(binop2.left.col_offset, 23)
+ self.assertEqual(binop2.right.col_offset, 27)
+
+ def __test_ast_line_numbers_nested(self):
+ expr = """
+a = 10
+f'{a * f"-{x()}-"}'"""
+ t = ast.parse(expr)
+ self.assertEqual(type(t), ast.Module)
+ self.assertEqual(len(t.body), 2)
+ # check `a = 10`
+ self.assertEqual(type(t.body[0]), ast.Assign)
+ self.assertEqual(t.body[0].lineno, 2)
+ # check `f'...'`
+ self.assertEqual(type(t.body[1]), ast.Expr)
+ self.assertEqual(type(t.body[1].value), ast.JoinedStr)
+ self.assertEqual(len(t.body[1].value.values), 1)
+ self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
+ self.assertEqual(t.body[1].lineno, 3)
+ self.assertEqual(t.body[1].value.lineno, 3)
+ self.assertEqual(t.body[1].value.values[0].lineno, 3)
+ # check the binop location
+ binop = t.body[1].value.values[0].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.JoinedStr)
+ self.assertEqual(binop.lineno, 3)
+ self.assertEqual(binop.left.lineno, 3)
+ self.assertEqual(binop.right.lineno, 3)
+ self.assertEqual(binop.col_offset, 3)
+ self.assertEqual(binop.left.col_offset, 3)
+ self.assertEqual(binop.right.col_offset, 7)
+ # check the nested call location
+ self.assertEqual(len(binop.right.values), 3)
+ self.assertEqual(type(binop.right.values[0]), ast.Constant)
+ self.assertEqual(type(binop.right.values[0].value), str)
+ self.assertEqual(type(binop.right.values[1]), ast.FormattedValue)
+ self.assertEqual(type(binop.right.values[2]), ast.Constant)
+ self.assertEqual(type(binop.right.values[2].value), str)
+ self.assertEqual(binop.right.values[0].lineno, 3)
+ self.assertEqual(binop.right.values[1].lineno, 3)
+ self.assertEqual(binop.right.values[2].lineno, 3)
+ call = binop.right.values[1].value
+ self.assertEqual(type(call), ast.Call)
+ self.assertEqual(call.lineno, 3)
+ self.assertEqual(call.col_offset, 11)
+
+ def __test_ast_line_numbers_duplicate_expression(self):
+ """Duplicate expression
+
+ NOTE: this is currently broken, always sets location of the first
+ expression.
+ """
+ expr = """
+a = 10
+f'{a * x()} {a * x()} {a * x()}'
+"""
+ t = ast.parse(expr)
+ self.assertEqual(type(t), ast.Module)
+ self.assertEqual(len(t.body), 2)
+ # check `a = 10`
+ self.assertEqual(type(t.body[0]), ast.Assign)
+ self.assertEqual(t.body[0].lineno, 2)
+ # check `f'...'`
+ self.assertEqual(type(t.body[1]), ast.Expr)
+ self.assertEqual(type(t.body[1].value), ast.JoinedStr)
+ self.assertEqual(len(t.body[1].value.values), 5)
+ self.assertEqual(type(t.body[1].value.values[0]), ast.FormattedValue)
+ self.assertEqual(type(t.body[1].value.values[1]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[1].value), str)
+ self.assertEqual(type(t.body[1].value.values[2]), ast.FormattedValue)
+ self.assertEqual(type(t.body[1].value.values[3]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[3].value), str)
+ self.assertEqual(type(t.body[1].value.values[4]), ast.FormattedValue)
+ self.assertEqual(t.body[1].lineno, 3)
+ self.assertEqual(t.body[1].value.lineno, 3)
+ self.assertEqual(t.body[1].value.values[0].lineno, 3)
+ self.assertEqual(t.body[1].value.values[1].lineno, 3)
+ self.assertEqual(t.body[1].value.values[2].lineno, 3)
+ self.assertEqual(t.body[1].value.values[3].lineno, 3)
+ self.assertEqual(t.body[1].value.values[4].lineno, 3)
+ # check the first binop location
+ binop = t.body[1].value.values[0].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.Call)
+ self.assertEqual(binop.lineno, 3)
+ self.assertEqual(binop.left.lineno, 3)
+ self.assertEqual(binop.right.lineno, 3)
+ self.assertEqual(binop.col_offset, 3)
+ self.assertEqual(binop.left.col_offset, 3)
+ self.assertEqual(binop.right.col_offset, 7)
+ # check the second binop location
+ binop = t.body[1].value.values[2].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.Call)
+ self.assertEqual(binop.lineno, 3)
+ self.assertEqual(binop.left.lineno, 3)
+ self.assertEqual(binop.right.lineno, 3)
+ self.assertEqual(binop.col_offset, 3) # FIXME: this is wrong
+ self.assertEqual(binop.left.col_offset, 3) # FIXME: this is wrong
+ self.assertEqual(binop.right.col_offset, 7) # FIXME: this is wrong
+ # check the third binop location
+ binop = t.body[1].value.values[4].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.Call)
+ self.assertEqual(binop.lineno, 3)
+ self.assertEqual(binop.left.lineno, 3)
+ self.assertEqual(binop.right.lineno, 3)
+ self.assertEqual(binop.col_offset, 3) # FIXME: this is wrong
+ self.assertEqual(binop.left.col_offset, 3) # FIXME: this is wrong
+ self.assertEqual(binop.right.col_offset, 7) # FIXME: this is wrong
+
+ def __test_ast_line_numbers_multiline_fstring(self):
+ # See bpo-30465 for details.
+ expr = """
+a = 10
+f'''
+ {a
+ *
+ x()}
+non-important content
+'''
+"""
+ t = ast.parse(expr)
+ self.assertEqual(type(t), ast.Module)
+ self.assertEqual(len(t.body), 2)
+ # check `a = 10`
+ self.assertEqual(type(t.body[0]), ast.Assign)
+ self.assertEqual(t.body[0].lineno, 2)
+ # check `f'...'`
+ self.assertEqual(type(t.body[1]), ast.Expr)
+ self.assertEqual(type(t.body[1].value), ast.JoinedStr)
+ self.assertEqual(len(t.body[1].value.values), 3)
+ self.assertEqual(type(t.body[1].value.values[0]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[0].value), str)
+ self.assertEqual(type(t.body[1].value.values[1]), ast.FormattedValue)
+ self.assertEqual(type(t.body[1].value.values[2]), ast.Constant)
+ self.assertEqual(type(t.body[1].value.values[2].value), str)
+ self.assertEqual(t.body[1].lineno, 3)
+ self.assertEqual(t.body[1].value.lineno, 3)
+ self.assertEqual(t.body[1].value.values[0].lineno, 3)
+ self.assertEqual(t.body[1].value.values[1].lineno, 3)
+ self.assertEqual(t.body[1].value.values[2].lineno, 3)
+ self.assertEqual(t.body[1].col_offset, 0)
+ self.assertEqual(t.body[1].value.col_offset, 0)
+ self.assertEqual(t.body[1].value.values[0].col_offset, 0)
+ self.assertEqual(t.body[1].value.values[1].col_offset, 0)
+ self.assertEqual(t.body[1].value.values[2].col_offset, 0)
+ # NOTE: the following lineno information and col_offset is correct for
+ # expressions within FormattedValues.
+ binop = t.body[1].value.values[1].value
+ self.assertEqual(type(binop), ast.BinOp)
+ self.assertEqual(type(binop.left), ast.Name)
+ self.assertEqual(type(binop.op), ast.Mult)
+ self.assertEqual(type(binop.right), ast.Call)
+ self.assertEqual(binop.lineno, 4)
+ self.assertEqual(binop.left.lineno, 4)
+ self.assertEqual(binop.right.lineno, 6)
+ self.assertEqual(binop.col_offset, 4)
+ self.assertEqual(binop.left.col_offset, 4)
+ self.assertEqual(binop.right.col_offset, 7)
+
def test_docstring(self):
def f():
f'''Not a docstring'''
- self.assertTrue(f.__doc__ is None)
+ self.assertIsNone(f.__doc__)
def g():
'''Not a docstring''' \
f''
- self.assertTrue(g.__doc__ is None)
+ self.assertIsNone(g.__doc__)
def __test_literal_eval(self):
with self.assertRaisesRegex(ValueError, 'malformed node or string'):
@@ -159,9 +405,27 @@ f'{a * x()}'"""
])
def test_mismatched_parens(self):
- self.assertAllRaise(SyntaxError, 'f-string: mismatched',
+ self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
+ r"does not match opening parenthesis '\('",
["f'{((}'",
])
+ self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\)' "
+ r"does not match opening parenthesis '\['",
+ ["f'{a[4)}'",
+ ])
+ self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\]' "
+ r"does not match opening parenthesis '\('",
+ ["f'{a(4]}'",
+ ])
+ self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
+ r"does not match opening parenthesis '\['",
+ ["f'{a[4}'",
+ ])
+ self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
+ r"does not match opening parenthesis '\('",
+ ["f'{a(4}'",
+ ])
+ self.assertRaises(SyntaxError, eval, "f'{" + "("*500 + "}'")
def test_double_braces(self):
self.assertEqual(f'{{', '{')
@@ -239,7 +503,9 @@ f'{a * x()}'"""
["f'{1#}'", # error because the expression becomes "(1#)"
"f'{3(#)}'",
"f'{#}'",
- "f'{)#}'", # When wrapped in parens, this becomes
+ ])
+ self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
+ ["f'{)#}'", # When wrapped in parens, this becomes
# '()#)'. Make sure that doesn't compile.
])
@@ -256,20 +522,23 @@ f'{a * x()}'"""
# Test around 256.
# for i in range(250, 260):
- # self.assertEqual(cy_eval(build_fstr(i), x=x, width=width), (x+' ')*i)
+ # self.assertEqual(eval(build_fstr(i)), (x+' ')*i)
self.assertEqual(
cy_eval('[' + ', '.join(build_fstr(i) for i in range(250, 260)) + ']', x=x, width=width),
[(x+' ')*i for i in range(250, 260)],
)
# Test concatenating 2 largs fstrings.
+ # self.assertEqual(eval(build_fstr(255)*256), (x+' ')*(255*256))
self.assertEqual(cy_eval(build_fstr(255)*3, x=x, width=width), (x+' ')*(255*3)) # CPython uses 255*256
s = build_fstr(253, '{x:{width}} ')
+ # self.assertEqual(eval(s), (x+' ')*254)
self.assertEqual(cy_eval(s, x=x, width=width), (x+' ')*254)
# Test lots of expressions and constants, concatenated.
s = "f'{1}' 'x' 'y'" * 1024
+ # self.assertEqual(eval(s), '1xy' * 1024)
self.assertEqual(cy_eval(s, x=x, width=width), '1xy' * 1024)
def test_format_specifier_expressions(self):
@@ -293,7 +562,7 @@ f'{a * x()}'"""
# This looks like a nested format spec.
])
- self.assertAllRaise(SyntaxError, "invalid syntax",
+ self.assertAllRaise(SyntaxError, "f-string: invalid syntax",
[# Invalid syntax inside a nested spec.
"f'{4:{/5}}'",
])
@@ -357,7 +626,7 @@ f'{a * x()}'"""
])
# Different error message is raised for other whitespace characters.
- self.assertAllRaise(SyntaxError, 'invalid character in identifier',
+ self.assertAllRaise(SyntaxError, r"invalid non-printable character U\+00A0",
["f'''{\xa0}'''",
#"\xa0",
])
@@ -369,12 +638,12 @@ f'{a * x()}'"""
# are added around it. But we shouldn't go from an invalid
# expression to a valid one. The added parens are just
# supposed to allow whitespace (including newlines).
- self.assertAllRaise(SyntaxError, 'invalid syntax',
+ self.assertAllRaise(SyntaxError, 'f-string: invalid syntax',
["f'{,}'",
"f'{,}'", # this is (,), which is an error
])
- self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
+ self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
["f'{3)+(4}'",
])
@@ -488,7 +757,7 @@ f'{a * x()}'"""
# lambda doesn't work without parens, because the colon
# makes the parser think it's a format_spec
- self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
+ self.assertAllRaise(SyntaxError, 'f-string: invalid syntax',
["f'{lambda x:x}'",
])
@@ -497,9 +766,11 @@ f'{a * x()}'"""
# a function into a generator
def fn(y):
f'y:{yield y*2}'
+ f'{yield}'
g = fn(4)
self.assertEqual(next(g), 8)
+ self.assertEqual(next(g), None)
def test_yield_send(self):
def fn(x):
@@ -616,8 +887,7 @@ f'{a * x()}'"""
self.assertEqual(f'{f"{y}"*3}', '555')
def test_invalid_string_prefixes(self):
- self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
- ["fu''",
+ single_quote_cases = ["fu''",
"uf''",
"Fu''",
"fU''",
@@ -638,8 +908,10 @@ f'{a * x()}'"""
"bf''",
"bF''",
"Bf''",
- "BF''",
- ])
+ "BF''",]
+ double_quote_cases = [case.replace("'", '"') for case in single_quote_cases]
+ self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
+ single_quote_cases + double_quote_cases)
def test_leading_trailing_spaces(self):
self.assertEqual(f'{ 3}', '3')
@@ -662,6 +934,12 @@ f'{a * x()}'"""
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 ')
@@ -801,12 +1079,6 @@ f'{a * x()}'"""
self.assertEqual('{d[a]}'.format(d=d), 'string')
self.assertEqual('{d[0]}'.format(d=d), 'integer')
- def test_invalid_expressions(self):
- self.assertAllRaise(SyntaxError, 'invalid syntax',
- [r"f'{a[4)}'",
- r"f'{a(4]}'",
- ])
-
def test_errors(self):
# see issue 26287
exc = ValueError if sys.version_info < (3, 4) else TypeError
@@ -819,6 +1091,16 @@ f'{a * x()}'"""
r"f'{1000:j}'",
])
+ def __test_filename_in_syntaxerror(self):
+ # see issue 38964
+ with temp_cwd() as cwd:
+ file_path = os.path.join(cwd, 't.py')
+ with open(file_path, 'w') as f:
+ f.write('f"{a b}"') # This generates a SyntaxError
+ _, _, stderr = assert_python_failure(file_path,
+ PYTHONIOENCODING='ascii')
+ self.assertIn(file_path.encode('ascii', 'backslashreplace'), stderr)
+
def test_loop(self):
for i in range(1000):
self.assertEqual(f'i:{i}', 'i:' + str(i))
@@ -840,5 +1122,129 @@ f'{a * x()}'"""
self.assertEqual(cy_eval('f"\\\n"'), '')
self.assertEqual(cy_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********')
+
+ self.assertRaises(SyntaxError, eval, "f'{C=]'")
+
+ # Make sure leading and following text works.
+ x = 'foo'
+ self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y')
+
+ # Make sure whitespace around the = works.
+ self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y')
+ self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y')
+ self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y')
+
+ # These next lines contains tabs. Backslash escapes don't
+ # work in f-strings.
+ # patchcheck doesn't like these tabs. So the only way to test
+ # this will be to dynamically created and exec the f-strings. But
+ # that's such a hassle I'll save it for another day. For now, convert
+ # the tabs to spaces just to shut up patchcheck.
+ #self.assertEqual(f'X{x =}Y', 'Xx\t='+repr(x)+'Y')
+ #self.assertEqual(f'X{x = }Y', 'Xx\t=\t'+repr(x)+'Y')
+ """
+
+ 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)
+ """
+
+ def test_invalid_syntax_error_message(self):
+ # with self.assertRaisesRegex(SyntaxError, "f-string: invalid syntax"):
+ # compile("f'{a $ b}'", "?", "exec")
+ self.assertAllRaise(CompileError, "f-string: invalid syntax", ["f'{a $ b}'"])
+
+
if __name__ == '__main__':
unittest.main()