Skip to content

Commit c6d3b1b

Browse files
authored
Merge pull request #1899 from TheAnyKey/TheAnyKey/p38_self_documenting_fstrings
The any key/p38 self documenting fstrings
2 parents 7d9209b + a535e3c commit c6d3b1b

File tree

2 files changed

+224
-11
lines changed

2 files changed

+224
-11
lines changed

parser/src/fstring.rs

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,33 @@ impl<'a> FStringParser<'a> {
2626
let mut spec = None;
2727
let mut delims = Vec::new();
2828
let mut conversion = None;
29+
let mut pred_expression_text = String::new();
30+
let mut trailing_seq = String::new();
2931

3032
while let Some(ch) = self.chars.next() {
3133
match ch {
34+
// can be integrated better with the remainign code, but as a starting point ok
35+
// in general I would do here a tokenizing of the fstrings to omit this peeking.
36+
'!' if self.chars.peek() == Some(&'=') => {
37+
expression.push_str("!=");
38+
self.chars.next();
39+
}
40+
41+
'=' if self.chars.peek() == Some(&'=') => {
42+
expression.push_str("==");
43+
self.chars.next();
44+
}
45+
46+
'>' if self.chars.peek() == Some(&'=') => {
47+
expression.push_str(">=");
48+
self.chars.next();
49+
}
50+
51+
'<' if self.chars.peek() == Some(&'=') => {
52+
expression.push_str("<=");
53+
self.chars.next();
54+
}
55+
3256
'!' if delims.is_empty() && self.chars.peek() != Some(&'=') => {
3357
if expression.trim().is_empty() {
3458
return Err(EmptyExpression);
@@ -46,10 +70,22 @@ impl<'a> FStringParser<'a> {
4670
}
4771
});
4872

49-
if self.chars.peek() != Some(&'}') {
73+
if let Some(&peek) = self.chars.peek() {
74+
if peek != '}' && peek != ':' {
75+
return Err(ExpectedRbrace);
76+
}
77+
} else {
5078
return Err(ExpectedRbrace);
5179
}
5280
}
81+
82+
// match a python 3.8 self documenting expression
83+
// format '{' PYTHON_EXPRESSION '=' FORMAT_SPECIFIER? '}'
84+
'=' if self.chars.peek() != Some(&'=') => {
85+
// check for delims empty?
86+
pred_expression_text = expression.to_string(); // safe expression before = to print it
87+
}
88+
5389
':' if delims.is_empty() => {
5490
let mut nested = false;
5591
let mut in_nested = false;
@@ -121,14 +157,35 @@ impl<'a> FStringParser<'a> {
121157
if expression.is_empty() {
122158
return Err(EmptyExpression);
123159
}
124-
return Ok(FormattedValue {
125-
value: Box::new(
126-
parse_expression(expression.trim())
127-
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
128-
),
129-
conversion,
130-
spec,
131-
});
160+
if pred_expression_text.is_empty() {
161+
return Ok(FormattedValue {
162+
value: Box::new(
163+
parse_expression(expression.trim())
164+
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
165+
),
166+
conversion,
167+
spec,
168+
});
169+
} else {
170+
return Ok(Joined {
171+
values: vec![
172+
Constant {
173+
value: pred_expression_text + "=",
174+
},
175+
Constant {
176+
value: trailing_seq,
177+
},
178+
FormattedValue {
179+
value: Box::new(
180+
parse_expression(expression.trim())
181+
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
182+
),
183+
conversion,
184+
spec,
185+
},
186+
],
187+
});
188+
}
132189
}
133190
'"' | '\'' => {
134191
expression.push(ch);
@@ -139,12 +196,14 @@ impl<'a> FStringParser<'a> {
139196
}
140197
}
141198
}
199+
' ' if !pred_expression_text.is_empty() => {
200+
trailing_seq.push(ch);
201+
}
142202
_ => {
143203
expression.push(ch);
144204
}
145205
}
146206
}
147-
148207
Err(UnclosedLbrace)
149208
}
150209

@@ -298,12 +357,35 @@ mod tests {
298357
);
299358
}
300359

