mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
Closes issue 27921: Disallow backslashes anywhere in f-strings. This is a temporary restriction. In 3.6 beta 2, the plan is to again allow backslashes in the string parts of f-strings, but disallow them in the expression parts.
This commit is contained in:
parent
3b09cd64e0
commit
6a4efce7a5
6 changed files with 69 additions and 98 deletions
|
@ -280,6 +280,6 @@ class saved_test_environment:
|
||||||
print(f"Warning -- {name} was modified by {self.testname}",
|
print(f"Warning -- {name} was modified by {self.testname}",
|
||||||
file=sys.stderr, flush=True)
|
file=sys.stderr, flush=True)
|
||||||
if self.verbose > 1:
|
if self.verbose > 1:
|
||||||
print(f" Before: {original}\n After: {current} ",
|
print(f" Before: {original}""\n"f" After: {current} ",
|
||||||
file=sys.stderr, flush=True)
|
file=sys.stderr, flush=True)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -96,30 +96,6 @@ f'{a * x()}'"""
|
||||||
self.assertEqual(f'', '')
|
self.assertEqual(f'', '')
|
||||||
self.assertEqual(f'a', 'a')
|
self.assertEqual(f'a', 'a')
|
||||||
self.assertEqual(f' ', ' ')
|
self.assertEqual(f' ', ' ')
|
||||||
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}',
|
|
||||||
'\N{GREEK CAPITAL LETTER DELTA}')
|
|
||||||
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}',
|
|
||||||
'\u0394')
|
|
||||||
self.assertEqual(f'\N{True}', '\u22a8')
|
|
||||||
self.assertEqual(rf'\N{True}', r'\NTrue')
|
|
||||||
|
|
||||||
def test_escape_order(self):
|
|
||||||
# note that hex(ord('{')) == 0x7b, so this
|
|
||||||
# string becomes f'a{4*10}b'
|
|
||||||
self.assertEqual(f'a\u007b4*10}b', 'a40b')
|
|
||||||
self.assertEqual(f'a\x7b4*10}b', 'a40b')
|
|
||||||
self.assertEqual(f'a\x7b4*10\N{RIGHT CURLY BRACKET}b', 'a40b')
|
|
||||||
self.assertEqual(f'{"a"!\N{LATIN SMALL LETTER R}}', "'a'")
|
|
||||||
self.assertEqual(f'{10\x3a02X}', '0A')
|
|
||||||
self.assertEqual(f'{10:02\N{LATIN CAPITAL LETTER X}}', '0A')
|
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed",
|
|
||||||
[r"""f'a{\u007b4*10}b'""", # mis-matched brackets
|
|
||||||
])
|
|
||||||
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation character',
|
|
||||||
[r"""f'{"a"\!r}'""",
|
|
||||||
r"""f'{a\!r}'""",
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_unterminated_string(self):
|
def test_unterminated_string(self):
|
||||||
self.assertAllRaise(SyntaxError, 'f-string: unterminated string',
|
self.assertAllRaise(SyntaxError, 'f-string: unterminated string',
|
||||||
|
@ -285,8 +261,6 @@ f'{a * x()}'"""
|
||||||
"f'{ !r}'",
|
"f'{ !r}'",
|
||||||
"f'{10:{ }}'",
|
"f'{10:{ }}'",
|
||||||
"f' { } '",
|
"f' { } '",
|
||||||
r"f'{\n}'",
|
|
||||||
r"f'{\n \n}'",
|
|
||||||
|
|
||||||
# Catch the empty expression before the
|
# Catch the empty expression before the
|
||||||
# invalid conversion.
|
# invalid conversion.
|
||||||
|
@ -328,24 +302,61 @@ f'{a * x()}'"""
|
||||||
["f'{\n}'",
|
["f'{\n}'",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def test_no_backslashes(self):
|
||||||
|
# See issue 27921
|
||||||
|
|
||||||
|
# These should work, but currently don't
|
||||||
|
self.assertAllRaise(SyntaxError, 'backslashes not allowed',
|
||||||
|
[r"f'\t'",
|
||||||
|
r"f'{2}\t'",
|
||||||
|
r"f'{2}\t{3}'",
|
||||||
|
r"f'\t{3}'",
|
||||||
|
|
||||||
|
r"f'\N{GREEK CAPITAL LETTER DELTA}'",
|
||||||
|
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}'",
|
||||||
|
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}'",
|
||||||
|
r"f'\N{GREEK CAPITAL LETTER DELTA}{3}'",
|
||||||
|
|
||||||
|
r"f'\u0394'",
|
||||||
|
r"f'{2}\u0394'",
|
||||||
|
r"f'{2}\u0394{3}'",
|
||||||
|
r"f'\u0394{3}'",
|
||||||
|
|
||||||
|
r"f'\U00000394'",
|
||||||
|
r"f'{2}\U00000394'",
|
||||||
|
r"f'{2}\U00000394{3}'",
|
||||||
|
r"f'\U00000394{3}'",
|
||||||
|
|
||||||
|
r"f'\x20'",
|
||||||
|
r"f'{2}\x20'",
|
||||||
|
r"f'{2}\x20{3}'",
|
||||||
|
r"f'\x20{3}'",
|
||||||
|
|
||||||
|
r"f'2\x20'",
|
||||||
|
r"f'2\x203'",
|
||||||
|
r"f'2\x203'",
|
||||||
|
])
|
||||||
|
|
||||||
|
# And these don't work now, and shouldn't work in the future.
|
||||||
|
self.assertAllRaise(SyntaxError, 'backslashes not allowed',
|
||||||
|
[r"f'{\'a\'}'",
|
||||||
|
r"f'{\t3}'",
|
||||||
|
])
|
||||||
|
|
||||||
|
# add this when backslashes are allowed again. see issue 27921
|
||||||
|
# these test will be needed because unicode names will be parsed
|
||||||
|
# differently once backslashes are allowed inside expressions
|
||||||
|
## def test_misformed_unicode_character_name(self):
|
||||||
|
## self.assertAllRaise(SyntaxError, 'xx',
|
||||||
|
## [r"f'\N'",
|
||||||
|
## [r"f'\N{'",
|
||||||
|
## [r"f'\N{GREEK CAPITAL LETTER DELTA'",
|
||||||
|
## ])
|
||||||
|
|
||||||
def test_newlines_in_expressions(self):
|
def test_newlines_in_expressions(self):
|
||||||
self.assertEqual(f'{0}', '0')
|
self.assertEqual(f'{0}', '0')
|
||||||
self.assertEqual(f'{0\n}', '0')
|
|
||||||
self.assertEqual(f'{0\r}', '0')
|
|
||||||
self.assertEqual(f'{\n0\n}', '0')
|
|
||||||
self.assertEqual(f'{\r0\r}', '0')
|
|
||||||
self.assertEqual(f'{\n0\r}', '0')
|
|
||||||
self.assertEqual(f'{\n0}', '0')
|
|
||||||
self.assertEqual(f'{3+\n4}', '7')
|
|
||||||
self.assertEqual(f'{3+\\\n4}', '7')
|
|
||||||
self.assertEqual(rf'''{3+
|
self.assertEqual(rf'''{3+
|
||||||
4}''', '7')
|
4}''', '7')
|
||||||
self.assertEqual(f'''{3+\
|
|
||||||
4}''', '7')
|
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed',
|
|
||||||
[r"f'{\n}'",
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_lambda(self):
|
def test_lambda(self):
|
||||||
x = 5
|
x = 5
|
||||||
|
@ -380,9 +391,6 @@ f'{a * x()}'"""
|
||||||
def test_expressions_with_triple_quoted_strings(self):
|
def test_expressions_with_triple_quoted_strings(self):
|
||||||
self.assertEqual(f"{'''x'''}", 'x')
|
self.assertEqual(f"{'''x'''}", 'x')
|
||||||
self.assertEqual(f"{'''eric's'''}", "eric's")
|
self.assertEqual(f"{'''eric's'''}", "eric's")
|
||||||
self.assertEqual(f'{"""eric\'s"""}', "eric's")
|
|
||||||
self.assertEqual(f"{'''eric\"s'''}", 'eric"s')
|
|
||||||
self.assertEqual(f'{"""eric"s"""}', 'eric"s')
|
|
||||||
|
|
||||||
# Test concatenation within an expression
|
# Test concatenation within an expression
|
||||||
self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy')
|
self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy')
|
||||||
|
@ -484,10 +492,6 @@ f'{a * x()}'"""
|
||||||
y = 5
|
y = 5
|
||||||
self.assertEqual(f'{f"{0}"*3}', '000')
|
self.assertEqual(f'{f"{0}"*3}', '000')
|
||||||
self.assertEqual(f'{f"{y}"*3}', '555')
|
self.assertEqual(f'{f"{y}"*3}', '555')
|
||||||
self.assertEqual(f'{f"{\'x\'}"*3}', 'xxx')
|
|
||||||
|
|
||||||
self.assertEqual(f"{r'x' f'{\"s\"}'}", 'xs')
|
|
||||||
self.assertEqual(f"{r'x'rf'{\"s\"}'}", 'xs')
|
|
||||||
|
|
||||||
def test_invalid_string_prefixes(self):
|
def test_invalid_string_prefixes(self):
|
||||||
self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
|
self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
|
||||||
|
@ -510,24 +514,14 @@ f'{a * x()}'"""
|
||||||
def test_leading_trailing_spaces(self):
|
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'{\t3}', '3')
|
|
||||||
self.assertEqual(f'{\t\t3}', '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\t}', '3')
|
|
||||||
self.assertEqual(f'{3\t\t}', '3')
|
|
||||||
|
|
||||||
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}',
|
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}',
|
||||||
'expr={1: 2}')
|
'expr={1: 2}')
|
||||||
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]} }',
|
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]} }',
|
||||||
'expr={1: 2}')
|
'expr={1: 2}')
|
||||||
|
|
||||||
def test_character_name(self):
|
|
||||||
self.assertEqual(f'{4}\N{GREEK CAPITAL LETTER DELTA}{3}',
|
|
||||||
'4\N{GREEK CAPITAL LETTER DELTA}3')
|
|
||||||
self.assertEqual(f'{{}}\N{GREEK CAPITAL LETTER DELTA}{3}',
|
|
||||||
'{}\N{GREEK CAPITAL LETTER DELTA}3')
|
|
||||||
|
|
||||||
def test_not_equal(self):
|
def test_not_equal(self):
|
||||||
# There's a special test for this because there's a special
|
# 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
|
# case in the f-string parser to look for != as not ending an
|
||||||
|
@ -554,10 +548,6 @@ f'{a * x()}'"""
|
||||||
# Not a conversion, but show that ! is allowed in a format spec.
|
# 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.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"}', '\u0394')
|
|
||||||
self.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"!r}', "'\u0394'")
|
|
||||||
self.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"!a}', "'\\u0394'")
|
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character',
|
self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character',
|
||||||
["f'{3!g}'",
|
["f'{3!g}'",
|
||||||
"f'{3!A}'",
|
"f'{3!A}'",
|
||||||
|
@ -565,9 +555,7 @@ f'{a * x()}'"""
|
||||||
"f'{3!A}'",
|
"f'{3!A}'",
|
||||||
"f'{3!!}'",
|
"f'{3!!}'",
|
||||||
"f'{3!:}'",
|
"f'{3!:}'",
|
||||||
"f'{3!\N{GREEK CAPITAL LETTER DELTA}}'",
|
|
||||||
"f'{3! s}'", # no space before conversion char
|
"f'{3! s}'", # no space before conversion char
|
||||||
"f'{x!\\x00:.<10}'",
|
|
||||||
])
|
])
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
|
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
|
||||||
|
@ -600,7 +588,6 @@ f'{a * x()}'"""
|
||||||
|
|
||||||
# Can't have { or } in a format spec.
|
# Can't have { or } in a format spec.
|
||||||
"f'{3:}>10}'",
|
"f'{3:}>10}'",
|
||||||
r"f'{3:\\}>10}'",
|
|
||||||
"f'{3:}}>10}'",
|
"f'{3:}}>10}'",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -620,10 +607,6 @@ f'{a * x()}'"""
|
||||||
"f'{'",
|
"f'{'",
|
||||||
])
|
])
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, 'invalid syntax',
|
|
||||||
[r"f'{3:\\{>10}'",
|
|
||||||
])
|
|
||||||
|
|
||||||
# But these are just normal strings.
|
# But these are just normal strings.
|
||||||
self.assertEqual(f'{"{"}', '{')
|
self.assertEqual(f'{"{"}', '{')
|
||||||
self.assertEqual(f'{"}"}', '}')
|
self.assertEqual(f'{"}"}', '}')
|
||||||
|
@ -712,34 +695,11 @@ f'{a * x()}'"""
|
||||||
"'": 'squote',
|
"'": 'squote',
|
||||||
'foo': 'bar',
|
'foo': 'bar',
|
||||||
}
|
}
|
||||||
self.assertEqual(f'{d["\'"]}', 'squote')
|
|
||||||
self.assertEqual(f"{d['\"']}", 'dquote')
|
|
||||||
|
|
||||||
self.assertEqual(f'''{d["'"]}''', 'squote')
|
self.assertEqual(f'''{d["'"]}''', 'squote')
|
||||||
self.assertEqual(f"""{d['"']}""", 'dquote')
|
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')
|
||||||
self.assertEqual(f'{d[\'foo\']}', 'bar')
|
|
||||||
self.assertEqual(f"{d[\"foo\"]}", 'bar')
|
|
||||||
|
|
||||||
def test_escaped_quotes(self):
|
|
||||||
d = {'"': 'a',
|
|
||||||
"'": 'b'}
|
|
||||||
|
|
||||||
self.assertEqual(fr"{d['\"']}", 'a')
|
|
||||||
self.assertEqual(fr'{d["\'"]}', 'b')
|
|
||||||
self.assertEqual(fr"{'\"'}", '"')
|
|
||||||
self.assertEqual(fr'{"\'"}', "'")
|
|
||||||
self.assertEqual(f'{"\\"3"}', '"3')
|
|
||||||
|
|
||||||
self.assertAllRaise(SyntaxError, 'f-string: unterminated string',
|
|
||||||
[r'''f'{"""\\}' ''', # Backslash at end of expression
|
|
||||||
])
|
|
||||||
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation',
|
|
||||||
[r"rf'{3\}'",
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -138,10 +138,6 @@ class UnparseTestCase(ASTTestCase):
|
||||||
# See issue 25180
|
# See issue 25180
|
||||||
self.check_roundtrip(r"""f'{f"{0}"*3}'""")
|
self.check_roundtrip(r"""f'{f"{0}"*3}'""")
|
||||||
self.check_roundtrip(r"""f'{f"{y}"*3}'""")
|
self.check_roundtrip(r"""f'{f"{y}"*3}'""")
|
||||||
self.check_roundtrip(r"""f'{f"{\'x\'}"*3}'""")
|
|
||||||
|
|
||||||
self.check_roundtrip(r'''f"{r'x' f'{\"s\"}'}"''')
|
|
||||||
self.check_roundtrip(r'''f"{r'x'rf'{\"s\"}'}"''')
|
|
||||||
|
|
||||||
def test_del_statement(self):
|
def test_del_statement(self):
|
||||||
self.check_roundtrip("del x, y, z")
|
self.check_roundtrip("del x, y, z")
|
||||||
|
|
|
@ -402,7 +402,7 @@ class StackSummary(list):
|
||||||
count += 1
|
count += 1
|
||||||
else:
|
else:
|
||||||
if count > 3:
|
if count > 3:
|
||||||
result.append(f' [Previous line repeated {count-3} more times]\n')
|
result.append(f' [Previous line repeated {count-3} more times]''\n')
|
||||||
last_file = frame.filename
|
last_file = frame.filename
|
||||||
last_line = frame.lineno
|
last_line = frame.lineno
|
||||||
last_name = frame.name
|
last_name = frame.name
|
||||||
|
@ -419,7 +419,7 @@ class StackSummary(list):
|
||||||
row.append(' {name} = {value}\n'.format(name=name, value=value))
|
row.append(' {name} = {value}\n'.format(name=name, value=value))
|
||||||
result.append(''.join(row))
|
result.append(''.join(row))
|
||||||
if count > 3:
|
if count > 3:
|
||||||
result.append(f' [Previous line repeated {count-3} more times]\n')
|
result.append(f' [Previous line repeated {count-3} more times]''\n')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,11 @@ What's New in Python 3.6.0 beta 1
|
||||||
Core and Builtins
|
Core and Builtins
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
- Issue #27921: Disallow backslashes in f-strings. This is a temporary
|
||||||
|
restriction: in beta 2, backslashes will only be disallowed inside
|
||||||
|
the braces (where the expressions are). This is a breaking change
|
||||||
|
from the 3.6 alpha releases.
|
||||||
|
|
||||||
- Issue #27870: A left shift of zero by a large integer no longer attempts
|
- Issue #27870: A left shift of zero by a large integer no longer attempts
|
||||||
to allocate large amounts of memory.
|
to allocate large amounts of memory.
|
||||||
|
|
||||||
|
|
10
Python/ast.c
10
Python/ast.c
|
@ -4958,6 +4958,16 @@ parsestr(struct compiling *c, const node *n, int *bytesmode, int *fmode)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Temporary hack: if this is an f-string, no backslashes are allowed. */
|
||||||
|
/* See issue 27921. */
|
||||||
|
if (*fmode && strchr(s, '\\') != NULL) {
|
||||||
|
/* Syntax error. At a later date fix this so it only checks for
|
||||||
|
backslashes within the braces. */
|
||||||
|
ast_error(c, n, "backslashes not allowed in f-strings");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Avoid invoking escape decoding routines if possible. */
|
/* Avoid invoking escape decoding routines if possible. */
|
||||||
rawmode = rawmode || strchr(s, '\\') == NULL;
|
rawmode = rawmode || strchr(s, '\\') == NULL;
|
||||||
if (*bytesmode) {
|
if (*bytesmode) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue