Skip to content

Commit 45903ad

Browse files
authored
Merge pull request #62 from isidentical/opinioted-fixes
2 parents 4790764 + 51f80f4 commit 45903ad

File tree

3 files changed

+836
-528
lines changed

3 files changed

+836
-528
lines changed

Grammar/python.gram

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ yield_stmt[stmt_ty]: y=yield_expr { _PyAST_Expr(y, EXTRA) }
194194

195195
assert_stmt[stmt_ty]: 'assert' a=expression b=[',' z=expression { z }] { _PyAST_Assert(a, b, EXTRA) }
196196

197-
import_stmt[stmt_ty]:
197+
import_stmt[stmt_ty]:
198198
| invalid_import
199199
| import_name
200200
| import_from
@@ -415,8 +415,8 @@ try_stmt[stmt_ty]:
415415
| invalid_try_stmt
416416
| 'try' &&':' b=block f=finally_block { _PyAST_Try(b, NULL, NULL, f, EXTRA) }
417417
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_block+ el=[else_block] f=[finally_block] { _PyAST_Try(b, ex, el, f, EXTRA) }
418-
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_star_block+ el=[else_block] f=[finally_block] {
419-
CHECK_VERSION(stmt_ty, 11, "Exception groups are",
418+
| 'try' &&':' b=block ex[asdl_excepthandler_seq*]=except_star_block+ el=[else_block] f=[finally_block] {
419+
CHECK_VERSION(stmt_ty, 11, "Exception groups are",
420420
_PyAST_TryStar(b, ex, el, f, EXTRA)) }
421421

422422

@@ -1263,7 +1263,7 @@ invalid_group:
12631263
invalid_import:
12641264
| a='import' dotted_name 'from' dotted_name {
12651265
RAISE_SYNTAX_ERROR_STARTING_FROM(a, "Did you mean to use 'from ... import ...' instead?") }
1266-
1266+
12671267
invalid_import_from_targets:
12681268
| import_from_as_names ',' NEWLINE {
12691269
RAISE_SYNTAX_ERROR("trailing comma not allowed without surrounding parentheses") }
@@ -1362,3 +1362,7 @@ invalid_replacement_field:
13621362
| '{' a=':' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before ':'") }
13631363
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: expression required before '!'") }
13641364
| '{' a='}' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: empty expression not allowed") }
1365+
| '{' (yield_expr | star_expressions) "="? invalid_conversion_character
1366+
invalid_conversion_character:
1367+
| a="!" &(':'|'}') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: missed conversion character") }
1368+
| a="!" !NAME { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: invalid conversion character") }

Lib/test/test_fstring.py

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,50 @@ def test_ast_line_numbers_with_parentheses(self):
411411

412412
expr = """
413413
x = (
414+
u'wat',
415+
u"wat",
416+
b'wat',
417+
b"wat",
418+
f'wat',
419+
f"wat",
420+
)
421+
422+
y = (
423+
u'''wat''',
424+
u\"\"\"wat\"\"\",
425+
b'''wat''',
426+
b\"\"\"wat\"\"\",
427+
f'''wat''',
428+
f\"\"\"wat\"\"\",
429+
)
430+
"""
431+
t = ast.parse(expr)
432+
self.assertEqual(type(t), ast.Module)
433+
self.assertEqual(len(t.body), 2)
434+
x, y = t.body
435+
436+
# Check the single quoted string offsets first.
437+
offsets = [
438+
(elt.col_offset, elt.end_col_offset)
439+
for elt in x.value.elts
440+
]
441+
self.assertTrue(all(
442+
offset == (4, 10)
443+
for offset in offsets
444+
))
445+
446+
# Check the triple quoted string offsets.
447+
offsets = [
448+
(elt.col_offset, elt.end_col_offset)
449+
for elt in y.value.elts
450+
]
451+
self.assertTrue(all(
452+
offset == (4, 14)
453+
for offset in offsets
454+
))
455+
456+
expr = """
457+
x = (
414458
'PERL_MM_OPT', (
415459
f'wat'
416460
f'some_string={f(x)} '
@@ -444,7 +488,11 @@ def test_ast_line_numbers_with_parentheses(self):
444488
self.assertEqual(wat2.lineno, 5)
445489
self.assertEqual(wat2.end_lineno, 6)
446490
self.assertEqual(wat2.col_offset, 32)
447-
self.assertEqual(wat2.end_col_offset, 18)
491+
# wat ends at the offset 17, but the whole f-string
492+
# ends at the offset 18 (since the quote is part of the
493+
# f-string but not the wat string)
494+
self.assertEqual(wat2.end_col_offset, 17)
495+
self.assertEqual(fstring.end_col_offset, 18)
448496

449497
def test_docstring(self):
450498
def f():
@@ -578,8 +626,14 @@ def test_compile_time_concat(self):
578626
self.assertEqual(f'' '' f'', '')
579627
self.assertEqual(f'' '' f'' '', '')
580628

629+
# This is not really [f'{'] + [f'}'] since we treat the inside
630+
# of braces as a purely new context, so it is actually f'{ and
631+
# then eval(' f') (a valid expression) and then }' which would
632+
# constitute a valid f-string.
633+
self.assertEqual(f'{' f'}', ' f')
634+
581635
self.assertAllRaise(SyntaxError, "expecting '}'",
582-
["f'{3' f'}'", # can't concat to get a valid f-string
636+
['''f'{3' f"}"''', # can't concat to get a valid f-string
583637
])
584638

585639
def test_comments(self):
@@ -743,10 +797,6 @@ def test_parens_in_expressions(self):
743797
["f'{3)+(4}'",
744798
])
745799

746-
self.assertAllRaise(SyntaxError, 'unterminated string literal',
747-
["f'{\n}'",
748-
])
749-
750800
def test_newlines_before_syntax_error(self):
751801
self.assertAllRaise(SyntaxError, "invalid syntax",
752802
["f'{.}'", "\nf'{.}'", "\n\nf'{.}'"])
@@ -825,18 +875,39 @@ def test_misformed_unicode_character_name(self):
825875
r"'\N{GREEK CAPITAL LETTER DELTA'",
826876
])
827877

828-
def test_no_backslashes_in_expression_part(self):
829-
self.assertAllRaise(SyntaxError, 'f-string expression part cannot include a backslash',
830-
[r"f'{\'a\'}'",
831-
r"f'{\t3}'",
832-
r"f'{\}'",
833-
r"rf'{\'a\'}'",
834-
r"rf'{\t3}'",
835-
r"rf'{\}'",
836-
r"""rf'{"\N{LEFT CURLY BRACKET}"}'""",
837-
r"f'{\n}'",
878+
def test_backslashes_in_expression_part(self):
879+
self.assertEqual(f"{(
880+
1 +
881+
2
882+
)}", "3")
883+
884+
self.assertEqual("\N{LEFT CURLY BRACKET}", '{')
885+
self.assertEqual(f'{"\N{LEFT CURLY BRACKET}"}', '{')
886+
self.assertEqual(rf'{"\N{LEFT CURLY BRACKET}"}', '{')
887+
888+
self.assertAllRaise(SyntaxError, 'empty expression not allowed',
889+
["f'{\n}'",
838890
])
839891

892+
def test_invalid_backslashes_inside_fstring_context(self):
893+
# All of these variations are invalid python syntax,
894+
# so they are also invalid in f-strings as well.
895+
cases = [
896+
formatting.format(expr=expr)
897+
for formatting in [
898+
"{expr}",
899+
"f'{{{expr}}}'",
900+
"rf'{{{expr}}}'",
901+
]
902+
for expr in [
903+
r"\'a\'",
904+
r"\t3",
905+
r"\\"[0],
906+
]
907+
]
908+
self.assertAllRaise(SyntaxError, 'unexpected character after line continuation',
909+
cases)
910+
840911
def test_no_escapes_for_braces(self):
841912
"""
842913
Only literal curly braces begin an expression.
@@ -1120,11 +1191,16 @@ def test_conversions(self):
11201191
"f'{3!:}'",
11211192
])
11221193

1123-
for conv in 'g', 'A', '3', 'G', '!', 'ä', ', ':
1194+
for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ':
11241195
self.assertAllRaise(SyntaxError,
11251196
"f-string: invalid conversion character %r: "
1126-
"expected 's', 'r', or 'a'" % conv,
1127-
["f'{3!" + conv + "}'"])
1197+
"expected 's', 'r', or 'a'" % conv_identifier,
1198+
["f'{3!" + conv_identifier + "}'"])
1199+
1200+
for conv_non_identifier in '3', '!':
1201+
self.assertAllRaise(SyntaxError,
1202+
"f-string: invalid conversion character",
1203+
["f'{3!" + conv_non_identifier + "}'"])
11281204

11291205
for conv in ' s', ' s ':
11301206
self.assertAllRaise(SyntaxError,

0 commit comments

Comments
 (0)