360+
#[test]
361+
fn test_fstring_parse_selfdocumenting_base() {
362+
let src = String::from("{user=}");
363+
let parse_ast = parse_fstring(&src);
364+
365+
assert!(parse_ast.is_ok());
366+
}
367+
368+
#[test]
369+
fn test_fstring_parse_selfdocumenting_base_more() {
370+
let src = String::from("mix {user=} with text and {second=}");
371+
let parse_ast = parse_fstring(&src);
372+
373+
assert!(parse_ast.is_ok());
374+
}
375+
376+
#[test]
377+
fn test_fstring_parse_selfdocumenting_format() {
378+
let src = String::from("{user=:>10}");
379+
let parse_ast = parse_fstring(&src);
380+
381+
assert!(parse_ast.is_ok());
382+
}
383+
301384
#[test]
302385
fn test_parse_invalid_fstring() {
303386
assert_eq!(parse_fstring("{5!a"), Err(ExpectedRbrace));
304387
assert_eq!(parse_fstring("{5!a1}"), Err(ExpectedRbrace));
305388
assert_eq!(parse_fstring("{5!"), Err(ExpectedRbrace));
306-
307389
assert_eq!(parse_fstring("abc{!a 'cat'}"), Err(EmptyExpression));
308390
assert_eq!(parse_fstring("{!a"), Err(EmptyExpression));
309391
assert_eq!(parse_fstring("{ !a}"), Err(EmptyExpression));
@@ -318,6 +400,8 @@ mod tests {
318400
assert_eq!(parse_fstring("{a:{b}"), Err(UnclosedLbrace));
319401
assert_eq!(parse_fstring("{"), Err(UnclosedLbrace));
320402

403+
assert_eq!(parse_fstring("{}"), Err(EmptyExpression));
404+
321405
// TODO: check for InvalidExpression enum?
322406
assert!(parse_fstring("{class}").is_err());
323407
}
@@ -328,4 +412,25 @@ mod tests {
328412
let parse_ast = parse_fstring(&source);
329413
assert!(parse_ast.is_ok());
330414
}
415+
416+
#[test]
417+
fn test_parse_fstring_equals() {
418+
let source = String::from("{42 == 42}");
419+
let parse_ast = parse_fstring(&source);
420+
assert!(parse_ast.is_ok());
421+
}
422+
423+
#[test]
424+
fn test_parse_fstring_selfdoc_prec_space() {
425+
let source = String::from("{x =}");
426+
let parse_ast = parse_fstring(&source);
427+
assert!(parse_ast.is_ok());
428+
}
429+
430+
#[test]
431+
fn test_parse_fstring_selfdoc_trailing_space() {
432+
let source = String::from("{x= }");
433+
let parse_ast = parse_fstring(&source);
434+
assert!(parse_ast.is_ok());
435+
}
331436
}

tests/snippets/fstrings.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
from testutils import assert_raises
2+
3+
#test only makes sense with python 3.8 or higher (or RustPython)
4+
import sys
5+
import platform
6+
if platform.python_implementation() == 'CPython':
7+
assert sys.version_info >= (3, 8), 'Incompatible Python Version, expected CPython 3.8 or later'
8+
elif platform.python_implementation == 'RustPython':
9+
# ok
10+
pass
11+
else:
12+
# other implementation - lets give it a try
13+
pass
14+
15+
16+
# lets start tersing
217
foo = 'bar'
318

419
assert f"{''}" == ''
@@ -17,6 +32,18 @@
1732
assert f'{16:0>+#10x}' == '00000+0x10'
1833
assert f"{{{(lambda x: f'hello, {x}')('world}')}" == '{hello, world}'
1934

35+
36+
# base test of self documenting strings
37+
#assert f'{foo=}' == 'foo=bar' # TODO ' missing
38+
39+
num=42
40+
41+
f'{num=}' # keep this line as it will fail when using a python 3.7 interpreter
42+
43+
assert f'{num=}' == 'num=42'
44+
assert f'{num=:>10}' == 'num= 42'
45+
46+
2047
spec = "0>+#10x"
2148
assert f"{16:{spec}}{foo}" == '00000+0x10bar'
2249

