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

Skip to content

Commit 451d0e3

Browse files
committed
Issue 27948: Allow backslashes in the literal string portion of f-strings, but not in the expressions. Also, require expressions to begin and end with literal curly braces.
1 parent 052828d commit 451d0e3

9 files changed

Lines changed: 329 additions & 349 deletions

File tree

Lib/http/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,7 +1060,7 @@ def _send_output(self, message_body=None, encode_chunked=False):
10601060

10611061
if encode_chunked and self._http_vsn == 11:
10621062
# chunked encoding
1063-
chunk = f'{len(chunk):X}''\r\n'.encode('ascii') + chunk \
1063+
chunk = f'{len(chunk):X}\r\n'.encode('ascii') + chunk \
10641064
+ b'\r\n'
10651065
self.send(chunk)
10661066

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"f" After: {current} ",
283+
print(f" Before: {original}\n After: {current} ",
284284
file=sys.stderr, flush=True)
285285
return False

Lib/test/test_faulthandler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,11 +735,11 @@ def test_raise_exception(self):
735735
('EXCEPTION_INT_DIVIDE_BY_ZERO', 'int divide by zero'),
736736
('EXCEPTION_STACK_OVERFLOW', 'stack overflow'),
737737
):
738-
self.check_windows_exception("""
738+
self.check_windows_exception(f"""
739739
import faulthandler
740740
faulthandler.enable()
741741
faulthandler._raise_exception(faulthandler._{exc})
742-
""".format(exc=exc),
742+
""",
743743
3,
744744
name)
745745

Lib/test/test_fstring.py

Lines changed: 86 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ def test_double_braces(self):
119119
self.assertEqual(f'a}}', 'a}')
120120
self.assertEqual(f'}}b', '}b')
121121
self.assertEqual(f'a}}b', 'a}b')
122+
self.assertEqual(f'{{}}', '{}')
123+
self.assertEqual(f'a{{}}', 'a{}')
124+
self.assertEqual(f'{{b}}', '{b}')
125+
self.assertEqual(f'{{}}c', '{}c')
126+
self.assertEqual(f'a{{b}}', 'a{b}')
127+
self.assertEqual(f'a{{}}c', 'a{}c')
128+
self.assertEqual(f'{{b}}c', '{b}c')
129+
self.assertEqual(f'a{{b}}c', 'a{b}c')
122130

123131
self.assertEqual(f'{{{10}', '{10')
124132
self.assertEqual(f'}}{10}', '}10')
@@ -302,56 +310,79 @@ def test_parens_in_expressions(self):
302310
["f'{\n}'",
303311
])
304312

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'",
313+
def test_backslashes_in_string_part(self):
314+
self.assertEqual(f'\t', '\t')
315+
self.assertEqual(r'\t', '\\t')
316+
self.assertEqual(rf'\t', '\\t')
317+
self.assertEqual(f'{2}\t', '2\t')
318+
self.assertEqual(f'{2}\t{3}', '2\t3')
319+
self.assertEqual(f'\t{3}', '\t3')
320+
321+
self.assertEqual(f'\u0394', '\u0394')
322+
self.assertEqual(r'\u0394', '\\u0394')
323+
self.assertEqual(rf'\u0394', '\\u0394')
324+
self.assertEqual(f'{2}\u0394', '2\u0394')
325+
self.assertEqual(f'{2}\u0394{3}', '2\u03943')
326+
self.assertEqual(f'\u0394{3}', '\u03943')
327+
328+
self.assertEqual(f'\U00000394', '\u0394')
329+
self.assertEqual(r'\U00000394', '\\U00000394')
330+
self.assertEqual(rf'\U00000394', '\\U00000394')
331+
self.assertEqual(f'{2}\U00000394', '2\u0394')
332+
self.assertEqual(f'{2}\U00000394{3}', '2\u03943')
333+
self.assertEqual(f'\U00000394{3}', '\u03943')
334+
335+
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}', '\u0394')
336+
self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
337+
self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}', '2\u03943')
338+
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}{3}', '\u03943')
339+
self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
340+
self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}3', '2\u03943')
341+
self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}3', '\u03943')
342+
343+
self.assertEqual(f'\x20', ' ')
344+
self.assertEqual(r'\x20', '\\x20')
345+
self.assertEqual(rf'\x20', '\\x20')
346+
self.assertEqual(f'{2}\x20', '2 ')
347+
self.assertEqual(f'{2}\x20{3}', '2 3')
348+
self.assertEqual(f'\x20{3}', ' 3')
349+
350+
self.assertEqual(f'2\x20', '2 ')
351+
self.assertEqual(f'2\x203', '2 3')
352+
self.assertEqual(f'\x203', ' 3')
353+
354+
def test_misformed_unicode_character_name(self):
355+
# These test are needed because unicode names are parsed
356+
# differently inside f-strings.
357+
self.assertAllRaise(SyntaxError, r"\(unicode error\) 'unicodeescape' codec can't decode bytes in position .*: malformed \\N character escape",
358+
[r"f'\N'",
359+
r"f'\N{'",
360+
r"f'\N{GREEK CAPITAL LETTER DELTA'",
361+
362+
# Here are the non-f-string versions,
363+
# which should give the same errors.
364+
r"'\N'",
365+
r"'\N{'",
366+
r"'\N{GREEK CAPITAL LETTER DELTA'",
338367
])
339368

