diff options
-rw-r--r-- | Lib/test/libregrtest/save_env.py | 2 | ||||
-rw-r--r-- | Lib/test/test_fstring.py | 142 | ||||
-rw-r--r-- | Lib/test/test_tools/test_unparse.py | 4 | ||||
-rw-r--r-- | Lib/traceback.py | 4 | ||||
-rw-r--r-- | Misc/NEWS | 5 | ||||
-rw-r--r-- | Python/ast.c | 10 |
6 files changed, 69 insertions, 98 deletions
diff --git a/Lib/test/libregrtest/save_env.py b/Lib/test/libregrtest/save_env.py index 96ad3af..eefbc14 100644 --- a/Lib/test/libregrtest/save_env.py +++ b/Lib/test/libregrtest/save_env.py @@ -280,6 +280,6 @@ class saved_test_environment: print(f"Warning -- {name} was modified by {self.testname}", file=sys.stderr, flush=True) if self.verbose > 1: - print(f" Before: {original}\n After: {current} ", + print(f" Before: {original}""\n"f" After: {current} ", file=sys.stderr, flush=True) return False diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 905ae63..2ba1b21 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -96,30 +96,6 @@ f'{a * x()}'""" self.assertEqual(f'', '') self.assertEqual(f'a', 'a') 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): self.assertAllRaise(SyntaxError, 'f-string: unterminated string', @@ -285,8 +261,6 @@ f'{a * x()}'""" "f'{ !r}'", "f'{10:{ }}'", "f' { } '", - r"f'{\n}'", - r"f'{\n \n}'", # Catch the empty expression before the # invalid conversion. @@ -328,24 +302,61 @@ f'{a * x()}'""" ["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): 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+ 4}''', '7') - self.assertEqual(f'''{3+\ -4}''', '7') - - self.assertAllRaise(SyntaxError, 'f-string: empty expression not allowed', - [r"f'{\n}'", - ]) def test_lambda(self): x = 5 @@ -380,9 +391,6 @@ f'{a * x()}'""" def test_expressions_with_triple_quoted_strings(self): 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') # Test concatenation within an expression self.assertEqual(f'{"x" """eric"s""" "y"}', 'xeric"sy') @@ -484,10 +492,6 @@ f'{a * x()}'""" y = 5 self.assertEqual(f'{f"{0}"*3}', '000') 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): self.assertAllRaise(SyntaxError, 'unexpected EOF while parsing', @@ -510,24 +514,14 @@ f'{a * x()}'""" def test_leading_trailing_spaces(self): 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\t}', '3') - self.assertEqual(f'{3\t\t}', '3') self.assertEqual(f'expr={ {x: y for x, y in [(1, 2), ]}}', 'expr={1: 2}') self.assertEqual(f'expr={ {x: y for x, y in [(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): # 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 @@ -554,10 +548,6 @@ f'{a * x()}'""" # Not a conversion, but show that ! is allowed in a format spec. 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', ["f'{3!g}'", "f'{3!A}'", @@ -565,9 +555,7 @@ f'{a * x()}'""" "f'{3!A}'", "f'{3!!}'", "f'{3!:}'", - "f'{3!\N{GREEK CAPITAL LETTER DELTA}}'", "f'{3! s}'", # no space before conversion char - "f'{x!\\x00:.<10}'", ]) self.assertAllRaise(SyntaxError, "f-string: expecting '}'", @@ -600,7 +588,6 @@ f'{a * x()}'""" # Can't have { or } in a format spec. "f'{3:}>10}'", - r"f'{3:\\}>10}'", "f'{3:}}>10}'", ]) @@ -620,10 +607,6 @@ f'{a * x()}'""" "f'{'", ]) - self.assertAllRaise(SyntaxError, 'invalid syntax', - [r"f'{3:\\{>10}'", - ]) - # But these are just normal strings. self.assertEqual(f'{"{"}', '{') self.assertEqual(f'{"}"}', '}') @@ -712,34 +695,11 @@ f'{a * x()}'""" "'": 'squote', 'foo': 'bar', } - self.assertEqual(f'{d["\'"]}', 'squote') - self.assertEqual(f"{d['\"']}", 'dquote') - self.assertEqual(f'''{d["'"]}''', 'squote') 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') - - 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__': unittest.main() diff --git a/Lib/test/test_tools/test_unparse.py b/Lib/test/test_tools/test_unparse.py index d91ade9..4a903b6 100644 --- a/Lib/test/test_tools/test_unparse.py +++ b/Lib/test/test_tools/test_unparse.py @@ -138,10 +138,6 @@ class UnparseTestCase(ASTTestCase): # See issue 25180 self.check_roundtrip(r"""f'{f"{0}"*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): self.check_roundtrip("del x, y, z") diff --git a/Lib/traceback.py b/Lib/traceback.py index a1cb5fb..6fc6436 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -402,7 +402,7 @@ class StackSummary(list): count += 1 else: 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_line = frame.lineno last_name = frame.name @@ -419,7 +419,7 @@ class StackSummary(list): row.append(' {name} = {value}\n'.format(name=name, value=value)) result.append(''.join(row)) 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 @@ -10,6 +10,11 @@ What's New in Python 3.6.0 beta 1 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 to allocate large amounts of memory. diff --git a/Python/ast.c b/Python/ast.c index b56fadd..0f9c193 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -4958,6 +4958,16 @@ parsestr(struct compiling *c, const node *n, int *bytesmode, int *fmode) 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. */ rawmode = rawmode || strchr(s, '\\') == NULL; if (*bytesmode) { |