summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomer Vromen <tomer.vromen@intel.com>2020-02-04 20:34:36 +0200
committerTomer Vromen <tomer.vromen@intel.com>2020-02-04 20:34:36 +0200
commitf7b14bcaaae689e243bb379713bbd28686ee7e19 (patch)
treef38534ed868035f2d889b28a2df8dc713a716d66
parent55193857195b6a36c70708fadbe01f3ed61c0a88 (diff)
downloadpygments-git-f7b14bcaaae689e243bb379713bbd28686ee7e19.tar.gz
Python f-strings: highlight expressions in curly braces
Fixes #1228
-rw-r--r--pygments/lexers/python.py121
-rw-r--r--tests/test_python.py682
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