diff --git a/py/lexer.c b/py/lexer.c index e7d0e81440..b22cc2eaea 100644 --- a/py/lexer.c +++ b/py/lexer.c @@ -361,13 +361,25 @@ STATIC void parse_string_literal(mp_lexer_t *lex, bool is_raw, bool is_fstring) vstr_add_byte(&lex->fstring_args, '('); // remember the start of this argument (if we need it for f'{a=}'). size_t i = lex->fstring_args.len; - // extract characters inside the { until we reach the - // format specifier or closing }. - // (MicroPython limitation) note: this is completely unaware of - // Python syntax and will not handle any expression containing '}' or ':'. - // e.g. f'{"}"}' or f'{foo({})}'. + // Extract characters inside the { until the bracket level + // is zero and we reach the conversion specifier '!', + // format specifier ':', or closing '}'. The conversion + // and format specifiers are left unchanged in the format + // string to be handled by str.format. + // (MicroPython limitation) note: this is completely + // unaware of Python syntax and will not handle any + // expression containing '}' or ':'. e.g. f'{"}"}' or f' + // {foo({})}'. However, detection of the '!' will + // specifically ensure that it's followed by [rs] and + // then either the format specifier or the closing + // brace. This allows the use of e.g. != in expressions. unsigned int nested_bracket_level = 0; - while (!is_end(lex) && (nested_bracket_level != 0 || !is_char_or(lex, ':', '}'))) { + while (!is_end(lex) && (nested_bracket_level != 0 + || !(is_char_or(lex, ':', '}') + || (is_char(lex, '!') + && is_char_following_or(lex, 'r', 's') + && is_char_following_following_or(lex, ':', '}')))) + ) { unichar c = CUR_CHAR(lex); if (c == '[' || c == '{') { nested_bracket_level += 1; diff --git a/tests/basics/string_fstring.py b/tests/basics/string_fstring.py index 8907a5c478..1a3960680e 100644 --- a/tests/basics/string_fstring.py +++ b/tests/basics/string_fstring.py @@ -61,3 +61,22 @@ except (ValueError, SyntaxError): print(f"a {1,} b") print(f"a {x,y,} b") print(f"a {x,1} b") + +# f-strings with conversion specifiers (only support !r and !s). +a = "123" +print(f"{a!r}") +print(f"{a!s}") +try: + eval('print(f"{a!x}")') +except (ValueError, SyntaxError): + # CPython detects this at compile time, MicroPython fails with ValueError + # when the str.format is executed. + print("ValueError") + +# Mixing conversion specifiers with formatting. +print(f"{a!r:8s}") +print(f"{a!s:8s}") + +# Still allow ! in expressions. +print(f"{'1' if a != '456' else '0'!r:8s}") +print(f"{'1' if a != '456' else '0'!s:8s}") diff --git a/tests/cpydiff/core_fstring_repr.py b/tests/cpydiff/core_fstring_repr.py index df80abf795..d37fb48db7 100644 --- a/tests/cpydiff/core_fstring_repr.py +++ b/tests/cpydiff/core_fstring_repr.py @@ -1,18 +1,8 @@ """ categories: Core -description: f-strings don't support the !r, !s, and !a conversions -cause: MicroPython is optimised for code space. -workaround: Use repr(), str(), and ascii() explicitly. +description: f-strings don't support !a conversions +cause: MicropPython does not implement ascii() +workaround: None """ - -class X: - def __repr__(self): - return "repr" - - def __str__(self): - return "str" - - -print(f"{X()!r}") -print(f"{X()!s}") +f"{'unicode text'!a}"