Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6a4efce

Browse files
committed
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.
1 parent 3b09cd6 commit 6a4efce

6 files changed

Lines changed: 69 additions & 98 deletions

File tree

Lib/test/libregrtest/save_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,6 @@ def __exit__(self, exc_type, exc_val, exc_tb):
280280
print(f"Warning -- {name} was modified by {self.testname}",
281281
file=sys.stderr, flush=True)
282282
if self.verbose > 1:
283-
print(f" Before: {original}\n After: {current} ",
283+
print(f" Before: {original}""\n"f" After: {current} ",
284284
file=sys.stderr, flush=True)
285285
return False

Lib/test/test_fstring.py

Lines changed: 51 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -96,30 +96,6 @@ def test_literal(self):
9696
self.assertEqual(f'', '')
9797
self.assertEqual(f'a', 'a')
9898
self.assertEqual(f' ', ' ')
99-
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}',
100-
'\N{GREEK CAPITAL LETTER DELTA}')
101-
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}',
102-
'\u0394')
103-
self.assertEqual(f'\N{True}', '\u22a8')
104-
self.assertEqual(rf'\N{True}', r'\NTrue')
105-
106-
def test_escape_order(self):
107-
# note that hex(ord('{')) == 0x7b, so this
108-
# string becomes f'a{4*10}b'
109-
self.assertEqual(f'a\u007b4*10}b', 'a40b')
110-
self.assertEqual(f'a\x7b4*10}b', 'a40b')
111-
self.assertEqual(f'a\x7b4*10\N{RIGHT CURLY BRACKET}b', 'a40b')
112-
self.assertEqual(f'{"a"!\N{LATIN SMALL LETTER R}}', "'a'")
113-
self.assertEqual(f'{10\x3a02X}', '0A')
114-
self.assertEqual(f'{10:02\N{LATIN CAPITAL LETTER X}}', '0A')
115-
116-
self.assertAllRaise(SyntaxError, "f-string: single '}' is not allowed",
117-
[r"""f'a{\u007b4*10}b'""", # mis-matched brackets
118-
])
119-
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation character',
120-
[r"""f'{"a"\!r}'""",
121-
r"""f'{a\!r}'""",
122-
])
12399

124100
def test_unterminated_string(self):
125101
self.assertAllRaise(SyntaxError, 'f-string: unterminated string',
@@ -285,8 +261,6 @@ def test_missing_expression(self):
285261
"f'{ !r}'",
286262
"f'{10:{ }}'",
287263
"f' { } '",
288-
r"f'{\n}'",
289-
r"f'{\n \n}'",
290264

291265
# Catch the empty expression before the
292266
# invalid conversion.
@@ -328,24 +302,61 @@ def test_parens_in_expressions(self):
328302
["f'{\n}'",
329303
])
330304

305+
def test_no_backslashes(self):
306+
# See issue 27921
307+
308+
# These should work, but currently don't
309+
self.assertAllRaise(SyntaxError, 'backslashes not allowed',
310+
[r"f'\t'",
311+
r"f'{2}\t'",
312+
r"f'{2}\t{3}'",
313+
r"f'\t{3}'",
314+
315+
r"f'\N{GREEK CAPITAL LETTER DELTA}'",
316+
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}'",
317+
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}'",
318+
r"f'\N{GREEK CAPITAL LETTER DELTA}{3}'",
319+
320+
r"f'\u0394'",
321+
r"f'{2}\u0394'",
322+
r"f'{2}\u0394{3}'",
323+
r"f'\u0394{3}'",
324+
325+
r"f'\U00000394'",
326+
r"f'{2}\U00000394'",
327+
r"f'{2}\U00000394{3}'",
328+
r"f'\U00000394{3}'",
329+
330+
r"f'\x20'",
331+
r"f'{2}\x20'",
332+
r"f'{2}\x20{3}'",
333+
r"f'\x20{3}'",
334+
335+
r"f'2\x20'",
336+
r"f'2\x203'",
337+
r"f'2\x203'",
338+
])
339+
340+
# And these don't work now, and shouldn't work in the future.
341+
self.assertAllRaise(SyntaxError, 'backslashes not allowed',
342+
[r"f'{\'a\'}'",
343+
r"f'{\t3}'",
344+
])
345+
346+
# add this when backslashes are allowed again. see issue 27921
347+
# these test will be needed because unicode names will be parsed
348+
# differently once backslashes are allowed inside expressions
349+
## def test_misformed_unicode_character_name(self):
350+
## self.assertAllRaise(SyntaxError, 'xx',
351+
## [r"f'\N'",
352+
## [r"f'\N{'",
353+
## [r"f'\N{GREEK CAPITAL LETTER DELTA'",
354+
## ])
355+
331356
def test_newlines_in_expressions(self):
332357
self.assertEqual(f'{0}', '0')
333-
self.assertEqual(f'{0\n}', '0')
334-
self.assertEqual(f'{0\r}', '0')
335-
self.assertEqual(f'{\n0\n}', '0')
336-
self.assertEqual(f'{\r0\r}', '0')
337-
self.assertEqual(f'{\n0\r}', '0')
338-
self.assertEqual(f'{\n0}', '0')
339-
self.assertEqual(f'{3+\n4}', '7')
340-
self.assertEqual(f'{3+\\\n4}', '7')
341358
self.assertEqual(rf'''{3+
342359
4}''', '7')
343-
self.assertEqual(f'''{3+\
344-
4}''', '7')
345-
346-
self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed',
347-
[r"f'{\n}'",
348-
])
349360