340-
# And these don't work now, and shouldn't work in the future.
341-
self.assertAllRaise(SyntaxError, 'backslashes not allowed',
369+
def test_no_backslashes_in_expression_part(self):
370+
self.assertAllRaise(SyntaxError, 'f-string expression part cannot include a backslash',
342371
[r"f'{\'a\'}'",
343372
r"f'{\t3}'",
373+
r"f'{\}'",
374+
r"rf'{\'a\'}'",
375+
r"rf'{\t3}'",
376+
r"rf'{\}'",
377+
r"""rf'{"\N{LEFT CURLY BRACKET}"}'""",
344378
])
345379

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-
## ])
380+
def test_no_escapes_for_braces(self):
381+
# \x7b is '{'. Make sure it doesn't start an expression.
382+
self.assertEqual(f'\x7b2}}', '{2}')
383+
self.assertEqual(f'\x7b2', '{2')
384+
self.assertEqual(f'\u007b2', '{2')
385+
self.assertEqual(f'\N{LEFT CURLY BRACKET}2\N{RIGHT CURLY BRACKET}', '{2}')
355386

356387
def test_newlines_in_expressions(self):
357388
self.assertEqual(f'{0}', '0')
@@ -509,6 +540,14 @@ def test_invalid_string_prefixes(self):
509540
"ruf''",
510541
"FUR''",
511542
"Fur''",
543+
"fb''",
544+
"fB''",
545+
"Fb''",
546+
"FB''",
547+
"bf''",
548+
"bF''",
549+
"Bf''",
550+
"BF''",
512551
])
513552

