From 7e58014aa2d4cd2806d16e59b2a971fdd98e9337 Mon Sep 17 00:00:00 2001 From: deathaxe Date: Sat, 15 Jul 2023 17:52:04 +0200 Subject: [PATCH] [Python] Fix string replacement fields (#3797) * [Python] Fix string replacement fields Fixes #3649 This commit closely implements python's format string syntax. see: https://docs.python.org/3.11/library/string.html#format-string-syntax It means, replacement field names must be valid qualified identifiers, even though python syntactically resolves all kinds of key names. Replacement field termination is handled separately for each kind of quotation. * [Python] Refactor f-strings This commit ... 1. aligns implementation of f-string replacement fields with normal string replacements. 2. implements f-string replacements for each kind of quotation in order to correctly bailout at EOL or terminating quotation mark type. Note: Nested quotes are supported in replacement expressions only as of python 3.12. They keep unsupported in format-specs. 3. moves escaped braces back into f-string-replacements context and adjusts its include positions (moves them before normal `escaped-chars`). 4. removes `meta.format-spec` from `:` to align scoping with replacement fields in normal strings. 5. adds some tests to verify syntax being prepared for python 3.12 --- Python/Python.sublime-syntax | 545 +++++++++++++++------ Python/tests/syntax_test_python_strings.py | 504 +++++++++++++++++-- 2 files changed, 843 insertions(+), 206 deletions(-) diff --git a/Python/Python.sublime-syntax b/Python/Python.sublime-syntax index bc0665b15d..0e0a1ad757 100644 --- a/Python/Python.sublime-syntax +++ b/Python/Python.sublime-syntax @@ -45,18 +45,8 @@ variables: exponent: (?:[eE][-+]?{{digits}}) path: '({{identifier}}[ ]*\.[ ]*)*{{identifier}}' illegal_names: (?:and|as|assert|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|not|or|pass|raise|return|try|while|with|yield) - format_spec: |- - (?x: - (?:.? [<>=^])? # fill align - [ +-]? # sign - \#? # alternate form - # technically, octal and hexadecimal integers are also supported as 'width', but rarely used - \d* # width - ,? # thousands separator - (?:\.\d+)? # precision - [bcdeEfFgGnosxX%]? # type - ) - strftime_spec: '(?:%(?:[aAwdbBGmyYHIpMSfzZjuUVWcxX%]|-[dmHIMSj]))' + + strftime_spec: (?:%(?:[aAwdbBGmyYHIpMSfzZjuUVWcxX%]|-[dmHIMSj])) augmented_assignment_operators: |- (?x: >>= | <<= | \*\*= | //= | \+= | -= | \*= | /= | %= | @= | &= | \|= | \^= ) @@ -2569,7 +2559,7 @@ contexts: triple-double-quoted-plain-raw-f-string-content: - include: string-prototype - - include: f-string-replacements + - include: triple-double-quoted-f-string-replacements triple-double-quoted-raw-f-strings: # Triple-quoted raw f-string, treated as regex @@ -2591,7 +2581,7 @@ contexts: triple-double-quoted-raw-f-string-content: - include: string-prototype - - include: f-string-replacements-regexp + - include: triple-double-quoted-f-string-replacements-regexp triple-double-quoted-plain-raw-u-strings: # Triple-quoted capital R raw string, unicode or not, no syntax embedding @@ -2659,7 +2649,7 @@ contexts: - include: string-prototype - include: escaped-unicode-chars - include: string-placeholders - - include: string-replacements + - include: triple-double-quoted-string-replacements triple-double-quoted-b-strings: # Triple-quoted string, bytes, no syntax embedding @@ -2680,7 +2670,7 @@ contexts: - include: string-continuations - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: triple-double-quoted-string-replacements triple-double-quoted-f-strings: # Triple-quoted f-string @@ -2699,10 +2689,9 @@ contexts: triple-double-quoted-f-string-content: - include: string-prototype - include: string-continuations - - include: escaped-f-string-braces + - include: triple-double-quoted-f-string-replacements - include: escaped-unicode-chars - include: escaped-chars - - include: f-string-replacements triple-double-quoted-u-strings: # Triple-quoted string, unicode or not, will detect SQL @@ -2747,7 +2736,88 @@ contexts: - include: escaped-unicode-chars - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: triple-double-quoted-string-replacements + + triple-double-quoted-string-replacements: + - include: escaped-string-braces + - match: (?=\{) + branch_point: triple-double-quoted-string-replacement + branch: + - triple-double-quoted-string-replacement + - string-replacement-fallback + + triple-double-quoted-string-replacement: + - match: \{ + scope: punctuation.definition.placeholder.begin.python + set: triple-double-quoted-string-replacement-body + + triple-double-quoted-string-replacement-body: + - meta_scope: constant.other.placeholder.python + - include: string-replacement-body + - match: \[ + scope: punctuation.section.brackets.begin.python + push: triple-double-string-replacement-brackets-body + - match: ':' + scope: punctuation.separator.format-spec.python + push: triple-double-quoted-string-replacement-formatspec-body + - match: (?!\w) # restrict field names to reduce risk of false positives + fail: triple-double-quoted-string-replacement + + triple-double-string-replacement-brackets-body: + - include: string-replacement-brackets-body + - include: triple-double-quoted-string-replacement-fail + + triple-double-quoted-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: string-replacement-formatspec-body + - match: \{ + scope: punctuation.definition.placeholder.begin.python + push: triple-double-quoted-string-replacement-body + - include: triple-double-quoted-string-replacement-fail + + triple-double-quoted-string-replacement-fail: + - match: (?="""|$) + fail: triple-double-quoted-string-replacement + + triple-double-quoted-f-string-replacements: + - include: f-string-replacements + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-meta + - triple-double-quoted-f-string-replacement-formatspec + - triple-double-quoted-f-string-replacement-expression + + triple-double-quoted-f-string-replacements-regexp: + # Same as f-string-replacements, but will reset the entire scope stack. + - include: f-string-replacements-regexp + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-regexp-meta + - triple-double-quoted-f-string-replacement-formatspec + - triple-double-quoted-f-string-replacement-expression + + triple-double-quoted-f-string-replacement-expression: + - meta_content_scope: source.python.embedded + - include: f-string-replacement-expression + + triple-double-quoted-f-string-replacement-formatspec: + - meta_include_prototype: false + - match: ':' + scope: punctuation.separator.format-spec.python + set: triple-double-quoted-f-string-replacement-formatspec-body + - include: triple-double-quoted-f-string-replacement-end + + triple-double-quoted-f-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: triple-double-quoted-f-string-replacement-end + - include: triple-double-quoted-f-string-replacements + + triple-double-quoted-f-string-replacement-end: + - include: f-string-replacement-end + - match: (?=""") + pop: 2 ###[ DOUBLE QUOTED STRINGS ]################################################## @@ -2830,7 +2900,7 @@ contexts: double-quoted-plain-raw-f-string-content: - include: string-prototype - - include: f-string-replacements + - include: double-quoted-f-string-replacements double-quoted-raw-f-strings: # Single-line raw f-string, treated as regex @@ -2852,7 +2922,7 @@ contexts: double-quoted-raw-f-string-content: - include: string-prototype - - include: f-string-replacements-regexp + - include: double-quoted-f-string-replacements-regexp double-quoted-plain-raw-u-strings: # Single-line capital R raw string, unicode or not, no syntax embedding @@ -2918,7 +2988,7 @@ contexts: double-quoted-sql-raw-u-string-content: - include: string-prototype - include: string-placeholders - - include: string-replacements + - include: double-quoted-string-replacements double-quoted-b-strings: # Single-line string, bytes @@ -2938,7 +3008,7 @@ contexts: - include: string-prototype - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: double-quoted-string-replacements double-quoted-f-strings: # Single-line f-string @@ -2956,10 +3026,9 @@ contexts: double-quoted-f-string-content: - include: string-prototype - - include: escaped-f-string-braces + - include: double-quoted-f-string-replacements - include: escaped-unicode-chars - include: escaped-chars - - include: f-string-replacements double-quoted-u-strings: - match: ([uU]?)(") @@ -3003,7 +3072,89 @@ contexts: - include: escaped-unicode-chars - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: double-quoted-string-replacements + + double-quoted-string-replacements: + - include: escaped-string-braces + - match: (?=\{) + branch_point: double-quoted-string-replacement + branch: + - double-quoted-string-replacement + - string-replacement-fallback + + double-quoted-string-replacement: + - match: \{ + scope: punctuation.definition.placeholder.begin.python + set: double-quoted-string-replacement-body + + double-quoted-string-replacement-body: + - meta_scope: constant.other.placeholder.python + - include: string-replacement-body + - match: \[ + scope: punctuation.section.brackets.begin.python + push: double-quoted-string-replacement-brackets-body + - match: ':' + scope: punctuation.separator.format-spec.python + push: double-quoted-string-replacement-formatspec-body + - match: (?!\w) # restrict field names to reduce risk of false positives + fail: double-quoted-string-replacement + + double-quoted-string-replacement-brackets-body: + - include: string-replacement-brackets-body + - include: double-quoted-string-replacement-fail + + double-quoted-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: string-replacement-formatspec-body + - match: \{ + scope: punctuation.definition.placeholder.begin.python + push: double-quoted-string-replacement-body + - include: double-quoted-string-replacement-fail + + double-quoted-string-replacement-fail: + - match: (?="|$) + fail: double-quoted-string-replacement + + double-quoted-f-string-replacements: + - include: f-string-replacements + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-meta + - double-quoted-f-string-replacement-formatspec + - double-quoted-f-string-replacement-expression + + double-quoted-f-string-replacements-regexp: + # Same as f-string-replacements, but will reset the entire scope stack. + - include: f-string-replacements-regexp + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-regexp-meta + - double-quoted-f-string-replacement-formatspec + - double-quoted-f-string-replacement-expression + + double-quoted-f-string-replacement-expression: + - meta_content_scope: source.python.embedded + - include: f-string-replacement-expression + - include: eol-pop + + double-quoted-f-string-replacement-formatspec: + - meta_include_prototype: false + - match: ':' + scope: punctuation.separator.format-spec.python + set: double-quoted-f-string-replacement-formatspec-body + - include: double-quoted-f-string-replacement-end + + double-quoted-f-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: double-quoted-f-string-replacement-end + - include: double-quoted-f-string-replacements + + double-quoted-f-string-replacement-end: + - include: f-string-replacement-end + - match: (?="|$) + pop: 2 ###[ TRIPLE SINGLE QUOTED STRINGS ]########################################### @@ -3081,7 +3232,7 @@ contexts: triple-single-quoted-plain-raw-f-string-content: - include: string-prototype - - include: f-string-replacements + - include: triple-single-quoted-f-string-replacements triple-single-quoted-raw-f-strings: # Triple-quoted raw f-string, treated as regex @@ -3103,7 +3254,7 @@ contexts: triple-single-quoted-raw-f-string-content: - include: string-prototype - - include: f-string-replacements-regexp + - include: triple-single-quoted-f-string-replacements-regexp triple-single-quoted-plain-raw-u-strings: # Triple-quoted capital R raw string, unicode or not, no syntax embedding @@ -3170,7 +3321,7 @@ contexts: - include: string-prototype - include: escaped-unicode-chars - include: string-placeholders - - include: string-replacements + - include: triple-single-quoted-string-replacements triple-single-quoted-b-strings: # Triple-quoted string, bytes, no syntax embedding @@ -3191,7 +3342,7 @@ contexts: - include: string-continuations - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: triple-single-quoted-string-replacements triple-single-quoted-f-strings: # Triple-quoted f-string @@ -3210,10 +3361,9 @@ contexts: triple-single-quoted-f-string-content: - include: string-prototype - include: string-continuations - - include: escaped-f-string-braces + - include: triple-single-quoted-f-string-replacements - include: escaped-unicode-chars - include: escaped-chars - - include: f-string-replacements triple-single-quoted-u-strings: # Triple-quoted string, unicode or not, will detect SQL @@ -3258,7 +3408,88 @@ contexts: - include: escaped-unicode-chars - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: triple-single-quoted-string-replacements + + triple-single-quoted-string-replacements: + - include: escaped-string-braces + - match: (?=\{) + branch_point: triple-single-quoted-string-replacement + branch: + - triple-single-quoted-string-replacement + - string-replacement-fallback + + triple-single-quoted-string-replacement: + - match: \{ + scope: punctuation.definition.placeholder.begin.python + set: triple-single-quoted-string-replacement-body + + triple-single-quoted-string-replacement-body: + - meta_scope: constant.other.placeholder.python + - include: string-replacement-body + - match: \[ + scope: punctuation.section.brackets.begin.python + push: triple-single-string-replacement-brackets-body + - match: ':' + scope: punctuation.separator.format-spec.python + push: triple-single-quoted-string-replacement-formatspec-body + - match: (?!\w) # restrict field names to reduce risk of false positives + fail: triple-single-quoted-string-replacement + + triple-single-string-replacement-brackets-body: + - include: string-replacement-brackets-body + - include: triple-single-quoted-string-replacement-fail + + triple-single-quoted-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: string-replacement-formatspec-body + - match: \{ + scope: punctuation.definition.placeholder.begin.python + push: triple-single-quoted-string-replacement-body + - include: triple-single-quoted-string-replacement-fail + + triple-single-quoted-string-replacement-fail: + - match: (?='''|$) + fail: triple-single-quoted-string-replacement + + triple-single-quoted-f-string-replacements: + - include: f-string-replacements + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-meta + - triple-single-quoted-f-string-replacement-formatspec + - triple-single-quoted-f-string-replacement-expression + + triple-single-quoted-f-string-replacements-regexp: + # Same as f-string-replacements, but will reset the entire scope stack. + - include: f-string-replacements-regexp + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-regexp-meta + - triple-single-quoted-f-string-replacement-formatspec + - triple-single-quoted-f-string-replacement-expression + + triple-single-quoted-f-string-replacement-expression: + - meta_content_scope: source.python.embedded + - include: f-string-replacement-expression + + triple-single-quoted-f-string-replacement-formatspec: + - meta_include_prototype: false + - match: ':' + scope: punctuation.separator.format-spec.python + set: triple-single-quoted-f-string-replacement-formatspec-body + - include: triple-single-quoted-f-string-replacement-end + + triple-single-quoted-f-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: triple-single-quoted-f-string-replacement-end + - include: triple-single-quoted-f-string-replacements + + triple-single-quoted-f-string-replacement-end: + - include: f-string-replacement-end + - match: (?=''') + pop: 2 ###[ SINGLE QUOTED STRINGS ]################################################## @@ -3389,7 +3620,7 @@ contexts: single-quoted-sql-raw-u-string-content: - include: string-prototype - include: string-placeholders - - include: string-replacements + - include: single-quoted-string-replacements single-quoted-plain-raw-f-strings: # Single-line raw f-string @@ -3407,7 +3638,7 @@ contexts: single-quoted-plain-raw-f-string-content: - include: string-prototype - - include: f-string-replacements + - include: single-quoted-f-string-replacements single-quoted-raw-f-strings: # Single-line raw f-string, treated as regex @@ -3429,7 +3660,7 @@ contexts: single-quoted-raw-f-string-content: - include: string-prototype - - include: f-string-replacements-regexp + - include: single-quoted-f-string-replacements-regexp single-quoted-b-strings: # Single-line string, bytes @@ -3449,7 +3680,7 @@ contexts: - include: string-prototype - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: single-quoted-string-replacements single-quoted-f-strings: # Single-line f-string @@ -3467,10 +3698,9 @@ contexts: single-quoted-f-string-content: - include: string-prototype - - include: escaped-f-string-braces + - include: single-quoted-f-string-replacements - include: escaped-unicode-chars - include: escaped-chars - - include: f-string-replacements single-quoted-u-strings: - match: ([uU]?)(') @@ -3514,7 +3744,89 @@ contexts: - include: escaped-unicode-chars - include: escaped-chars - include: string-placeholders - - include: string-replacements + - include: single-quoted-string-replacements + + single-quoted-string-replacements: + - include: escaped-string-braces + - match: (?=\{) + branch_point: single-quoted-string-replacement + branch: + - single-quoted-string-replacement + - string-replacement-fallback + + single-quoted-string-replacement: + - match: \{ + scope: punctuation.definition.placeholder.begin.python + set: single-quoted-string-replacement-body + + single-quoted-string-replacement-body: + - meta_scope: constant.other.placeholder.python + - include: string-replacement-body + - match: \[ + scope: punctuation.section.brackets.begin.python + push: single-string-replacement-brackets-body + - match: ':' + scope: punctuation.separator.format-spec.python + push: single-quoted-string-replacement-formatspec-body + - match: (?!\w) # restrict field names to reduce risk of false positives + fail: single-quoted-string-replacement + + single-string-replacement-brackets-body: + - include: string-replacement-brackets-body + - include: single-quoted-string-replacement-fail + + single-quoted-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: string-replacement-formatspec-body + - match: \{ + scope: punctuation.definition.placeholder.begin.python + push: single-quoted-string-replacement-body + - include: single-quoted-string-replacement-fail + + single-quoted-string-replacement-fail: + - match: (?='|$) + fail: single-quoted-string-replacement + + single-quoted-f-string-replacements: + - include: f-string-replacements + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-meta + - single-quoted-f-string-replacement-formatspec + - single-quoted-f-string-replacement-expression + + single-quoted-f-string-replacements-regexp: + # Same as f-string-replacements, but will reset the entire scope stack. + - include: f-string-replacements-regexp + - match: \{ + scope: punctuation.section.interpolation.begin.python + push: + - f-string-replacement-regexp-meta + - single-quoted-f-string-replacement-formatspec + - single-quoted-f-string-replacement-expression + + single-quoted-f-string-replacement-expression: + - meta_content_scope: source.python.embedded + - include: f-string-replacement-expression + - include: eol-pop + + single-quoted-f-string-replacement-formatspec: + - meta_include_prototype: false + - match: ':' + scope: punctuation.separator.format-spec.python + set: single-quoted-f-string-replacement-formatspec-body + - include: single-quoted-f-string-replacement-end + + single-quoted-f-string-replacement-formatspec-body: + - meta_content_scope: meta.format-spec.python constant.other.format-spec.python + - include: single-quoted-f-string-replacement-end + - include: single-quoted-f-string-replacements + + single-quoted-f-string-replacement-end: + - include: f-string-replacement-end + - match: (?='|$) + pop: 2 ###[ STRING CONTENTS ]######################################################## @@ -3544,13 +3856,9 @@ contexts: - match: \\N\{[-a-zA-Z ]+\} scope: constant.character.escape.unicode.name.python - escaped-f-string-braces: - # special-case the '\{{' sequence because it has higher priority than the deprecated '\{' - - match: (\\)(\{\{|\}\}) - scope: constant.character.escape.backslash.regexp - captures: - 1: invalid.deprecated.character.escape.python - 2: constant.character.escape.python + escaped-string-braces: + - match: \{\{|\}\} + scope: constant.character.escape.python string-placeholders: - match: |- # printf style @@ -3572,80 +3880,43 @@ contexts: - match: '{{strftime_spec}}' scope: constant.other.placeholder.python - string-replacements: - - match: \{\{|\}\} - scope: constant.character.escape.python - # https://docs.python.org/3.6/library/string.html#formatstrings - # Technically allows almost every character for the key, - # but those are rarely used if ever. - - match: |- # simple form - (?x) - (\{) - (?: [\w.\[\]]+)? # field_name - ( ! [ars])? # conversion - (?: (:) ({{format_spec}}| # format_spec OR - [^}%]*%.[^}]*) # any format-like string - )? - (\}) - scope: constant.other.placeholder.python - captures: - 1: punctuation.definition.placeholder.begin.python - 2: storage.modifier.conversion.python - 3: punctuation.separator.format-spec.python - 4: meta.format-spec.python constant.other.format-spec.python - 5: punctuation.definition.placeholder.end.python - - match: (?=\{[^{}"']+\{[^"']*\}) # complex (nested) form - branch_point: string-replacement - branch: - - string-replacement - - string-replacement-fallback - - string-replacement: - - match: \{ - scope: punctuation.definition.placeholder.begin.python - set: string-replacement-body - string-replacement-body: - - meta_scope: constant.other.placeholder.python - match: \} scope: punctuation.definition.placeholder.end.python pop: 1 - # TODO could match numeric indices or everything else as a key - # and also [] indexing - - match: '![ars]' + - match: \. + scope: punctuation.accessor.dot.python + - match: '![ars]\b' scope: storage.modifier.conversion.python - - match: ':' - scope: punctuation.separator.format-spec.python - push: string-replacement-formatspec-body - - match: '[{"''\n]' - fail: string-replacement + - include: escaped-chars - string-replacement-formatspec-body: - - meta_content_scope: meta.format-spec.python constant.other.format-spec.python - - match: (?=\}) + string-replacement-brackets-body: + - meta_scope: meta.brackets.python + - match: \] + scope: punctuation.section.brackets.end.python pop: 1 - - match: (?=\{) - push: string-replacement + - include: eol-pop + - include: escaped-chars + + string-replacement-formatspec-body: + - match: \} + scope: punctuation.definition.placeholder.end.python + pop: 2 + - include: escaped-chars string-replacement-fallback: - match: \{ - scope: meta.debug.string-replacement-fallback.python pop: 1 f-string-replacements: - # https://www.python.org/dev/peps/pep-0498/ + # https://peps.python.org/pep-0498 + # https://peps.python.org/pep-0701 # https://docs.python.org/3.6/reference/lexical_analysis.html#f-strings - - match: \{\{|\}\} + - match: (\\)?(?:\{\{|\}\}) scope: constant.character.escape.python - - match: \{\s*\} - scope: invalid.illegal.empty-expression.python - - match: \{ - scope: punctuation.section.interpolation.begin.python - push: - - f-string-replacement-meta - - f-string-replacement-modifier - - f-string-replacement-expression - - include: illegal-stray-braces + captures: + 1: invalid.deprecated.character.escape.python + - include: invalid-f-string-replacements f-string-replacement-meta: - clear_scopes: 1 @@ -3653,38 +3924,14 @@ contexts: - meta_scope: meta.interpolation.python - include: immediately-pop - f-string-replacement-expression: - - meta_content_scope: source.python.embedded - - match: (?=\s*(?:=\s*)?(![^=]|:|\})) - pop: 1 - - match: \\ - scope: invalid.illegal.backslash-in-fstring.python - - include: expression-in-a-sequence - - f-string-replacement-modifier: - - include: f-string-replacement-end - - include: f-string-replacement-common - - match: = - scope: storage.modifier.debug.python - f-string-replacements-regexp: # Same as f-string-replacements, but will reset the entire scope stack # and has an additional match. - - match: \\(\{\{|\}\}) - scope: constant.character.escape.backslash.regexp + - match: \\?(\{\{|\}\}) + scope: constant.character.escape.regexp captures: 1: constant.character.escape.python - - match: \{\{|\}\} - scope: constant.character.escape.python - - match: \{\s*\} - scope: invalid.illegal.empty-expression.python - - match: \{ - scope: punctuation.section.interpolation.begin.python - push: - - f-string-replacement-regexp-meta - - f-string-replacement-regexp-modifier - - f-string-replacement-regexp-expression - - include: illegal-stray-braces + - include: invalid-f-string-replacements f-string-replacement-regexp-meta: # Same as f-string-replacement, but with clear_scopes: true @@ -3693,38 +3940,26 @@ contexts: - meta_scope: source.python meta.string.python meta.interpolation.python - include: immediately-pop - f-string-replacement-regexp-expression: - - meta_content_scope: source.python.embedded - - match: (?=![^=]|:|\}) + f-string-replacement-expression: + - match: (?:\s*(=)\s*)?(![ars])?(?=[:}]) + captures: + 1: storage.modifier.debug.python + 2: storage.modifier.conversion.python pop: 1 - match: \\ scope: invalid.illegal.backslash-in-fstring.python - include: expression-in-a-sequence - f-string-replacement-regexp-modifier: - - include: f-string-replacement-end - - include: f-string-replacement-common - - f-string-replacement-common: - - match: '![ars]' - scope: storage.modifier.conversion.python - - match: ':' - scope: meta.format-spec.python constant.other.format-spec.python punctuation.separator.format-spec.python - set: f-string-format-spec - - f-string-format-spec: - - meta_content_scope: meta.format-spec.python constant.other.format-spec.python - # Because replacements can also be used *within* the format-spec, - # basically any character is valid and matching {{format_spec}} is useless. - # - match: '{{format_spec}}' - - include: f-string-replacement-end - - include: f-string-replacements - f-string-replacement-end: - match: \} scope: punctuation.section.interpolation.end.python pop: 2 + invalid-f-string-replacements: + - match: \{\s*\} + scope: invalid.illegal.empty-expression.python + - include: illegal-stray-braces + # for use by inheriting syntaxes to easily inject string interpolation # in any kind of quoted or unquoted string string-prototype: [] diff --git a/Python/tests/syntax_test_python_strings.py b/Python/tests/syntax_test_python_strings.py index 735256dff6..58576ac1ad 100644 --- a/Python/tests/syntax_test_python_strings.py +++ b/Python/tests/syntax_test_python_strings.py @@ -529,6 +529,7 @@ # ^^^^^^ constant.other.placeholder.python "Weight in tons {0.weight}" # 'weight' attribute of first positional arg # ^^^^^^^^^^ constant.other.placeholder.python +# ^ punctuation.accessor.dot.python "Units destroyed: {players[0]}" # First element of keyword argument 'players'. # ^^^^^^^^^^^^ constant.other.placeholder.python "Harold's a clever {0!s}" # Calls str() on the argument first @@ -586,6 +587,46 @@ # ^^^^^^^^^^^ constant.other.placeholder constant.other.placeholder # ^^ punctuation.definition.placeholder.end +# Nested quotes and escaped quotes + +"Testing {foo['bar']:'^9}".format(foo={"bar": 1000}) +# <- meta.string.python string.quoted.double.python punctuation.definition.string.begin.python +#^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string.python string.quoted.double.python +# ^^^^^^^^^^^^^^^^ constant.other.placeholder +# ^ punctuation.separator.format-spec.python +# ^ punctuation.definition.string.end.python +# ^ punctuation.accessor.dot.python + +"Testing {foo[\"bar\"]:\"^9}".format(foo={"bar": 1000}) +# <- meta.string.python string.quoted.double.python punctuation.definition.string.begin.python +#^^^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string.python string.quoted.double.python +# ^^^^^^^^^^^^^^^^^^^ constant.other.placeholder +# ^^ constant.character.escape.python +# ^^ constant.character.escape.python +# ^ punctuation.separator.format-spec.python +# ^^ constant.character.escape.python +# ^ punctuation.definition.string.end.python +# ^ punctuation.accessor.dot.python + +'Testing {foo["bar"]:"^9}'.format(foo={"bar": 1000}) +# <- meta.string.python string.quoted.single.python punctuation.definition.string.begin.python +#^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string.python string.quoted.single.python +# ^^^^^^^^^^^^^^^^ constant.other.placeholder +# ^ punctuation.separator.format-spec.python +# ^ punctuation.definition.string.end.python +# ^ punctuation.accessor.dot.python + +'Testing {foo[\'bar\']:\'^9}'.format(foo={"bar": 1000}) +# <- meta.string.python string.quoted.single.python punctuation.definition.string.begin.python +#^^^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.string.python string.quoted.single.python +# ^^^^^^^^^^^^^^^^^^^ constant.other.placeholder +# ^^ constant.character.escape.python +# ^^ constant.character.escape.python +# ^ punctuation.separator.format-spec.python +# ^^ constant.character.escape.python +# ^ punctuation.definition.string.end.python +# ^ punctuation.accessor.dot.python + a=["aaaa{", "bbbb{"] # ^ - constant.other.placeholder # ^ punctuation.definition.string.end.python @@ -602,40 +643,91 @@ # so some of these matches are incorrect because of implementation details. # Not format specs -"{:{ }" # unclosed +"{:{ }" # unclosed # ^ - constant.other.placeholder -'{{foo!r:4.2}' # escaped opening +'{:{ }' # unclosed # ^ - constant.other.placeholder -'{{foo!r:4.2}}' # escaped opening and closing +"{{foo!r:4.2}" # escaped opening # ^ - constant.other.placeholder -'{foo!a:ran{dom}' # unclosed +'{{foo!r:4.2}' # escaped opening +# ^ - constant.other.placeholder +"{{foo!r:4.2}}" # escaped opening and closing +# ^ - constant.other.placeholder +'{{foo!r:4.2}}' # escaped opening and closing +# ^ - constant.other.placeholder +"{foo!a:ran{dom}" # unclosed # ^ - constant.other.placeholder '{foo!a:ran{dom}' # unclosed # ^ - constant.other.placeholder +# Incomplete field elements +"{foo[" # unclosed elemnt index +#^^^^^ - constant.other.placeholder +'{foo[' # unclosed elemnt index +#^^^^^ - constant.other.placeholder +"{foo[}" # unclosed elemnt index +#^^^^^^ - constant.other.placeholder +'{foo[}' # unclosed elemnt index +#^^^^^^ - constant.other.placeholder +"{foo[""]}" # unsupported nested quotes +#^^^^^^^^^ - constant.other.placeholder +'{foo['']}' # unsupported nested quotes +#^^^^^^^^^ - constant.other.placeholder + # Invalid field names +"{foo{d}}" +# ^ - constant.other.placeholder '{foo{d}}' # ^ - constant.other.placeholder "{:{ {}}" # issue 2232 # ^ - constant.other.placeholder +'{:{ {}}' # issue 2232 +# ^ - constant.other.placeholder +"{foo.!a:d}" # incomplete accessor (in simple form) +# ^ constant.other.placeholder +# ^ punctuation.accessor.dot '{foo.!a:d}' # incomplete accessor (in simple form) # ^ constant.other.placeholder +# ^ punctuation.accessor.dot -# Syntactically correct, but hardly come up in real code +# Issue 3649 +{"msg": "Type '{ _client: { _user?: { [x: string]: unknown; | undefined; }"} +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ string.quoted - constant.other.placeholder +# ^ punctuation.section.mapping.end.python + +# Syntactically correct, but hardly come up in real code and not covered by +# https://docs.python.org/3.11/library/string.html#format-string-syntax "{:{ ()}}".format(0, **{" ()": "d"}) == '0' -# ^ constant.other.placeholder +#^^^^^^^^^ meta.string.python string.quoted.double.python +#^^^^^^ - constant.other.placeholder '{foo/bar}'.format(**{"foo/bar": 1}) == '1' -# ^ - constant.other.placeholder +#^^^^^^^^^^ meta.string.python string.quoted.single.python +#^^^^^^^^^ - constant.other.placeholder +# ^ punctuation.definition.string.end.python # Legal but non-standard format spec -'{foo:{{w}}.{{p}}}' -# ^ - constant.other.placeholder -'{foo:w}}}' -# ^ - constant.other.placeholder -'{foo!a:random}' -# ^ - constant.other.placeholder +'{foo:^}}}' # valid format specifier +#^^^^^^^ constant.other.placeholder +#^ punctuation.definition.placeholder.begin +# ^ punctuation.separator.format-spec +# ^ punctuation.definition.placeholder.end +# ^^ constant.character.escape.python +'{foo:w}}}' # invalid format code +#^^^^^^^ constant.other.placeholder +#^ punctuation.definition.placeholder.begin +# ^ punctuation.separator.format-spec +# ^ punctuation.definition.placeholder.end +# ^^ constant.character.escape.python +'{foo:{{w}}.{{p}}}' # invalid format specifier +#^^^^^^^^^^^^^^^^^ - constant.other.placeholder +# ^^ constant.character.escape +# ^^ constant.character.escape +# ^^ constant.character.escape +# ^^ constant.character.escape +'{foo!a:random}' # invalid format specifier +#^^^^^^^^^^^^^^ constant.other.placeholder '{foo!a:ran{d}om}' # nested specification -# ^ constant.other.placeholder +#^^^^^^^^^^^^^^^^ constant.other.placeholder f"string" # <- storage.type.string @@ -653,15 +745,15 @@ f"{size:.2f}" # ^ meta.string.python - meta.interpolation -# ^^^^^ meta.string.python meta.interpolation.python - meta.format-spec -# ^^^^ meta.string.python meta.interpolation.python meta.format-spec.python - meta.format-spec meta.format-spec +# ^^^^^^ meta.string.python meta.interpolation.python - meta.format-spec +# ^^^ meta.string.python meta.interpolation.python meta.format-spec.python - meta.format-spec meta.format-spec # ^ meta.string.python meta.interpolation.python - meta.format-spec # ^ meta.string.python string.quoted.double.python - meta.interpolation # ^ punctuation.definition.string.begin.python # ^ punctuation.section.interpolation.begin.python # ^^^^ meta.path.python meta.generic-name.python -# ^^^^ constant.other.format-spec.python # ^ punctuation.separator.format-spec.python +# ^^^ constant.other.format-spec.python # ^ punctuation.section.interpolation.end.python # ^ punctuation.definition.string.end.python @@ -688,12 +780,17 @@ # ^ source.regexp.python meta.group.regexp punctuation.section.group.end.regexp line = re.sub(rf" ?\{{\\i.?\}}({x})\{{\\i.?\}}", r"\1", line) -# ^^^ constant.character.escape.backslash.regexp -# ^^ constant.character.escape.python +# ^ constant.character.escape.regexp - constant.character.escape.python +# ^^ constant.character.escape.regexp constant.character.escape.python # ^^ constant.character.escape.regexp -# ^^^ constant.character.escape.backslash.regexp -# ^^ constant.character.escape.python +# ^ constant.character.escape.regexp - constant.character.escape.python +# ^^ constant.character.escape.regexp constant.character.escape.python # ^ punctuation.section.interpolation.begin.python +# ^ constant.character.escape.regexp - constant.character.escape.python +# ^^ constant.character.escape.regexp constant.character.escape.python +# ^^ constant.character.escape.regexp +# ^ constant.character.escape.regexp - constant.character.escape.python +# ^^ constant.character.escape.regexp constant.character.escape.python match = re.match(r'(?Pa)?b(?(test)c|d)', line) # ^^^^^^^^^^ meta.group.regexp @@ -753,9 +850,14 @@ # ^^^^ meta.modifier # ^ storage.modifier.mode + +############################### +# f-strings +############################### + f"\{{{x}\}} test" # ^ invalid.deprecated.character.escape.python -# ^^ constant.character.escape.python +# ^^^ constant.character.escape.python # ^ punctuation.section.interpolation.begin.python f"{something}" @@ -774,7 +876,7 @@ # ^^^^ source source.python.embedded constant.language # ^^^^^^^ - source source.python.embedded # ^^ storage.modifier.conversion - constant.other.format-spec -# ^^^^ constant.other.format-spec +# ^^^ constant.other.format-spec # ^ punctuation.section.interpolation.end # ^ punctuation.definition.string.end # ^ source - meta, string, source source @@ -792,30 +894,30 @@ # ^^ punctuation.section.interpolation.end.python - source source # ^^ constant.character.escape # ^^^^^^^^^^^^^^^^^^^^^^^^^^ meta.interpolation.python -# ^^^^^^^^^^^^^^^^^^^^ meta.format-spec.python +# ^^^^^^ - meta.format-spec +# ^^^^^^^^^^^^^^^^^^^ meta.format-spec.python +# ^^^^ - meta.format-spec # ^^^^^^ - meta.interpolation.python meta.interpolation.python # ^^^^^^^ meta.interpolation.python meta.interpolation.python # ^ - meta.interpolation.python meta.interpolation.python # ^^^^^^^^^^^ meta.interpolation.python meta.interpolation.python # ^^^ - meta.interpolation.python meta.interpolation.python + rf"{value:{width!s:d}}" -# <- storage.type.string.python - string -# ^^^^^^^^^^^^^^^^^^^^^ meta.string +# <- storage.type.string - meta.string - string +#^ storage.type.string - meta.string - string +# ^ meta.string string.quoted.double punctuation.definition.string.begin +# ^^^^^^^ meta.string.python meta.interpolation.python - meta.format-spec +# ^^^^^^^^^ meta.string meta.interpolation meta.format-spec meta.interpolation - meta.format-spec meta.format-spec +# ^ meta.string meta.interpolation meta.format-spec meta.interpolation meta.format-spec +# ^^ meta.string meta.interpolation punctuation.section.interpolation.end +# ^^^^^ source source.python.embedded +# ^ punctuation.separator.format-spec +# ^ punctuation.section.interpolation.begin # ^^^^^ source source.python.embedded # ^^ storage.modifier.conversion -# ^^ constant.other.format-spec - -F""" {} {\} } -#^^^^^^^^^^^ meta.string -#^^^ punctuation.definition.string.begin -# ^^ invalid.illegal.empty-expression -# ^ invalid.illegal.backslash-in-fstring -# ^ invalid.illegal.stray -""" - -fr''' -# ^ - invalid -''' +# ^ punctuation.separator.format-spec +# ^ constant.other.format-spec # Most of these were inspired by # https://github.com/python/cpython/commit/9a4135e939bc223f592045a38e0f927ba170da32 @@ -828,9 +930,11 @@ f'{x=!a}' # ^ storage.modifier.debug.python f'{x=!s:*^20}' +# ^^^^^^^^^^^ meta.string.python meta.interpolation.python # ^ storage.modifier.debug.python # ^^ storage.modifier.conversion.python -# ^^^^^ meta.format-spec.python +# ^ punctuation.separator.format-spec.python - constant.other.format-spec +# ^^^^ constant.other.format-spec.python f'{"Σ"=}' # ^ storage.modifier.debug.python f'{"Σ"= }' @@ -865,22 +969,320 @@ f'{f(a="3=")}' # ^^^^ -storage.modifier.debug.python -f" { -% ^ invalid.illegal.unclosed-string - # TODO make this test pass - }" - -f' \ - {1 + 2!a:02f}' -#^^^^^^^^^^^^^^ meta.string -# ^^^^^ source source.python.embedded - f"{d for d in range(10)}" # yes, this doesn't make sense # ^^^ keyword.control.loop.for.generator.python +# Nested f-strings + +f"These are the things: {", ".join(things)}" +#^^^^^^^^^^^^^^^^^^^^^^^ meta.string.python string.quoted.double.python - string string +# ^ meta.string.python meta.interpolation.python - string +# ^^^^ meta.string.python meta.interpolation.python meta.string.python string.quoted.double.python +# ^^^^^^^^^^^^^^ meta.string.python meta.interpolation.python - string +# ^ meta.string.python string.quoted.double.python - string string + +f"{source.removesuffix(".py")}.c: $(srcdir)/{source}" +#^ meta.string.python string.quoted.double.python +# ^^^^^^^^^^^^^^^^^^^^^ meta.string.python meta.interpolation.python - string +# ^^^^^ meta.string.python meta.interpolation.python meta.string.python string.quoted.double.python +# ^^ meta.string.python meta.interpolation.python - string +# ^^^^^^^^^^^^^^ meta.string.python string.quoted.double.python - meta.interpolation +# ^^^^^^^^ meta.string.python meta.interpolation.python - string +# ^ meta.string.python string.quoted.double.python + +f"{f"{f"infinite"}":{f"{foo}"}.{"bar"}}" +# <- storage.type.string.python +#^ meta.string.python string.quoted.double.python punctuation.definition.string.begin.python +# ^ meta.string.python meta.interpolation.python punctuation.section.interpolation.begin.python +# ^ meta.string.python meta.interpolation.python storage.type.string.python +# ^ meta.string.python meta.interpolation.python meta.string.python string.quoted.double.python punctuation.definition.string.begin.python +# ^ meta.string.python meta.interpolation.python meta.string.python meta.interpolation.python punctuation.section.interpolation.begin.python +# ^ meta.string.python meta.interpolation.python meta.string.python meta.interpolation.python storage.type.string.python +# ^^^^^^^^^^ meta.string.python meta.interpolation.python meta.string.python meta.interpolation.python meta.string.python string.quoted.double.python +# ^ meta.string.python meta.interpolation.python meta.string.python meta.interpolation.python punctuation.section.interpolation.end.python +# ^ meta.string.python meta.interpolation.python meta.string.python string.quoted.double.python punctuation.definition.string.end.python +# ^ meta.string.python meta.interpolation.python punctuation.separator.format-spec.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python punctuation.section.interpolation.begin.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python source.python.embedded storage.type.string.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python source.python.embedded meta.string.python string.quoted.double.python punctuation.definition.string.begin.python +# ^^^^^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python source.python.embedded meta.string.python meta.interpolation.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python source.python.embedded meta.string.python string.quoted.double.python punctuation.definition.string.end.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python punctuation.section.interpolation.end.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python constant.other.format-spec.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python punctuation.section.interpolation.begin.python +# ^^^^^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python source.python.embedded meta.string.python string.quoted.double.python +# ^ meta.string.python meta.interpolation.python meta.format-spec.python meta.interpolation.python punctuation.section.interpolation.end.python +# ^ meta.string.python meta.interpolation.python punctuation.section.interpolation.end.python +# ^ meta.string.python string.quoted.double.python punctuation.definition.string.end.python +# ^ - meta.string + +# Incomplete strings or premature bailouts + +f" +#^^ meta.string.python string.quoted.double.python +# ^ invalid.illegal.unclosed-string + +# <- - meta.string - string +# this test is to ensure we're not matching anything here anymore + f' +#^^ meta.string.python string.quoted.single.python # ^ invalid.illegal.unclosed-string +# <- - meta.string - string +# this test is to ensure we're not matching anything here anymore + +f" \ + +# <- meta.string.python string.quoted.double.python invalid.illegal.unclosed-string.python + +# <- - meta.string - string +# this test is to ensure we're not matching anything here anymore + +f' \ -# <- - meta +# <- meta.string.python string.quoted.single.python invalid.illegal.unclosed-string.python + +# <- - meta.string - string # this test is to ensure we're not matching anything here anymore + +f" \ + {1 + 2!a:02f}" +# <- meta.string.python string.quoted.double.python +#^^^^^^^^^^^^^ meta.string.python meta.interpolation.python - string.quoted +# ^^^^^ source source.python.embedded +# ^^ storage.modifier.conversion.python +# ^ punctuation.separator.format-spec.python +# ^^^ meta.format-spec.python constant.other.format-spec.python +# ^ punctuation.section.interpolation.end.python +# ^ meta.string.python string.quoted.double.python punctuation.definition.string.end.python + +f' \ + {1 + 2!a:02f}' +# <- meta.string.python string.quoted.single.python +#^^^^^^^^^^^^^ meta.string.python meta.interpolation.python - string.quoted +# ^^^^^ source source.python.embedded +# ^^ storage.modifier.conversion.python +# ^ punctuation.separator.format-spec.python +# ^^^ meta.format-spec.python constant.other.format-spec.python +# ^ punctuation.section.interpolation.end.python +# ^ meta.string.python string.quoted.single.python punctuation.definition.string.end.python + +f" { +#^^^^ meta.string.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.unclosed-string.python + }" +# ^^^ - meta.string +# ^ invalid.illegal.stray.python +# ^ punctuation.definition.string.begin.python +# ^ invalid.illegal.unclosed-string.python + +f' { +#^^^^ meta.string.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.unclosed-string.python + }' +# ^^^ - meta.string +# ^ invalid.illegal.stray.python +# ^ punctuation.definition.string.begin.python +# ^ invalid.illegal.unclosed-string.python + +f" { \ +#^^^^^^ meta.string.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ invalid.illegal.unclosed-string.python + }" +# ^^^ - meta.string +# ^ invalid.illegal.stray.python +# ^ punctuation.definition.string.begin.python +# ^ invalid.illegal.unclosed-string.python + +f' { \ +#^^^^ meta.string.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ invalid.illegal.unclosed-string.python + }' +# ^^^ - meta.string +# ^ invalid.illegal.stray.python +# ^ punctuation.definition.string.begin.python +# ^ invalid.illegal.unclosed-string.python + +f" { +:2d +# <- - meta.string - meta.interpolation +#^^ - meta.string - meta.interpolation + +f' { +:2d +# <- - meta.string - meta.interpolation +#^^ - meta.string - meta.interpolation + +f" { +!a:2d +# <- - meta.string - meta.interpolation +#^^^^ - meta.string - meta.interpolation + +f' { +!a:2d +# <- - meta.string - meta.interpolation +#^^^^ - meta.string - meta.interpolation + +f" {} {\} }" +#^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python + +f' {} {\} }' +#^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python + +f"{foo:"} # nested quotes terminate formatspec +#^^^^^^^ meta.string.python +# ^ invalid.illegal.stray.python - meta.string +f'{foo:'} # nested quotes terminate formatspec +#^^^^^^^ meta.string.python +# ^ invalid.illegal.stray.python - meta.string + +f"{"foo":"} # nested quotes supported by python 3.12+ +#^^^^^^^^^ meta.string.python +# ^ invalid.illegal.stray.python - meta.string +f'{'foo':'} # nested quotes supported by python 3.12+ +#^^^^^^^^^ meta.string.python +# ^ invalid.illegal.stray.python - meta.string + + +############################### +# f-block-strings +############################### + +f"""""" +# <- storage.type.string.python +#^^^^^^ meta.string.python string.quoted.double.block.python +#^^^ punctuation.definition.string.begin.python +# ^^^ punctuation.definition.string.end.python + +f'''''' +# <- storage.type.string.python +#^^^^^^ meta.string.python string.quoted.single.block.python +#^^^ punctuation.definition.string.begin.python +# ^^^ punctuation.definition.string.end.python + +f""" +# <- storage.type.string.python +#^^^^ meta.string.python string.quoted.double.block.python +#^^^ punctuation.definition.string.begin.python +# ^ - punctuation - invalid +""" +# <- meta.string.python string.quoted.double.block.python punctuation.definition.string.end.python +#^^ meta.string.python string.quoted.double.block.python punctuation.definition.string.end.python +# ^ - meta.string - string - punctuation + +f''' +# <- storage.type.string.python +#^^^^ meta.string.python string.quoted.single.block.python +#^^^ punctuation.definition.string.begin.python +# ^ - punctuation - invalid +''' +# <- meta.string.python string.quoted.single.block.python punctuation.definition.string.end.python +#^^ meta.string.python string.quoted.single.block.python punctuation.definition.string.end.python +# ^ - meta.string - string - punctuation + +f""" {} {\} } +#^^^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python +""" + +f''' {} {\} } +#^^^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python +''' + +fr""" +# <- storage.type.string.python +#^ storage.type.string.python +# ^^^^ meta.string.python string.quoted.double.block.python +# ^^^ punctuation.definition.string.begin.python +# ^ - punctuation - invalid +""" +# <- meta.string.python string.quoted.double.block.python punctuation.definition.string.end.python +#^^ meta.string.python string.quoted.double.block.python punctuation.definition.string.end.python +# ^ - meta.string - string - punctuation + +fr''' +# <- storage.type.string.python +#^ storage.type.string.python +# ^^^^ meta.string.python string.quoted.single.block.python +# ^^^ punctuation.definition.string.begin.python +# ^ - punctuation - invalid +''' +# <- meta.string.python string.quoted.single.block.python punctuation.definition.string.end.python +#^^ meta.string.python string.quoted.single.block.python punctuation.definition.string.end.python +# ^ - meta.string - string - punctuation + +fr""" {} {\} } +# <- storage.type.string.python +#^ storage.type.string.python +# ^^^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python +""" + +fr''' {} {\} } +# <- storage.type.string.python +#^ storage.type.string.python +# ^^^^^^^^^^^^^ meta.string.python +# ^^ invalid.illegal.empty-expression.python +# ^ punctuation.section.interpolation.begin.python +# ^ invalid.illegal.backslash-in-fstring.python +# ^ punctuation.section.interpolation.end.python +# ^ invalid.illegal.stray.python +''' + +f"""{ +# ^^ meta.string.python meta.interpolation.python - invalid +# ^ source.python.embedded + foo +# ^^^ meta.string.python meta.interpolation.python source.python.embedded meta.path.python meta.generic-name.python + !a:2d +# ^^^^^^ meta.string.python meta.interpolation.python - source.python.embedded +# ^^ storage.modifier.conversion.python +# ^ punctuation.separator.format-spec.python +# ^^^ meta.format-spec.python constant.other.format-spec.python +}""" +# <- meta.string.python meta.interpolation.python punctuation.section.interpolation.end.python +#^^^ meta.string.python string.quoted.double.block.python punctuation.definition.string.end.python + +f'''{ +# ^^ meta.string.python meta.interpolation.python - invalid +# ^ source.python.embedded + foo +# ^^^ meta.string.python meta.interpolation.python source.python.embedded meta.path.python meta.generic-name.python + !a:2d +# ^^^^^^ meta.string.python meta.interpolation.python - source.python.embedded +# ^^ storage.modifier.conversion.python +# ^ punctuation.separator.format-spec.python +# ^^^ meta.format-spec.python constant.other.format-spec.python +}''' +# <- meta.string.python meta.interpolation.python punctuation.section.interpolation.end.python +#^^^ meta.string.python string.quoted.single.block.python punctuation.definition.string.end.python