Skip to content

The any key/p38 self documenting fstrings #1899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 116 additions & 11 deletions parser/src/fstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,33 @@ impl<'a> FStringParser<'a> {
let mut spec = None;
let mut delims = Vec::new();
let mut conversion = None;
let mut pred_expression_text = String::new();
let mut trailing_seq = String::new();

while let Some(ch) = self.chars.next() {
match ch {
// can be integrated better with the remainign code, but as a starting point ok
// in general I would do here a tokenizing of the fstrings to omit this peeking.
'!' if self.chars.peek() == Some(&'=') => {
expression.push_str("!=");
self.chars.next();
}

'=' if self.chars.peek() == Some(&'=') => {
expression.push_str("==");
self.chars.next();
}

'>' if self.chars.peek() == Some(&'=') => {
expression.push_str(">=");
self.chars.next();
}

'<' if self.chars.peek() == Some(&'=') => {
expression.push_str("<=");
self.chars.next();
}

'!' if delims.is_empty() && self.chars.peek() != Some(&'=') => {
if expression.trim().is_empty() {
return Err(EmptyExpression);
Expand All @@ -46,10 +70,22 @@ impl<'a> FStringParser<'a> {
}
});

if self.chars.peek() != Some(&'}') {
if let Some(&peek) = self.chars.peek() {
if peek != '}' && peek != ':' {
return Err(ExpectedRbrace);
}
} else {
return Err(ExpectedRbrace);
}
}

// match a python 3.8 self documenting expression
// format '{' PYTHON_EXPRESSION '=' FORMAT_SPECIFIER? '}'
'=' if self.chars.peek() != Some(&'=') => {
// check for delims empty?
pred_expression_text = expression.to_string(); // safe expression before = to print it
}

':' if delims.is_empty() => {
let mut nested = false;
let mut in_nested = false;
Expand Down Expand Up @@ -121,14 +157,35 @@ impl<'a> FStringParser<'a> {
if expression.is_empty() {
return Err(EmptyExpression);
}
return Ok(FormattedValue {
value: Box::new(
parse_expression(expression.trim())
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
),
conversion,
spec,
});
if pred_expression_text.is_empty() {
return Ok(FormattedValue {
value: Box::new(
parse_expression(expression.trim())
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
),
conversion,
spec,
});
} else {
return Ok(Joined {
values: vec![
Constant {
value: pred_expression_text + "=",
},
Constant {
value: trailing_seq,
},
FormattedValue {
value: Box::new(
parse_expression(expression.trim())
.map_err(|e| InvalidExpression(Box::new(e.error)))?,
),
conversion,
spec,
},
],
});
}
}
'"' | '\'' => {
expression.push(ch);
Expand All @@ -139,12 +196,14 @@ impl<'a> FStringParser<'a> {
}
}
}
' ' if !pred_expression_text.is_empty() => {
trailing_seq.push(ch);
}
_ => {
expression.push(ch);
}
}
}

Err(UnclosedLbrace)
}

Expand Down Expand Up @@ -298,12 +357,35 @@ mod tests {
);
}

#[test]
fn test_fstring_parse_selfdocumenting_base() {
let src = String::from("{user=}");
let parse_ast = parse_fstring(&src);

assert!(parse_ast.is_ok());
}

#[test]
fn test_fstring_parse_selfdocumenting_base_more() {
let src = String::from("mix {user=} with text and {second=}");
let parse_ast = parse_fstring(&src);

assert!(parse_ast.is_ok());
}

#[test]
fn test_fstring_parse_selfdocumenting_format() {
let src = String::from("{user=:>10}");
let parse_ast = parse_fstring(&src);

assert!(parse_ast.is_ok());
}

#[test]
fn test_parse_invalid_fstring() {
assert_eq!(parse_fstring("{5!a"), Err(ExpectedRbrace));
assert_eq!(parse_fstring("{5!a1}"), Err(ExpectedRbrace));
assert_eq!(parse_fstring("{5!"), Err(ExpectedRbrace));

assert_eq!(parse_fstring("abc{!a 'cat'}"), Err(EmptyExpression));
assert_eq!(parse_fstring("{!a"), Err(EmptyExpression));
assert_eq!(parse_fstring("{ !a}"), Err(EmptyExpression));
Expand All @@ -318,6 +400,8 @@ mod tests {
assert_eq!(parse_fstring("{a:{b}"), Err(UnclosedLbrace));
assert_eq!(parse_fstring("{"), Err(UnclosedLbrace));

assert_eq!(parse_fstring("{}"), Err(EmptyExpression));

// TODO: check for InvalidExpression enum?
assert!(parse_fstring("{class}").is_err());
}
Expand All @@ -328,4 +412,25 @@ mod tests {
let parse_ast = parse_fstring(&source);
assert!(parse_ast.is_ok());
}

#[test]
fn test_parse_fstring_equals() {
let source = String::from("{42 == 42}");
let parse_ast = parse_fstring(&source);
assert!(parse_ast.is_ok());
}

#[test]
fn test_parse_fstring_selfdoc_prec_space() {
let source = String::from("{x =}");
let parse_ast = parse_fstring(&source);
assert!(parse_ast.is_ok());
}

#[test]
fn test_parse_fstring_selfdoc_trailing_space() {
let source = String::from("{x= }");
let parse_ast = parse_fstring(&source);
assert!(parse_ast.is_ok());
}
}
108 changes: 108 additions & 0 deletions tests/snippets/fstrings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from testutils import assert_raises

