diff options
| author | Tomer Vromen <tomer.vromen@intel.com> | 2020-02-04 20:34:36 +0200 |
|---|---|---|
| committer | Tomer Vromen <tomer.vromen@intel.com> | 2020-02-04 20:34:36 +0200 |
| commit | f7b14bcaaae689e243bb379713bbd28686ee7e19 (patch) | |
| tree | f38534ed868035f2d889b28a2df8dc713a716d66 | |
| parent | 55193857195b6a36c70708fadbe01f3ed61c0a88 (diff) | |
| download | pygments-git-f7b14bcaaae689e243bb379713bbd28686ee7e19.tar.gz | |
Python f-strings: highlight expressions in curly braces
Fixes #1228
| -rw-r--r-- | pygments/lexers/python.py | 121 | ||||
| -rw-r--r-- | tests/test_python.py | 682 |
2 files changed, 788 insertions, 15 deletions
diff --git a/pygments/lexers/python.py b/pygments/lexers/python.py index 8e1debac..24067d59 100644 --- a/pygments/lexers/python.py +++ b/pygments/lexers/python.py @@ -85,6 +85,20 @@ class PythonLexer(RegexLexer): # newlines are an error (use "nl" state) ] + def fstring_rules(ttype): + return [ + # Assuming that a '}' is the closing brace after format specifier. + # Sadly, this means that we won't detect syntax error. But it's + # more important to parse correct syntax correctly, than to + # highlight invalid syntax. + (r'\}', String.Interpol), + (r'\{', String.Interpol, 'expr-inside-fstring'), + # backslashes, quotes and formatting signs must be parsed one at a time + (r'[^\\\'"{}\n]+', ttype), + (r'[\'"\\]', ttype), + # newlines are an error (use "nl" state) + ] + tokens = { 'root': [ (r'\n', Text), @@ -92,14 +106,10 @@ class PythonLexer(RegexLexer): bygroups(Text, String.Affix, String.Doc)), (r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')", bygroups(Text, String.Affix, String.Doc)), - (r'[^\S\n]+', Text), (r'\A#!.+$', Comment.Hashbang), (r'#.*$', Comment.Single), - (r'!=|==|<<|>>|:=|[-~+/*%=<>&^|.]', Operator), - (r'[]{}:(),;[]', Punctuation), (r'\\\n', Text), (r'\\', Text), - (r'(in|is|and|or|not)\b', Operator.Word), include('keywords'), (r'(def)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'funcname'), (r'(class)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'classname'), @@ -107,30 +117,84 @@ class PythonLexer(RegexLexer): 'fromimport'), (r'(import)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Text), 'import'), - include('builtins'), - include('magicfuncs'), - include('magicvars'), + include('expr'), + ], + 'expr': [ + # raw f-strings + ('(?i)(rf|fr)(""")', + bygroups(String.Affix, String.Double), 'tdqf'), + ("(?i)(rf|fr)(''')", + bygroups(String.Affix, String.Single), 'tsqf'), + ('(?i)(rf|fr)(")', + bygroups(String.Affix, String.Double), 'dqf'), + ("(?i)(rf|fr)(')", + bygroups(String.Affix, String.Single), 'sqf'), + # non-raw f-strings + ('([fF])(""")', bygroups(String.Affix, String.Double), + combined('fstringescape', 'tdqf')), + ("([fF])(''')", bygroups(String.Affix, String.Single), + combined('fstringescape', 'tsqf')), + ('([fF])(")', bygroups(String.Affix, String.Double), + combined('fstringescape', 'dqf')), + ("([fF])(')", bygroups(String.Affix, String.Single), + combined('fstringescape', 'sqf')), # raw strings - ('(?i)(rb|br|fr|rf|r)(""")', + ('(?i)(rb|br|r)(""")', bygroups(String.Affix, String.Double), 'tdqs'), - ("(?i)(rb|br|fr|rf|r)(''')", + ("(?i)(rb|br|r)(''')", bygroups(String.Affix, String.Single), 'tsqs'), - ('(?i)(rb|br|fr|rf|r)(")', + ('(?i)(rb|br|r)(")', bygroups(String.Affix, String.Double), 'dqs'), - ("(?i)(rb|br|fr|rf|r)(')", + ("(?i)(rb|br|r)(')", bygroups(String.Affix, String.Single), 'sqs'), # non-raw strings - ('([uUbBfF]?)(""")', bygroups(String.Affix, String.Double), + ('([uUbB]?)(""")', bygroups(String.Affix, String.Double), combined('stringescape', 'tdqs')), - ("([uUbBfF]?)(''')", bygroups(String.Affix, String.Single), + ("([uUbB]?)(''')", bygroups(String.Affix, String.Single), combined('stringescape', 'tsqs')), - ('([uUbBfF]?)(")', bygroups(String.Affix, String.Double), + ('([uUbB]?)(")', bygroups(String.Affix, String.Double), combined('stringescape', 'dqs')), - ("([uUbBfF]?)(')", bygroups(String.Affix, String.Single), + ("([uUbB]?)(')", bygroups(String.Affix, String.Single), combined('stringescape', 'sqs')), + (r'[^\S\n]+', Text), + (r'!=|==|<<|>>|:=|[-~+/*%=<>&^|.]', Operator), + (r'[]{}:(),;[]', Punctuation), + (r'(in|is|and|or|not)\b', Operator.Word), + include('expr-keywords'), + include('builtins'), + include('magicfuncs'), + include('magicvars'), include('name'), include('numbers'), ], + 'expr-inside-fstring': [ + (r'[{([]', Punctuation, 'expr-inside-fstring-inner'), + # without format specifier + (r'(=\s*)?' # debug (https://bugs.python.org/issue36817) + r'(\![sraf])?' # conversion + r'}', String.Interpol, '#pop'), + # with format specifier + # we'll catch the remaining '}' in the outer scope + (r'(=\s*)?' # debug (https://bugs.python.org/issue36817) + r'(\![sraf])?' # conversion + r':', String.Interpol, '#pop'), + (r'[^\S]+', Text), # allow new lines + include('expr'), + ], + 'expr-inside-fstring-inner': [ + (r'[{([]', Punctuation, 'expr-inside-fstring-inner'), + (r'[])}]', Punctuation, '#pop'), + (r'[^\S]+', Text), # allow new lines + include('expr'), + ], + 'expr-keywords': [ + # Based on https://docs.python.org/3/reference/expressions.html + (words(( + 'async for', 'await', 'else', 'for', 'if', 'lambda', + 'yield', 'yield from'), suffix=r'\b'), + Keyword), + (words(('True', 'False', 'None'), suffix=r'\b'), Keyword.Constant), + ], 'keywords': [ (words(( 'assert', 'async', 'await', 'break', 'continue', 'del', 'elif', @@ -252,12 +316,29 @@ class PythonLexer(RegexLexer): (uni_name, Name.Namespace), default('#pop'), ], + 'fstringescape': [ + ('{{', String.Escape), + ('}}', String.Escape), + include('stringescape'), + ], 'stringescape': [ (r'\\([\\abfnrtv"\']|\n|N\{.*?\}|u[a-fA-F0-9]{4}|' r'U[a-fA-F0-9]{8}|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape) ], + 'fstrings-single': fstring_rules(String.Single), + 'fstrings-double': fstring_rules(String.Double), 'strings-single': innerstring_rules(String.Single), 'strings-double': innerstring_rules(String.Double), + 'dqf': [ + (r'"', String.Double, '#pop'), + (r'\\\\|\\"|\\\n', String.Escape), # included here for raw strings + include('fstrings-double') + ], + 'sqf': [ + (r"'", String.Single, '#pop'), + (r"\\\\|\\'|\\\n", String.Escape), # included here for raw strings + include('fstrings-single') + ], 'dqs': [ (r'"', String.Double, '#pop'), (r'\\\\|\\"|\\\n', String.Escape), # included here for raw strings @@ -268,6 +349,16 @@ class PythonLexer(RegexLexer): (r"\\\\|\\'|\\\n", String.Escape), # included here for raw strings include('strings-single') ], + 'tdqf': [ + (r'"""', String.Double, '#pop'), + include('fstrings-double'), + (r'\n', String.Double) + ], + 'tsqf': [ + (r"'''", String.Single, '#pop'), + include('fstrings-single'), + (r'\n', String.Single) + ], 'tdqs': [ (r'"""', String.Double, '#pop'), include('strings-double'), diff --git a/tests/test_python.py b/tests/test_python.py index b0922adf..23bb6ed1 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -12,6 +12,8 @@ import pytest from pygments.lexers import PythonLexer, Python3Lexer from pygments.token import Token +import re + @pytest.fixture(scope='module') def lexer2(): @@ -158,3 +160,683 @@ def test_walrus_operator(lexer3): (Token.Text, '\n'), ] assert list(lexer3.get_tokens(fragment)) == tokens + + +def test_fstring(lexer3): + """ + Tests that the lexer can parse f-strings + """ + fragments_and_tokens = ( + # examples from PEP-0498 + ( + "f'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'My name is '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'name'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ', my age next year is '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'age'), + (Token.Operator, '+'), + (Token.Literal.Number.Integer, '1'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ', my anniversary is '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'anniversary'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Single, '%A, %B %d, %Y'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '.'), + (Token.Literal.String.Single, "'"), + (Token.Text, u'\n') + ] + ), ( + "f'He said his name is {name!r}.'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'He said his name is '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'name'), + (Token.Literal.String.Interpol, '!r}'), + (Token.Literal.String.Single, '.'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + + ), ( + "f'input={value:#06x}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'input='), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'value'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Single, '#06x'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """f'{"quoted string"}'\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Double, 'quoted string'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """f'{f"{inner}"}'\n""", # not in the PEP + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'inner'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + # SyntaxError: f-string expression part cannot include a backslash + "f'{\\'quoted string\\'}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Error, '\\'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'quoted string'), + (Token.Literal.String.Escape, "\\'"), + (Token.Literal.String.Single, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{{ {4*10} }}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Escape, '{{'), + (Token.Literal.String.Single, ' '), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.Number.Integer, '4'), + (Token.Operator, '*'), + (Token.Literal.Number.Integer, '10'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ' '), + (Token.Literal.String.Escape, '}}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{{{4*10}}}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Escape, '{{'), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.Number.Integer, '4'), + (Token.Operator, '*'), + (Token.Literal.Number.Integer, '10'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Escape, '}}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "fr'x={4*10}'\n", + [ + (Token.Literal.String.Affix, 'fr'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, "x="), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.Number.Integer, '4'), + (Token.Operator, '*'), + (Token.Literal.Number.Integer, '10'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """f'abc {a["x"]} def'\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'abc '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'a'), + (Token.Punctuation, '['), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Double, 'x'), + (Token.Literal.String.Double, '"'), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ' def'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'''abc {a['x']} def'''\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'''"), + (Token.Literal.String.Single, 'abc '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'a'), + (Token.Punctuation, '['), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'x'), + (Token.Literal.String.Single, "'"), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ' def'), + (Token.Literal.String.Single, "'''"), + (Token.Text, '\n') + ] + ), ( + """f'''{x ++1}'''\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'''"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'x'), + (Token.Text, '\n'), + (Token.Operator, '+'), + (Token.Literal.Number.Integer, '1'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'''"), + (Token.Text, '\n') + ] + ), ( + """f'''{d[0 +]}'''\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'''"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'd'), + (Token.Punctuation, '['), + (Token.Literal.Number.Integer, '0'), + (Token.Text, '\n'), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'''"), + (Token.Text, '\n') + ] + ), ( + "f'result: {value:{width}.{precision}}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'result: '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'value'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'width'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '.'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'precision'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "'a' 'b' f'{x}' '{c}' f'str<{y:^4}>' 'd' 'e'\n", + [ + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'a'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'b'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'x'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{c}'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'str<'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'y'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Single, '^4'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '>'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'd'), + (Token.Literal.String.Single, "'"), + (Token.Text, ' '), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'e'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{i}:{d[i]}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'i'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ':'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'd'), + (Token.Punctuation, '['), + (Token.Name, 'i'), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'x = {x:+3}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, "x = "), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'x'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Single, '+3'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{fn(lst,2)} {fn(lst,3)}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'fn'), + (Token.Punctuation, '('), + (Token.Name, 'lst'), + (Token.Punctuation, ','), + (Token.Literal.Number.Integer, '2'), + (Token.Punctuation, ')'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ' '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'fn'), + (Token.Punctuation, '('), + (Token.Name, 'lst'), + (Token.Punctuation, ','), + (Token.Literal.Number.Integer, '3'), + (Token.Punctuation, ')'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'mapping is { {a:b for (a, b) in ((1, 2), (3, 4))} }'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'mapping is '), + (Token.Literal.String.Interpol, '{'), + (Token.Text, ' '), + (Token.Punctuation, '{'), + (Token.Name, 'a'), + (Token.Punctuation, ':'), + (Token.Name, 'b'), + (Token.Text, ' '), + (Token.Keyword, 'for'), + (Token.Text, ' '), + (Token.Punctuation, '('), + (Token.Name, 'a'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Name, 'b'), + (Token.Punctuation, ')'), + (Token.Text, ' '), + (Token.Operator.Word, 'in'), + (Token.Text, ' '), + (Token.Punctuation, '('), + (Token.Punctuation, '('), + (Token.Literal.Number.Integer, '1'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Literal.Number.Integer, '2'), + (Token.Punctuation, ')'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Punctuation, '('), + (Token.Literal.Number.Integer, '3'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Literal.Number.Integer, '4'), + (Token.Punctuation, ')'), + (Token.Punctuation, ')'), + (Token.Punctuation, '}'), + (Token.Text, ' '), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """f'a={d["a"]}'\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'a='), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'd'), + (Token.Punctuation, '['), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Double, 'a'), + (Token.Literal.String.Double, '"'), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'a={d[a]}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, 'a='), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'd'), + (Token.Punctuation, '['), + (Token.Name, 'a'), + (Token.Punctuation, ']'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "fr'{header}:\\s+'\n", + [ + (Token.Literal.String.Affix, 'fr'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'header'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ':'), + (Token.Literal.String.Single, '\\'), + (Token.Literal.String.Single, 's+'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{a!r}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'a'), + (Token.Literal.String.Interpol, '!r}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'{(lambda x: x*2)(3)}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Punctuation, '('), + (Token.Keyword, 'lambda'), + (Token.Text, ' '), + (Token.Name, 'x'), + (Token.Punctuation, ':'), + (Token.Text, ' '), + (Token.Name, 'x'), + (Token.Operator, '*'), + (Token.Literal.Number.Integer, '2'), + (Token.Punctuation, ')'), + (Token.Punctuation, '('), + (Token.Literal.Number.Integer, '3'), + (Token.Punctuation, ')'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "extra = f'{extra},waiters:{len(self._waiters)}'\n", + [ + (Token.Name, 'extra'), + (Token.Text, ' '), + (Token.Operator, '='), + (Token.Text, ' '), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'extra'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, ',waiters:'), + (Token.Literal.String.Interpol, '{'), + (Token.Name.Builtin, 'len'), + (Token.Punctuation, '('), + (Token.Name.Builtin.Pseudo, 'self'), + (Token.Operator, '.'), + (Token.Name, '_waiters'), + (Token.Punctuation, ')'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + 'message.append(f" [line {lineno:2d}]")\n', + [ + (Token.Name, 'message'), + (Token.Operator, '.'), + (Token.Name, 'append'), + (Token.Punctuation, '('), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Double, ' [line '), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'lineno'), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Double, '2d'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Double, ']'), + (Token.Literal.String.Double, '"'), + (Token.Punctuation, ')'), + (Token.Text, '\n') + ] + ), + # Examples from https://bugs.python.org/issue36817 + ( + 'f"{foo=}"\n', + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'foo'), + (Token.Literal.String.Interpol, '=}'), + (Token.Literal.String.Double, '"'), + (Token.Text, '\n') + ] + ), ( + "f'{foo=!s}'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'foo'), + (Token.Literal.String.Interpol, '=!s}'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + 'f"{math.pi=!f:.2f}"\n', + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'math'), + (Token.Operator, '.'), + (Token.Name, 'pi'), + (Token.Literal.String.Interpol, '=!f:'), + (Token.Literal.String.Double, '.2f'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Double, '"'), + (Token.Text, '\n') + ] + ), ( + 'f"{ chr(65) =}"\n', + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '{'), + (Token.Text, ' '), + (Token.Name.Builtin, 'chr'), + (Token.Punctuation, '('), + (Token.Literal.Number.Integer, '65'), + (Token.Punctuation, ')'), + (Token.Text, ' '), + (Token.Literal.String.Interpol, '=}'), + (Token.Literal.String.Double, '"'), + (Token.Text, '\n') + ] + ), ( + 'f"{chr(65) = }"\n', + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Interpol, '{'), + (Token.Name.Builtin, 'chr'), + (Token.Punctuation, '('), + (Token.Literal.Number.Integer, '65'), + (Token.Punctuation, ')'), + (Token.Text, ' '), + (Token.Literal.String.Interpol, '= }'), + (Token.Literal.String.Double, '"'), + (Token.Text, '\n') + ] + ), ( + "f'*{n=:30}*'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'n'), + (Token.Literal.String.Interpol, '=:'), + (Token.Literal.String.Single, '30'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + "f'*{n=!r:30}*'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'n'), + (Token.Literal.String.Interpol, '=!r:'), + (Token.Literal.String.Single, '30'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """f"*{f'{n=}':30}*"\n""", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Double, '"'), + (Token.Literal.String.Double, '*'), + (Token.Literal.String.Interpol, '{'), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'n'), + (Token.Literal.String.Interpol, '=}'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Interpol, ':'), + (Token.Literal.String.Double, '30'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Double, '*'), + (Token.Literal.String.Double, '"'), + (Token.Text, '\n') + ] + ), ( + "f'*{n=:+<30}*'\n", + [ + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'"), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'n'), + (Token.Literal.String.Interpol, '=:'), + (Token.Literal.String.Single, '+<30'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, '*'), + (Token.Literal.String.Single, "'"), + (Token.Text, '\n') + ] + ), ( + """ + f'''{foo + = !s:20}'''\n""", + [ + (Token.Text, ' '), + (Token.Literal.String.Affix, 'f'), + (Token.Literal.String.Single, "'''"), + (Token.Literal.String.Interpol, '{'), + (Token.Name, 'foo'), + (Token.Text, '\n '), + (Token.Literal.String.Interpol, '= !s:'), + (Token.Literal.String.Single, '20'), + (Token.Literal.String.Interpol, '}'), + (Token.Literal.String.Single, "'''"), + (Token.Text, '\n') + ] + ) + + ) + + for fragment,tokens in fragments_and_tokens: + assert list(lexer3.get_tokens(fragment)) == tokens + + # Now switch between single and double quotes, to cover both cases equally + rep = {"'":'"', '"':"'"} + pattern = re.compile("|".join(rep.keys())) + for fragment,tokens in fragments_and_tokens: + fragment = pattern.sub(lambda m: rep[m.group(0)], fragment) + tokens = list(tokens) + for i,(token,match) in enumerate(tokens): + if token == Token.Literal.String.Single: + token = Token.Literal.String.Double + elif token == Token.Literal.String.Double: + token = Token.Literal.String.Single + match = pattern.sub(lambda m: rep[m.group(0)], match) + tokens[i] = (token, match) + assert list(lexer3.get_tokens(fragment)) == tokens |
