From 40a9ddad4e5597b0cd68adb168b57f4ffe7b7030 Mon Sep 17 00:00:00 2001 From: Myeongseon Choi Date: Thu, 5 Dec 2024 15:04:56 +0900 Subject: [PATCH] update test_fstring.py from cpython 3.12.7 add expectedFailure to tag what should rustpython do add comment for some syntaxerror which make test run broken --- Lib/test/test_fstring.py | 1748 +++++++++++++++++++++++++------------- 1 file changed, 1155 insertions(+), 593 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index b1cd9b1604..b3a38dd652 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -8,15 +8,18 @@ # Unicode identifiers in tests is allowed by PEP 3131. import ast +import datetime import os import re import types import decimal import unittest +import warnings +from test import support from test.support.os_helper import temp_cwd -from test.support.script_helper import assert_python_failure +from test.support.script_helper import assert_python_failure, assert_python_ok -a_global = 'global variable' +a_global = "global variable" # You could argue that I'm too strict in looking for specific error # values with assertRaisesRegex, but without it it's way too easy to @@ -25,6 +28,7 @@ a_global = 'global variable' # worthwhile tradeoff. When I switched to this method, I found many # examples where I wasn't testing what I thought I was. + class TestCase(unittest.TestCase): def assertAllRaise(self, exception_type, regex, error_strings): for str in error_strings: @@ -36,43 +40,45 @@ class TestCase(unittest.TestCase): # Make sure __format__ is looked up on the type, not the instance. class X: def __format__(self, spec): - return 'class' + return "class" x = X() # Add a bound __format__ method to the 'y' instance, but not # the 'x' instance. y = X() - y.__format__ = types.MethodType(lambda self, spec: 'instance', y) + y.__format__ = types.MethodType(lambda self, spec: "instance", y) - self.assertEqual(f'{y}', format(y)) - self.assertEqual(f'{y}', 'class') + self.assertEqual(f"{y}", format(y)) + self.assertEqual(f"{y}", "class") self.assertEqual(format(x), format(y)) # __format__ is not called this way, but still make sure it # returns what we expect (so we can make sure we're bypassing # it). - self.assertEqual(x.__format__(''), 'class') - self.assertEqual(y.__format__(''), 'instance') + self.assertEqual(x.__format__(""), "class") + self.assertEqual(y.__format__(""), "instance") # This is how __format__ is actually called. - self.assertEqual(type(x).__format__(x, ''), 'class') - self.assertEqual(type(y).__format__(y, ''), 'class') + self.assertEqual(type(x).__format__(x, ""), "class") + self.assertEqual(type(y).__format__(y, ""), "class") def test_ast(self): # Inspired by http://bugs.python.org/issue24975 class X: def __init__(self): self.called = False + def __call__(self): self.called = True return 4 + x = X() expr = """ a = 10 f'{a * x()}'""" t = ast.parse(expr) - c = compile(t, '', 'exec') + c = compile(t, "", "exec") # Make sure x was not called. self.assertFalse(x.called) @@ -278,7 +284,6 @@ f'{a * x()} {a * x()} {a * x()}' self.assertEqual(binop.right.col_offset, 27) def test_ast_numbers_fstring_with_formatting(self): - t = ast.parse('f"Here is that pesky {xxx:.3f} again"') self.assertEqual(len(t.body), 1) self.assertEqual(t.body[0].lineno, 1) @@ -300,6 +305,8 @@ f'{a * x()} {a * x()} {a * x()}' self.assertEqual(name.col_offset, 22) self.assertEqual(name.end_col_offset, 25) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_ast_line_numbers_multiline_fstring(self): # See bpo-30465 for details. expr = """ @@ -329,13 +336,13 @@ non-important content 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[1].lineno, 4) + self.assertEqual(t.body[1].value.values[2].lineno, 6) 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) + self.assertEqual(t.body[1].value.values[0].col_offset, 4) + self.assertEqual(t.body[1].value.values[1].col_offset, 2) + self.assertEqual(t.body[1].value.values[2].col_offset, 11) # NOTE: the following lineno information and col_offset is correct for # expressions within FormattedValues. binop = t.body[1].value.values[1].value @@ -366,19 +373,21 @@ a = f''' self.assertEqual(t.body[0].lineno, 2) self.assertEqual(t.body[0].value.lineno, 2) self.assertEqual(t.body[0].value.values[0].lineno, 2) - self.assertEqual(t.body[0].value.values[1].lineno, 2) - self.assertEqual(t.body[0].value.values[2].lineno, 2) + self.assertEqual(t.body[0].value.values[1].lineno, 3) + self.assertEqual(t.body[0].value.values[2].lineno, 3) self.assertEqual(t.body[0].col_offset, 0) self.assertEqual(t.body[0].value.col_offset, 4) - self.assertEqual(t.body[0].value.values[0].col_offset, 4) - self.assertEqual(t.body[0].value.values[1].col_offset, 4) - self.assertEqual(t.body[0].value.values[2].col_offset, 4) + self.assertEqual(t.body[0].value.values[0].col_offset, 8) + self.assertEqual(t.body[0].value.values[1].col_offset, 10) + self.assertEqual(t.body[0].value.values[2].col_offset, 17) # Check {blech} self.assertEqual(t.body[0].value.values[1].value.lineno, 3) self.assertEqual(t.body[0].value.values[1].value.end_lineno, 3) self.assertEqual(t.body[0].value.values[1].value.col_offset, 11) self.assertEqual(t.body[0].value.values[1].value.end_col_offset, 16) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_ast_line_numbers_with_parentheses(self): expr = """ x = ( @@ -387,6 +396,20 @@ x = ( t = ast.parse(expr) self.assertEqual(type(t), ast.Module) self.assertEqual(len(t.body), 1) + # check the joinedstr location + joinedstr = t.body[0].value + self.assertEqual(type(joinedstr), ast.JoinedStr) + self.assertEqual(joinedstr.lineno, 3) + self.assertEqual(joinedstr.end_lineno, 3) + self.assertEqual(joinedstr.col_offset, 4) + self.assertEqual(joinedstr.end_col_offset, 17) + # check the formatted value location + fv = t.body[0].value.values[1] + self.assertEqual(type(fv), ast.FormattedValue) + self.assertEqual(fv.lineno, 3) + self.assertEqual(fv.end_lineno, 3) + self.assertEqual(fv.col_offset, 7) + self.assertEqual(fv.end_col_offset, 16) # check the test(t) location call = t.body[0].value.values[1].value self.assertEqual(type(call), ast.Call) @@ -397,6 +420,38 @@ x = ( expr = """ x = ( + u'wat', + u"wat", + b'wat', + b"wat", + f'wat', + f"wat", +) + +y = ( + u'''wat''', + u\"\"\"wat\"\"\", + b'''wat''', + b\"\"\"wat\"\"\", + f'''wat''', + f\"\"\"wat\"\"\", +) + """ + t = ast.parse(expr) + self.assertEqual(type(t), ast.Module) + self.assertEqual(len(t.body), 2) + x, y = t.body + + # Check the single quoted string offsets first. + offsets = [(elt.col_offset, elt.end_col_offset) for elt in x.value.elts] + self.assertTrue(all(offset == (4, 10) for offset in offsets)) + + # Check the triple quoted string offsets. + offsets = [(elt.col_offset, elt.end_col_offset) for elt in y.value.elts] + self.assertTrue(all(offset == (4, 14) for offset in offsets)) + + expr = """ +x = ( 'PERL_MM_OPT', ( f'wat' f'some_string={f(x)} ' @@ -415,9 +470,9 @@ x = ( # check the first wat self.assertEqual(type(wat1), ast.Constant) self.assertEqual(wat1.lineno, 4) - self.assertEqual(wat1.end_lineno, 6) - self.assertEqual(wat1.col_offset, 12) - self.assertEqual(wat1.end_col_offset, 18) + self.assertEqual(wat1.end_lineno, 5) + self.assertEqual(wat1.col_offset, 14) + self.assertEqual(wat1.end_col_offset, 26) # check the call call = middle.value self.assertEqual(type(call), ast.Call) @@ -427,427 +482,673 @@ x = ( self.assertEqual(call.end_col_offset, 31) # check the second wat self.assertEqual(type(wat2), ast.Constant) - self.assertEqual(wat2.lineno, 4) + self.assertEqual(wat2.lineno, 5) self.assertEqual(wat2.end_lineno, 6) - self.assertEqual(wat2.col_offset, 12) - self.assertEqual(wat2.end_col_offset, 18) + self.assertEqual(wat2.col_offset, 32) + # wat ends at the offset 17, but the whole f-string + # ends at the offset 18 (since the quote is part of the + # f-string but not the wat string) + self.assertEqual(wat2.end_col_offset, 17) + self.assertEqual(fstring.end_col_offset, 18) + + def test_ast_fstring_empty_format_spec(self): + expr = "f'{expr:}'" + + mod = ast.parse(expr) + self.assertEqual(type(mod), ast.Module) + self.assertEqual(len(mod.body), 1) + + fstring = mod.body[0].value + self.assertEqual(type(fstring), ast.JoinedStr) + self.assertEqual(len(fstring.values), 1) + + fv = fstring.values[0] + self.assertEqual(type(fv), ast.FormattedValue) + + format_spec = fv.format_spec + self.assertEqual(type(format_spec), ast.JoinedStr) + self.assertEqual(len(format_spec.values), 0) # TODO: RUSTPYTHON @unittest.expectedFailure def test_docstring(self): def f(): - f'''Not a docstring''' + f"""Not a docstring""" + self.assertIsNone(f.__doc__) + def g(): - '''Not a docstring''' \ - f'' + """Not a docstring""" f"" + self.assertIsNone(g.__doc__) def test_literal_eval(self): - with self.assertRaisesRegex(ValueError, 'malformed node or string'): + with self.assertRaisesRegex(ValueError, "malformed node or string"): ast.literal_eval("f'x'") def test_ast_compile_time_concat(self): - x = [''] + x = [""] expr = """x[0] = 'foo' f'{3}'""" t = ast.parse(expr) - c = compile(t, '', 'exec') + c = compile(t, "", "exec") exec(c) - self.assertEqual(x[0], 'foo3') + self.assertEqual(x[0], "foo3") def test_compile_time_concat_errors(self): - self.assertAllRaise(SyntaxError, - 'cannot mix bytes and nonbytes literals', - [r"""f'' b''""", - r"""b'' f''""", - ]) + self.assertAllRaise( + SyntaxError, + "cannot mix bytes and nonbytes literals", + [ + r"""f'' b''""", + r"""b'' f''""", + ], + ) def test_literal(self): - self.assertEqual(f'', '') - self.assertEqual(f'a', 'a') - self.assertEqual(f' ', ' ') + self.assertEqual(f"", "") + self.assertEqual(f"a", "a") + self.assertEqual(f" ", " ") def test_unterminated_string(self): - self.assertAllRaise(SyntaxError, 'f-string: unterminated string', - [r"""f'{"x'""", - r"""f'{"x}'""", - r"""f'{("x'""", - r"""f'{("x}'""", - ]) + self.assertAllRaise( + SyntaxError, + "unterminated string", + [ + r"""f'{"x'""", + r"""f'{"x}'""", + r"""f'{("x'""", + r"""f'{("x}'""", + ], + ) + @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") def test_mismatched_parens(self): - 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 + "}'") + self.assertAllRaise( + SyntaxError, + r"closing parenthesis '\}' " r"does not match opening parenthesis '\('", + [ + "f'{((}'", + ], + ) + self.assertAllRaise( + SyntaxError, + r"closing parenthesis '\)' " r"does not match opening parenthesis '\['", + [ + "f'{a[4)}'", + ], + ) + self.assertAllRaise( + SyntaxError, + r"closing parenthesis '\]' " r"does not match opening parenthesis '\('", + [ + "f'{a(4]}'", + ], + ) + self.assertAllRaise( + SyntaxError, + r"closing parenthesis '\}' " r"does not match opening parenthesis '\['", + [ + "f'{a[4}'", + ], + ) + self.assertAllRaise( + SyntaxError, + r"closing parenthesis '\}' " r"does not match opening parenthesis '\('", + [ + "f'{a(4}'", + ], + ) + self.assertRaises(SyntaxError, eval, "f'{" + "(" * 500 + "}'") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") + def test_fstring_nested_too_deeply(self): + self.assertAllRaise( + SyntaxError, + "f-string: expressions nested too deeply", + ['f"{1+2:{1+2:{1+1:{1}}}}"'], + ) + + def create_nested_fstring(n): + if n == 0: + return "1+1" + prev = create_nested_fstring(n - 1) + return f'f"{{{prev}}}"' + + self.assertAllRaise( + SyntaxError, "too many nested f-strings", [create_nested_fstring(160)] + ) + + def test_syntax_error_in_nested_fstring(self): + # See gh-104016 for more information on this crash + self.assertAllRaise( + SyntaxError, "invalid syntax", ['f"{1 1:' + ('{f"1:' * 199)] + ) def test_double_braces(self): - self.assertEqual(f'{{', '{') - self.assertEqual(f'a{{', 'a{') - self.assertEqual(f'{{b', '{b') - self.assertEqual(f'a{{b', 'a{b') - self.assertEqual(f'}}', '}') - self.assertEqual(f'a}}', 'a}') - self.assertEqual(f'}}b', '}b') - self.assertEqual(f'a}}b', 'a}b') - self.assertEqual(f'{{}}', '{}') - self.assertEqual(f'a{{}}', 'a{}') - self.assertEqual(f'{{b}}', '{b}') - self.assertEqual(f'{{}}c', '{}c') - self.assertEqual(f'a{{b}}', 'a{b}') - self.assertEqual(f'a{{}}c', 'a{}c') - self.assertEqual(f'{{b}}c', '{b}c') - self.assertEqual(f'a{{b}}c', 'a{b}c') + self.assertEqual(f"{{", "{") + self.assertEqual(f"a{{", "a{") + self.assertEqual(f"{{b", "{b") + self.assertEqual(f"a{{b", "a{b") + self.assertEqual(f"}}", "}") + self.assertEqual(f"a}}", "a}") + self.assertEqual(f"}}b", "}b") + self.assertEqual(f"a}}b", "a}b") + self.assertEqual(f"{{}}", "{}") + self.assertEqual(f"a{{}}", "a{}") + self.assertEqual(f"{{b}}", "{b}") + self.assertEqual(f"{{}}c", "{}c") + self.assertEqual(f"a{{b}}", "a{b}") + self.assertEqual(f"a{{}}c", "a{}c") + self.assertEqual(f"{{b}}c", "{b}c") + self.assertEqual(f"a{{b}}c", "a{b}c") - self.assertEqual(f'{{{10}', '{10') - self.assertEqual(f'}}{10}', '}10') - self.assertEqual(f'}}{{{10}', '}{10') - self.assertEqual(f'}}a{{{10}', '}a{10') + self.assertEqual(f"{{{10}", "{10") + self.assertEqual(f"}}{10}", "}10") + self.assertEqual(f"}}{{{10}", "}{10") + self.assertEqual(f"}}a{{{10}", "}a{10") - self.assertEqual(f'{10}{{', '10{') - self.assertEqual(f'{10}}}', '10}') - self.assertEqual(f'{10}}}{{', '10}{') - self.assertEqual(f'{10}}}a{{' '}', '10}a{}') + self.assertEqual(f"{10}{{", "10{") + self.assertEqual(f"{10}}}", "10}") + self.assertEqual(f"{10}}}{{", "10}{") + self.assertEqual(f"{10}}}a{{" "}", "10}a{}") # Inside of strings, don't interpret doubled brackets. - self.assertEqual(f'{"{{}}"}', '{{}}') + self.assertEqual(f'{"{{}}"}', "{{}}") - self.assertAllRaise(TypeError, 'unhashable type', - ["f'{ {{}} }'", # dict in a set - ]) + self.assertAllRaise( + TypeError, + "unhashable type", + [ + "f'{ {{}} }'", # dict in a set + ], + ) def test_compile_time_concat(self): - x = 'def' - self.assertEqual('abc' f'## {x}ghi', 'abc## defghi') - self.assertEqual('abc' f'{x}' 'ghi', 'abcdefghi') - self.assertEqual('abc' f'{x}' 'gh' f'i{x:4}', 'abcdefghidef ') - self.assertEqual('{x}' f'{x}', '{x}def') - self.assertEqual('{x' f'{x}', '{xdef') - self.assertEqual('{x}' f'{x}', '{x}def') - self.assertEqual('{{x}}' f'{x}', '{{x}}def') - self.assertEqual('{{x' f'{x}', '{{xdef') - self.assertEqual('x}}' f'{x}', 'x}}def') - self.assertEqual(f'{x}' 'x}}', 'defx}}') - self.assertEqual(f'{x}' '', 'def') - self.assertEqual('' f'{x}' '', 'def') - self.assertEqual('' f'{x}', 'def') - self.assertEqual(f'{x}' '2', 'def2') - self.assertEqual('1' f'{x}' '2', '1def2') - self.assertEqual('1' f'{x}', '1def') - self.assertEqual(f'{x}' f'-{x}', 'def-def') - self.assertEqual('' f'', '') - self.assertEqual('' f'' '', '') - self.assertEqual('' f'' '' f'', '') - self.assertEqual(f'', '') - self.assertEqual(f'' '', '') - self.assertEqual(f'' '' f'', '') - self.assertEqual(f'' '' f'' '', '') + x = "def" + self.assertEqual("abc" f"## {x}ghi", "abc## defghi") + self.assertEqual("abc" f"{x}" "ghi", "abcdefghi") + self.assertEqual("abc" f"{x}" "gh" f"i{x:4}", "abcdefghidef ") + self.assertEqual("{x}" f"{x}", "{x}def") + self.assertEqual("{x" f"{x}", "{xdef") + self.assertEqual("{x}" f"{x}", "{x}def") + self.assertEqual("{{x}}" f"{x}", "{{x}}def") + self.assertEqual("{{x" f"{x}", "{{xdef") + self.assertEqual("x}}" f"{x}", "x}}def") + self.assertEqual(f"{x}" "x}}", "defx}}") + self.assertEqual(f"{x}" "", "def") + self.assertEqual("" f"{x}" "", "def") + self.assertEqual("" f"{x}", "def") + self.assertEqual(f"{x}" "2", "def2") + self.assertEqual("1" f"{x}" "2", "1def2") + self.assertEqual("1" f"{x}", "1def") + self.assertEqual(f"{x}" f"-{x}", "def-def") + self.assertEqual("" f"", "") + self.assertEqual("" f"" "", "") + self.assertEqual("" f"" "" f"", "") + self.assertEqual(f"", "") + self.assertEqual(f"" "", "") + self.assertEqual(f"" "" f"", "") + self.assertEqual(f"" "" f"" "", "") - self.assertAllRaise(SyntaxError, "f-string: expecting '}'", - ["f'{3' f'}'", # can't concat to get a valid f-string - ]) + # This is not really [f'{'] + [f'}'] since we treat the inside + # of braces as a purely new context, so it is actually f'{ and + # then eval(' f') (a valid expression) and then }' which would + # constitute a valid f-string. + # TODO: RUSTPYTHON SyntaxError + # self.assertEqual(f'{' f'}', " f") + + self.assertAllRaise( + SyntaxError, + "expecting '}'", + [ + '''f'{3' f"}"''', # can't concat to get a valid f-string + ], + ) # TODO: RUSTPYTHON @unittest.expectedFailure def test_comments(self): # These aren't comments, since they're in strings. - d = {'#': 'hash'} - self.assertEqual(f'{"#"}', '#') - self.assertEqual(f'{d["#"]}', 'hash') + d = {"#": "hash"} + self.assertEqual(f'{"#"}', "#") + self.assertEqual(f'{d["#"]}', "hash") - self.assertAllRaise(SyntaxError, "f-string expression part cannot include '#'", - ["f'{1#}'", # error because the expression becomes "(1#)" - "f'{3(#)}'", - "f'{#}'", - ]) - self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'", - ["f'{)#}'", # When wrapped in parens, this becomes - # '()#)'. Make sure that doesn't compile. - ]) + self.assertAllRaise( + SyntaxError, + "'{' was never closed", + [ + "f'{1#}'", # error because everything after '#' is a comment + "f'{#}'", + "f'one: {1#}'", + "f'{1# one} {2 this is a comment still#}'", + ], + ) + self.assertAllRaise( + SyntaxError, + r"f-string: unmatched '\)'", + [ + "f'{)#}'", # When wrapped in parens, this becomes + # '()#)'. Make sure that doesn't compile. + ], + ) + self.assertEqual( + f"""A complex trick: { +2 # two +}""", + "A complex trick: 2", + ) + self.assertEqual( + f""" +{ +40 # forty ++ # plus +2 # two +}""", + "\n42", + ) + self.assertEqual( + f""" +{ +40 # forty ++ # plus +2 # two +}""", + "\n42", + ) +# TODO: RUSTPYTHON SyntaxError +# self.assertEqual( +# f""" +# # this is not a comment +# { # the following operation it's +# 3 # this is a number +# * 2}""", +# "\n# this is not a comment\n6", +# ) + self.assertEqual( + f""" +{# f'a {comment}' +86 # constant +# nothing more +}""", + "\n86", + ) + + self.assertAllRaise( + SyntaxError, + r"f-string: valid expression required before '}'", + [ + """f''' +{ +# only a comment +}''' +""", # this is equivalent to f'{}' + ], + ) def test_many_expressions(self): # Create a string with many expressions in it. Note that # because we have a space in here as a literal, we're actually # going to use twice as many ast nodes: one for each literal # plus one for each expression. - def build_fstr(n, extra=''): - return "f'" + ('{x} ' * n) + extra + "'" + def build_fstr(n, extra=""): + return "f'" + ("{x} " * n) + extra + "'" - x = 'X' + x = "X" width = 1 # Test around 256. for i in range(250, 260): - self.assertEqual(eval(build_fstr(i)), (x+' ')*i) + self.assertEqual(eval(build_fstr(i)), (x + " ") * i) # Test concatenating 2 largs fstrings. - self.assertEqual(eval(build_fstr(255)*256), (x+' ')*(255*256)) + self.assertEqual(eval(build_fstr(255) * 256), (x + " ") * (255 * 256)) - s = build_fstr(253, '{x:{width}} ') - self.assertEqual(eval(s), (x+' ')*254) + s = build_fstr(253, "{x:{width}} ") + self.assertEqual(eval(s), (x + " ") * 254) # Test lots of expressions and constants, concatenated. s = "f'{1}' 'x' 'y'" * 1024 - self.assertEqual(eval(s), '1xy' * 1024) + self.assertEqual(eval(s), "1xy" * 1024) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_format_specifier_expressions(self): width = 10 precision = 4 - value = decimal.Decimal('12.34567') - self.assertEqual(f'result: {value:{width}.{precision}}', 'result: 12.35') - self.assertEqual(f'result: {value:{width!r}.{precision}}', 'result: 12.35') - self.assertEqual(f'result: {value:{width:0}.{precision:1}}', 'result: 12.35') - self.assertEqual(f'result: {value:{1}{0:0}.{precision:1}}', 'result: 12.35') - self.assertEqual(f'result: {value:{ 1}{ 0:0}.{ precision:1}}', 'result: 12.35') - self.assertEqual(f'{10:#{1}0x}', ' 0xa') - self.assertEqual(f'{10:{"#"}1{0}{"x"}}', ' 0xa') - self.assertEqual(f'{-10:-{"#"}1{0}x}', ' -0xa') - self.assertEqual(f'{-10:{"-"}#{1}0{"x"}}', ' -0xa') - self.assertEqual(f'{10:#{3 != {4:5} and width}x}', ' 0xa') + value = decimal.Decimal("12.34567") + self.assertEqual(f"result: {value:{width}.{precision}}", "result: 12.35") + self.assertEqual(f"result: {value:{width!r}.{precision}}", "result: 12.35") + self.assertEqual( + f"result: {value:{width:0}.{precision:1}}", "result: 12.35" + ) + self.assertEqual( + f"result: {value:{1}{0:0}.{precision:1}}", "result: 12.35" + ) + self.assertEqual( + f"result: {value:{ 1}{ 0:0}.{ precision:1}}", "result: 12.35" + ) + self.assertEqual(f"{10:#{1}0x}", " 0xa") + self.assertEqual(f'{10:{"#"}1{0}{"x"}}', " 0xa") + self.assertEqual(f'{-10:-{"#"}1{0}x}', " -0xa") + self.assertEqual(f'{-10:{"-"}#{1}0{"x"}}', " -0xa") + self.assertEqual(f"{10:#{3 != {4:5} and width}x}", " 0xa") - self.assertAllRaise(SyntaxError, "f-string: expecting '}'", - ["""f'{"s"!r{":10"}}'""", + # TODO: RUSTPYTHON SyntaxError + # self.assertEqual( + # f"result: {value:{width:{0}}.{precision:1}}", "result: 12.35" + # ) - # This looks like a nested format spec. - ]) - self.assertAllRaise(SyntaxError, "f-string: invalid syntax", - [# Invalid syntax inside a nested spec. - "f'{4:{/5}}'", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting ':' or '}'", + [ + """f'{"s"!r{":10"}}'""", + # This looks like a nested format spec. + ], + ) - self.assertAllRaise(SyntaxError, "f-string: expressions nested too deeply", - [# Can't nest format specifiers. - "f'result: {value:{width:{0}}.{precision:1}}'", - ]) - self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character', - [# No expansion inside conversion or for - # the : or ! itself. - """f'{"s"!{"r"}}'""", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting a valid expression after '{'", + [ # Invalid syntax inside a nested spec. + "f'{4:{/5}}'", + ], + ) + + self.assertAllRaise( + SyntaxError, + "f-string: invalid conversion character", + [ # No expansion inside conversion or for + # the : or ! itself. + """f'{"s"!{"r"}}'""", + ], + ) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_custom_format_specifier(self): + class CustomFormat: + def __format__(self, format_spec): + return format_spec + + self.assertEqual(f"{CustomFormat():\n}", "\n") + self.assertEqual(f"{CustomFormat():\u2603}", "☃") + with self.assertWarns(SyntaxWarning): + exec(r'f"{F():¯\_(ツ)_/¯}"', {"F": CustomFormat}) def test_side_effect_order(self): class X: def __init__(self): self.i = 0 + def __format__(self, spec): self.i += 1 return str(self.i) x = X() - self.assertEqual(f'{x} {x}', '1 2') + self.assertEqual(f"{x} {x}", "1 2") # TODO: RUSTPYTHON @unittest.expectedFailure def test_missing_expression(self): - self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed', - ["f'{}'", - "f'{ }'" - "f' {} '", - "f'{10:{ }}'", - "f' { } '", + self.assertAllRaise( + SyntaxError, + "f-string: valid expression required before '}'", + [ + "f'{}'", + "f'{ }'" "f' {} '", + "f'{10:{ }}'", + "f' { } '", + # The Python parser ignores also the following + # whitespace characters in additional to a space. + "f'''{\t\f\r\n}'''", + ], + ) - # The Python parser ignores also the following - # whitespace characters in additional to a space. - "f'''{\t\f\r\n}'''", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: valid expression required before '!'", + [ + "f'{!r}'", + "f'{ !r}'", + "f'{!}'", + "f'''{\t\f\r\n!a}'''", + # Catch empty expression before the + # missing closing brace. + "f'{!'", + "f'{!s:'", + # Catch empty expression before the + # invalid conversion. + "f'{!x}'", + "f'{ !xr}'", + "f'{!x:}'", + "f'{!x:a}'", + "f'{ !xr:}'", + "f'{ !xr:a}'", + ], + ) - # Different error messeges are raised when a specfier ('!', ':' or '=') is used after an empty expression - self.assertAllRaise(SyntaxError, "f-string: expression required before '!'", - ["f'{!r}'", - "f'{ !r}'", - "f'{!}'", - "f'''{\t\f\r\n!a}'''", + self.assertAllRaise( + SyntaxError, + "f-string: valid expression required before ':'", + [ + "f'{:}'", + "f'{ :!}'", + "f'{:2}'", + "f'''{\t\f\r\n:a}'''", + "f'{:'", + "F'{[F'{:'}[F'{:'}]]]", + ], + ) - # Catch empty expression before the - # missing closing brace. - "f'{!'", - "f'{!s:'", - - # Catch empty expression before the - # invalid conversion. - "f'{!x}'", - "f'{ !xr}'", - "f'{!x:}'", - "f'{!x:a}'", - "f'{ !xr:}'", - "f'{ !xr:a}'", - ]) - - self.assertAllRaise(SyntaxError, "f-string: expression required before ':'", - ["f'{:}'", - "f'{ :!}'", - "f'{:2}'", - "f'''{\t\f\r\n:a}'''", - "f'{:'", - ]) - - self.assertAllRaise(SyntaxError, "f-string: expression required before '='", - ["f'{=}'", - "f'{ =}'", - "f'{ =:}'", - "f'{ =!}'", - "f'''{\t\f\r\n=}'''", - "f'{='", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: valid expression required before '='", + [ + "f'{=}'", + "f'{ =}'", + "f'{ =:}'", + "f'{ =!}'", + "f'''{\t\f\r\n=}'''", + "f'{='", + ], + ) # Different error message is raised for other whitespace characters. - self.assertAllRaise(SyntaxError, r"invalid non-printable character U\+00A0", - ["f'''{\xa0}'''", - "\xa0", - ]) + self.assertAllRaise( + SyntaxError, + r"invalid non-printable character U\+00A0", + [ + "f'''{\xa0}'''", + "\xa0", + ], + ) # TODO: RUSTPYTHON @unittest.expectedFailure def test_parens_in_expressions(self): - self.assertEqual(f'{3,}', '(3,)') + self.assertEqual(f"{3,}", "(3,)") - # Add these because when an expression is evaluated, parens - # 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, 'f-string: invalid syntax', - ["f'{,}'", - "f'{,}'", # this is (,), which is an error - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting a valid expression after '{'", + [ + "f'{,}'", + ], + ) - self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'", - ["f'{3)+(4}'", - ]) + self.assertAllRaise( + SyntaxError, + r"f-string: unmatched '\)'", + [ + "f'{3)+(4}'", + ], + ) - self.assertAllRaise(SyntaxError, 'unterminated string literal', - ["f'{\n}'", - ]) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_newlines_before_syntax_error(self): - self.assertAllRaise(SyntaxError, "invalid syntax", - ["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting a valid expression after '{'", + ["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"], + ) # TODO: RUSTPYTHON @unittest.expectedFailure def test_backslashes_in_string_part(self): - self.assertEqual(f'\t', '\t') - self.assertEqual(r'\t', '\\t') - self.assertEqual(rf'\t', '\\t') - self.assertEqual(f'{2}\t', '2\t') - self.assertEqual(f'{2}\t{3}', '2\t3') - self.assertEqual(f'\t{3}', '\t3') + self.assertEqual(f"\t", "\t") + self.assertEqual(r"\t", "\\t") + self.assertEqual(rf"\t", "\\t") + self.assertEqual(f"{2}\t", "2\t") + self.assertEqual(f"{2}\t{3}", "2\t3") + self.assertEqual(f"\t{3}", "\t3") - self.assertEqual(f'\u0394', '\u0394') - self.assertEqual(r'\u0394', '\\u0394') - self.assertEqual(rf'\u0394', '\\u0394') - self.assertEqual(f'{2}\u0394', '2\u0394') - self.assertEqual(f'{2}\u0394{3}', '2\u03943') - self.assertEqual(f'\u0394{3}', '\u03943') + self.assertEqual(f"\u0394", "\u0394") + self.assertEqual(r"\u0394", "\\u0394") + self.assertEqual(rf"\u0394", "\\u0394") + self.assertEqual(f"{2}\u0394", "2\u0394") + self.assertEqual(f"{2}\u0394{3}", "2\u03943") + self.assertEqual(f"\u0394{3}", "\u03943") - self.assertEqual(f'\U00000394', '\u0394') - self.assertEqual(r'\U00000394', '\\U00000394') - self.assertEqual(rf'\U00000394', '\\U00000394') - self.assertEqual(f'{2}\U00000394', '2\u0394') - self.assertEqual(f'{2}\U00000394{3}', '2\u03943') - self.assertEqual(f'\U00000394{3}', '\u03943') + self.assertEqual(f"\U00000394", "\u0394") + self.assertEqual(r"\U00000394", "\\U00000394") + self.assertEqual(rf"\U00000394", "\\U00000394") + self.assertEqual(f"{2}\U00000394", "2\u0394") + self.assertEqual(f"{2}\U00000394{3}", "2\u03943") + self.assertEqual(f"\U00000394{3}", "\u03943") - self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}', '\u0394') - self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}', '2\u0394') - self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}', '2\u03943') - self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}{3}', '\u03943') - self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}', '2\u0394') - self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}3', '2\u03943') - self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}3', '\u03943') + self.assertEqual(f"\N{GREEK CAPITAL LETTER DELTA}", "\u0394") + self.assertEqual(f"{2}\N{GREEK CAPITAL LETTER DELTA}", "2\u0394") + self.assertEqual(f"{2}\N{GREEK CAPITAL LETTER DELTA}{3}", "2\u03943") + self.assertEqual(f"\N{GREEK CAPITAL LETTER DELTA}{3}", "\u03943") + self.assertEqual(f"2\N{GREEK CAPITAL LETTER DELTA}", "2\u0394") + self.assertEqual(f"2\N{GREEK CAPITAL LETTER DELTA}3", "2\u03943") + self.assertEqual(f"\N{GREEK CAPITAL LETTER DELTA}3", "\u03943") - self.assertEqual(f'\x20', ' ') - self.assertEqual(r'\x20', '\\x20') - self.assertEqual(rf'\x20', '\\x20') - self.assertEqual(f'{2}\x20', '2 ') - self.assertEqual(f'{2}\x20{3}', '2 3') - self.assertEqual(f'\x20{3}', ' 3') + self.assertEqual(f"\x20", " ") + self.assertEqual(r"\x20", "\\x20") + self.assertEqual(rf"\x20", "\\x20") + self.assertEqual(f"{2}\x20", "2 ") + self.assertEqual(f"{2}\x20{3}", "2 3") + self.assertEqual(f"\x20{3}", " 3") - self.assertEqual(f'2\x20', '2 ') - self.assertEqual(f'2\x203', '2 3') - self.assertEqual(f'\x203', ' 3') + self.assertEqual(f"2\x20", "2 ") + self.assertEqual(f"2\x203", "2 3") + self.assertEqual(f"\x203", " 3") - with self.assertWarns(DeprecationWarning): # invalid escape sequence + with self.assertWarns(SyntaxWarning): # invalid escape sequence value = eval(r"f'\{6*7}'") - self.assertEqual(value, '\\42') - self.assertEqual(f'\\{6*7}', '\\42') - self.assertEqual(fr'\{6*7}', '\\42') + self.assertEqual(value, "\\42") + with self.assertWarns(SyntaxWarning): # invalid escape sequence + value = eval(r"f'\g'") + self.assertEqual(value, "\\g") + self.assertEqual(f"\\{6*7}", "\\42") + self.assertEqual(rf"\{6*7}", "\\42") - AMPERSAND = 'spam' + AMPERSAND = "spam" # Get the right unicode character (&), or pick up local variable # depending on the number of backslashes. - self.assertEqual(f'\N{AMPERSAND}', '&') - self.assertEqual(f'\\N{AMPERSAND}', '\\Nspam') - self.assertEqual(fr'\N{AMPERSAND}', '\\Nspam') - self.assertEqual(f'\\\N{AMPERSAND}', '\\&') + self.assertEqual(f"\N{AMPERSAND}", "&") + self.assertEqual(f"\\N{AMPERSAND}", "\\Nspam") + self.assertEqual(rf"\N{AMPERSAND}", "\\Nspam") + self.assertEqual(f"\\\N{AMPERSAND}", "\\&") # TODO: RUSTPYTHON @unittest.expectedFailure def test_misformed_unicode_character_name(self): # These test are needed because unicode names are parsed # differently inside f-strings. - self.assertAllRaise(SyntaxError, r"\(unicode error\) 'unicodeescape' codec can't decode bytes in position .*: malformed \\N character escape", - [r"f'\N'", - r"f'\N '", - r"f'\N '", # See bpo-46503. - r"f'\N{'", - r"f'\N{GREEK CAPITAL LETTER DELTA'", - - # Here are the non-f-string versions, - # which should give the same errors. - r"'\N'", - r"'\N '", - r"'\N '", - r"'\N{'", - r"'\N{GREEK CAPITAL LETTER DELTA'", - ]) + self.assertAllRaise( + SyntaxError, + r"\(unicode error\) 'unicodeescape' codec can't decode bytes in position .*: malformed \\N character escape", + [ + r"f'\N'", + r"f'\N '", + r"f'\N '", # See bpo-46503. + r"f'\N{'", + r"f'\N{GREEK CAPITAL LETTER DELTA'", + # Here are the non-f-string versions, + # which should give the same errors. + r"'\N'", + r"'\N '", + r"'\N '", + r"'\N{'", + r"'\N{GREEK CAPITAL LETTER DELTA'", + ], + ) # TODO: RUSTPYTHON @unittest.expectedFailure - def test_no_backslashes_in_expression_part(self): - self.assertAllRaise(SyntaxError, 'f-string expression part cannot include a backslash', - [r"f'{\'a\'}'", - r"f'{\t3}'", - r"f'{\}'", - r"rf'{\'a\'}'", - r"rf'{\t3}'", - r"rf'{\}'", - r"""rf'{"\N{LEFT CURLY BRACKET}"}'""", - r"f'{\n}'", - ]) + def test_backslashes_in_expression_part(self): + # TODO: RUSTPYTHON SyntaxError + # self.assertEqual( + # f"{( + # 1 + + # 2 + # )}", + # "3", + # ) + + self.assertEqual("\N{LEFT CURLY BRACKET}", "{") + self.assertEqual(f'{"\N{LEFT CURLY BRACKET}"}', "{") + self.assertEqual(rf'{"\N{LEFT CURLY BRACKET}"}', "{") + + self.assertAllRaise( + SyntaxError, + "f-string: valid expression required before '}'", + [ + "f'{\n}'", + ], + ) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_invalid_backslashes_inside_fstring_context(self): + # All of these variations are invalid python syntax, + # so they are also invalid in f-strings as well. + cases = [ + formatting.format(expr=expr) + for formatting in [ + "{expr}", + "f'{{{expr}}}'", + "rf'{{{expr}}}'", + ] + for expr in [ + r"\'a\'", + r"\t3", + r"\\"[0], + ] + ] + self.assertAllRaise( + SyntaxError, "unexpected character after line continuation", cases + ) def test_no_escapes_for_braces(self): """ Only literal curly braces begin an expression. """ # \x7b is '{'. - self.assertEqual(f'\x7b1+1}}', '{1+1}') - self.assertEqual(f'\x7b1+1', '{1+1') - self.assertEqual(f'\u007b1+1', '{1+1') - self.assertEqual(f'\N{LEFT CURLY BRACKET}1+1\N{RIGHT CURLY BRACKET}', '{1+1}') + self.assertEqual(f"\x7b1+1}}", "{1+1}") + self.assertEqual(f"\x7b1+1", "{1+1") + self.assertEqual(f"\u007b1+1", "{1+1") + self.assertEqual(f"\N{LEFT CURLY BRACKET}1+1\N{RIGHT CURLY BRACKET}", "{1+1}") def test_newlines_in_expressions(self): - self.assertEqual(f'{0}', '0') - self.assertEqual(rf'''{3+ -4}''', '7') + self.assertEqual(f"{0}", "0") + self.assertEqual( + rf"""{3+ +4}""", + "7", + ) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_lambda(self): x = 5 self.assertEqual(f'{(lambda y:x*y)("8")!r}', "'88888'") @@ -855,17 +1156,99 @@ x = ( self.assertEqual(f'{(lambda y:x*y)("8"):10}', "88888 ") # lambda doesn't work without parens, because the colon - # makes the parser think it's a format_spec - self.assertAllRaise(SyntaxError, 'f-string: invalid syntax', - ["f'{lambda x:x}'", - ]) + # makes the parser think it's a format_spec + # emit warning if we can match a format_spec + self.assertAllRaise( + SyntaxError, + "f-string: lambda expressions are not allowed " "without parentheses", + [ + "f'{lambda x:x}'", + "f'{lambda :x}'", + "f'{lambda *arg, :x}'", + "f'{1, lambda:x}'", + "f'{lambda x:}'", + "f'{lambda :}'", + ], + ) + # Ensure the detection of invalid lambdas doesn't trigger detection + # for valid lambdas in the second error pass + with self.assertRaisesRegex(SyntaxError, "invalid syntax"): + compile("lambda name_3=f'{name_4}': {name_3}\n1 $ 1", "", "exec") + + # but don't emit the paren warning in general cases + with self.assertRaisesRegex( + SyntaxError, "f-string: expecting a valid expression after '{'" + ): + eval("f'{+ lambda:None}'") + + def test_valid_prefixes(self): + self.assertEqual(f"{1}", "1") + self.assertEqual(Rf"{2}", "2") + self.assertEqual(Rf"{3}", "3") + + def test_roundtrip_raw_quotes(self): + self.assertEqual(rf"\'", "\\'") + self.assertEqual(rf"\"", '\\"') + self.assertEqual(rf"\"\'", "\\\"\\'") + self.assertEqual(rf"\'\"", "\\'\\\"") + self.assertEqual(rf"\"\'\"", '\\"\\\'\\"') + self.assertEqual(rf"\'\"\'", "\\'\\\"\\'") + self.assertEqual(rf"\"\'\"\'", "\\\"\\'\\\"\\'") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_fstring_backslash_before_double_bracket(self): + deprecated_cases = [ + (r"f'\{{\}}'", "\\{\\}"), + (r"f'\{{'", "\\{"), + (r"f'\{{{1+1}'", "\\{2"), + (r"f'\}}{1+1}'", "\\}2"), + (r"f'{1+1}\}}'", "2\\}"), + ] + + for case, expected_result in deprecated_cases: + with self.subTest(case=case, expected_result=expected_result): + with self.assertWarns(SyntaxWarning): + result = eval(case) + self.assertEqual(result, expected_result) + self.assertEqual(rf"\{{\}}", "\\{\\}") + self.assertEqual(rf"\{{", "\\{") + self.assertEqual(rf"\{{{1+1}", "\\{2") + self.assertEqual(rf"\}}{1+1}", "\\}2") + self.assertEqual(rf"{1+1}\}}", "2\\}") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_fstring_backslash_before_double_bracket_warns_once(self): + with self.assertWarns(SyntaxWarning) as w: + eval(r"f'\{{'") + self.assertEqual(len(w.warnings), 1) + self.assertEqual(w.warnings[0].category, SyntaxWarning) + + def test_fstring_backslash_prefix_raw(self): + self.assertEqual(f"\\", "\\") + self.assertEqual(f"\\\\", "\\\\") + self.assertEqual(rf"\\", r"\\") + self.assertEqual(rf"\\\\", r"\\\\") + self.assertEqual(rf"\\", r"\\") + self.assertEqual(rf"\\\\", r"\\\\") + self.assertEqual(Rf"\\", R"\\") + self.assertEqual(Rf"\\\\", R"\\\\") + self.assertEqual(Rf"\\", R"\\") + self.assertEqual(Rf"\\\\", R"\\\\") + self.assertEqual(Rf"\\", R"\\") + self.assertEqual(Rf"\\\\", R"\\\\") + + def test_fstring_format_spec_greedy_matching(self): + self.assertEqual(f"{1:}}}", "1}") + self.assertEqual(f"{1:>3{5}}}}", " 1}") def test_yield(self): # Not terribly useful, but make sure the yield turns # a function into a generator def fn(y): - f'y:{yield y*2}' - f'{yield}' + f"y:{yield y*2}" + f"{yield}" g = fn(4) self.assertEqual(next(g), 8) @@ -873,257 +1256,329 @@ x = ( def test_yield_send(self): def fn(x): - yield f'x:{yield (lambda i: x * i)}' + yield f"x:{yield (lambda i: x * i)}" g = fn(10) the_lambda = next(g) self.assertEqual(the_lambda(4), 40) - self.assertEqual(g.send('string'), 'x:string') + self.assertEqual(g.send("string"), "x:string") - def test_expressions_with_triple_quoted_strings(self): - self.assertEqual(f"{'''x'''}", 'x') - # TODO: RUSTPYTHON self.assertEqual(f"{'''eric's'''}", "eric's") + # TODO: RUSTPYTHON SyntaxError + # def test_expressions_with_triple_quoted_strings(self): + # self.assertEqual(f"{'''x'''}", 'x') + # self.assertEqual(f"{'''eric's'''}", "eric's") - # Test concatenation within an expression - # TODO: RUSTPYTHON self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy') - # TODO: RUSTPYTHON self.assertEqual(f'{"x" """eric"s"""}', 'xeric"s') - # TODO: RUSTPYTHON self.assertEqual(f'{"""eric"s""" "y"}', 'eric"sy') - # TODO: RUSTPYTHON self.assertEqual(f'{"""x""" """eric"s""" "y"}', 'xeric"sy') - # TODO: RUSTPYTHON self.assertEqual(f'{"""x""" """eric"s""" """y"""}', 'xeric"sy') - # TODO: RUSTPYTHON self.assertEqual(f'{r"""x""" """eric"s""" """y"""}', 'xeric"sy') + # # Test concatenation within an expression + # self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy') + # self.assertEqual(f'{"x" """eric"s"""}', 'xeric"s') + # self.assertEqual(f'{"""eric"s""" "y"}', 'eric"sy') + # self.assertEqual(f'{"""x""" """eric"s""" "y"}', 'xeric"sy') + # self.assertEqual(f'{"""x""" """eric"s""" """y"""}', 'xeric"sy') + # self.assertEqual(f'{r"""x""" """eric"s""" """y"""}', 'xeric"sy') def test_multiple_vars(self): x = 98 - y = 'abc' - self.assertEqual(f'{x}{y}', '98abc') + y = "abc" + self.assertEqual(f"{x}{y}", "98abc") - self.assertEqual(f'X{x}{y}', 'X98abc') - self.assertEqual(f'{x}X{y}', '98Xabc') - self.assertEqual(f'{x}{y}X', '98abcX') + self.assertEqual(f"X{x}{y}", "X98abc") + self.assertEqual(f"{x}X{y}", "98Xabc") + self.assertEqual(f"{x}{y}X", "98abcX") - self.assertEqual(f'X{x}Y{y}', 'X98Yabc') - self.assertEqual(f'X{x}{y}Y', 'X98abcY') - self.assertEqual(f'{x}X{y}Y', '98XabcY') + self.assertEqual(f"X{x}Y{y}", "X98Yabc") + self.assertEqual(f"X{x}{y}Y", "X98abcY") + self.assertEqual(f"{x}X{y}Y", "98XabcY") - self.assertEqual(f'X{x}Y{y}Z', 'X98YabcZ') + self.assertEqual(f"X{x}Y{y}Z", "X98YabcZ") def test_closure(self): def outer(x): def inner(): - return f'x:{x}' + return f"x:{x}" + return inner - self.assertEqual(outer('987')(), 'x:987') - self.assertEqual(outer(7)(), 'x:7') + self.assertEqual(outer("987")(), "x:987") + self.assertEqual(outer(7)(), "x:7") def test_arguments(self): y = 2 - def f(x, width): - return f'x={x*y:{width}}' - self.assertEqual(f('foo', 10), 'x=foofoo ') - x = 'bar' - self.assertEqual(f(10, 10), 'x= 20') + def f(x, width): + return f"x={x*y:{width}}" + + self.assertEqual(f("foo", 10), "x=foofoo ") + x = "bar" + self.assertEqual(f(10, 10), "x= 20") def test_locals(self): value = 123 - self.assertEqual(f'v:{value}', 'v:123') + self.assertEqual(f"v:{value}", "v:123") def test_missing_variable(self): with self.assertRaises(NameError): - f'v:{value}' + f"v:{value}" def test_missing_format_spec(self): class O: def __format__(self, spec): if not spec: - return '*' + return "*" return spec - self.assertEqual(f'{O():x}', 'x') - self.assertEqual(f'{O()}', '*') - self.assertEqual(f'{O():}', '*') + self.assertEqual(f"{O():x}", "x") + self.assertEqual(f"{O()}", "*") + self.assertEqual(f"{O():}", "*") - self.assertEqual(f'{3:}', '3') - self.assertEqual(f'{3!s:}', '3') + self.assertEqual(f"{3:}", "3") + self.assertEqual(f"{3!s:}", "3") def test_global(self): - self.assertEqual(f'g:{a_global}', 'g:global variable') - self.assertEqual(f'g:{a_global!r}', "g:'global variable'") + self.assertEqual(f"g:{a_global}", "g:global variable") + self.assertEqual(f"g:{a_global!r}", "g:'global variable'") - a_local = 'local variable' - self.assertEqual(f'g:{a_global} l:{a_local}', - 'g:global variable l:local variable') - self.assertEqual(f'g:{a_global!r}', - "g:'global variable'") - self.assertEqual(f'g:{a_global} l:{a_local!r}', - "g:global variable l:'local variable'") + a_local = "local variable" + self.assertEqual( + f"g:{a_global} l:{a_local}", "g:global variable l:local variable" + ) + self.assertEqual(f"g:{a_global!r}", "g:'global variable'") + self.assertEqual( + f"g:{a_global} l:{a_local!r}", "g:global variable l:'local variable'" + ) - self.assertIn("module 'unittest' from", f'{unittest}') + self.assertIn("module 'unittest' from", f"{unittest}") def test_shadowed_global(self): - a_global = 'really a local' - self.assertEqual(f'g:{a_global}', 'g:really a local') - self.assertEqual(f'g:{a_global!r}', "g:'really a local'") + a_global = "really a local" + self.assertEqual(f"g:{a_global}", "g:really a local") + self.assertEqual(f"g:{a_global!r}", "g:'really a local'") - a_local = 'local variable' - self.assertEqual(f'g:{a_global} l:{a_local}', - 'g:really a local l:local variable') - self.assertEqual(f'g:{a_global!r}', - "g:'really a local'") - self.assertEqual(f'g:{a_global} l:{a_local!r}', - "g:really a local l:'local variable'") + a_local = "local variable" + self.assertEqual( + f"g:{a_global} l:{a_local}", "g:really a local l:local variable" + ) + self.assertEqual(f"g:{a_global!r}", "g:'really a local'") + self.assertEqual( + f"g:{a_global} l:{a_local!r}", "g:really a local l:'local variable'" + ) def test_call(self): def foo(x): - return 'x=' + str(x) + return "x=" + str(x) - self.assertEqual(f'{foo(10)}', 'x=10') + self.assertEqual(f"{foo(10)}", "x=10") def test_nested_fstrings(self): y = 5 - self.assertEqual(f'{f"{0}"*3}', '000') - self.assertEqual(f'{f"{y}"*3}', '555') + self.assertEqual(f'{f"{0}"*3}', "000") + self.assertEqual(f'{f"{y}"*3}', "555") def test_invalid_string_prefixes(self): - single_quote_cases = ["fu''", - "uf''", - "Fu''", - "fU''", - "Uf''", - "uF''", - "ufr''", - "urf''", - "fur''", - "fru''", - "rfu''", - "ruf''", - "FUR''", - "Fur''", - "fb''", - "fB''", - "Fb''", - "FB''", - "bf''", - "bF''", - "Bf''", - "BF''",] + single_quote_cases = [ + "fu''", + "uf''", + "Fu''", + "fU''", + "Uf''", + "uF''", + "ufr''", + "urf''", + "fur''", + "fru''", + "rfu''", + "ruf''", + "FUR''", + "Fur''", + "fb''", + "fB''", + "Fb''", + "FB''", + "bf''", + "bF''", + "Bf''", + "BF''", + ] double_quote_cases = [case.replace("'", '"') for case in single_quote_cases] - self.assertAllRaise(SyntaxError, 'invalid syntax', - single_quote_cases + double_quote_cases) + self.assertAllRaise( + SyntaxError, "invalid syntax", single_quote_cases + double_quote_cases + ) def test_leading_trailing_spaces(self): - self.assertEqual(f'{ 3}', '3') - self.assertEqual(f'{ 3}', '3') - self.assertEqual(f'{3 }', '3') - self.assertEqual(f'{3 }', '3') + self.assertEqual(f"{ 3}", "3") + self.assertEqual(f"{ 3}", "3") + self.assertEqual(f"{3 }", "3") + self.assertEqual(f"{3 }", "3") - self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}', - 'expr={1: 2}') - self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]} }', - 'expr={1: 2}') + self.assertEqual(f"expr={ {x: y for x, y in [(1, 2), ]}}", "expr={1: 2}") + self.assertEqual(f"expr={ {x: y for x, y in [(1, 2), ]} }", "expr={1: 2}") def test_not_equal(self): # There's a special test for this because there's a special # case in the f-string parser to look for != as not ending an # expression. Normally it would, while looking for !s or !r. - self.assertEqual(f'{3!=4}', 'True') - self.assertEqual(f'{3!=4:}', 'True') - self.assertEqual(f'{3!=4!s}', 'True') - self.assertEqual(f'{3!=4!s:.3}', 'Tru') + self.assertEqual(f"{3!=4}", "True") + self.assertEqual(f"{3!=4:}", "True") + 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') + self.assertEqual(f"{0==1}", "False") + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_conversions(self): - self.assertEqual(f'{3.14:10.10}', ' 3.14') - self.assertEqual(f'{3.14!s:10.10}', '3.14 ') - self.assertEqual(f'{3.14!r:10.10}', '3.14 ') - self.assertEqual(f'{3.14!a:10.10}', '3.14 ') + self.assertEqual(f"{3.14:10.10}", " 3.14") + self.assertEqual(f"{3.14!s:10.10}", "3.14 ") + self.assertEqual(f"{3.14!r:10.10}", "3.14 ") + self.assertEqual(f"{3.14!a:10.10}", "3.14 ") - self.assertEqual(f'{"a"}', 'a') + self.assertEqual(f'{"a"}', "a") self.assertEqual(f'{"a"!r}', "'a'") self.assertEqual(f'{"a"!a}', "'a'") + # Conversions can have trailing whitespace after them since it + # does not provide any significance + # TODO: RUSTPYTHON SyntaxError + # self.assertEqual(f"{3!s }", "3") + # self.assertEqual(f"{3.14!s :10.10}", "3.14 ") + # Not a conversion. self.assertEqual(f'{"a!r"}', "a!r") # Not a conversion, but show that ! is allowed in a format spec. - self.assertEqual(f'{3.14:!<10.10}', '3.14!!!!!!') + self.assertEqual(f"{3.14:!<10.10}", "3.14!!!!!!") - self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character', - ["f'{3!g}'", - "f'{3!A}'", - "f'{3!3}'", - "f'{3!G}'", - "f'{3!!}'", - "f'{3!:}'", - "f'{3! s}'", # no space before conversion char - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting '}'", + [ + "f'{3!'", + "f'{3!s'", + "f'{3!g'", + ], + ) - self.assertAllRaise(SyntaxError, "f-string: expecting '}'", - ["f'{x!s{y}}'", - "f'{3!ss}'", - "f'{3!ss:}'", - "f'{3!ss:s}'", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: missing conversion character", + [ + "f'{3!}'", + "f'{3!:'", + "f'{3!:}'", + ], + ) + + for conv_identifier in "g", "A", "G", "ä", "ɐ": + self.assertAllRaise( + SyntaxError, + "f-string: invalid conversion character %r: " + "expected 's', 'r', or 'a'" % conv_identifier, + ["f'{3!" + conv_identifier + "}'"], + ) + + for conv_non_identifier in "3", "!": + self.assertAllRaise( + SyntaxError, + "f-string: invalid conversion character", + ["f'{3!" + conv_non_identifier + "}'"], + ) + + for conv in " s", " s ": + self.assertAllRaise( + SyntaxError, + "f-string: conversion type must come right after the" + " exclamanation mark", + ["f'{3!" + conv + "}'"], + ) + + self.assertAllRaise( + SyntaxError, + "f-string: invalid conversion character 'ss': " "expected 's', 'r', or 'a'", + [ + "f'{3!ss}'", + "f'{3!ss:}'", + "f'{3!ss:s}'", + ], + ) def test_assignment(self): - self.assertAllRaise(SyntaxError, r'invalid syntax', - ["f'' = 3", - "f'{0}' = x", - "f'{x}' = x", - ]) + self.assertAllRaise( + SyntaxError, + r"invalid syntax", + [ + "f'' = 3", + "f'{0}' = x", + "f'{x}' = x", + ], + ) def test_del(self): - self.assertAllRaise(SyntaxError, 'invalid syntax', - ["del f''", - "del '' f''", - ]) + self.assertAllRaise( + SyntaxError, + "invalid syntax", + [ + "del f''", + "del '' f''", + ], + ) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_mismatched_braces(self): - self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed", - ["f'{{}'", - "f'{{}}}'", - "f'}'", - "f'x}'", - "f'x}x'", - r"f'\u007b}'", + self.assertAllRaise( + SyntaxError, + "f-string: single '}' is not allowed", + [ + "f'{{}'", + "f'{{}}}'", + "f'}'", + "f'x}'", + "f'x}x'", + r"f'\u007b}'", + # Can't have { or } in a format spec. + "f'{3:}>10}'", + "f'{3:}}>10}'", + ], + ) - # Can't have { or } in a format spec. - "f'{3:}>10}'", - "f'{3:}}>10}'", - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting '}'", + [ + "f'{3'", + "f'{3!'", + "f'{3:'", + "f'{3!s'", + "f'{3!s:'", + "f'{3!s:3'", + "f'x{'", + "f'x{x'", + "f'{x'", + "f'{3:s'", + "f'{{{'", + "f'{{}}{'", + "f'{'", + "f'{i='", # See gh-93418. + ], + ) - self.assertAllRaise(SyntaxError, "f-string: expecting '}'", - ["f'{3:{{>10}'", - "f'{3'", - "f'{3!'", - "f'{3:'", - "f'{3!s'", - "f'{3!s:'", - "f'{3!s:3'", - "f'x{'", - "f'x{x'", - "f'{x'", - "f'{3:s'", - "f'{{{'", - "f'{{}}{'", - "f'{'", - "f'x{<'", # See bpo-46762. - "f'x{>'", - "f'{i='", # See gh-93418. - ]) + self.assertAllRaise( + SyntaxError, + "f-string: expecting a valid expression after '{'", + [ + "f'{3:{{>10}'", + ], + ) # But these are just normal strings. - self.assertEqual(f'{"{"}', '{') - self.assertEqual(f'{"}"}', '}') - self.assertEqual(f'{3:{"}"}>10}', '}}}}}}}}}3') - self.assertEqual(f'{2:{"{"}>10}', '{{{{{{{{{2') + self.assertEqual(f'{"{"}', "{") + self.assertEqual(f'{"}"}', "}") + self.assertEqual(f'{3:{"}"}>10}', "}}}}}}}}}3") + self.assertEqual(f'{2:{"{"}>10}', "{{{{{{{{{2") def test_if_conditional(self): # There's special logic in compile.c to test if the @@ -1132,7 +1587,7 @@ x = ( def test_fstring(x, expected): flag = 0 - if f'{x}': + if f"{x}": flag = 1 else: flag = 2 @@ -1140,7 +1595,7 @@ x = ( def test_concat_empty(x, expected): flag = 0 - if '' f'{x}': + if "" f"{x}": flag = 1 else: flag = 2 @@ -1148,141 +1603,153 @@ x = ( def test_concat_non_empty(x, expected): flag = 0 - if ' ' f'{x}': + if " " f"{x}": flag = 1 else: flag = 2 self.assertEqual(flag, expected) - test_fstring('', 2) - test_fstring(' ', 1) + test_fstring("", 2) + test_fstring(" ", 1) - test_concat_empty('', 2) - test_concat_empty(' ', 1) + test_concat_empty("", 2) + test_concat_empty(" ", 1) - test_concat_non_empty('', 1) - test_concat_non_empty(' ', 1) + test_concat_non_empty("", 1) + test_concat_non_empty(" ", 1) def test_empty_format_specifier(self): - x = 'test' - self.assertEqual(f'{x}', 'test') - self.assertEqual(f'{x:}', 'test') - self.assertEqual(f'{x!s:}', 'test') - self.assertEqual(f'{x!r:}', "'test'") + x = "test" + self.assertEqual(f"{x}", "test") + self.assertEqual(f"{x:}", "test") + self.assertEqual(f"{x!s:}", "test") + self.assertEqual(f"{x!r:}", "'test'") # TODO: RUSTPYTHON d[0] error @unittest.expectedFailure def test_str_format_differences(self): - d = {'a': 'string', - 0: 'integer', - } + d = { + "a": "string", + 0: "integer", + } a = 0 - self.assertEqual(f'{d[0]}', 'integer') - self.assertEqual(f'{d["a"]}', 'string') - self.assertEqual(f'{d[a]}', 'integer') - self.assertEqual('{d[a]}'.format(d=d), 'string') - self.assertEqual('{d[0]}'.format(d=d), 'integer') + self.assertEqual(f"{d[0]}", "integer") + self.assertEqual(f'{d["a"]}', "string") + self.assertEqual(f"{d[a]}", "integer") + self.assertEqual("{d[a]}".format(d=d), "string") + self.assertEqual("{d[0]}".format(d=d), "integer") # TODO: RUSTPYTHON @unittest.expectedFailure def test_errors(self): # see issue 26287 - self.assertAllRaise(TypeError, 'unsupported', - [r"f'{(lambda: 0):x}'", - r"f'{(0,):x}'", - ]) - self.assertAllRaise(ValueError, 'Unknown format code', - [r"f'{1000:j}'", - r"f'{1000:j}'", - ]) + self.assertAllRaise( + TypeError, + "unsupported", + [ + r"f'{(lambda: 0):x}'", + r"f'{(0,):x}'", + ], + ) + self.assertAllRaise( + ValueError, + "Unknown format code", + [ + r"f'{1000:j}'", + 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', encoding="utf-8") 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) + file_path = os.path.join(cwd, "t.py") + with open(file_path, "w", encoding="utf-8") 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)) + self.assertEqual(f"i:{i}", "i:" + str(i)) def test_dict(self): - d = {'"': 'dquote', - "'": 'squote', - 'foo': 'bar', - } - self.assertEqual(f'''{d["'"]}''', 'squote') - self.assertEqual(f"""{d['"']}""", 'dquote') + d = { + '"': "dquote", + "'": "squote", + "foo": "bar", + } + self.assertEqual(f"""{d["'"]}""", "squote") + self.assertEqual(f"""{d['"']}""", "dquote") - self.assertEqual(f'{d["foo"]}', 'bar') - self.assertEqual(f"{d['foo']}", 'bar') + self.assertEqual(f'{d["foo"]}', "bar") + self.assertEqual(f"{d['foo']}", "bar") def test_backslash_char(self): # Check eval of a backslash followed by a control char. # See bpo-30682: this used to raise an assert in pydebug mode. - self.assertEqual(eval('f"\\\n"'), '') - self.assertEqual(eval('f"\\\r"'), '') + 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 = "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')) + 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') + 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') + self.assertEqual(f"{tenπ=:.2f}", "tenπ=31.40") # Also test with Unicode in non-identifiers. - self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'') + self.assertEqual(f'{"Σ"=}', "\"Σ\"='Σ'") # Make sure nested fstrings still work. - self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****') + 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") + pi = "π" + self.assertEqual(f"alpha α {pi=} ω omega", "alpha α pi='π' ω omega") # Check multi-line expressions. - self.assertEqual(f'''{ + self.assertEqual( + f"""{ 3 -=}''', '\n3\n=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(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'{"="}', '=') + 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') + self.assertEqual(f"{x:=10}", " 20") # Test named function parameters, to make sure '=' parsing works # there. @@ -1291,36 +1758,54 @@ 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(f'{f(a="3=")}', "0") + self.assertEqual(x, "3=") + self.assertEqual(f"{f(a=4)}", "3=") self.assertEqual(x, 4) + # Check debug expressions in format spec + y = 20 + self.assertEqual(f"{2:{y=}}", "yyyyyyyyyyyyyyyyyyy2") + self.assertEqual( + f"{datetime.datetime.now():h1{y=}h2{y=}h3{y=}}", "h1y=20h2y=20h3y=20" + ) + # Make sure __format__ is being called. class C: def __format__(self, s): - return f'FORMAT-{s}' - def __repr__(self): - return 'REPR' + return f"FORMAT-{s}" - 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 __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.assertEqual(f"{C():{20=}}", "FORMAT-20=20") 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') + 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') + 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") + self.assertEqual(f"sadsd {1 + 1 = :{1 + 1:1d}f}", "sadsd 1 + 1 = 2.000000") + +# TODO: RUSTPYTHON SyntaxError +# self.assertEqual( +# f"{1+2 = # my comment +# }", +# "1+2 = \n 3", +# ) # These next lines contains tabs. Backslash escapes don't # work in f-strings. @@ -1328,23 +1813,25 @@ x = ( # 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') + # 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') + self.assertEqual(f"{x:=10}", " 20") # This is an assignment expression, which requires parens. - self.assertEqual(f'{(x:=10)}', '10') + self.assertEqual(f"{(x:=10)}", "10") self.assertEqual(x, 10) # TODO: RUSTPYTHON @unittest.expectedFailure def test_invalid_syntax_error_message(self): - with self.assertRaisesRegex(SyntaxError, "f-string: invalid syntax"): + with self.assertRaisesRegex( + SyntaxError, "f-string: expecting '=', or '!', or ':', or '}'" + ): compile("f'{a $ b}'", "?", "exec") # TODO: RUSTPYTHON @@ -1352,37 +1839,112 @@ x = ( def test_with_two_commas_in_format_specifier(self): error_msg = re.escape("Cannot specify ',' with ','.") with self.assertRaisesRegex(ValueError, error_msg): - f'{1:,,}' + f"{1:,,}" # TODO: RUSTPYTHON @unittest.expectedFailure def test_with_two_underscore_in_format_specifier(self): error_msg = re.escape("Cannot specify '_' with '_'.") with self.assertRaisesRegex(ValueError, error_msg): - f'{1:__}' + f"{1:__}" # TODO: RUSTPYTHON @unittest.expectedFailure def test_with_a_commas_and_an_underscore_in_format_specifier(self): error_msg = re.escape("Cannot specify both ',' and '_'.") with self.assertRaisesRegex(ValueError, error_msg): - f'{1:,_}' + f"{1:,_}" # TODO: RUSTPYTHON @unittest.expectedFailure def test_with_an_underscore_and_a_comma_in_format_specifier(self): error_msg = re.escape("Cannot specify both ',' and '_'.") with self.assertRaisesRegex(ValueError, error_msg): - f'{1:_,}' + f"{1:_,}" + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_syntax_error_for_starred_expressions(self): - error_msg = re.escape("cannot use starred expression here") - with self.assertRaisesRegex(SyntaxError, error_msg): + with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"): compile("f'{*a}'", "?", "exec") - error_msg = re.escape("cannot use double starred expression here") - with self.assertRaisesRegex(SyntaxError, error_msg): + with self.assertRaisesRegex( + SyntaxError, "f-string: expecting a valid expression after '{'" + ): compile("f'{**a}'", "?", "exec") -if __name__ == '__main__': + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_not_closing_quotes(self): + self.assertAllRaise(SyntaxError, "unterminated f-string literal", ['f"', "f'"]) + self.assertAllRaise( + SyntaxError, "unterminated triple-quoted f-string literal", ['f"""', "f'''"] + ) + # Ensure that the errors are reported at the correct line number. + data = '''\ +x = 1 + 1 +y = 2 + 2 +z = f""" +sdfjnsdfjsdf +sdfsdfs{1+ +2} dfigdf {3+ +4}sdufsd"" +''' + try: + compile(data, "?", "exec") + except SyntaxError as e: + self.assertEqual(e.text, 'z = f"""') + self.assertEqual(e.lineno, 3) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_syntax_error_after_debug(self): + self.assertAllRaise( + SyntaxError, + "f-string: expecting a valid expression after '{'", + [ + "f'{1=}{;'", + "f'{1=}{+;'", + "f'{1=}{2}{;'", + "f'{1=}{3}{;'", + ], + ) + self.assertAllRaise( + SyntaxError, + "f-string: expecting '=', or '!', or ':', or '}'", + [ + "f'{1=}{1;'", + "f'{1=}{1;}'", + ], + ) + + def test_debug_in_file(self): + with temp_cwd(): + script = "script.py" + with open("script.py", "w") as f: + f.write(f"""\ +print(f'''{{ +3 +=}}''')""") + + _, stdout, _ = assert_python_ok(script) + self.assertEqual( + stdout.decode("utf-8").strip().replace("\r\n", "\n").replace("\r", "\n"), + "3\n=3", + ) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_syntax_warning_infinite_recursion_in_file(self): + with temp_cwd(): + script = "script.py" + with open(script, "w") as f: + f.write(r"print(f'\{1}')") + + _, stdout, stderr = assert_python_ok(script) + self.assertIn(rb"\1", stdout) + self.assertEqual(len(stderr.strip().splitlines()), 2) + + +if __name__ == "__main__": unittest.main()