Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
94a7b48
added initial implementation of self defining expressions to fstring …
TheAnyKey Apr 28, 2020
34478d3
implemented selfdocumenting fstrings and tests from cpython- current …
TheAnyKey Apr 29, 2020
c25353c
minor cleanup
TheAnyKey Apr 29, 2020
e7ec972
now conversion flags can be followed by a format specifier and added …
TheAnyKey Apr 29, 2020
972503c
fixed qute handling in tests
TheAnyKey Apr 29, 2020
8e8e839
activated further test, removed todos
TheAnyKey Apr 29, 2020
3ccb117
fixed fmt
TheAnyKey Apr 29, 2020
3e8cde7
Removed incompatibilty with CPython, fixed parser error handling
TheAnyKey Apr 30, 2020
80a9710
fixed clippy; tests fail explicitly for python 3.7 and below
TheAnyKey Apr 30, 2020
f823af1
skipping inkompatible test for the moment
TheAnyKey Apr 30, 2020
620115d
recommit
TheAnyKey May 1, 2020
90123df
Initial implementation of Py3.9 dict union and python-level tests
TheAnyKey May 2, 2020
155f3ca
first complete? implementation. TODO: Cleanup, remove warnings, clipp…
TheAnyKey May 2, 2020
9a8b8e7
fixed clippy, fmt, warnings, etc.. Improved tests and extended testut…
TheAnyKey May 2, 2020
f6f9754
changed reference python version to 3.9 - lets see what happens
TheAnyKey May 2, 2020
a285474
fixed issued in testutils.skip_if_unsupported and testutils.fail_if_u…
TheAnyKey May 2, 2020
41849ab
fixed: returned wrong error when union opration is invoked with other…
TheAnyKey May 3, 2020
1697ce0
reverted toolchain settings to python version 3.8 as 3.9 is so far no…
TheAnyKey May 3, 2020
0faf968
fixed typo
TheAnyKey May 3, 2020
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
129 changes: 120 additions & 9 deletions parser/src/fstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,37 @@ 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('!');
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);
Expand All @@ -46,10 +74,19 @@ impl<'a> FStringParser<'a> {
}
});

if self.chars.peek() != Some(&'}') {
let peek = self.chars.peek();
if peek != Some(&'}') && peek != Some(&':') {
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 +158,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,6 +197,11 @@ impl<'a> FStringParser<'a> {
}
}
}

' ' if !pred_expression_text.is_empty() => {
trailing_seq.push(ch);
}

_ => {
expression.push(ch);
}
Expand Down Expand Up @@ -298,9 +361,34 @@ 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));

Expand All @@ -318,6 +406,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 +418,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());
}
}
83 changes: 83 additions & 0 deletions tests/snippets/dict_union.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

import testutils

def test_dunion_ior0():
a={1:2,2:3}
b={3:4,5:6}
a|=b

assert a == {1:2,2:3,3:4,5:6}, f"wrong value assigned {a=}"
assert b == {3:4,5:6}, f"right hand side modified, {b=}"

def test_dunion_or0():
a={1:2,2:3}
b={3:4,5:6}
c=a|b

assert a == {1:2,2:3}, f"left hand side of non-assignment operator modified {a=}"
assert b == {3:4,5:6}, f"right hand side of non-assignment operator modified, {b=}"
assert c == {1:2,2:3, 3:4, 5:6}, f"unexpected result of dict union {c=}"


def test_dunion_or1():
a={1:2,2:3}
b={3:4,5:6}
c=a.__or__(b)

assert a == {1:2,2:3}, f"left hand side of non-assignment operator modified {a=}"
assert b == {3:4,5:6}, f"right hand side of non-assignment operator modified, {b=}"
assert c == {1:2,2:3, 3:4, 5:6}, f"unexpected result of dict union {c=}"


def test_dunion_ror0():
a={1:2,2:3}
b={3:4,5:6}
c=b.__ror__(a)

assert a == {1:2,2:3}, f"left hand side of non-assignment operator modified {a=}"
assert b == {3:4,5:6}, f"right hand side of non-assignment operator modified, {b=}"
assert c == {1:2,2:3, 3:4, 5:6}, f"unexpected result of dict union {c=}"


def test_dunion_other_types():
def perf_test_or(other_obj):
d={1:2}
try:
d.__or__(other_obj)
except:
return True
return False

def perf_test_ior(other_obj):
d={1:2}
try:
d.__ior__(other_obj)
except:
return True
return False

def perf_test_ror(other_obj):
d={1:2}
try:
d.__ror__(other_obj)
except:
return True
return False

test_fct={'__or__':perf_test_or, '__ror__':perf_test_ror, '__ior__':perf_test_ior}
others=['FooBar', 42, [36], set([19]), ['aa'], None]
for tfn,tf in test_fct.items():
for other in others:
assert tf(other), f"Failed: dict {tfn}, accepted {other}"




testutils.skip_if_unsupported(3,9,test_dunion_ior0)
testutils.skip_if_unsupported(3,9,test_dunion_or0)
testutils.skip_if_unsupported(3,9,test_dunion_or1)
testutils.skip_if_unsupported(3,9,test_dunion_ror0)
testutils.skip_if_unsupported(3,9,test_dunion_other_types)



109 changes: 109 additions & 0 deletions tests/snippets/fstrings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
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.major == 3, 'Incompatible Python Version, expected CPython 3.8 or later'
assert sys.version_info.minor == 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 +33,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 +89,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')
Loading