Skip to content

Commit 9e03b69

Browse files
Merge pull request RustPython#538 from RustPython/joey/f-string-conversion-flags
Support f-string conversion flags
2 parents 22928be + f050acb commit 9e03b69

File tree

9 files changed

+302
-230
lines changed

9 files changed

+302
-230
lines changed

parser/src/ast.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,25 @@ pub enum Number {
357357
Complex { real: f64, imag: f64 },
358358
}
359359

360+
/// Transforms a value prior to formatting it.
361+
#[derive(Copy, Clone, Debug, PartialEq)]
362+
pub enum ConversionFlag {
363+
/// Converts by calling `str(<value>)`.
364+
Str,
365+
/// Converts by calling `ascii(<value>)`.
366+
Ascii,
367+
/// Converts by calling `repr(<value>)`.
368+
Repr,
369+
}
370+
360371
#[derive(Debug, PartialEq)]
361372
pub enum StringGroup {
362373
Constant {
363374
value: String,
364375
},
365376
FormattedValue {
366377
value: Box<Expression>,
378+
conversion: Option<ConversionFlag>,
367379
spec: String,
368380
},
369381
Joined {

parser/src/fstring.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use std::iter;
2+
use std::mem;
3+
use std::str;
4+
5+
use lalrpop_util::ParseError as LalrpopError;
6+
7+
use crate::ast::{ConversionFlag, StringGroup};
8+
use crate::lexer::{LexicalError, Location, Tok};
9+
use crate::parser::parse_expression;
10+
11+
use self::FStringError::*;
12+
use self::StringGroup::*;
13+
14+
// TODO: consolidate these with ParseError
15+
#[derive(Debug, PartialEq)]
16+
pub enum FStringError {
17+
UnclosedLbrace,
18+
UnopenedRbrace,
19+
InvalidExpression,
20+
InvalidConversionFlag,
21+
EmptyExpression,
22+
MismatchedDelimiter,
23+
}
24+
25+
impl From<FStringError> for LalrpopError<Location, Tok, LexicalError> {
26+
fn from(_err: FStringError) -> Self {
27+
lalrpop_util::ParseError::User {
28+
error: LexicalError::StringError,
29+
}
30+
}
31+
}
32+
33+
struct FStringParser<'a> {
34+
chars: iter::Peekable<str::Chars<'a>>,
35+
}
36+
37+
impl<'a> FStringParser<'a> {
38+
fn new(source: &'a str) -> Self {
39+
Self {
40+
chars: source.chars().peekable(),
41+
}
42+
}
43+
44+
fn parse_formatted_value(&mut self) -> Result<StringGroup, FStringError> {
45+
let mut expression = String::new();
46+
let mut spec = String::new();
47+
let mut delims = Vec::new();
48+
let mut conversion = None;
49+
50+
while let Some(ch) = self.chars.next() {
51+
match ch {
52+
'!' if delims.is_empty() => {
53+
conversion = Some(match self.chars.next() {
54+
Some('s') => ConversionFlag::Str,
55+
Some('a') => ConversionFlag::Ascii,
56+
Some('r') => ConversionFlag::Repr,
57+
Some(_) => {
58+
return Err(InvalidConversionFlag);
59+
}
60+
None => {
61+
break;
62+
}
63+
})
64+
}
65+
':' if delims.is_empty() => {
66+
while let Some(&next) = self.chars.peek() {
67+
if next != '}' {
68+
spec.push(next);
69+
self.chars.next();
70+
} else {
71+
break;
72+
}
73+
}
74+
}
75+
'(' | '{' | '[' => {
76+
expression.push(ch);
77+
delims.push(ch);
78+
}
79+
')' => {
80+
if delims.pop() != Some('(') {
81+
return Err(MismatchedDelimiter);
82+
}
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);
96+
}
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+
}
116+
}
117+
}
118+
_ => {
119+
expression.push(ch);
120+
}
121+
}
122+
}
123+
124+
return Err(UnclosedLbrace);
125+
}
126+
127+
fn parse(mut self) -> Result<StringGroup, FStringError> {
128+
let mut content = String::new();
129+
let mut values = vec![];
130+
131+
while let Some(ch) = self.chars.next() {
132+
match ch {
133+
'{' => {
134+
if let Some('{') = self.chars.peek() {
135+
self.chars.next();
136+
content.push('{');
137+
} else {
138+
if !content.is_empty() {
139+
values.push(Constant {
140+
value: mem::replace(&mut content, String::new()),
141+
});
142+
}
143+
144+
values.push(self.parse_formatted_value()?);
145+
}
146+
}
147+
'}' => {
148+
if let Some('}') = self.chars.peek() {
149+
self.chars.next();
150+
content.push('}');
151+
} else {
152+
return Err(UnopenedRbrace);
153+
}
154+
}
155+
_ => {
156+
content.push(ch);
157+
}
158+
}
159+
}
160+
161+
if !content.is_empty() {
162+
values.push(Constant { value: content })
163+
}
164+
165+
Ok(match values.len() {
166+
0 => Constant {
167+
value: String::new(),
168+
},
169+
1 => values.into_iter().next().unwrap(),
170+
_ => Joined { values },
171+
})
172+
}
173+
}
174+
175+
pub fn parse_fstring(source: &str) -> Result<StringGroup, FStringError> {
176+
FStringParser::new(source).parse()
177+
}
178+
179+
#[cfg(test)]
180+
mod tests {
181+
use crate::ast;
182+
183+
use super::*;
184+
185+
fn mk_ident(name: &str) -> ast::Expression {
186+
ast::Expression::Identifier {
187+
name: name.to_owned(),
188+
}
189+
}
190+
191+
#[test]
192+
fn test_parse_fstring() {
193+
let source = String::from("{a}{ b }{{foo}}");
194+
let parse_ast = parse_fstring(&source).unwrap();
195+
196+
assert_eq!(
197+
parse_ast,
198+
Joined {
199+
values: vec![
200+
FormattedValue {
201+
value: Box::new(mk_ident("a")),
202+
conversion: None,
203+
spec: String::new(),
204+
},
205+
FormattedValue {
206+
value: Box::new(mk_ident("b")),
207+
conversion: None,
208+
spec: String::new(),
209+
},
210+
Constant {
211+
value: "{foo}".to_owned()
212+
}
213+
]
214+
}
215+
);
216+
}
217+
218+
#[test]
219+
fn test_parse_empty_fstring() {
220+
assert_eq!(
221+
parse_fstring(""),
222+
Ok(Constant {
223+
value: String::new(),
224+
}),
225+
);
226+
}
227+
228+
#[test]
229+
fn test_parse_invalid_fstring() {
230+
assert_eq!(parse_fstring("{"), Err(UnclosedLbrace));
231+
assert_eq!(parse_fstring("}"), Err(UnopenedRbrace));
232+
assert_eq!(parse_fstring("{class}"), Err(InvalidExpression));
233+
}
234+
}

parser/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ extern crate log;
33

44
pub mod ast;
55
pub mod error;
6+
mod fstring;
67
pub mod lexer;
78
pub mod parser;
89
#[cfg_attr(rustfmt, rustfmt_skip)]

0 commit comments

Comments
 (0)