From 34478d3db7b6482ce21f3f537e71e89bfe01f482 Mon Sep 17 00:00:00 2001 From: TheAnyKey Date: Wed, 29 Apr 2020 15:37:47 -0400 Subject: [PATCH] implemented selfdocumenting fstrings and tests from cpython- current handling of quotes is too simple --- parser/src/fstring.rs | 68 +++++++++++++++++++++++++++++--- tests/snippets/fstrings.py | 81 +++++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/parser/src/fstring.rs b/parser/src/fstring.rs index 409fa7746..1bf6a465e 100644 --- a/parser/src/fstring.rs +++ b/parser/src/fstring.rs @@ -27,9 +27,36 @@ impl<'a> FStringParser<'a> { 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('!'); + expression.push('='); + self.chars.next(); + } + + '=' if self.chars.peek() == Some(&'=') => { + expression.push('='); + expression.push('='); + self.chars.next(); + } + + '>' if self.chars.peek() == Some(&'=') => { + expression.push('>'); + expression.push('='); + self.chars.next(); + } + + '<' if self.chars.peek() == Some(&'=') => { + expression.push('<'); + expression.push('='); + self.chars.next(); + } + '!' if delims.is_empty() && self.chars.peek() != Some(&'=') => { if expression.trim().is_empty() { return Err(EmptyExpression); @@ -54,8 +81,9 @@ impl<'a> FStringParser<'a> { // match a python 3.8 self documenting expression // format '{' PYTHON_EXPRESSION '=' FORMAT_SPECIFIER? '}' - '=' if self.chars.peek() != Some(&'=') => { - pred_expression_text = expression.trim().to_string(); // use expression and remove whitespace + '=' if self.chars.peek() != Some(&'=') => { // check for delims empty? + pred_expression_text = expression.to_string(); // safe expression before = to print it + // pred_expression_text contains trailing spaces, which are trimmed somewhere later before printing, this needs to be prevented orovercome here } ':' if delims.is_empty() => { @@ -143,7 +171,11 @@ impl<'a> FStringParser<'a> { return Ok(Joined{ values:vec![ Constant{ - value:pred_expression_text.to_owned() + value:pred_expression_text.to_owned()+"=" + }, + + Constant { + value:trailing_seq.to_owned() }, FormattedValue { @@ -167,6 +199,11 @@ impl<'a> FStringParser<'a> { } } } + + ' ' if !pred_expression_text.is_empty() => { + trailing_seq.push(ch); + } + _ => { expression.push(ch); } @@ -370,9 +407,7 @@ mod tests { assert_eq!(parse_fstring("{a:{b}"), Err(UnclosedLbrace)); assert_eq!(parse_fstring("{"), Err(UnclosedLbrace)); - // TODO: which err? - //assert_eq!(parse_fstring("{}"), Err()); // which one? - assert!(parse_fstring("{}").is_err()); // take this for the moment + assert_eq!(parse_fstring("{}"), Err(EmptyExpression)); // TODO: check for InvalidExpression enum? assert!(parse_fstring("{class}").is_err()); @@ -384,4 +419,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()); + } } diff --git a/tests/snippets/fstrings.py b/tests/snippets/fstrings.py index 2d25abf6d..8b1e1d5cd 100644 --- a/tests/snippets/fstrings.py +++ b/tests/snippets/fstrings.py @@ -1,4 +1,4 @@ -from testutils import assert_raises +#from testutils import assert_raises foo = 'bar' assert f"{''}" == '' @@ -17,6 +17,13 @@ assert fr'x={4*10}\n' == 'x=40\\n' assert f'{16:0>+#10x}' == '00000+0x10' assert f"{{{(lambda x: f'hello, {x}')('world}')}" == '{hello, world}' +assert f'{foo=}' == 'foo=bar' + +num=42 +assert f'{num=}' == 'num=42' +assert f'{num=:>10}' == 'num= 42' + + spec = "0>+#10x" assert f"{16:{spec}}{foo}" == '00000+0x10bar' @@ -61,3 +68,75 @@ assert f'>{v}' == '>\u262e' assert f'>{v!r}' == ">'\u262e'" assert f'>{v!s}' == '>\u262e' assert f'>{v!a}' == r">'\u262e'" + + + + + +### 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: remove trim in impl +# self.assertEqual(f'{x=!s}', 'x=' + str(x)) # TODO : implement format specs properly +# self.assertEqual(f'{x=!r}', 'x=' + x) #repr(x)) +#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')) +# 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 ' + +# TODO remove trim +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') \ No newline at end of file