Skip to content

Commit ae8145d

Browse files
f-strings: allow ':' and '!' to be used in the expression
1 parent ddc154a commit ae8145d

File tree

2 files changed

+49
-27
lines changed

2 files changed

+49
-27
lines changed

parser/src/fstring.rs

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub enum FStringError {
1818
UnopenedRbrace,
1919
InvalidExpression,
2020
InvalidConversionFlag,
21+
EmptyExpression,
22+
MismatchedDelimiter,
2123
}
2224

2325
impl From<FStringError> for LalrpopError<Location, Tok, LexicalError> {
@@ -42,12 +44,12 @@ impl<'a> FStringParser<'a> {
4244
fn parse_formatted_value(&mut self) -> Result<StringGroup, FStringError> {
4345
let mut expression = String::new();
4446
let mut spec = String::new();
45-
let mut depth = 0;
47+
let mut delims = Vec::new();
4648
let mut conversion = None;
4749

4850
while let Some(ch) = self.chars.next() {
4951
match ch {
50-
'!' if depth == 0 => {
52+
'!' if delims.is_empty() => {
5153
conversion = Some(match self.chars.next() {
5254
Some('s') => ConversionFlag::Str,
5355
Some('a') => ConversionFlag::Ascii,
@@ -60,7 +62,7 @@ impl<'a> FStringParser<'a> {
6062
}
6163
})
6264
}
63-
':' if depth == 0 => {
65+
':' if delims.is_empty() => {
6466
while let Some(&next) = self.chars.peek() {
6567
if next != '}' {
6668
spec.push(next);
@@ -70,31 +72,47 @@ impl<'a> FStringParser<'a> {
7072
}
7173
}
7274
}
73-
'{' => {
74-
if let Some('{') = self.chars.peek() {
75-
expression.push_str("{{");
76-
self.chars.next();
77-
} else {
78-
expression.push('{');
79-
depth += 1;
75+
'(' | '{' | '[' => {
76+
expression.push(ch);
77+
delims.push(ch);
78+
}
79+
')' => {
80+
if delims.pop() != Some('(') {
81+
return Err(MismatchedDelimiter);
8082
}
83+
expression.push(ch);
84+
}
85+
']' => {
86+
if delims.pop() != Some('[') {
87+
return Err(MismatchedDelimiter);
88+
}
89+
expression.push(ch);
90+
}
91+
'}' if !delims.is_empty() => {
92+
if delims.pop() != Some('{') {
93+
return Err(MismatchedDelimiter);
94+
}
95+
expression.push(ch);
8196
}
8297
'}' => {
83-
if let Some('}') = self.chars.peek() {
84-
expression.push_str("}}");
85-
self.chars.next();
86-
} else if depth > 0 {
87-
expression.push('}');
88-
depth -= 1;
89-
} else {
90-
return Ok(FormattedValue {
91-
value: Box::new(
92-
parse_expression(expression.trim())
93-
.map_err(|_| InvalidExpression)?,
94-
),
95-
conversion,
96-
spec,
97-
});
98+
if expression.is_empty() {
99+
return Err(EmptyExpression);
100+
}
101+
return Ok(FormattedValue {
102+
value: Box::new(
103+
parse_expression(expression.trim()).map_err(|_| InvalidExpression)?,
104+
),
105+
conversion,
106+
spec,
107+
});
108+
}
109+
'"' | '\'' => {
110+
expression.push(ch);
111+
while let Some(next) = self.chars.next() {
112+
expression.push(next);
113+
if next == ch {
114+
break;
115+
}
98116
}
99117
}
100118
_ => {

tests/snippets/fstrings.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111
assert f'{f"{{"}' == '{'
1212
assert f'{f"}}"}' == '}'
1313
assert f'{foo}' f"{foo}" 'foo' == 'barbarfoo'
14-
#assert f'{"!:"}' == '!:'
15-
#assert f"{1 != 2}" == 'True'
14+
assert f'{"!:"}' == '!:'
1615
assert fr'x={4*10}\n' == 'x=40\\n'
1716
assert f'{16:0>+#10x}' == '00000+0x10'
1817

18+
# Normally `!` cannot appear outside of delimiters in the expression but
19+
# cpython makes an exception for `!=`, so we should too.
20+
21+
# assert f'{1 != 2}' == 'True'
22+
1923

2024
# conversion flags
2125

0 commit comments

Comments
 (0)