350361
def test_lambda(self):
351362
x = 5
@@ -380,9 +391,6 @@ def fn(x):
380391
def test_expressions_with_triple_quoted_strings(self):
381392
self.assertEqual(f"{'''x'''}", 'x')
382393
self.assertEqual(f"{'''eric's'''}", "eric's")
383-
self.assertEqual(f'{"""eric\'s"""}', "eric's")
384-
self.assertEqual(f"{'''eric\"s'''}", 'eric"s')
385-
self.assertEqual(f'{"""eric"s"""}', 'eric"s')
386394

387395
# Test concatenation within an expression
388396
self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy')
@@ -484,10 +492,6 @@ def test_nested_fstrings(self):
484492
y = 5
485493
self.assertEqual(f'{f"{0}"*3}', '000')
486494
self.assertEqual(f'{f"{y}"*3}', '555')
487-
self.assertEqual(f'{f"{\'x\'}"*3}', 'xxx')
488-
489-
self.assertEqual(f"{r'x' f'{\"s\"}'}", 'xs')
490-
self.assertEqual(f"{r'x'rf'{\"s\"}'}", 'xs')
491495

492496
def test_invalid_string_prefixes(self):
493497
self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing',
@@ -510,24 +514,14 @@ def test_invalid_string_prefixes(self):
510514
def test_leading_trailing_spaces(self):
511515
self.assertEqual(f'{ 3}', '3')
512516
self.assertEqual(f'{ 3}', '3')
513-
self.assertEqual(f'{\t3}', '3')
514-
self.assertEqual(f'{\t\t3}', '3')
515517
self.assertEqual(f'{3 }', '3')
516518
self.assertEqual(f'{3 }', '3')
517-
self.assertEqual(f'{3\t}', '3')
518-
self.assertEqual(f'{3\t\t}', '3')
519519

520520
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}',
521521
'expr={1: 2}')
522522
self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]} }',
523523
'expr={1: 2}')
524524

525-
def test_character_name(self):
526-
self.assertEqual(f'{4}\N{GREEK CAPITAL LETTER DELTA}{3}',
527-
'4\N{GREEK CAPITAL LETTER DELTA}3')
528-
self.assertEqual(f'{{}}\N{GREEK CAPITAL LETTER DELTA}{3}',
529-
'{}\N{GREEK CAPITAL LETTER DELTA}3')
530-
531525
def test_not_equal(self):
532526
# There's a special test for this because there's a special
533527
# case in the f-string parser to look for != as not ending an
@@ -554,20 +548,14 @@ def test_conversions(self):
554548
# Not a conversion, but show that ! is allowed in a format spec.
555549
self.assertEqual(f'{3.14:!<10.10}', '3.14!!!!!!')
556550

557-
self.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"}', '\u0394')
558-
self.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"!r}', "'\u0394'")
559-
self.assertEqual(f'{"\N{GREEK CAPITAL LETTER DELTA}"!a}', "'\\u0394'")
560-
561551
self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character',
562552
["f'{3!g}'",
563553
"f'{3!A}'",
564554
"f'{3!A}'",
565555
"f'{3!A}'",
566556
"f'{3!!}'",
567557
"f'{3!:}'",
568-
"f'{3!\N{GREEK CAPITAL LETTER DELTA}}'",
569558
"f'{3! s}'", # no space before conversion char
570-
"f'{x!\\x00:.<10}'",
571559
])
572560

573561
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
@@ -600,7 +588,6 @@ def test_mismatched_braces(self):
600588

601589
# Can't have { or } in a format spec.
602590
"f'{3:}>10}'",
603-
r"f'{3:\\}>10}'",
604591
"f'{3:}}>10}'",
605592
])
606593