#test only makes sense with python 3.8 or higher (or RustPython)
import sys
import platform
if platform.python_implementation() == 'CPython':
assert sys.version_info >= (3, 8), 'Incompatible Python Version, expected CPython 3.8 or later'
elif platform.python_implementation == 'RustPython':
# ok
pass
else:
# other implementation - lets give it a try
pass


# lets start tersing
foo = 'bar'

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


# base test of self documenting strings
#assert f'{foo=}' == 'foo=bar' # TODO ' missing

num=42

f'{num=}' # keep this line as it will fail when using a python 3.7 interpreter

assert f'{num=}' == 'num=42'
assert f'{num=:>10}' == 'num= 42'


spec = "0>+#10x"
assert f"{16:{spec}}{foo}" == '00000+0x10bar'

Expand Down Expand Up @@ -61,3 +88,84 @@ def __str__(self):
assert f'>{v!r}' == ">'\u262e'"
assert f'>{v!s}' == '>\u262e'
assert f'>{v!a}' == r">'\u262e'"



# Test format specifier after conversion flag
#assert f'{"42"!s:<5}' == '42 ', '#' + f'{"42"!s:5}' +'#' # TODO: default alignment in cpython is left

assert f'{"42"!s:<5}' == '42 ', '#' + f'{"42"!s:<5}' +'#'
assert f'{"42"!s:>5}' == ' 42', '#' + f'{"42"!s:>5}' +'#'

#assert f'{"42"=!s:5}' == '"42"=42 ', '#'+ f'{"42"=!s:5}' +'#' # TODO default alingment in cpython is left
assert f'{"42"=!s:<5}' == '"42"=42 ', '#'+ f'{"42"=!s:<5}' +'#'
assert f'{"42"=!s:>5}' == '"42"= 42', '#'+ f'{"42"=!s:>5}' +'#'



### Tests for fstring selfdocumenting form CPython

class C:
def assertEqual(self, a,b):
assert a==b, "{0} == {1}".format(a,b)

self=C()

x = 'A string'
self.assertEqual(f'{10=}', '10=10')
# self.assertEqual(f'{x=}', 'x=' + x )#repr(x)) # TODO: add ' when printing strings
# self.assertEqual(f'{x =}', 'x =' + x )# + repr(x)) # TODO: implement ' handling
self.assertEqual(f'{x=!s}', 'x=' + str(x))
# # self.assertEqual(f'{x=!r}', 'x=' + x) #repr(x)) # !r not supported
# self.assertEqual(f'{x=!a}', 'x=' + ascii(x))

x = 2.71828
self.assertEqual(f'{x=:.2f}', 'x=' + format(x, '.2f'))
self.assertEqual(f'{x=:}', 'x=' + format(x, ''))
self.assertEqual(f'{x=!r:^20}', 'x=' + format(repr(x), '^20')) #TODO formatspecifier after conversion flsg is currently not supported (also for classical fstrings)
self.assertEqual(f'{x=!s:^20}', 'x=' + format(str(x), '^20'))
self.assertEqual(f'{x=!a:^20}', 'x=' + format(ascii(x), '^20'))

x = 9
self.assertEqual(f'{3*x+15=}', '3*x+15=42')

# There is code in ast.c that deals with non-ascii expression values. So,
# use a unicode identifier to trigger that.
tenπ = 31.4
self.assertEqual(f'{tenπ=:.2f}', 'tenπ=31.40')

# Also test with Unicode in non-identifiers.
#self.assertEqual(f'{"Σ"=}', '"Σ"=\'Σ\'') ' TODO ' missing

# Make sure nested fstrings still work.
self.assertEqual(f'{f"{3.1415=:.1f}":*^20}', '*****3.1415=3.1*****')

# Make sure text before and after an expression with = works
# correctly.
pi = 'π'
#self.assertEqual(f'alpha α {pi=} ω omega', "alpha α pi='π' ω omega") # ' missing around pi

# Check multi-line expressions.
#self.assertEqual(f'''{3=}''', '\n3\n=3') # TODO: multiline f strings not supported, seems to be an rustpython issue

# Since = is handled specially, make sure all existing uses of
# it still work.

self.assertEqual(f'{0==1}', 'False')
self.assertEqual(f'{0!=1}', 'True')
self.assertEqual(f'{0<=1}', 'True')
self.assertEqual(f'{0>=1}', 'False')

# Make sure leading and following text works.
# x = 'foo'
#self.assertEqual(f'X{x=}Y', 'Xx='+repr(x)+'Y') # TODO '
# self.assertEqual(f'X{x=}Y', 'Xx='+x+'Y') # just for the moment

# Make sure whitespace around the = works.
# self.assertEqual(f'X{x =}Y', 'Xx ='+repr(x)+'Y') # TODO '
# self.assertEqual(f'X{x= }Y', 'Xx= '+repr(x)+'Y') # TODO '
# self.assertEqual(f'X{x = }Y', 'Xx = '+repr(x)+'Y') # TODO '

# self.assertEqual(f'X{x =}Y', 'Xx ='+x+'Y')
# self.assertEqual(f'X{x= }Y', 'Xx= '+x+'Y')
# self.assertEqual(f'X{x = }Y', 'Xx = '+x+'Y')