diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index c0c987ca2f..e01d491d22 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -9,6 +9,7 @@ import ast import datetime +import dis import os import re import types @@ -19,7 +20,7 @@ from test.support.os_helper import temp_cwd 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 @@ -28,7 +29,6 @@ # 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: @@ -40,45 +40,43 @@ def test__format__lookup(self): # 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) @@ -284,6 +282,7 @@ def test_ast_line_numbers_duplicate_expression(self): 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) @@ -441,12 +440,24 @@ def test_ast_line_numbers_with_parentheses(self): 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)) + 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)) + 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 = ( @@ -507,612 +518,532 @@ def test_ast_fstring_empty_format_spec(self): self.assertEqual(type(format_spec), ast.JoinedStr) self.assertEqual(len(format_spec.values), 0) + def test_ast_fstring_format_spec(self): + expr = "f'{1:{name}}'" + + 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), 1) + + format_spec_value = format_spec.values[0] + self.assertEqual(type(format_spec_value), ast.FormattedValue) + self.assertEqual(format_spec_value.value.id, 'name') + + expr = "f'{1:{name1}{name2}}'" + + 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), 2) + + format_spec_value = format_spec.values[0] + self.assertEqual(type(format_spec_value), ast.FormattedValue) + self.assertEqual(format_spec_value.value.id, 'name1') + + format_spec_value = format_spec.values[1] + self.assertEqual(type(format_spec_value), ast.FormattedValue) + self.assertEqual(format_spec_value.value.id, 'name2') + + 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') - # TODO: RUSTPYTHON - @unittest.expectedFailure 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' ', ' ') # TODO: RUSTPYTHON @unittest.expectedFailure def test_unterminated_string(self): - self.assertAllRaise( - SyntaxError, - "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}'""", + ]) # TODO: RUSTPYTHON @unittest.expectedFailure @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") def test_mismatched_parens(self): - 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 + "}'") + 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}}}}"'], - ) + 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) + prev = create_nested_fstring(n-1) return f'f"{{{prev}}}"' - self.assertAllRaise( - SyntaxError, "too many nested f-strings", [create_nested_fstring(160)] - ) + 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)] - ) + 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"{{{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'{{', '{') + 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'{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 + ]) - # TODO: RUSTPYTHON - @unittest.expectedFailure 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'' '', '') # 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.assertEqual(f'{' f'}', ' f') - self.assertAllRaise( - SyntaxError, - "expecting '}'", - [ - '''f'{3' f"}"''', # can't concat to get a valid f-string - ], - ) + 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") - - 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: { + d = {'#': 'hash'} + self.assertEqual(f'{"#"}', '#') + self.assertEqual(f'{d["#"]}', 'hash') + + 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""" +}''', 'A complex trick: 2') + self.assertEqual(f''' { 40 # forty + # plus 2 # two -}""", - "\n42", - ) - self.assertEqual( - f""" +}''', '\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""" +}''', '\n42') + + 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'{}' - ], - ) +}''', '\n86') + + # TODO: RUSTPYTHON +# 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") - - # TODO: RUSTPYTHON SyntaxError - # self.assertEqual( - # f"result: {value:{width:{0}}.{precision:1}}", "result: 12.35" - # ) - - - self.assertAllRaise( - SyntaxError, - "f-string: expecting ':' or '}'", - [ - """f'{"s"!r{":10"}}'""", - # This looks like a nested format spec. - ], - ) - - - 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"}}'""", - ], - ) + 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.assertEqual(f'result: {value:{width:{0}}.{precision:1}}', 'result: 12.35') + + # TODO: RUSTPYTHON + # self.assertAllRaise(SyntaxError, "f-string: expecting ':' or '}'", + # ["""f'{"s"!r{":10"}}'""", + # # This looks like a nested format spec. + # ]) + # TODO: RUSTPYTHON + # self.assertAllRaise(SyntaxError, + # "f-string: expecting a valid expression after '{'", + # [# Invalid syntax inside a nested spec. + # "f'{4:{/5}}'", + # ]) + # TODO: RUSTPYTHON + # 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}) + self.assertEqual(f'{CustomFormat():\n}', '\n') + self.assertEqual(f'{CustomFormat():\u2603}', '☃') + # TODO: RUSTPYTHON + # 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: 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}'''", - ], - ) - - 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}'", - ], - ) - - self.assertAllRaise( - SyntaxError, - "f-string: valid expression required before ':'", - [ - "f'{:}'", - "f'{ :!}'", - "f'{:2}'", - "f'''{\t\f\r\n:a}'''", - "f'{:'", - "F'{[F'{:'}[F'{:'}]]]", - ], - ) - - self.assertAllRaise( - SyntaxError, - "f-string: valid 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'{10:{ }}'", + "f' { } '", + + # 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}'", + ]) + + self.assertAllRaise(SyntaxError, + "f-string: valid expression required before ':'", + ["f'{:}'", + "f'{ :!}'", + "f'{:2}'", + "f'''{\t\f\r\n:a}'''", + "f'{:'", + "F'{[F'{:'}[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.assertAllRaise( - SyntaxError, - "f-string: expecting a valid expression after '{'", - [ - "f'{,}'", - ], - ) - - self.assertAllRaise( - SyntaxError, - r"f-string: unmatched '\)'", - [ - "f'{3)+(4}'", - ], - ) + self.assertEqual(f'{3,}', '(3,)') + + self.assertAllRaise(SyntaxError, + "f-string: expecting a valid expression after '{'", + ["f'{,}'", + ]) + + self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'", + ["f'{3)+(4}'", + ]) # TODO: RUSTPYTHON @unittest.expectedFailure def test_newlines_before_syntax_error(self): - self.assertAllRaise( - SyntaxError, - "f-string: expecting a valid expression after '{'", - ["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"\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"\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"2\x20", "2 ") - self.assertEqual(f"2\x203", "2 3") - self.assertEqual(f"\x203", " 3") - - with self.assertWarns(SyntaxWarning): # invalid escape sequence - value = eval(r"f'\{6*7}'") - 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" + 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'\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'\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') + + # TODO: RUSTPYTHON + # with self.assertWarns(SyntaxWarning): # invalid escape sequence + # value = eval(r"f'\{6*7}'") + # 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(fr'\{6*7}', '\\42') + + 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(rf"\N{AMPERSAND}", "\\Nspam") - self.assertEqual(f"\\\N{AMPERSAND}", "\\&") + self.assertEqual(f'\N{AMPERSAND}', '&') + self.assertEqual(f'\\N{AMPERSAND}', '\\Nspam') + self.assertEqual(fr'\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_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}'", - ], - ) + 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}"}', '{') + + # TODO: RUSTPYTHON + # 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. @@ -1129,30 +1060,25 @@ def test_invalid_backslashes_inside_fstring_context(self): r"\\"[0], ] ] - self.assertAllRaise( - SyntaxError, "unexpected character after line continuation", cases - ) + # TODO: RUSTPYTHON + # 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'") @@ -1162,64 +1088,63 @@ def test_lambda(self): # lambda doesn't work without parens, because the colon # 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 :}'", - ], - ) + # TODO: RUSTPYTHON + # 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") + # TODO: RUSTPYTHON + # 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}'") + # TODO: RUSTPYTHON + # 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") + self.assertEqual(F'{1}', "1") + self.assertEqual(FR'{2}', "2") + self.assertEqual(fR'{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"\"\'\"\'", "\\\"\\'\\\"\\'") + self.assertEqual(fr"\'", "\\'") + self.assertEqual(fr'\"', '\\"') + self.assertEqual(fr'\"\'', '\\"\\\'') + self.assertEqual(fr'\'\"', '\\\'\\"') + self.assertEqual(fr'\"\'\"', '\\"\\\'\\"') + self.assertEqual(fr'\'\"\'', '\\\'\\"\\\'') + self.assertEqual(fr'\"\'\"\'', '\\"\\\'\\"\\\'') - # 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\\}"), + (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 + # 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(fr'\{{\}}', '\\{\\}') + self.assertEqual(fr'\{{', '\\{') + self.assertEqual(fr'\{{{1+1}', '\\{2') + self.assertEqual(fr'\}}{1+1}', '\\}2') + self.assertEqual(fr'{1+1}\}}', '2\\}') # TODO: RUSTPYTHON @unittest.expectedFailure @@ -1230,18 +1155,18 @@ def test_fstring_backslash_before_double_bracket_warns_once(self): 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"\\\\") + self.assertEqual(f'\\', '\\') + self.assertEqual(f'\\\\', '\\\\') + self.assertEqual(fr'\\', r'\\') + self.assertEqual(fr'\\\\', r'\\\\') + self.assertEqual(rf'\\', r'\\') + self.assertEqual(rf'\\\\', r'\\\\') + self.assertEqual(Rf'\\', R'\\') + self.assertEqual(Rf'\\\\', R'\\\\') + self.assertEqual(fR'\\', R'\\') + self.assertEqual(fR'\\\\', R'\\\\') + self.assertEqual(FR'\\', R'\\') + self.assertEqual(FR'\\\\', R'\\\\') def test_fstring_format_spec_greedy_matching(self): self.assertEqual(f"{1:}}}", "1}") @@ -1251,8 +1176,8 @@ 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) @@ -1260,331 +1185,295 @@ def fn(y): 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') - # TODO: RUSTPYTHON SyntaxError - # def test_expressions_with_triple_quoted_strings(self): - # self.assertEqual(f"{'''x'''}", 'x') - # self.assertEqual(f"{'''eric's'''}", "eric's") + 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 - # 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') + # 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}}" + return f'x={x*y:{width}}' - self.assertEqual(f("foo", 10), "x=foofoo ") - x = "bar" - self.assertEqual(f(10, 10), "x= 20") + 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_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_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'") 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 - ) + # TODO: RUSTPYTHON + # 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 ") + 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.assertAllRaise( - SyntaxError, - "f-string: expecting '}'", - [ - "f'{3!'", - "f'{3!s'", - "f'{3!g'", - ], - ) - - 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}'", - ], - ) + self.assertEqual(f'{3.14:!<10.10}', '3.14!!!!!!') + + # TODO: RUSTPYTHON + # self.assertAllRaise(SyntaxError, "f-string: expecting '}'", + # ["f'{3!'", + # "f'{3!s'", + # "f'{3!g'", + # ]) + + # TODO: RUSTPYTHON + # self.assertAllRaise(SyntaxError, 'f-string: missing conversion character', + # ["f'{3!}'", + # "f'{3!:'", + # "f'{3!:}'", + # ]) + + # TODO: RUSTPYTHON + # 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 + "}'"]) + + # TODO: RUSTPYTHON + # for conv_non_identifier in '3', '!': + # self.assertAllRaise(SyntaxError, + # "f-string: invalid conversion character", + # ["f'{3!" + conv_non_identifier + "}'"]) + + # TODO: RUSTPYTHON + # for conv in ' s', ' s ': + # self.assertAllRaise(SyntaxError, + # "f-string: conversion type must come right after the" + # " exclamanation mark", + # ["f'{3!" + conv + "}'"]) + + # TODO: RUSTPYTHON + # 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", + ]) # TODO: RUSTPYTHON @unittest.expectedFailure 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}'", - # 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 a valid expression after '{'", - [ - "f'{3:{{>10}'", - ], - ) + 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}'", + ]) + + 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 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 @@ -1593,7 +1482,7 @@ def test_if_conditional(self): def test_fstring(x, expected): flag = 0 - if f"{x}": + if f'{x}': flag = 1 else: flag = 2 @@ -1601,7 +1490,7 @@ def test_fstring(x, expected): def test_concat_empty(x, expected): flag = 0 - if "" f"{x}": + if '' f'{x}': flag = 1 else: flag = 2 @@ -1609,153 +1498,139 @@ def test_concat_empty(x, expected): 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. @@ -1764,54 +1639,48 @@ def f(a): 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" - ) + 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}" - + return f'FORMAT-{s}' def __repr__(self): - return "REPR" + 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.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", -# ) + self.assertEqual(f"{1+2 = # my comment + }", '1+2 = \n 3') # These next lines contains tabs. Backslash escapes don't # work in f-strings. @@ -1819,25 +1688,32 @@ def __repr__(self): # 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_debug_expressions_are_raw_strings(self): + + self.assertEqual(f'{b"\N{OX}"=}', 'b"\\N{OX}"=b\'\\\\N{OX}\'') + self.assertEqual(f'{r"\xff"=}', 'r"\\xff"=\'\\\\xff\'') + self.assertEqual(f'{r"\n"=}', 'r"\\n"=\'\\\\n\'') + self.assertEqual(f"{'\''=}", "'\\''=\"'\"") + self.assertEqual(f'{'\xc5'=}', r"'\xc5'='Å'") 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: expecting '=', or '!', or ':', or '}'" - ): + with self.assertRaisesRegex(SyntaxError, + "f-string: expecting '=', or '!', or ':', or '}'"): compile("f'{a $ b}'", "?", "exec") # TODO: RUSTPYTHON @@ -1845,28 +1721,28 @@ def test_invalid_syntax_error_message(self): 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 @@ -1874,18 +1750,16 @@ def test_syntax_error_for_starred_expressions(self): with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"): compile("f'{*a}'", "?", "exec") - with self.assertRaisesRegex( - SyntaxError, "f-string: expecting a valid expression after '{'" - ): + with self.assertRaisesRegex(SyntaxError, + "f-string: expecting a valid expression after '{'"): compile("f'{**a}'", "?", "exec") # 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'''"] - ) + 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 @@ -1905,52 +1779,72 @@ def test_not_closing_quotes(self): # 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;}'", - ], - ) + 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: + 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", - ) + 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: + 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.assertIn(rb'\1', stdout) self.assertEqual(len(stderr.strip().splitlines()), 2) + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_fstring_without_formatting_bytecode(self): + # f-string without any formatting should emit the same bytecode + # as a normal string. See gh-99606. + def get_code(s): + return [(i.opname, i.oparg) for i in dis.get_instructions(s)] + + for s in ["", "some string"]: + self.assertEqual(get_code(f"'{s}'"), get_code(f"f'{s}'")) + + def test_gh129093(self): + self.assertEqual(f'{1==2=}', '1==2=False') + self.assertEqual(f'{1 == 2=}', '1 == 2=False') + self.assertEqual(f'{1!=2=}', '1!=2=True') + self.assertEqual(f'{1 != 2=}', '1 != 2=True') + + self.assertEqual(f'{(1) != 2=}', '(1) != 2=True') + self.assertEqual(f'{(1*2) != (3)=}', '(1*2) != (3)=True') + + self.assertEqual(f'{1 != 2 == 3 != 4=}', '1 != 2 == 3 != 4=False') + self.assertEqual(f'{1 == 2 != 3 == 4=}', '1 == 2 != 3 == 4=False') + + self.assertEqual(f'{f'{1==2=}'=}', "f'{1==2=}'='1==2=False'") + self.assertEqual(f'{f'{1 == 2=}'=}', "f'{1 == 2=}'='1 == 2=False'") + self.assertEqual(f'{f'{1!=2=}'=}', "f'{1!=2=}'='1!=2=True'") + self.assertEqual(f'{f'{1 != 2=}'=}', "f'{1 != 2=}'='1 != 2=True'") + -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/common/src/hash.rs b/common/src/hash.rs index 88af9b6f56..2504aae8f8 100644 --- a/common/src/hash.rs +++ b/common/src/hash.rs @@ -53,6 +53,15 @@ impl HashSecret { fix_sentinel(mod_int(self.hash_one(data) as _)) } + pub fn hash_integer(&self, data: T) -> PyHash + where + T: Into, + { + let bigint = data.into(); + let result = hash_bigint(&bigint); + result + } + pub fn hash_iter<'a, T: 'a, I, F, E>(&self, iter: I, hash_func: F) -> Result where I: IntoIterator, @@ -86,6 +95,11 @@ pub fn hash_pointer(value: usize) -> PyHash { hash as _ } +#[inline] +pub fn hash_integer(data: T) -> PyHash { + fix_sentinel(mod_int(data.to_i64().unwrap())) +} + #[inline] pub fn hash_float(value: f64) -> Option { // cpython _Py_HashDouble diff --git a/compiler/codegen/src/compile.rs b/compiler/codegen/src/compile.rs index 18215003ee..6fb62e9651 100644 --- a/compiler/codegen/src/compile.rs +++ b/compiler/codegen/src/compile.rs @@ -4253,14 +4253,14 @@ impl Compiler<'_> { FStringElement::Expression(fstring_expr) => { let mut conversion = fstring_expr.conversion; - if let Some(DebugText { leading, trailing }) = &fstring_expr.debug_text { - let range = fstring_expr.expression.range(); - let source = self.source_code.get_range(range); - let text = [leading, source, trailing].concat(); - - self.emit_load_const(ConstantData::Str { value: text.into() }); - element_count += 1; - } + // if let Some(DebugText { leading, trailing }) = &fstring_expr.debug_text { + // let range = fstring_expr.expression.range(); + // let source = self.source_code.get_range(range); + // let text = [leading, source, trailing].concat(); + // + // self.emit_load_const(ConstantData::Str { value: text.into() }); + // element_count += 1; + // } match &fstring_expr.format_spec { None => { diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs index 390a2d5669..735a0dccca 100644 --- a/compiler/src/lib.rs +++ b/compiler/src/lib.rs @@ -91,12 +91,16 @@ pub fn compile( source_path: &str, opts: CompileOpts, ) -> Result { + if source_path == "/home/hbina085/git/RustPython/ggg_syntax_warning.py" { + println!("hello"); + } // TODO: do this less hackily; ruff's parser should translate a CRLF line // break in a multiline string into just an LF in the parsed value #[cfg(windows)] let source = &source.replace("\r\n", "\n"); let source_code = SourceCode::new(source_path, source); - _compile(source_code, mode, opts) + let result = _compile(source_code, mode, opts); + result // let index = LineIndex::from_source_text(source); // let source_code = SourceCode::new(source, &index); // let mut locator = LinearLocator::new(source); @@ -128,7 +132,9 @@ fn _compile( }; let parsed = parser::parse(source_code.text, parser_mode.into()) .map_err(|err| CompileError::from_ruff_parse_error(err, &source_code))?; + println!("parsed:\n{:#?}", parsed); let ast = parsed.into_syntax(); + println!("ast:\n{:#?}", ast); compile::compile_top(ast, source_code, mode, opts).map_err(|e| e.into()) } diff --git a/extra_tests/snippets/builtin_object.py b/extra_tests/snippets/builtin_object.py index 5a12afbf45..ba4ff44711 100644 --- a/extra_tests/snippets/builtin_object.py +++ b/extra_tests/snippets/builtin_object.py @@ -1,6 +1,7 @@ class MyObject: pass + assert not MyObject() == MyObject() assert MyObject() != MyObject() myobj = MyObject() @@ -24,3 +25,10 @@ class MyObject: assert not hasattr(obj, 'a') obj.__dict__ = {'a': 1} assert obj.a == 1 + +# Value inside the formatter goes through a different path of resolution. +# Check that it still works all the same +d = { + 0: "ab", +} +assert "ab ab" == "{k[0]} {vv}".format(k=d, vv=d[0]) diff --git a/vm/src/dict_inner.rs b/vm/src/dict_inner.rs index 56ebc5ebaf..7fa6855e4d 100644 --- a/vm/src/dict_inner.rs +++ b/vm/src/dict_inner.rs @@ -17,6 +17,7 @@ use crate::{ object::{Traverse, TraverseFn}, }; use num_traits::ToPrimitive; +use rustpython_common::hash::hash_integer; use std::{fmt, mem::size_of, ops::ControlFlow}; // HashIndex is intended to be same size with hash::PyHash @@ -993,8 +994,8 @@ impl DictKey for usize { *self } - fn key_hash(&self, vm: &VirtualMachine) -> PyResult { - Ok(vm.state.hash_secret.hash_value(self)) + fn key_hash(&self, _vm: &VirtualMachine) -> PyResult { + Ok(hash_integer(*self)) } fn key_is(&self, _other: &PyObject) -> bool {