@@ -61,3 +88,84 @@ def __str__(self):
6188
assert f'>{v!r}' == ">'\u262e'"
6289
assert f'>{v!s}' == '>\u262e'
6390
assert f'>{v!a}' == r">'\u262e'"
91+
92+
93+
94+
# Test format specifier after conversion flag
95+
#assert f'{"42"!s:<5}' == '42 ', '#' + f'{"42"!s:5}' +'#' # TODO: default alignment in cpython is left
96+
97+
assert f'{"42"!s:<5}' == '42 ', '#' + f'{"42"!s:<5}' +'#'
98+
assert f'{"42"!s:>5}' == ' 42', '#' + f'{"42"!s:>5}' +'#'
99+
100+
#assert f'{"42"=!s:5}' == '"42"=42 ', '#'+ f'{"42"=!s:5}' +'#' # TODO default alingment in cpython is left
101+
assert f'{"42"=!s:<5}' == '"42"=42 ', '#'+ f'{"42"=!s:<5}' +'#'
102+
assert f'{"42"=!s:>5}' == '"42"= 42', '#'+ f'{"42"=!s:>5}' +'#'
103+
104+
105+
106+
### Tests for fstring selfdocumenting form CPython
107+
108+
class C:
109+
def assertEqual(self, a,b):
110+
assert a==b, "{0} == {1}".format(a,b)
111+
112+
self=C()
113+
114+
x = 'A string'
115+
self.assertEqual(f'{10=}', '10=10')
116+
# self.assertEqual(f'{x=}', 'x=' + x )#repr(x)) # TODO: add ' when printing strings
117+
# self.assertEqual(f'{x =}', 'x =' + x )# + repr(x)) # TODO: implement ' handling
118+
self.assertEqual(f'{x=!s}', 'x=' + str(x))
119+
# # self.assertEqual(f'{x=!r}', 'x=' + x) #repr(x)) # !r not supported
120+
# self.assertEqual(f'{x=!a}', 'x=' + ascii(x))
121+
122+
x = 2.71828
123+
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
124+
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
125+
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20')) #TODO formatspecifier after conversion flsg is currently not supported (also for classical fstrings)
126+
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
127+
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))
128+
129+
x = 9
130+
self.assertEqual(f'{3*x+15=}', '3*x+15=42')
131+
132+
# There is code in ast.c that deals with non-ascii expression values. So,
133+
# use a unicode identifier to trigger that.
134+
tenπ = 31.4
135+
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')
136+
137+
# Also test with Unicode in non-identifiers.
138+
#self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'') ' TODO ' missing
139+
140+
# Make sure nested fstrings still work.
141+
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')
142+
143+
# Make sure text before and after an expression with = works
144+
# correctly.
145+
pi = 'π'
146+
#self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega") # ' missing around pi
147+
148+
# Check multi-line expressions.
149+
#self.assertEqual(f'''{3=}''', '\n3\n=3') # TODO: multiline f strings not supported, seems to be an rustpython issue
150+
151+
# Since = is handled specially, make sure all existing uses of
152+
# it still work.
153+
154+
self.assertEqual(f'{0==1}', 'False')
155+
self.assertEqual(f'{0!=1}', 'True')
156+
self.assertEqual(f'{0<=1}', 'True')
157+
self.assertEqual(f'{0>=1}', 'False')
158+
159+
# Make sure leading and following text works.
160+
# x = 'foo'
161+
#self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y') # TODO '
162+
# self.assertEqual(f'X{x=}Y', 'Xx='+x+'Y') # just for the moment
163+
164+
# Make sure whitespace around the = works.
165+
# self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y') # TODO '
166+
# self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y') # TODO '
167+
# self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y') # TODO '
168+
169+
# self.assertEqual(f'X{x =}Y', 'Xx ='+x+'Y')
170+
# self.assertEqual(f'X{x= }Y', 'Xx= '+x+'Y')
171+
# self.assertEqual(f'X{x = }Y', 'Xx = '+x+'Y')

0 commit comments

Comments
 (0)