Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 45 additions & 38 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ def test_ast_line_numbers_multiline_fstring(self):
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)

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 4 != 5
def test_ast_line_numbers_with_parentheses(self):
expr = """
x = (
Expand Down Expand Up @@ -587,7 +587,6 @@ def test_ast_compile_time_concat(self):
exec(c)
self.assertEqual(x[0], 'foo3')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_time_concat_errors(self):
self.assertAllRaise(SyntaxError,
'cannot mix bytes and nonbytes literals',
Expand All @@ -600,7 +599,6 @@ def test_literal(self):
self.assertEqual(f'a', 'a')
self.assertEqual(f' ', ' ')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unterminated_string(self):
self.assertAllRaise(SyntaxError, 'unterminated string',
[r"""f'{"x'""",
Expand All @@ -609,7 +607,7 @@ def test_unterminated_string(self):
r"""f'{("x}'""",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_mismatched_parens(self):
self.assertAllRaise(SyntaxError, r"closing parenthesis '\}' "
Expand All @@ -632,24 +630,35 @@ def test_mismatched_parens(self):
r"does not match opening parenthesis '\('",
["f'{a(4}'",
])
self.assertRaises(SyntaxError, eval, "f'{" + "("*500 + "}'")
self.assertRaises(SyntaxError, eval, "f'{" + "("*20 + "}'")

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: No exception raised
@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 raises_syntax_or_memory_error(txt):
try:
eval(txt)
except SyntaxError:
pass
except MemoryError:
pass
except Exception as ex:
self.fail(f"Should raise SyntaxError or MemoryError, not {type(ex)}")
else:
self.fail("No exception raised")

raises_syntax_or_memory_error('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)])
raises_syntax_or_memory_error(create_nested_fstring(160))
raises_syntax_or_memory_error("f'{" + "("*100 + "}'")
raises_syntax_or_memory_error("f'{" + "("*1000 + "}'")
raises_syntax_or_memory_error("f'{" + "("*10_000 + "}'")

def test_syntax_error_in_nested_fstring(self):
# See gh-104016 for more information on this crash
Expand Down Expand Up @@ -692,7 +701,7 @@ def test_double_braces(self):
["f'{ {{}} }'", # dict in a set
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_time_concat(self):
x = 'def'
self.assertEqual('abc' f'## {x}ghi', 'abc## defghi')
Expand Down Expand Up @@ -730,7 +739,7 @@ def test_compile_time_concat(self):
['''f'{3' f"}"''', # can't concat to get a valid f-string
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_comments(self):
# These aren't comments, since they're in strings.
d = {'#': 'hash'}
Expand Down Expand Up @@ -807,7 +816,7 @@ def build_fstr(n, extra=''):
s = "f'{1}' 'x' 'y'" * 1024
self.assertEqual(eval(s), '1xy' * 1024)

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_format_specifier_expressions(self):
width = 10
precision = 4
Expand Down Expand Up @@ -841,7 +850,6 @@ def test_format_specifier_expressions(self):
"""f'{"s"!{"r"}}'""",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_custom_format_specifier(self):
class CustomFormat:
def __format__(self, format_spec):
Expand All @@ -863,7 +871,7 @@ def __format__(self, spec):
x = X()
self.assertEqual(f'{x} {x}', '1 2')

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_missing_expression(self):
self.assertAllRaise(SyntaxError,
"f-string: valid expression required before '}'",
Expand Down Expand Up @@ -926,7 +934,7 @@ def test_missing_expression(self):
"\xa0",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_parens_in_expressions(self):
self.assertEqual(f'{3,}', '(3,)')

Expand All @@ -939,13 +947,12 @@ def test_parens_in_expressions(self):
["f'{3)+(4}'",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_newlines_before_syntax_error(self):
self.assertAllRaise(SyntaxError,
"f-string: expecting a valid expression after '{'",
["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"])

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_backslashes_in_string_part(self):
self.assertEqual(f'\t', '\t')
self.assertEqual(r'\t', '\\t')
Expand Down Expand Up @@ -1004,7 +1011,7 @@ def test_backslashes_in_string_part(self):
self.assertEqual(fr'\N{AMPERSAND}', '\\Nspam')
self.assertEqual(f'\\\N{AMPERSAND}', '\\&')

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_misformed_unicode_character_name(self):
# These test are needed because unicode names are parsed
# differently inside f-strings.
Expand All @@ -1024,7 +1031,7 @@ def test_misformed_unicode_character_name(self):
r"'\N{GREEK CAPITAL LETTER DELTA'",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_backslashes_in_expression_part(self):
self.assertEqual(f"{(
1 +
Expand All @@ -1040,7 +1047,6 @@ def test_backslashes_in_expression_part(self):
["f'{\n}'",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
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.
Expand Down Expand Up @@ -1075,7 +1081,7 @@ def test_newlines_in_expressions(self):
self.assertEqual(rf'''{3+
4}''', '7')

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (<string>, line 1)"
def test_lambda(self):
x = 5
self.assertEqual(f'{(lambda y:x*y)("8")!r}', "'88888'")
Expand Down Expand Up @@ -1118,7 +1124,6 @@ def test_roundtrip_raw_quotes(self):
self.assertEqual(fr'\'\"\'', '\\\'\\"\\\'')
self.assertEqual(fr'\"\'\"\'', '\\"\\\'\\"\\\'')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_fstring_backslash_before_double_bracket(self):
deprecated_cases = [
(r"f'\{{\}}'", '\\{\\}'),
Expand All @@ -1138,7 +1143,6 @@ def test_fstring_backslash_before_double_bracket(self):
self.assertEqual(fr'\}}{1+1}', '\\}2')
self.assertEqual(fr'{1+1}\}}', '2\\}')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_fstring_backslash_before_double_bracket_warns_once(self):
with self.assertWarns(SyntaxWarning) as w:
eval(r"f'\{{'")
Expand Down Expand Up @@ -1288,6 +1292,7 @@ def test_nested_fstrings(self):
self.assertEqual(f'{f"{0}"*3}', '000')
self.assertEqual(f'{f"{y}"*3}', '555')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_invalid_string_prefixes(self):
single_quote_cases = ["fu''",
"uf''",
Expand All @@ -1312,7 +1317,7 @@ def test_invalid_string_prefixes(self):
"Bf''",
"BF''",]
double_quote_cases = [case.replace("'", '"') for case in single_quote_cases]
self.assertAllRaise(SyntaxError, 'invalid syntax',
self.assertAllRaise(SyntaxError, 'prefixes are incompatible',
single_quote_cases + double_quote_cases)

def test_leading_trailing_spaces(self):
Expand Down Expand Up @@ -1342,7 +1347,7 @@ def test_equal_equal(self):

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

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_conversions(self):
self.assertEqual(f'{3.14:10.10}', ' 3.14')
self.assertEqual(f'{1.25!s:10.10}', '1.25 ')
Expand All @@ -1367,7 +1372,6 @@ def test_conversions(self):
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
["f'{3!'",
"f'{3!s'",
"f'{3!g'",
])

self.assertAllRaise(SyntaxError, 'f-string: missing conversion character',
Expand Down Expand Up @@ -1408,14 +1412,13 @@ def test_assignment(self):
"f'{x}' = x",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_del(self):
self.assertAllRaise(SyntaxError, 'invalid syntax',
["del f''",
"del '' f''",
])

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mismatched_braces(self):
self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed",
["f'{{}'",
Expand Down Expand Up @@ -1514,7 +1517,6 @@ def test_str_format_differences(self):
self.assertEqual('{d[a]}'.format(d=d), 'string')
self.assertEqual('{d[0]}'.format(d=d), 'integer')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_errors(self):
# see issue 26287
self.assertAllRaise(TypeError, 'unsupported',
Expand Down Expand Up @@ -1557,7 +1559,6 @@ def test_backslash_char(self):
self.assertEqual(eval('f"\\\n"'), '')
self.assertEqual(eval('f"\\\r"'), '')

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '1+2 = # my comment\n 3' != '1+2 = \n 3'
def test_debug_conversion(self):
x = 'A string'
self.assertEqual(f'{x=}', 'x=' + repr(x))
Expand Down Expand Up @@ -1705,7 +1706,7 @@ def test_walrus(self):
self.assertEqual(f'{(x:=10)}', '10')
self.assertEqual(x, 10)

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting '=', or '!', or ':', or '}'" does not match "invalid syntax (?, line 1)"
def test_invalid_syntax_error_message(self):
with self.assertRaisesRegex(SyntaxError,
"f-string: expecting '=', or '!', or ':', or '}'"):
Expand All @@ -1731,7 +1732,7 @@ def test_with_an_underscore_and_a_comma_in_format_specifier(self):
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:_,}'

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (?, line 1)"
def test_syntax_error_for_starred_expressions(self):
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
compile("f'{*a}'", "?", "exec")
Expand All @@ -1740,7 +1741,7 @@ def test_syntax_error_for_starred_expressions(self):
"f-string: expecting a valid expression after '{'"):
compile("f'{**a}'", "?", "exec")

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON; -
def test_not_closing_quotes(self):
self.assertAllRaise(SyntaxError, "unterminated f-string literal", ['f"', "f'"])
self.assertAllRaise(SyntaxError, "unterminated triple-quoted f-string literal",
Expand All @@ -1760,7 +1761,7 @@ def test_not_closing_quotes(self):
except SyntaxError as e:
self.assertEqual(e.text, 'z = f"""')
self.assertEqual(e.lineno, 3)
@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntax_error_after_debug(self):
self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'",
[
Expand Down Expand Up @@ -1788,7 +1789,6 @@ def test_debug_in_file(self):
self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'),
"3\n=3")

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntax_warning_infinite_recursion_in_file(self):
with temp_cwd():
script = 'script.py'
Expand Down Expand Up @@ -1878,6 +1878,13 @@ def __format__(self, format):
# Test multiple format specs in same raw f-string
self.assertEqual(rf"{UnchangedFormat():\xFF} {UnchangedFormat():\n}", '\\xFF \\n')