514553
def test_leading_trailing_spaces(self):
@@ -551,8 +590,8 @@ def test_conversions(self):
551590
self.assertAllRaise(SyntaxError, 'f-string: invalid conversion character',
552591
["f'{3!g}'",
553592
"f'{3!A}'",
554-
"f'{3!A}'",
555-
"f'{3!A}'",
593+
"f'{3!3}'",
594+
"f'{3!G}'",
556595
"f'{3!!}'",
557596
"f'{3!:}'",
558597
"f'{3! s}'", # no space before conversion char
@@ -601,6 +640,7 @@ def test_mismatched_braces(self):
601640
"f'{3!s:3'",
602641
"f'x{'",
603642
"f'x{x'",
643+
"f'{x'",
604644
"f'{3:s'",
605645
"f'{{{'",
606646
"f'{{}}{'",

Lib/test/test_tools/test_unparse.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,12 @@ def test_files(self):
285285
if test.support.verbose:
286286
print('Testing %s' % filename)
287287

288-
# it's very much a hack that I'm skipping these files, but
289-
# I can't figure out why they fail. I'll fix it when I
290-
# address issue #27948.
291-
if os.path.basename(filename) in ('test_fstring.py', 'test_traceback.py'):
288+
# Some f-strings are not correctly round-tripped by
289+
# Tools/parser/unparse.py. See issue 28002 for details.
290+
# We need to skip files that contain such f-strings.
291+
if os.path.basename(filename) in ('test_fstring.py', ):
292292
if test.support.verbose:
293-
print(f'Skipping {filename}: see issue 27921')
293+
print(f'Skipping {filename}: see issue 28002')
294294
continue
295295

296296
with self.subTest(filename=filename):

Lib/test/test_traceback.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,13 @@ def f():
326326
lineno_f = f.__code__.co_firstlineno
327327
result_f = (
328328
'Traceback (most recent call last):\n'
329-
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display''\n'
329+
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
330330
' f()\n'
331-
f' File "{__file__}", line {lineno_f+1}, in f''\n'
331+
f' File "{__file__}", line {lineno_f+1}, in f\n'
332332
' f()\n'
333-
f' File "{__file__}", line {lineno_f+1}, in f''\n'
333+
f' File "{__file__}", line {lineno_f+1}, in f\n'
334334
' f()\n'
335-
f' File "{__file__}", line {lineno_f+1}, in f''\n'
335+
f' File "{__file__}", line {lineno_f+1}, in f\n'
336336
' f()\n'
337337
# XXX: The following line changes depending on whether the tests
338338
# are run through the interactive interpreter or with -m
@@ -371,20 +371,20 @@ def g(count=10):
371371

372372
lineno_g = g.__code__.co_firstlineno
373373
result_g = (
374-
f' File "{__file__}", line {lineno_g+2}, in g''\n'
374+
f' File "{__file__}", line {lineno_g+2}, in g\n'
375375
' return g(count-1)\n'
376-
f' File "{__file__}", line {lineno_g+2}, in g''\n'
376+
f' File "{__file__}", line {lineno_g+2}, in g\n'
377377
' return g(count-1)\n'
378-
f' File "{__file__}", line {lineno_g+2}, in g''\n'
378+
f' File "{__file__}", line {lineno_g+2}, in g\n'
379379
' return g(count-1)\n'
380380
' [Previous line repeated 6 more times]\n'
381-
f' File "{__file__}", line {lineno_g+3}, in g''\n'
381+
f' File "{__file__}", line {lineno_g+3}, in g\n'
382382
' raise ValueError\n'
383383
'ValueError\n'
384384
)
385385
tb_line = (
386386
'Traceback (most recent call last):\n'
387-
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display''\n'
387+
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
388388
' g()\n'
389389
)
390390
expected = (tb_line + result_g).splitlines()
@@ -408,16 +408,16 @@ def h(count=10):
408408
lineno_h = h.__code__.co_firstlineno
409409
result_h = (
410410
'Traceback (most recent call last):\n'
411-
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display''\n'
411+
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
412412
' h()\n'
413-
f' File "{__file__}", line {lineno_h+2}, in h''\n'
413+
f' File "{__file__}", line {lineno_h+2}, in h\n'
414414
' return h(count-1)\n'
415-
f' File "{__file__}", line {lineno_h+2}, in h''\n'
415+
f' File "{__file__}", line {lineno_h+2}, in h\n'
416416
' return h(count-1)\n'
417-
f' File "{__file__}", line {lineno_h+2}, in h''\n'
417+
f' File "{__file__}", line {lineno_h+2}, in h\n'
418418
' return h(count-1)\n'
419419
' [Previous line repeated 6 more times]\n'
420-
f' File "{__file__}", line {lineno_h+3}, in h''\n'
420+
f' File "{__file__}", line {lineno_h+3}, in h\n'
421421
' g()\n'
422422
)
423423
expected = (result_h + result_g).splitlines()

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ What's New in Python 3.6.0 beta 1
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #27948: In f-strings, only allow backslashes inside the braces
14+
(where the expressions are). This is a breaking change from the 3.6
15+
alpha releases, where backslashes are allowed anywhere in an
16+
f-string. Also, require that expressions inside f-strings be
17+
enclosed within literal braces, and not escapes like
18+
f'\x7b"hi"\x7d'.
19+
1320
- Issue #28046: Remove platform-specific directories from sys.path.
1421

1522
- Issue #25758: Prevents zipimport from unnecessarily encoding a filename
@@ -56,11 +63,6 @@ Core and Builtins
5663
- Issue #27355: Removed support for Windows CE. It was never finished,
5764
and Windows CE is no longer a relevant platform for Python.
5865

59-
- Issue #27921: Disallow backslashes in f-strings. This is a temporary
60-
restriction: in beta 2, backslashes will only be disallowed inside
61-
the braces (where the expressions are). This is a breaking change
62-
from the 3.6 alpha releases.
63-
6466
- Implement PEP 523.
6567

6668
- Issue #27870: A left shift of zero by a large integer no longer attempts

0 commit comments

Comments
 (0)