@@ -620,10 +607,6 @@ def test_mismatched_braces(self):
620607
"f'{'",
621608
])
622609

623-
self.assertAllRaise(SyntaxError, 'invalid syntax',
624-
[r"f'{3:\\{>10}'",
625-
])
626-
627610
# But these are just normal strings.
628611
self.assertEqual(f'{"{"}', '{')
629612
self.assertEqual(f'{"}"}', '}')
@@ -712,34 +695,11 @@ def test_dict(self):
712695
"'": 'squote',
713696
'foo': 'bar',
714697
}
715-
self.assertEqual(f'{d["\'"]}', 'squote')
716-
self.assertEqual(f"{d['\"']}", 'dquote')
717-
718698
self.assertEqual(f'''{d["'"]}''', 'squote')
719699
self.assertEqual(f"""{d['"']}""", 'dquote')
720700

721701
self.assertEqual(f'{d["foo"]}', 'bar')
722702
self.assertEqual(f"{d['foo']}", 'bar')
723-
self.assertEqual(f'{d[\'foo\']}', 'bar')
724-
self.assertEqual(f"{d[\"foo\"]}", 'bar')
725-
726-
def test_escaped_quotes(self):
727-
d = {'"': 'a',
728-
"'": 'b'}
729-
730-
self.assertEqual(fr"{d['\"']}", 'a')
731-
self.assertEqual(fr'{d["\'"]}', 'b')
732-
self.assertEqual(fr"{'\"'}", '"')
733-
self.assertEqual(fr'{"\'"}', "'")
734-
self.assertEqual(f'{"\\"3"}', '"3')
735-
736-
self.assertAllRaise(SyntaxError, 'f-string: unterminated string',
737-
[r'''f'{"""\\}' ''', # Backslash at end of expression
738-
])
739-
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation',
740-
[r"rf'{3\}'",
741-
])
742-
743703

744704
if __name__ == '__main__':
745705
unittest.main()

Lib/test/test_tools/test_unparse.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,6 @@ def test_fstrings(self):
138138
# See issue 25180
139139
self.check_roundtrip(r"""f'{f"{0}"*3}'""")
140140
self.check_roundtrip(r"""f'{f"{y}"*3}'""")
141-
self.check_roundtrip(r"""f'{f"{\'x\'}"*3}'""")
142-
143-
self.check_roundtrip(r'''f"{r'x' f'{\"s\"}'}"''')
144-
self.check_roundtrip(r'''f"{r'x'rf'{\"s\"}'}"''')
145141

146142
def test_del_statement(self):
147143
self.check_roundtrip("del x, y, z")

Lib/traceback.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def format(self):
402402
count += 1
403403
else:
404404
if count > 3:
405-
result.append(f' [Previous line repeated {count-3} more times]\n')
405+
result.append(f' [Previous line repeated {count-3} more times]''\n')
406406
last_file = frame.filename
407407
last_line = frame.lineno
408408
last_name = frame.name
@@ -419,7 +419,7 @@ def format(self):
419419
row.append(' {name} = {value}\n'.format(name=name, value=value))
420420
result.append(''.join(row))
421421
if count > 3:
422-
result.append(f' [Previous line repeated {count-3} more times]\n')
422+
result.append(f' [Previous line repeated {count-3} more times]''\n')
423423
return result
424424

425425

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ What's New in Python 3.6.0 beta 1
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #27921: Disallow backslashes in f-strings. This is a temporary
14+
restriction: in beta 2, backslashes will only be disallowed inside
15+
the braces (where the expressions are). This is a breaking change
16+
from the 3.6 alpha releases.
17+
1318
- Issue #27870: A left shift of zero by a large integer no longer attempts
1419
to allocate large amounts of memory.
1520

Python/ast.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4958,6 +4958,16 @@ parsestr(struct compiling *c, const node *n, int *bytesmode, int *fmode)
49584958
return NULL;
49594959
}
49604960
}
4961+
4962+
/* Temporary hack: if this is an f-string, no backslashes are allowed. */
4963+
/* See issue 27921. */
4964+
if (*fmode && strchr(s, '\\') != NULL) {
4965+
/* Syntax error. At a later date fix this so it only checks for
4966+
backslashes within the braces. */
4967+
ast_error(c, n, "backslashes not allowed in f-strings");
4968+
return NULL;
4969+
}
4970+
49614971
/* Avoid invoking escape decoding routines if possible. */
49624972
rawmode = rawmode || strchr(s, '\\') == NULL;
49634973
if (*bytesmode) {

0 commit comments

Comments
 (0)