def test_gh139516(self):
with temp_cwd():
script = 'script.py'
with open(script, 'wb') as f:
f.write('''def f(a): pass\nf"{f(a=lambda: 'à'\n)}"'''.encode())
assert_python_ok(script)


if __name__ == '__main__':
unittest.main()
28 changes: 27 additions & 1 deletion crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8450,7 +8450,12 @@ impl Compiler {
if let Some(ast::DebugText { leading, trailing }) = &fstring_expr.debug_text {
let range = fstring_expr.expression.range();
let source = self.source_file.slice(range);
let text = [leading, source, trailing].concat();
let text = [
strip_fstring_debug_comments(leading).as_str(),
source,
strip_fstring_debug_comments(trailing).as_str(),
]
.concat();

self.emit_load_const(ConstantData::Str { value: text.into() });
element_count += 1;
Expand Down Expand Up @@ -8786,6 +8791,27 @@ impl ToU32 for usize {
}
}

/// Strip Python comments from f-string debug text (leading/trailing around `=`).
/// A comment starts with `#` and extends to the end of the line.
/// The newline character itself is preserved.
fn strip_fstring_debug_comments(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut in_comment = false;
for ch in text.chars() {
if in_comment {
if ch == '\n' {
in_comment = false;
result.push(ch);
}
} else if ch == '#' {
in_comment = true;
} else {
result.push(ch);
}
}
result
}

#[cfg(test)]
mod ruff_tests {
use super::*;
Expand Down
Loading
Loading