Commit 451d0e38 authored by Eric V. Smith's avatar Eric V. Smith

Issue 27948: Allow backslashes in the literal string portion of f-strings, but...

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.
parent 052828db
...@@ -1060,7 +1060,7 @@ class HTTPConnection: ...@@ -1060,7 +1060,7 @@ class HTTPConnection:
if encode_chunked and self._http_vsn == 11: if encode_chunked and self._http_vsn == 11:
# chunked encoding # chunked encoding
chunk = f'{len(chunk):X}''\r\n'.encode('ascii') + chunk \ chunk = f'{len(chunk):X}\r\n'.encode('ascii') + chunk \
+ b'\r\n' + b'\r\n'
self.send(chunk) self.send(chunk)
......
...@@ -280,6 +280,6 @@ class saved_test_environment: ...@@ -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"f" After: {current} ", print(f" Before: {original}\n After: {current} ",
file=sys.stderr, flush=True) file=sys.stderr, flush=True)
return False return False
...@@ -735,11 +735,11 @@ class FaultHandlerTests(unittest.TestCase): ...@@ -735,11 +735,11 @@ class FaultHandlerTests(unittest.TestCase):
('EXCEPTION_INT_DIVIDE_BY_ZERO', 'int divide by zero'), ('EXCEPTION_INT_DIVIDE_BY_ZERO', 'int divide by zero'),
('EXCEPTION_STACK_OVERFLOW', 'stack overflow'), ('EXCEPTION_STACK_OVERFLOW', 'stack overflow'),
): ):
self.check_windows_exception(""" self.check_windows_exception(f"""
import faulthandler import faulthandler
faulthandler.enable() faulthandler.enable()
faulthandler._raise_exception(faulthandler._{exc}) faulthandler._raise_exception(faulthandler._{exc})
""".format(exc=exc), """,
3, 3,
name) name)
......
...@@ -119,6 +119,14 @@ f'{a * x()}'""" ...@@ -119,6 +119,14 @@ f'{a * x()}'"""
self.assertEqual(f'a}}', 'a}') self.assertEqual(f'a}}', 'a}')
self.assertEqual(f'}}b', '}b') self.assertEqual(f'}}b', '}b')
self.assertEqual(f'a}}b', 'a}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'}}{10}', '}10')
...@@ -302,56 +310,79 @@ f'{a * x()}'""" ...@@ -302,56 +310,79 @@ f'{a * x()}'"""
["f'{\n}'", ["f'{\n}'",
]) ])
def test_no_backslashes(self): def test_backslashes_in_string_part(self):
# See issue 27921 self.assertEqual(f'\t', '\t')
self.assertEqual(r'\t', '\\t')
# These should work, but currently don't self.assertEqual(rf'\t', '\\t')
self.assertAllRaise(SyntaxError, 'backslashes not allowed', self.assertEqual(f'{2}\t', '2\t')
[r"f'\t'", self.assertEqual(f'{2}\t{3}', '2\t3')
r"f'{2}\t'", self.assertEqual(f'\t{3}', '\t3')
r"f'{2}\t{3}'",
r"f'\t{3}'", self.assertEqual(f'\u0394', '\u0394')
self.assertEqual(r'\u0394', '\\u0394')
r"f'\N{GREEK CAPITAL LETTER DELTA}'", self.assertEqual(rf'\u0394', '\\u0394')
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}'", self.assertEqual(f'{2}\u0394', '2\u0394')
r"f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}'", self.assertEqual(f'{2}\u0394{3}', '2\u03943')
r"f'\N{GREEK CAPITAL LETTER DELTA}{3}'", self.assertEqual(f'\u0394{3}', '\u03943')
r"f'\u0394'", self.assertEqual(f'\U00000394', '\u0394')
r"f'{2}\u0394'", self.assertEqual(r'\U00000394', '\\U00000394')
r"f'{2}\u0394{3}'", self.assertEqual(rf'\U00000394', '\\U00000394')
r"f'\u0394{3}'", self.assertEqual(f'{2}\U00000394', '2\u0394')
self.assertEqual(f'{2}\U00000394{3}', '2\u03943')
r"f'\U00000394'", self.assertEqual(f'\U00000394{3}', '\u03943')
r"f'{2}\U00000394'",
r"f'{2}\U00000394{3}'", self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}', '\u0394')
r"f'\U00000394{3}'", self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
self.assertEqual(f'{2}\N{GREEK CAPITAL LETTER DELTA}{3}', '2\u03943')
r"f'\x20'", self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}{3}', '\u03943')
r"f'{2}\x20'", self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}', '2\u0394')
r"f'{2}\x20{3}'", self.assertEqual(f'2\N{GREEK CAPITAL LETTER DELTA}3', '2\u03943')
r"f'\x20{3}'", self.assertEqual(f'\N{GREEK CAPITAL LETTER DELTA}3', '\u03943')
r"f'2\x20'", self.assertEqual(f'\x20', ' ')
r"f'2\x203'", self.assertEqual(r'\x20', '\\x20')
r"f'2\x203'", 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')
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{GREEK CAPITAL LETTER DELTA'",
# Here are the non-f-string versions,
# which should give the same errors.
r"'\N'",
r"'\N{'",
r"'\N{GREEK CAPITAL LETTER DELTA'",
]) ])
# And these don't work now, and shouldn't work in the future. def test_no_backslashes_in_expression_part(self):
self.assertAllRaise(SyntaxError, 'backslashes not allowed', self.assertAllRaise(SyntaxError, 'f-string expression part cannot include a backslash',
[r"f'{\'a\'}'", [r"f'{\'a\'}'",
r"f'{\t3}'", r"f'{\t3}'",
r"f'{\}'",
r"rf'{\'a\'}'",
r"rf'{\t3}'",
r"rf'{\}'",
r"""rf'{"\N{LEFT CURLY BRACKET}"}'""",
]) ])
# add this when backslashes are allowed again. see issue 27921 def test_no_escapes_for_braces(self):
# these test will be needed because unicode names will be parsed # \x7b is '{'. Make sure it doesn't start an expression.
# differently once backslashes are allowed inside expressions self.assertEqual(f'\x7b2}}', '{2}')
## def test_misformed_unicode_character_name(self): self.assertEqual(f'\x7b2', '{2')
## self.assertAllRaise(SyntaxError, 'xx', self.assertEqual(f'\u007b2', '{2')
## [r"f'\N'", self.assertEqual(f'\N{LEFT CURLY BRACKET}2\N{RIGHT CURLY BRACKET}', '{2}')
## [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')
...@@ -509,6 +540,14 @@ f'{a * x()}'""" ...@@ -509,6 +540,14 @@ f'{a * x()}'"""
"ruf''", "ruf''",
"FUR''", "FUR''",
"Fur''", "Fur''",
"fb''",
"fB''",
"Fb''",
"FB''",
"bf''",
"bF''",
"Bf''",
"BF''",
]) ])
def test_leading_trailing_spaces(self): def test_leading_trailing_spaces(self):
...@@ -551,8 +590,8 @@ f'{a * x()}'""" ...@@ -551,8 +590,8 @@ f'{a * x()}'"""
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}'",
"f'{3!A}'", "f'{3!3}'",
"f'{3!A}'", "f'{3!G}'",
"f'{3!!}'", "f'{3!!}'",
"f'{3!:}'", "f'{3!:}'",
"f'{3! s}'", # no space before conversion char "f'{3! s}'", # no space before conversion char
...@@ -601,6 +640,7 @@ f'{a * x()}'""" ...@@ -601,6 +640,7 @@ f'{a * x()}'"""
"f'{3!s:3'", "f'{3!s:3'",
"f'x{'", "f'x{'",
"f'x{x'", "f'x{x'",
"f'{x'",
"f'{3:s'", "f'{3:s'",
"f'{{{'", "f'{{{'",
"f'{{}}{'", "f'{{}}{'",
......
...@@ -285,12 +285,12 @@ class DirectoryTestCase(ASTTestCase): ...@@ -285,12 +285,12 @@ class DirectoryTestCase(ASTTestCase):
if test.support.verbose: if test.support.verbose:
print('Testing %s' % filename) print('Testing %s' % filename)
# it's very much a hack that I'm skipping these files, but # Some f-strings are not correctly round-tripped by
# I can't figure out why they fail. I'll fix it when I # Tools/parser/unparse.py. See issue 28002 for details.
# address issue #27948. # We need to skip files that contain such f-strings.
if os.path.basename(filename) in ('test_fstring.py', 'test_traceback.py'): if os.path.basename(filename) in ('test_fstring.py', ):
if test.support.verbose: if test.support.verbose:
print(f'Skipping {filename}: see issue 27921') print(f'Skipping {filename}: see issue 28002')
continue continue
with self.subTest(filename=filename): with self.subTest(filename=filename):
......
...@@ -326,13 +326,13 @@ class TracebackFormatTests(unittest.TestCase): ...@@ -326,13 +326,13 @@ class TracebackFormatTests(unittest.TestCase):
lineno_f = f.__code__.co_firstlineno lineno_f = f.__code__.co_firstlineno
result_f = ( result_f = (
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display''\n' f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
' f()\n' ' f()\n'
f' File "{__file__}", line {lineno_f+1}, in f''\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
f' File "{__file__}", line {lineno_f+1}, in f''\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
f' File "{__file__}", line {lineno_f+1}, in f''\n' f' File "{__file__}", line {lineno_f+1}, in f\n'
' f()\n' ' f()\n'
# XXX: The following line changes depending on whether the tests # XXX: The following line changes depending on whether the tests
# are run through the interactive interpreter or with -m # are run through the interactive interpreter or with -m
...@@ -371,20 +371,20 @@ class TracebackFormatTests(unittest.TestCase): ...@@ -371,20 +371,20 @@ class TracebackFormatTests(unittest.TestCase):
lineno_g = g.__code__.co_firstlineno lineno_g = g.__code__.co_firstlineno
result_g = ( result_g = (
f' File "{__file__}", line {lineno_g+2}, in g''\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
f' File "{__file__}", line {lineno_g+2}, in g''\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
f' File "{__file__}", line {lineno_g+2}, in g''\n' f' File "{__file__}", line {lineno_g+2}, in g\n'
' return g(count-1)\n' ' return g(count-1)\n'
' [Previous line repeated 6 more times]\n' ' [Previous line repeated 6 more times]\n'
f' File "{__file__}", line {lineno_g+3}, in g''\n' f' File "{__file__}", line {lineno_g+3}, in g\n'
' raise ValueError\n' ' raise ValueError\n'
'ValueError\n' 'ValueError\n'
) )
tb_line = ( tb_line = (
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display''\n' f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
' g()\n' ' g()\n'
) )
expected = (tb_line + result_g).splitlines() expected = (tb_line + result_g).splitlines()
...@@ -408,16 +408,16 @@ class TracebackFormatTests(unittest.TestCase): ...@@ -408,16 +408,16 @@ class TracebackFormatTests(unittest.TestCase):
lineno_h = h.__code__.co_firstlineno lineno_h = h.__code__.co_firstlineno
result_h = ( result_h = (
'Traceback (most recent call last):\n' 'Traceback (most recent call last):\n'
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display''\n' f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
' h()\n' ' h()\n'
f' File "{__file__}", line {lineno_h+2}, in h''\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
f' File "{__file__}", line {lineno_h+2}, in h''\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
f' File "{__file__}", line {lineno_h+2}, in h''\n' f' File "{__file__}", line {lineno_h+2}, in h\n'
' return h(count-1)\n' ' return h(count-1)\n'
' [Previous line repeated 6 more times]\n' ' [Previous line repeated 6 more times]\n'
f' File "{__file__}", line {lineno_h+3}, in h''\n' f' File "{__file__}", line {lineno_h+3}, in h\n'
' g()\n' ' g()\n'
) )
expected = (result_h + result_g).splitlines() expected = (result_h + result_g).splitlines()
......
...@@ -402,7 +402,7 @@ class StackSummary(list): ...@@ -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): ...@@ -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,13 @@ What's New in Python 3.6.0 beta 1 ...@@ -10,6 +10,13 @@ What's New in Python 3.6.0 beta 1
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #27948: In f-strings, only allow backslashes inside the braces
(where the expressions are). This is a breaking change from the 3.6
alpha releases, where backslashes are allowed anywhere in an
f-string. Also, require that expressions inside f-strings be
enclosed within literal braces, and not escapes like
f'\x7b"hi"\x7d'.
- Issue #28046: Remove platform-specific directories from sys.path. - Issue #28046: Remove platform-specific directories from sys.path.
- Issue #25758: Prevents zipimport from unnecessarily encoding a filename - Issue #25758: Prevents zipimport from unnecessarily encoding a filename
...@@ -56,11 +63,6 @@ Core and Builtins ...@@ -56,11 +63,6 @@ Core and Builtins
- Issue #27355: Removed support for Windows CE. It was never finished, - Issue #27355: Removed support for Windows CE. It was never finished,
and Windows CE is no longer a relevant platform for Python. and Windows CE is no longer a relevant platform for Python.
- 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.
- Implement PEP 523. - Implement PEP 523.
- 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
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment