Add PEP 701 support (#3822)
Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Co-authored-by: hauntsaninja <hauntsaninja@gmail.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
parent
944b99aa91
commit
551ede2825
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
<!-- Include any especially major or disruptive changes here -->
|
<!-- Include any especially major or disruptive changes here -->
|
||||||
|
|
||||||
|
- Add support for the new Python 3.12 f-string syntax introduced by PEP 701 (#3822)
|
||||||
|
|
||||||
### Stable style
|
### Stable style
|
||||||
|
|
||||||
<!-- Changes that affect Black's stable style -->
|
<!-- Changes that affect Black's stable style -->
|
||||||
|
@ -69,13 +69,7 @@
|
|||||||
from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
|
from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
|
||||||
from black.mode import Mode as Mode # re-exported
|
from black.mode import Mode as Mode # re-exported
|
||||||
from black.mode import Preview, TargetVersion, supports_feature
|
from black.mode import Preview, TargetVersion, supports_feature
|
||||||
from black.nodes import (
|
from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms
|
||||||
STARS,
|
|
||||||
is_number_token,
|
|
||||||
is_simple_decorator_expression,
|
|
||||||
is_string_token,
|
|
||||||
syms,
|
|
||||||
)
|
|
||||||
from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
|
from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
|
||||||
from black.parsing import ( # noqa F401
|
from black.parsing import ( # noqa F401
|
||||||
ASTSafetyError,
|
ASTSafetyError,
|
||||||
@ -91,7 +85,6 @@
|
|||||||
sanitized_lines,
|
sanitized_lines,
|
||||||
)
|
)
|
||||||
from black.report import Changed, NothingChanged, Report
|
from black.report import Changed, NothingChanged, Report
|
||||||
from black.trans import iter_fexpr_spans
|
|
||||||
from blib2to3.pgen2 import token
|
from blib2to3.pgen2 import token
|
||||||
from blib2to3.pytree import Leaf, Node
|
from blib2to3.pytree import Leaf, Node
|
||||||
|
|
||||||
@ -1265,7 +1258,10 @@ def _format_str_once(
|
|||||||
elt = EmptyLineTracker(mode=mode)
|
elt = EmptyLineTracker(mode=mode)
|
||||||
split_line_features = {
|
split_line_features = {
|
||||||
feature
|
feature
|
||||||
for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
|
for feature in {
|
||||||
|
Feature.TRAILING_COMMA_IN_CALL,
|
||||||
|
Feature.TRAILING_COMMA_IN_DEF,
|
||||||
|
}
|
||||||
if supports_feature(versions, feature)
|
if supports_feature(versions, feature)
|
||||||
}
|
}
|
||||||
block: Optional[LinesBlock] = None
|
block: Optional[LinesBlock] = None
|
||||||
@ -1337,15 +1333,14 @@ def get_features_used( # noqa: C901
|
|||||||
}
|
}
|
||||||
|
|
||||||
for n in node.pre_order():
|
for n in node.pre_order():
|
||||||
if is_string_token(n):
|
if n.type == token.FSTRING_START:
|
||||||
value_head = n.value[:2]
|
features.add(Feature.F_STRINGS)
|
||||||
if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
|
elif (
|
||||||
features.add(Feature.F_STRINGS)
|
n.type == token.RBRACE
|
||||||
if Feature.DEBUG_F_STRINGS not in features:
|
and n.parent is not None
|
||||||
for span_beg, span_end in iter_fexpr_spans(n.value):
|
and any(child.type == token.EQUAL for child in n.parent.children)
|
||||||
if n.value[span_beg : span_end - 1].rstrip().endswith("="):
|
):
|
||||||
features.add(Feature.DEBUG_F_STRINGS)
|
features.add(Feature.DEBUG_F_STRINGS)
|
||||||
break
|
|
||||||
|
|
||||||
elif is_number_token(n):
|
elif is_number_token(n):
|
||||||
if "_" in n.value:
|
if "_" in n.value:
|
||||||
|
@ -502,6 +502,45 @@ def visit_NUMBER(self, leaf: Leaf) -> Iterator[Line]:
|
|||||||
normalize_numeric_literal(leaf)
|
normalize_numeric_literal(leaf)
|
||||||
yield from self.visit_default(leaf)
|
yield from self.visit_default(leaf)
|
||||||
|
|
||||||
|
def visit_fstring(self, node: Node) -> Iterator[Line]:
|
||||||
|
# currently we don't want to format and split f-strings at all.
|
||||||
|
string_leaf = _fstring_to_string(node)
|
||||||
|
node.replace(string_leaf)
|
||||||
|
yield from self.visit_STRING(string_leaf)
|
||||||
|
|
||||||
|
# TODO: Uncomment Implementation to format f-string children
|
||||||
|
# fstring_start = node.children[0]
|
||||||
|
# fstring_end = node.children[-1]
|
||||||
|
# assert isinstance(fstring_start, Leaf)
|
||||||
|
# assert isinstance(fstring_end, Leaf)
|
||||||
|
|
||||||
|
# quote_char = fstring_end.value[0]
|
||||||
|
# quote_idx = fstring_start.value.index(quote_char)
|
||||||
|
# prefix, quote = (
|
||||||
|
# fstring_start.value[:quote_idx],
|
||||||
|
# fstring_start.value[quote_idx:]
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if not is_docstring(node, self.mode):
|
||||||
|
# prefix = normalize_string_prefix(prefix)
|
||||||
|
|
||||||
|
# assert quote == fstring_end.value
|
||||||
|
|
||||||
|
# is_raw_fstring = "r" in prefix or "R" in prefix
|
||||||
|
# middles = [
|
||||||
|
# leaf
|
||||||
|
# for leaf in node.leaves()
|
||||||
|
# if leaf.type == token.FSTRING_MIDDLE
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# if self.mode.string_normalization:
|
||||||
|
# middles, quote = normalize_fstring_quotes(quote, middles, is_raw_fstring)
|
||||||
|
|
||||||
|
# fstring_start.value = prefix + quote
|
||||||
|
# fstring_end.value = quote
|
||||||
|
|
||||||
|
# yield from self.visit_default(node)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""You are in a twisty little maze of passages."""
|
"""You are in a twisty little maze of passages."""
|
||||||
self.current_line = Line(mode=self.mode)
|
self.current_line = Line(mode=self.mode)
|
||||||
@ -535,6 +574,12 @@ def __post_init__(self) -> None:
|
|||||||
self.visit_guard = partial(v, keywords=Ø, parens={"if"})
|
self.visit_guard = partial(v, keywords=Ø, parens={"if"})
|
||||||
|
|
||||||
|
|
||||||
|
def _fstring_to_string(node: Node) -> Leaf:
|
||||||
|
"""Converts an fstring node back to a string node."""
|
||||||
|
string_without_prefix = str(node)[len(node.prefix) :]
|
||||||
|
return Leaf(token.STRING, string_without_prefix, prefix=node.prefix)
|
||||||
|
|
||||||
|
|
||||||
def _hugging_power_ops_line_to_string(
|
def _hugging_power_ops_line_to_string(
|
||||||
line: Line,
|
line: Line,
|
||||||
features: Collection[Feature],
|
features: Collection[Feature],
|
||||||
|
@ -72,7 +72,12 @@ def append(
|
|||||||
|
|
||||||
Inline comments are put aside.
|
Inline comments are put aside.
|
||||||
"""
|
"""
|
||||||
has_value = leaf.type in BRACKETS or bool(leaf.value.strip())
|
has_value = (
|
||||||
|
leaf.type in BRACKETS
|
||||||
|
# empty fstring-middles must not be truncated
|
||||||
|
or leaf.type == token.FSTRING_MIDDLE
|
||||||
|
or bool(leaf.value.strip())
|
||||||
|
)
|
||||||
if not has_value:
|
if not has_value:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ class Feature(Enum):
|
|||||||
DEBUG_F_STRINGS = 16
|
DEBUG_F_STRINGS = 16
|
||||||
PARENTHESIZED_CONTEXT_MANAGERS = 17
|
PARENTHESIZED_CONTEXT_MANAGERS = 17
|
||||||
TYPE_PARAMS = 18
|
TYPE_PARAMS = 18
|
||||||
|
FSTRING_PARSING = 19
|
||||||
FORCE_OPTIONAL_PARENTHESES = 50
|
FORCE_OPTIONAL_PARENTHESES = 50
|
||||||
|
|
||||||
# __future__ flags
|
# __future__ flags
|
||||||
@ -156,6 +157,7 @@ class Feature(Enum):
|
|||||||
Feature.EXCEPT_STAR,
|
Feature.EXCEPT_STAR,
|
||||||
Feature.VARIADIC_GENERICS,
|
Feature.VARIADIC_GENERICS,
|
||||||
Feature.TYPE_PARAMS,
|
Feature.TYPE_PARAMS,
|
||||||
|
Feature.FSTRING_PARSING,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,13 @@
|
|||||||
OPENING_BRACKETS: Final = set(BRACKET.keys())
|
OPENING_BRACKETS: Final = set(BRACKET.keys())
|
||||||
CLOSING_BRACKETS: Final = set(BRACKET.values())
|
CLOSING_BRACKETS: Final = set(BRACKET.values())
|
||||||
BRACKETS: Final = OPENING_BRACKETS | CLOSING_BRACKETS
|
BRACKETS: Final = OPENING_BRACKETS | CLOSING_BRACKETS
|
||||||
ALWAYS_NO_SPACE: Final = CLOSING_BRACKETS | {token.COMMA, STANDALONE_COMMENT}
|
ALWAYS_NO_SPACE: Final = CLOSING_BRACKETS | {
|
||||||
|
token.COMMA,
|
||||||
|
STANDALONE_COMMENT,
|
||||||
|
token.FSTRING_MIDDLE,
|
||||||
|
token.FSTRING_END,
|
||||||
|
token.BANG,
|
||||||
|
}
|
||||||
|
|
||||||
RARROW = 55
|
RARROW = 55
|
||||||
|
|
||||||
@ -211,6 +217,9 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
|
|||||||
}:
|
}:
|
||||||
return NO
|
return NO
|
||||||
|
|
||||||
|
if t == token.LBRACE and p.type == syms.fstring_replacement_field:
|
||||||
|
return NO
|
||||||
|
|
||||||
prev = leaf.prev_sibling
|
prev = leaf.prev_sibling
|
||||||
if not prev:
|
if not prev:
|
||||||
prevp = preceding_leaf(p)
|
prevp = preceding_leaf(p)
|
||||||
@ -272,6 +281,9 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
|
|||||||
elif prev.type in OPENING_BRACKETS:
|
elif prev.type in OPENING_BRACKETS:
|
||||||
return NO
|
return NO
|
||||||
|
|
||||||
|
elif prev.type == token.BANG:
|
||||||
|
return NO
|
||||||
|
|
||||||
if p.type in {syms.parameters, syms.arglist}:
|
if p.type in {syms.parameters, syms.arglist}:
|
||||||
# untyped function signatures or calls
|
# untyped function signatures or calls
|
||||||
if not prev or prev.type != token.COMMA:
|
if not prev or prev.type != token.COMMA:
|
||||||
@ -393,6 +405,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # no
|
|||||||
elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument:
|
elif prevp.type == token.EQUAL and prevp_parent.type == syms.argument:
|
||||||
return NO
|
return NO
|
||||||
|
|
||||||
|
# TODO: add fstring here?
|
||||||
elif t in {token.NAME, token.NUMBER, token.STRING}:
|
elif t in {token.NAME, token.NUMBER, token.STRING}:
|
||||||
return NO
|
return NO
|
||||||
|
|
||||||
@ -542,31 +555,32 @@ def is_arith_like(node: LN) -> bool:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def is_docstring(leaf: Leaf, mode: Mode) -> bool:
|
def is_docstring(node: NL, mode: Mode) -> bool:
|
||||||
if leaf.type != token.STRING:
|
if isinstance(node, Leaf):
|
||||||
return False
|
if node.type != token.STRING:
|
||||||
|
return False
|
||||||
|
|
||||||
prefix = get_string_prefix(leaf.value)
|
prefix = get_string_prefix(node.value)
|
||||||
if set(prefix).intersection("bBfF"):
|
if set(prefix).intersection("bBfF"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Preview.unify_docstring_detection in mode
|
Preview.unify_docstring_detection in mode
|
||||||
and leaf.parent
|
and node.parent
|
||||||
and leaf.parent.type == syms.simple_stmt
|
and node.parent.type == syms.simple_stmt
|
||||||
and not leaf.parent.prev_sibling
|
and not node.parent.prev_sibling
|
||||||
and leaf.parent.parent
|
and node.parent.parent
|
||||||
and leaf.parent.parent.type == syms.file_input
|
and node.parent.parent.type == syms.file_input
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if prev_siblings_are(
|
if prev_siblings_are(
|
||||||
leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
|
node.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Multiline docstring on the same line as the `def`.
|
# Multiline docstring on the same line as the `def`.
|
||||||
if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]):
|
if prev_siblings_are(node.parent, [syms.parameters, token.COLON, syms.simple_stmt]):
|
||||||
# `syms.parameters` is only used in funcdefs and async_funcdefs in the Python
|
# `syms.parameters` is only used in funcdefs and async_funcdefs in the Python
|
||||||
# grammar. We're safe to return True without further checks.
|
# grammar. We're safe to return True without further checks.
|
||||||
return True
|
return True
|
||||||
@ -954,10 +968,6 @@ def is_rpar_token(nl: NL) -> TypeGuard[Leaf]:
|
|||||||
return nl.type == token.RPAR
|
return nl.type == token.RPAR
|
||||||
|
|
||||||
|
|
||||||
def is_string_token(nl: NL) -> TypeGuard[Leaf]:
|
|
||||||
return nl.type == token.STRING
|
|
||||||
|
|
||||||
|
|
||||||
def is_number_token(nl: NL) -> TypeGuard[Leaf]:
|
def is_number_token(nl: NL) -> TypeGuard[Leaf]:
|
||||||
return nl.type == token.NUMBER
|
return nl.type == token.NUMBER
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Final, List, Match, Pattern
|
from typing import Final, List, Match, Pattern, Tuple
|
||||||
|
|
||||||
from black._width_table import WIDTH_TABLE
|
from black._width_table import WIDTH_TABLE
|
||||||
from blib2to3.pytree import Leaf
|
from blib2to3.pytree import Leaf
|
||||||
@ -169,8 +169,7 @@ def _cached_compile(pattern: str) -> Pattern[str]:
|
|||||||
def normalize_string_quotes(s: str) -> str:
|
def normalize_string_quotes(s: str) -> str:
|
||||||
"""Prefer double quotes but only if it doesn't cause more escaping.
|
"""Prefer double quotes but only if it doesn't cause more escaping.
|
||||||
|
|
||||||
Adds or removes backslashes as appropriate. Doesn't parse and fix
|
Adds or removes backslashes as appropriate.
|
||||||
strings nested in f-strings.
|
|
||||||
"""
|
"""
|
||||||
value = s.lstrip(STRING_PREFIX_CHARS)
|
value = s.lstrip(STRING_PREFIX_CHARS)
|
||||||
if value[:3] == '"""':
|
if value[:3] == '"""':
|
||||||
@ -211,6 +210,7 @@ def normalize_string_quotes(s: str) -> str:
|
|||||||
s = f"{prefix}{orig_quote}{body}{orig_quote}"
|
s = f"{prefix}{orig_quote}{body}{orig_quote}"
|
||||||
new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body)
|
new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body)
|
||||||
new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body)
|
new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body)
|
||||||
|
|
||||||
if "f" in prefix.casefold():
|
if "f" in prefix.casefold():
|
||||||
matches = re.findall(
|
matches = re.findall(
|
||||||
r"""
|
r"""
|
||||||
@ -240,6 +240,71 @@ def normalize_string_quotes(s: str) -> str:
|
|||||||
return f"{prefix}{new_quote}{new_body}{new_quote}"
|
return f"{prefix}{new_quote}{new_body}{new_quote}"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_fstring_quotes(
|
||||||
|
quote: str,
|
||||||
|
middles: List[Leaf],
|
||||||
|
is_raw_fstring: bool,
|
||||||
|
) -> Tuple[List[Leaf], str]:
|
||||||
|
"""Prefer double quotes but only if it doesn't cause more escaping.
|
||||||
|
|
||||||
|
Adds or removes backslashes as appropriate.
|
||||||
|
"""
|
||||||
|
if quote == '"""':
|
||||||
|
return middles, quote
|
||||||
|
|
||||||
|
elif quote == "'''":
|
||||||
|
new_quote = '"""'
|
||||||
|
elif quote == '"':
|
||||||
|
new_quote = "'"
|
||||||
|
else:
|
||||||
|
new_quote = '"'
|
||||||
|
|
||||||
|
unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
|
||||||
|
escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
|
||||||
|
escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){quote}")
|
||||||
|
if is_raw_fstring:
|
||||||
|
for middle in middles:
|
||||||
|
if unescaped_new_quote.search(middle.value):
|
||||||
|
# There's at least one unescaped new_quote in this raw string
|
||||||
|
# so converting is impossible
|
||||||
|
return middles, quote
|
||||||
|
|
||||||
|
# Do not introduce or remove backslashes in raw strings, just use double quote
|
||||||
|
return middles, '"'
|
||||||
|
|
||||||
|
new_segments = []
|
||||||
|
for middle in middles:
|
||||||
|
segment = middle.value
|
||||||
|
# remove unnecessary escapes
|
||||||
|
new_segment = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", segment)
|
||||||
|
if segment != new_segment:
|
||||||
|
# Consider the string without unnecessary escapes as the original
|
||||||
|
middle.value = new_segment
|
||||||
|
|
||||||
|
new_segment = sub_twice(escaped_orig_quote, rf"\1\2{quote}", new_segment)
|
||||||
|
new_segment = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_segment)
|
||||||
|
new_segments.append(new_segment)
|
||||||
|
|
||||||
|
if new_quote == '"""' and new_segments[-1].endswith('"'):
|
||||||
|
# edge case:
|
||||||
|
new_segments[-1] = new_segments[-1][:-1] + '\\"'
|
||||||
|
|
||||||
|
for middle, new_segment in zip(middles, new_segments):
|
||||||
|
orig_escape_count = middle.value.count("\\")
|
||||||
|
new_escape_count = new_segment.count("\\")
|
||||||
|
|
||||||
|
if new_escape_count > orig_escape_count:
|
||||||
|
return middles, quote # Do not introduce more escaping
|
||||||
|
|
||||||
|
if new_escape_count == orig_escape_count and quote == '"':
|
||||||
|
return middles, quote # Prefer double quotes
|
||||||
|
|
||||||
|
for middle, new_segment in zip(middles, new_segments):
|
||||||
|
middle.value = new_segment
|
||||||
|
|
||||||
|
return middles, new_quote
|
||||||
|
|
||||||
|
|
||||||
def normalize_unicode_escape_sequences(leaf: Leaf) -> None:
|
def normalize_unicode_escape_sequences(leaf: Leaf) -> None:
|
||||||
"""Replace hex codes in Unicode escape sequences with lowercase representation."""
|
"""Replace hex codes in Unicode escape sequences with lowercase representation."""
|
||||||
text = leaf.value
|
text = leaf.value
|
||||||
|
@ -163,7 +163,7 @@ atom: ('(' [yield_expr|testlist_gexp] ')' |
|
|||||||
'[' [listmaker] ']' |
|
'[' [listmaker] ']' |
|
||||||
'{' [dictsetmaker] '}' |
|
'{' [dictsetmaker] '}' |
|
||||||
'`' testlist1 '`' |
|
'`' testlist1 '`' |
|
||||||
NAME | NUMBER | STRING+ | '.' '.' '.')
|
NAME | NUMBER | (STRING | fstring)+ | '.' '.' '.')
|
||||||
listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
|
listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
|
||||||
testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
|
testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
|
||||||
lambdef: 'lambda' [varargslist] ':' test
|
lambdef: 'lambda' [varargslist] ':' test
|
||||||
@ -254,3 +254,8 @@ case_block: "case" patterns [guard] ':' suite
|
|||||||
guard: 'if' namedexpr_test
|
guard: 'if' namedexpr_test
|
||||||
patterns: pattern (',' pattern)* [',']
|
patterns: pattern (',' pattern)* [',']
|
||||||
pattern: (expr|star_expr) ['as' expr]
|
pattern: (expr|star_expr) ['as' expr]
|
||||||
|
|
||||||
|
fstring: FSTRING_START fstring_middle* FSTRING_END
|
||||||
|
fstring_middle: fstring_replacement_field | FSTRING_MIDDLE
|
||||||
|
fstring_replacement_field: '{' (yield_expr | testlist_star_expr) ['='] [ "!" NAME ] [ ':' fstring_format_spec* ] '}'
|
||||||
|
fstring_format_spec: FSTRING_MIDDLE | fstring_replacement_field
|
||||||
|
@ -167,7 +167,9 @@ def parse_tokens(self, tokens: Iterable[GoodTokenInfo], debug: bool = False) ->
|
|||||||
if type in {token.INDENT, token.DEDENT}:
|
if type in {token.INDENT, token.DEDENT}:
|
||||||
prefix = _prefix
|
prefix = _prefix
|
||||||
lineno, column = end
|
lineno, column = end
|
||||||
if value.endswith("\n"):
|
# FSTRING_MIDDLE is the only token that can end with a newline, and
|
||||||
|
# `end` will point to the next line. For that case, don't increment lineno.
|
||||||
|
if value.endswith("\n") and type != token.FSTRING_MIDDLE:
|
||||||
lineno += 1
|
lineno += 1
|
||||||
column = 0
|
column = 0
|
||||||
else:
|
else:
|
||||||
|
@ -218,6 +218,7 @@ def report(self) -> None:
|
|||||||
//= DOUBLESLASHEQUAL
|
//= DOUBLESLASHEQUAL
|
||||||
-> RARROW
|
-> RARROW
|
||||||
:= COLONEQUAL
|
:= COLONEQUAL
|
||||||
|
! BANG
|
||||||
"""
|
"""
|
||||||
|
|
||||||
opmap = {}
|
opmap = {}
|
||||||
|
@ -66,7 +66,11 @@
|
|||||||
ASYNC: Final = 57
|
ASYNC: Final = 57
|
||||||
ERRORTOKEN: Final = 58
|
ERRORTOKEN: Final = 58
|
||||||
COLONEQUAL: Final = 59
|
COLONEQUAL: Final = 59
|
||||||
N_TOKENS: Final = 60
|
FSTRING_START: Final = 60
|
||||||
|
FSTRING_MIDDLE: Final = 61
|
||||||
|
FSTRING_END: Final = 62
|
||||||
|
BANG: Final = 63
|
||||||
|
N_TOKENS: Final = 64
|
||||||
NT_OFFSET: Final = 256
|
NT_OFFSET: Final = 256
|
||||||
# --end constants--
|
# --end constants--
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
function to which the 5 fields described above are passed as 5 arguments,
|
function to which the 5 fields described above are passed as 5 arguments,
|
||||||
each time a new token is found."""
|
each time a new token is found."""
|
||||||
|
|
||||||
|
import builtins
|
||||||
import sys
|
import sys
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
@ -49,12 +50,17 @@
|
|||||||
DEDENT,
|
DEDENT,
|
||||||
ENDMARKER,
|
ENDMARKER,
|
||||||
ERRORTOKEN,
|
ERRORTOKEN,
|
||||||
|
FSTRING_END,
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
FSTRING_START,
|
||||||
INDENT,
|
INDENT,
|
||||||
|
LBRACE,
|
||||||
NAME,
|
NAME,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
NL,
|
NL,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OP,
|
OP,
|
||||||
|
RBRACE,
|
||||||
STRING,
|
STRING,
|
||||||
tok_name,
|
tok_name,
|
||||||
)
|
)
|
||||||
@ -120,14 +126,32 @@ def _combinations(*l: str) -> Set[str]:
|
|||||||
Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''"
|
Single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''"
|
||||||
# Tail end of """ string.
|
# Tail end of """ string.
|
||||||
Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""'
|
Double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""'
|
||||||
_litprefix = r"(?:[uUrRbBfF]|[rR][fFbB]|[fFbBuU][rR])?"
|
_litprefix = r"(?:[uUrRbB]|[rR][bB]|[bBuU][rR])?"
|
||||||
Triple = group(_litprefix + "'''", _litprefix + '"""')
|
_fstringlitprefix = r"(?:rF|FR|Fr|fr|RF|F|rf|f|Rf|fR)"
|
||||||
# Single-line ' or " string.
|
Triple = group(
|
||||||
String = group(
|
_litprefix + "'''",
|
||||||
_litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*'",
|
_litprefix + '"""',
|
||||||
_litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*"',
|
_fstringlitprefix + '"""',
|
||||||
|
_fstringlitprefix + "'''",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# beginning of a single quoted f-string. must not end with `{{` or `\N{`
|
||||||
|
SingleLbrace = r"[^'\\{]*(?:(?:\\N{|\\.|{{)[^'\\{]*)*(?<!\\N){(?!{)"
|
||||||
|
DoubleLbrace = r'[^"\\{]*(?:(?:\\N{|\\.|{{)[^"\\{]*)*(?<!\\N){(?!{)'
|
||||||
|
|
||||||
|
# beginning of a triple quoted f-string. must not end with `{{` or `\N{`
|
||||||
|
Single3Lbrace = r"[^'{]*(?:(?:\\N{|\\[^{]|{{|'(?!''))[^'{]*)*(?<!\\N){(?!{)"
|
||||||
|
Double3Lbrace = r'[^"{]*(?:(?:\\N{|\\[^{]|{{|"(?!""))[^"{]*)*(?<!\\N){(?!{)'
|
||||||
|
|
||||||
|
# ! format specifier inside an fstring brace, ensure it's not a `!=` token
|
||||||
|
Bang = Whitespace + group("!") + r"(?!=)"
|
||||||
|
bang = re.compile(Bang)
|
||||||
|
Colon = Whitespace + group(":")
|
||||||
|
colon = re.compile(Colon)
|
||||||
|
|
||||||
|
FstringMiddleAfterColon = group(Whitespace + r".*?") + group("{", "}")
|
||||||
|
fstring_middle_after_colon = re.compile(FstringMiddleAfterColon)
|
||||||
|
|
||||||
# Because of leftmost-then-longest match semantics, be sure to put the
|
# Because of leftmost-then-longest match semantics, be sure to put the
|
||||||
# longest operators first (e.g., if = came before ==, == would get
|
# longest operators first (e.g., if = came before ==, == would get
|
||||||
# recognized as two instances of =).
|
# recognized as two instances of =).
|
||||||
@ -147,42 +171,70 @@ def _combinations(*l: str) -> Set[str]:
|
|||||||
Special = group(r"\r?\n", r"[:;.,`@]")
|
Special = group(r"\r?\n", r"[:;.,`@]")
|
||||||
Funny = group(Operator, Bracket, Special)
|
Funny = group(Operator, Bracket, Special)
|
||||||
|
|
||||||
|
_string_middle_single = r"[^\n'\\]*(?:\\.[^\n'\\]*)*"
|
||||||
|
_string_middle_double = r'[^\n"\\]*(?:\\.[^\n"\\]*)*'
|
||||||
|
|
||||||
|
# FSTRING_MIDDLE and LBRACE, must not end with a `{{` or `\N{`
|
||||||
|
_fstring_middle_single = r"[^\n'{]*(?:(?:\\N{|\\[^{]|{{)[^\n'{]*)*(?<!\\N)({)(?!{)"
|
||||||
|
_fstring_middle_double = r'[^\n"{]*(?:(?:\\N{|\\[^{]|{{)[^\n"{]*)*(?<!\\N)({)(?!{)'
|
||||||
|
|
||||||
# First (or only) line of ' or " string.
|
# First (or only) line of ' or " string.
|
||||||
ContStr = group(
|
ContStr = group(
|
||||||
_litprefix + r"'[^\n'\\]*(?:\\.[^\n'\\]*)*" + group("'", r"\\\r?\n"),
|
_litprefix + "'" + _string_middle_single + group("'", r"\\\r?\n"),
|
||||||
_litprefix + r'"[^\n"\\]*(?:\\.[^\n"\\]*)*' + group('"', r"\\\r?\n"),
|
_litprefix + '"' + _string_middle_double + group('"', r"\\\r?\n"),
|
||||||
|
group(_fstringlitprefix + "'") + _fstring_middle_single,
|
||||||
|
group(_fstringlitprefix + '"') + _fstring_middle_double,
|
||||||
|
group(_fstringlitprefix + "'") + _string_middle_single + group("'", r"\\\r?\n"),
|
||||||
|
group(_fstringlitprefix + '"') + _string_middle_double + group('"', r"\\\r?\n"),
|
||||||
)
|
)
|
||||||
PseudoExtras = group(r"\\\r?\n", Comment, Triple)
|
PseudoExtras = group(r"\\\r?\n", Comment, Triple)
|
||||||
PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name)
|
PseudoToken = Whitespace + group(PseudoExtras, Number, Funny, ContStr, Name)
|
||||||
|
|
||||||
pseudoprog: Final = re.compile(PseudoToken, re.UNICODE)
|
pseudoprog: Final = re.compile(PseudoToken, re.UNICODE)
|
||||||
single3prog = re.compile(Single3)
|
|
||||||
double3prog = re.compile(Double3)
|
|
||||||
|
|
||||||
_strprefixes = (
|
singleprog = re.compile(Single)
|
||||||
_combinations("r", "R", "f", "F")
|
singleprog_plus_lbrace = re.compile(group(SingleLbrace, Single))
|
||||||
| _combinations("r", "R", "b", "B")
|
doubleprog = re.compile(Double)
|
||||||
| {"u", "U", "ur", "uR", "Ur", "UR"}
|
doubleprog_plus_lbrace = re.compile(group(DoubleLbrace, Double))
|
||||||
)
|
|
||||||
|
single3prog = re.compile(Single3)
|
||||||
|
single3prog_plus_lbrace = re.compile(group(Single3Lbrace, Single3))
|
||||||
|
double3prog = re.compile(Double3)
|
||||||
|
double3prog_plus_lbrace = re.compile(group(Double3Lbrace, Double3))
|
||||||
|
|
||||||
|
_strprefixes = _combinations("r", "R", "b", "B") | {"u", "U", "ur", "uR", "Ur", "UR"}
|
||||||
|
_fstring_prefixes = _combinations("r", "R", "f", "F") - {"r", "R"}
|
||||||
|
|
||||||
endprogs: Final = {
|
endprogs: Final = {
|
||||||
"'": re.compile(Single),
|
"'": singleprog,
|
||||||
'"': re.compile(Double),
|
'"': doubleprog,
|
||||||
"'''": single3prog,
|
"'''": single3prog,
|
||||||
'"""': double3prog,
|
'"""': double3prog,
|
||||||
|
**{f"{prefix}'": singleprog for prefix in _strprefixes},
|
||||||
|
**{f'{prefix}"': doubleprog for prefix in _strprefixes},
|
||||||
|
**{f"{prefix}'": singleprog_plus_lbrace for prefix in _fstring_prefixes},
|
||||||
|
**{f'{prefix}"': doubleprog_plus_lbrace for prefix in _fstring_prefixes},
|
||||||
**{f"{prefix}'''": single3prog for prefix in _strprefixes},
|
**{f"{prefix}'''": single3prog for prefix in _strprefixes},
|
||||||
**{f'{prefix}"""': double3prog for prefix in _strprefixes},
|
**{f'{prefix}"""': double3prog for prefix in _strprefixes},
|
||||||
|
**{f"{prefix}'''": single3prog_plus_lbrace for prefix in _fstring_prefixes},
|
||||||
|
**{f'{prefix}"""': double3prog_plus_lbrace for prefix in _fstring_prefixes},
|
||||||
}
|
}
|
||||||
|
|
||||||
triple_quoted: Final = (
|
triple_quoted: Final = (
|
||||||
{"'''", '"""'}
|
{"'''", '"""'}
|
||||||
| {f"{prefix}'''" for prefix in _strprefixes}
|
| {f"{prefix}'''" for prefix in _strprefixes | _fstring_prefixes}
|
||||||
| {f'{prefix}"""' for prefix in _strprefixes}
|
| {f'{prefix}"""' for prefix in _strprefixes | _fstring_prefixes}
|
||||||
)
|
)
|
||||||
single_quoted: Final = (
|
single_quoted: Final = (
|
||||||
{"'", '"'}
|
{"'", '"'}
|
||||||
| {f"{prefix}'" for prefix in _strprefixes}
|
| {f"{prefix}'" for prefix in _strprefixes | _fstring_prefixes}
|
||||||
| {f'{prefix}"' for prefix in _strprefixes}
|
| {f'{prefix}"' for prefix in _strprefixes | _fstring_prefixes}
|
||||||
|
)
|
||||||
|
fstring_prefix: Final = (
|
||||||
|
{f"{prefix}'" for prefix in _fstring_prefixes}
|
||||||
|
| {f'{prefix}"' for prefix in _fstring_prefixes}
|
||||||
|
| {f"{prefix}'''" for prefix in _fstring_prefixes}
|
||||||
|
| {f'{prefix}"""' for prefix in _fstring_prefixes}
|
||||||
)
|
)
|
||||||
|
|
||||||
tabsize = 8
|
tabsize = 8
|
||||||
@ -415,6 +467,19 @@ def untokenize(iterable: Iterable[TokenInfo]) -> str:
|
|||||||
return ut.untokenize(iterable)
|
return ut.untokenize(iterable)
|
||||||
|
|
||||||
|
|
||||||
|
def is_fstring_start(token: str) -> bool:
|
||||||
|
return builtins.any(token.startswith(prefix) for prefix in fstring_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def _split_fstring_start_and_middle(token: str) -> Tuple[str, str]:
|
||||||
|
for prefix in fstring_prefix:
|
||||||
|
_, prefix, rest = token.partition(prefix)
|
||||||
|
if prefix != "":
|
||||||
|
return prefix, rest
|
||||||
|
|
||||||
|
raise ValueError(f"Token {token!r} is not a valid f-string start")
|
||||||
|
|
||||||
|
|
||||||
def generate_tokens(
|
def generate_tokens(
|
||||||
readline: Callable[[], str], grammar: Optional[Grammar] = None
|
readline: Callable[[], str], grammar: Optional[Grammar] = None
|
||||||
) -> Iterator[GoodTokenInfo]:
|
) -> Iterator[GoodTokenInfo]:
|
||||||
@ -433,7 +498,12 @@ def generate_tokens(
|
|||||||
and the line on which the token was found. The line passed is the
|
and the line on which the token was found. The line passed is the
|
||||||
logical line; continuation lines are included.
|
logical line; continuation lines are included.
|
||||||
"""
|
"""
|
||||||
lnum = parenlev = continued = 0
|
lnum = parenlev = fstring_level = continued = 0
|
||||||
|
parenlev_stack: List[int] = []
|
||||||
|
inside_fstring_braces = False
|
||||||
|
inside_fstring_colon = False
|
||||||
|
formatspec = ""
|
||||||
|
bracelev = 0
|
||||||
numchars: Final[str] = "0123456789"
|
numchars: Final[str] = "0123456789"
|
||||||
contstr, needcont = "", 0
|
contstr, needcont = "", 0
|
||||||
contline: Optional[str] = None
|
contline: Optional[str] = None
|
||||||
@ -449,7 +519,8 @@ def generate_tokens(
|
|||||||
async_def_nl = False
|
async_def_nl = False
|
||||||
|
|
||||||
strstart: Tuple[int, int]
|
strstart: Tuple[int, int]
|
||||||
endprog: Pattern[str]
|
endprog_stack: List[Pattern[str]] = []
|
||||||
|
formatspec_start: Tuple[int, int]
|
||||||
|
|
||||||
while 1: # loop over lines in stream
|
while 1: # loop over lines in stream
|
||||||
try:
|
try:
|
||||||
@ -463,16 +534,72 @@ def generate_tokens(
|
|||||||
assert contline is not None
|
assert contline is not None
|
||||||
if not line:
|
if not line:
|
||||||
raise TokenError("EOF in multi-line string", strstart)
|
raise TokenError("EOF in multi-line string", strstart)
|
||||||
|
endprog = endprog_stack[-1]
|
||||||
endmatch = endprog.match(line)
|
endmatch = endprog.match(line)
|
||||||
if endmatch:
|
if endmatch:
|
||||||
pos = end = endmatch.end(0)
|
end = endmatch.end(0)
|
||||||
yield (
|
token = contstr + line[:end]
|
||||||
STRING,
|
spos = strstart
|
||||||
contstr + line[:end],
|
epos = (lnum, end)
|
||||||
strstart,
|
tokenline = contline + line
|
||||||
(lnum, end),
|
if fstring_level == 0 and not is_fstring_start(token):
|
||||||
contline + line,
|
yield (STRING, token, spos, epos, tokenline)
|
||||||
)
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
else:
|
||||||
|
if is_fstring_start(token):
|
||||||
|
fstring_level += 1
|
||||||
|
fstring_start, token = _split_fstring_start_and_middle(token)
|
||||||
|
fstring_start_epos = (lnum, spos[1] + len(fstring_start))
|
||||||
|
yield (
|
||||||
|
FSTRING_START,
|
||||||
|
fstring_start,
|
||||||
|
spos,
|
||||||
|
fstring_start_epos,
|
||||||
|
tokenline,
|
||||||
|
)
|
||||||
|
# increase spos to the end of the fstring start
|
||||||
|
spos = fstring_start_epos
|
||||||
|
|
||||||
|
if token.endswith("{"):
|
||||||
|
fstring_middle, lbrace = token[:-1], token[-1]
|
||||||
|
fstring_middle_epos = lbrace_spos = (lnum, end - 1)
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
fstring_middle,
|
||||||
|
spos,
|
||||||
|
fstring_middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
yield (LBRACE, lbrace, lbrace_spos, epos, line)
|
||||||
|
inside_fstring_braces = True
|
||||||
|
else:
|
||||||
|
if token.endswith(('"""', "'''")):
|
||||||
|
fstring_middle, fstring_end = token[:-3], token[-3:]
|
||||||
|
fstring_middle_epos = end_spos = (lnum, end - 3)
|
||||||
|
else:
|
||||||
|
fstring_middle, fstring_end = token[:-1], token[-1]
|
||||||
|
fstring_middle_epos = end_spos = (lnum, end - 1)
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
fstring_middle,
|
||||||
|
spos,
|
||||||
|
fstring_middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
yield (
|
||||||
|
FSTRING_END,
|
||||||
|
fstring_end,
|
||||||
|
end_spos,
|
||||||
|
epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
fstring_level -= 1
|
||||||
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
if fstring_level > 0:
|
||||||
|
inside_fstring_braces = True
|
||||||
|
pos = end
|
||||||
contstr, needcont = "", 0
|
contstr, needcont = "", 0
|
||||||
contline = None
|
contline = None
|
||||||
elif needcont and line[-2:] != "\\\n" and line[-3:] != "\\\r\n":
|
elif needcont and line[-2:] != "\\\n" and line[-3:] != "\\\r\n":
|
||||||
@ -491,7 +618,8 @@ def generate_tokens(
|
|||||||
contline = contline + line
|
contline = contline + line
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif parenlev == 0 and not continued: # new statement
|
# new statement
|
||||||
|
elif parenlev == 0 and not continued and not inside_fstring_braces:
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
column = 0
|
column = 0
|
||||||
@ -559,6 +687,98 @@ def generate_tokens(
|
|||||||
continued = 0
|
continued = 0
|
||||||
|
|
||||||
while pos < max:
|
while pos < max:
|
||||||
|
if fstring_level > 0 and not inside_fstring_braces:
|
||||||
|
endprog = endprog_stack[-1]
|
||||||
|
endmatch = endprog.match(line, pos)
|
||||||
|
if endmatch: # all on one line
|
||||||
|
start, end = endmatch.span(0)
|
||||||
|
token = line[start:end]
|
||||||
|
if token.endswith(('"""', "'''")):
|
||||||
|
middle_token, end_token = token[:-3], token[-3:]
|
||||||
|
middle_epos = end_spos = (lnum, end - 3)
|
||||||
|
else:
|
||||||
|
middle_token, end_token = token[:-1], token[-1]
|
||||||
|
middle_epos = end_spos = (lnum, end - 1)
|
||||||
|
# TODO: unsure if this can be safely removed
|
||||||
|
if stashed:
|
||||||
|
yield stashed
|
||||||
|
stashed = None
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
middle_token,
|
||||||
|
(lnum, pos),
|
||||||
|
middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
if not token.endswith("{"):
|
||||||
|
yield (
|
||||||
|
FSTRING_END,
|
||||||
|
end_token,
|
||||||
|
end_spos,
|
||||||
|
(lnum, end),
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
fstring_level -= 1
|
||||||
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
if fstring_level > 0:
|
||||||
|
inside_fstring_braces = True
|
||||||
|
else:
|
||||||
|
yield (LBRACE, "{", (lnum, end - 1), (lnum, end), line)
|
||||||
|
inside_fstring_braces = True
|
||||||
|
pos = end
|
||||||
|
continue
|
||||||
|
else: # multiple lines
|
||||||
|
strstart = (lnum, end)
|
||||||
|
contstr = line[end:]
|
||||||
|
contline = line
|
||||||
|
break
|
||||||
|
|
||||||
|
if inside_fstring_colon:
|
||||||
|
match = fstring_middle_after_colon.match(line, pos)
|
||||||
|
if match is None:
|
||||||
|
formatspec += line[pos:]
|
||||||
|
pos = max
|
||||||
|
continue
|
||||||
|
|
||||||
|
start, end = match.span(1)
|
||||||
|
token = line[start:end]
|
||||||
|
formatspec += token
|
||||||
|
|
||||||
|
brace_start, brace_end = match.span(2)
|
||||||
|
brace_or_nl = line[brace_start:brace_end]
|
||||||
|
if brace_or_nl == "\n":
|
||||||
|
pos = brace_end
|
||||||
|
|
||||||
|
yield (FSTRING_MIDDLE, formatspec, formatspec_start, (lnum, end), line)
|
||||||
|
formatspec = ""
|
||||||
|
|
||||||
|
if brace_or_nl == "{":
|
||||||
|
yield (OP, "{", (lnum, brace_start), (lnum, brace_end), line)
|
||||||
|
bracelev += 1
|
||||||
|
end = brace_end
|
||||||
|
|
||||||
|
inside_fstring_colon = False
|
||||||
|
pos = end
|
||||||
|
continue
|
||||||
|
|
||||||
|
if inside_fstring_braces and parenlev == 0:
|
||||||
|
match = bang.match(line, pos)
|
||||||
|
if match:
|
||||||
|
start, end = match.span(1)
|
||||||
|
yield (OP, "!", (lnum, start), (lnum, end), line)
|
||||||
|
pos = end
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = colon.match(line, pos)
|
||||||
|
if match:
|
||||||
|
start, end = match.span(1)
|
||||||
|
yield (OP, ":", (lnum, start), (lnum, end), line)
|
||||||
|
inside_fstring_colon = True
|
||||||
|
formatspec_start = (lnum, end)
|
||||||
|
pos = end
|
||||||
|
continue
|
||||||
|
|
||||||
pseudomatch = pseudoprog.match(line, pos)
|
pseudomatch = pseudoprog.match(line, pos)
|
||||||
if pseudomatch: # scan for tokens
|
if pseudomatch: # scan for tokens
|
||||||
start, end = pseudomatch.span(1)
|
start, end = pseudomatch.span(1)
|
||||||
@ -571,7 +791,7 @@ def generate_tokens(
|
|||||||
yield (NUMBER, token, spos, epos, line)
|
yield (NUMBER, token, spos, epos, line)
|
||||||
elif initial in "\r\n":
|
elif initial in "\r\n":
|
||||||
newline = NEWLINE
|
newline = NEWLINE
|
||||||
if parenlev > 0:
|
if parenlev > 0 or inside_fstring_braces:
|
||||||
newline = NL
|
newline = NL
|
||||||
elif async_def:
|
elif async_def:
|
||||||
async_def_nl = True
|
async_def_nl = True
|
||||||
@ -588,17 +808,72 @@ def generate_tokens(
|
|||||||
yield (COMMENT, token, spos, epos, line)
|
yield (COMMENT, token, spos, epos, line)
|
||||||
elif token in triple_quoted:
|
elif token in triple_quoted:
|
||||||
endprog = endprogs[token]
|
endprog = endprogs[token]
|
||||||
|
endprog_stack.append(endprog)
|
||||||
|
parenlev_stack.append(parenlev)
|
||||||
|
parenlev = 0
|
||||||
|
if is_fstring_start(token):
|
||||||
|
yield (FSTRING_START, token, spos, epos, line)
|
||||||
|
fstring_level += 1
|
||||||
|
|
||||||
endmatch = endprog.match(line, pos)
|
endmatch = endprog.match(line, pos)
|
||||||
if endmatch: # all on one line
|
if endmatch: # all on one line
|
||||||
pos = endmatch.end(0)
|
|
||||||
token = line[start:pos]
|
|
||||||
if stashed:
|
if stashed:
|
||||||
yield stashed
|
yield stashed
|
||||||
stashed = None
|
stashed = None
|
||||||
yield (STRING, token, spos, (lnum, pos), line)
|
if not is_fstring_start(token):
|
||||||
|
pos = endmatch.end(0)
|
||||||
|
token = line[start:pos]
|
||||||
|
epos = (lnum, pos)
|
||||||
|
yield (STRING, token, spos, epos, line)
|
||||||
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
else:
|
||||||
|
end = endmatch.end(0)
|
||||||
|
token = line[pos:end]
|
||||||
|
spos, epos = (lnum, pos), (lnum, end)
|
||||||
|
if not token.endswith("{"):
|
||||||
|
fstring_middle, fstring_end = token[:-3], token[-3:]
|
||||||
|
fstring_middle_epos = fstring_end_spos = (lnum, end - 3)
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
fstring_middle,
|
||||||
|
spos,
|
||||||
|
fstring_middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
yield (
|
||||||
|
FSTRING_END,
|
||||||
|
fstring_end,
|
||||||
|
fstring_end_spos,
|
||||||
|
epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
fstring_level -= 1
|
||||||
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
if fstring_level > 0:
|
||||||
|
inside_fstring_braces = True
|
||||||
|
else:
|
||||||
|
fstring_middle, lbrace = token[:-1], token[-1]
|
||||||
|
fstring_middle_epos = lbrace_spos = (lnum, end - 1)
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
fstring_middle,
|
||||||
|
spos,
|
||||||
|
fstring_middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
yield (LBRACE, lbrace, lbrace_spos, epos, line)
|
||||||
|
inside_fstring_braces = True
|
||||||
|
pos = end
|
||||||
else:
|
else:
|
||||||
strstart = (lnum, start) # multiple lines
|
# multiple lines
|
||||||
contstr = line[start:]
|
if is_fstring_start(token):
|
||||||
|
strstart = (lnum, pos)
|
||||||
|
contstr = line[pos:]
|
||||||
|
else:
|
||||||
|
strstart = (lnum, start)
|
||||||
|
contstr = line[start:]
|
||||||
contline = line
|
contline = line
|
||||||
break
|
break
|
||||||
elif (
|
elif (
|
||||||
@ -606,17 +881,18 @@ def generate_tokens(
|
|||||||
or token[:2] in single_quoted
|
or token[:2] in single_quoted
|
||||||
or token[:3] in single_quoted
|
or token[:3] in single_quoted
|
||||||
):
|
):
|
||||||
|
maybe_endprog = (
|
||||||
|
endprogs.get(initial)
|
||||||
|
or endprogs.get(token[:2])
|
||||||
|
or endprogs.get(token[:3])
|
||||||
|
)
|
||||||
|
assert maybe_endprog is not None, f"endprog not found for {token}"
|
||||||
|
endprog = maybe_endprog
|
||||||
if token[-1] == "\n": # continued string
|
if token[-1] == "\n": # continued string
|
||||||
|
endprog_stack.append(endprog)
|
||||||
|
parenlev_stack.append(parenlev)
|
||||||
|
parenlev = 0
|
||||||
strstart = (lnum, start)
|
strstart = (lnum, start)
|
||||||
maybe_endprog = (
|
|
||||||
endprogs.get(initial)
|
|
||||||
or endprogs.get(token[1])
|
|
||||||
or endprogs.get(token[2])
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
maybe_endprog is not None
|
|
||||||
), f"endprog not found for {token}"
|
|
||||||
endprog = maybe_endprog
|
|
||||||
contstr, needcont = line[start:], 1
|
contstr, needcont = line[start:], 1
|
||||||
contline = line
|
contline = line
|
||||||
break
|
break
|
||||||
@ -624,7 +900,57 @@ def generate_tokens(
|
|||||||
if stashed:
|
if stashed:
|
||||||
yield stashed
|
yield stashed
|
||||||
stashed = None
|
stashed = None
|
||||||
yield (STRING, token, spos, epos, line)
|
|
||||||
|
if not is_fstring_start(token):
|
||||||
|
yield (STRING, token, spos, epos, line)
|
||||||
|
else:
|
||||||
|
if pseudomatch[20] is not None:
|
||||||
|
fstring_start = pseudomatch[20]
|
||||||
|
offset = pseudomatch.end(20) - pseudomatch.start(1)
|
||||||
|
elif pseudomatch[22] is not None:
|
||||||
|
fstring_start = pseudomatch[22]
|
||||||
|
offset = pseudomatch.end(22) - pseudomatch.start(1)
|
||||||
|
elif pseudomatch[24] is not None:
|
||||||
|
fstring_start = pseudomatch[24]
|
||||||
|
offset = pseudomatch.end(24) - pseudomatch.start(1)
|
||||||
|
else:
|
||||||
|
fstring_start = pseudomatch[26]
|
||||||
|
offset = pseudomatch.end(26) - pseudomatch.start(1)
|
||||||
|
|
||||||
|
start_epos = (lnum, start + offset)
|
||||||
|
yield (FSTRING_START, fstring_start, spos, start_epos, line)
|
||||||
|
fstring_level += 1
|
||||||
|
endprog = endprogs[fstring_start]
|
||||||
|
endprog_stack.append(endprog)
|
||||||
|
parenlev_stack.append(parenlev)
|
||||||
|
parenlev = 0
|
||||||
|
|
||||||
|
end_offset = pseudomatch.end(1) - 1
|
||||||
|
fstring_middle = line[start + offset : end_offset]
|
||||||
|
middle_spos = (lnum, start + offset)
|
||||||
|
middle_epos = (lnum, end_offset)
|
||||||
|
yield (
|
||||||
|
FSTRING_MIDDLE,
|
||||||
|
fstring_middle,
|
||||||
|
middle_spos,
|
||||||
|
middle_epos,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
if not token.endswith("{"):
|
||||||
|
end_spos = (lnum, end_offset)
|
||||||
|
end_epos = (lnum, end_offset + 1)
|
||||||
|
yield (FSTRING_END, token[-1], end_spos, end_epos, line)
|
||||||
|
fstring_level -= 1
|
||||||
|
endprog_stack.pop()
|
||||||
|
parenlev = parenlev_stack.pop()
|
||||||
|
if fstring_level > 0:
|
||||||
|
inside_fstring_braces = True
|
||||||
|
else:
|
||||||
|
end_spos = (lnum, end_offset)
|
||||||
|
end_epos = (lnum, end_offset + 1)
|
||||||
|
yield (LBRACE, "{", end_spos, end_epos, line)
|
||||||
|
inside_fstring_braces = True
|
||||||
|
|
||||||
elif initial.isidentifier(): # ordinary name
|
elif initial.isidentifier(): # ordinary name
|
||||||
if token in ("async", "await"):
|
if token in ("async", "await"):
|
||||||
if async_keywords or async_def:
|
if async_keywords or async_def:
|
||||||
@ -669,8 +995,22 @@ def generate_tokens(
|
|||||||
stashed = None
|
stashed = None
|
||||||
yield (NL, token, spos, (lnum, pos), line)
|
yield (NL, token, spos, (lnum, pos), line)
|
||||||
continued = 1
|
continued = 1
|
||||||
|
elif (
|
||||||
|
initial == "}"
|
||||||
|
and parenlev == 0
|
||||||
|
and bracelev == 0
|
||||||
|
and fstring_level > 0
|
||||||
|
):
|
||||||
|
yield (RBRACE, token, spos, epos, line)
|
||||||
|
inside_fstring_braces = False
|
||||||
else:
|
else:
|
||||||
if initial in "([{":
|
if parenlev == 0 and bracelev > 0 and initial == "}":
|
||||||
|
bracelev -= 1
|
||||||
|
# if we're still inside fstrings, we're still part of the format spec
|
||||||
|
if inside_fstring_braces:
|
||||||
|
inside_fstring_colon = True
|
||||||
|
formatspec_start = (lnum, pos)
|
||||||
|
elif initial in "([{":
|
||||||
parenlev += 1
|
parenlev += 1
|
||||||
elif initial in ")]}":
|
elif initial in ")]}":
|
||||||
parenlev -= 1
|
parenlev -= 1
|
||||||
@ -689,6 +1029,8 @@ def generate_tokens(
|
|||||||
for _indent in indents[1:]: # pop remaining indent levels
|
for _indent in indents[1:]: # pop remaining indent levels
|
||||||
yield (DEDENT, "", (lnum, 0), (lnum, 0), "")
|
yield (DEDENT, "", (lnum, 0), (lnum, 0), "")
|
||||||
yield (ENDMARKER, "", (lnum, 0), (lnum, 0), "")
|
yield (ENDMARKER, "", (lnum, 0), (lnum, 0), "")
|
||||||
|
assert len(endprog_stack) == 0
|
||||||
|
assert len(parenlev_stack) == 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # testing
|
if __name__ == "__main__": # testing
|
||||||
|
@ -70,6 +70,10 @@ class _python_symbols(Symbols):
|
|||||||
file_input: int
|
file_input: int
|
||||||
flow_stmt: int
|
flow_stmt: int
|
||||||
for_stmt: int
|
for_stmt: int
|
||||||
|
fstring: int
|
||||||
|
fstring_format_spec: int
|
||||||
|
fstring_middle: int
|
||||||
|
fstring_replacement_field: int
|
||||||
funcdef: int
|
funcdef: int
|
||||||
global_stmt: int
|
global_stmt: int
|
||||||
guard: int
|
guard: int
|
||||||
|
224
tests/data/cases/pep_701.py
Normal file
224
tests/data/cases/pep_701.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# flags: --minimum-version=3.12
|
||||||
|
x = f"foo"
|
||||||
|
x = f'foo'
|
||||||
|
x = f"""foo"""
|
||||||
|
x = f'''foo'''
|
||||||
|
x = f"foo {{ bar {{ baz"
|
||||||
|
x = f"foo {{ {2 + 2}bar {{ baz"
|
||||||
|
x = f'foo {{ {2 + 2}bar {{ baz'
|
||||||
|
x = f"""foo {{ {2 + 2}bar {{ baz"""
|
||||||
|
x = f'''foo {{ {2 + 2}bar {{ baz'''
|
||||||
|
|
||||||
|
# edge case: FSTRING_MIDDLE containing only whitespace should not be stripped
|
||||||
|
x = f"{a} {b}"
|
||||||
|
|
||||||
|
x = f"foo {
|
||||||
|
2 + 2
|
||||||
|
} bar baz"
|
||||||
|
|
||||||
|
x = f"foo {{ {"a {2 + 2} b"}bar {{ baz"
|
||||||
|
x = f"foo {{ {f'a {2 + 2} b'}bar {{ baz"
|
||||||
|
x = f"foo {{ {f"a {2 + 2} b"}bar {{ baz"
|
||||||
|
|
||||||
|
x = f"foo {{ {f'a {f"a {2 + 2} b"} b'}bar {{ baz"
|
||||||
|
x = f"foo {{ {f"a {f"a {2 + 2} b"} b"}bar {{ baz"
|
||||||
|
|
||||||
|
x = """foo {{ {2 + 2}bar
|
||||||
|
baz"""
|
||||||
|
|
||||||
|
|
||||||
|
x = f"""foo {{ {2 + 2}bar {{ baz"""
|
||||||
|
|
||||||
|
x = f"""foo {{ {
|
||||||
|
2 + 2
|
||||||
|
}bar {{ baz"""
|
||||||
|
|
||||||
|
|
||||||
|
x = f"""foo {{ {
|
||||||
|
2 + 2
|
||||||
|
}bar
|
||||||
|
baz"""
|
||||||
|
|
||||||
|
x = f"""foo {{ a
|
||||||
|
foo {2 + 2}bar {{ baz
|
||||||
|
|
||||||
|
x = f"foo {{ {
|
||||||
|
2 + 2 # comment
|
||||||
|
}bar"
|
||||||
|
|
||||||
|
{{ baz
|
||||||
|
|
||||||
|
}} buzz
|
||||||
|
|
||||||
|
{print("abc" + "def"
|
||||||
|
)}
|
||||||
|
abc"""
|
||||||
|
|
||||||
|
# edge case: end triple quotes at index zero
|
||||||
|
f"""foo {2+2} bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
f' \' {f"'"} \' '
|
||||||
|
f" \" {f'"'} \" "
|
||||||
|
|
||||||
|
x = f"a{2+2:=^72}b"
|
||||||
|
x = f"a{2+2:x}b"
|
||||||
|
|
||||||
|
rf'foo'
|
||||||
|
rf'{foo}'
|
||||||
|
|
||||||
|
f"{x:{y}d}"
|
||||||
|
|
||||||
|
x = f"a{2+2:=^{x}}b"
|
||||||
|
x = f"a{2+2:=^{foo(x+y**2):something else}}b"
|
||||||
|
x = f"a{2+2:=^{foo(x+y**2):something else}one more}b"
|
||||||
|
f'{(abc:=10)}'
|
||||||
|
|
||||||
|
f"This is a really long string, but just make sure that you reflow fstrings {
|
||||||
|
2+2:d
|
||||||
|
}"
|
||||||
|
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
|
||||||
|
|
||||||
|
f"{2+2=}"
|
||||||
|
f"{2+2 = }"
|
||||||
|
f"{ 2 + 2 = }"
|
||||||
|
|
||||||
|
f"""foo {
|
||||||
|
datetime.datetime.now():%Y
|
||||||
|
%m
|
||||||
|
%d
|
||||||
|
}"""
|
||||||
|
|
||||||
|
f"{
|
||||||
|
X
|
||||||
|
!r
|
||||||
|
}"
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"xxxxxxxxxxxIncorrect --line-ranges format, expect START-END, found"
|
||||||
|
f" {lines_str!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \
|
||||||
|
got {escape}"
|
||||||
|
|
||||||
|
x = f'\N{GREEK CAPITAL LETTER DELTA} \N{SNOWMAN} {x}'
|
||||||
|
fr'\{{\}}'
|
||||||
|
|
||||||
|
f"""
|
||||||
|
WITH {f'''
|
||||||
|
{1}_cte AS ()'''}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# output
|
||||||
|
|
||||||
|
x = f"foo"
|
||||||
|
x = f"foo"
|
||||||
|
x = f"""foo"""
|
||||||
|
x = f"""foo"""
|
||||||
|
x = f"foo {{ bar {{ baz"
|
||||||
|
x = f"foo {{ {2 + 2}bar {{ baz"
|
||||||
|
x = f"foo {{ {2 + 2}bar {{ baz"
|
||||||
|
x = f"""foo {{ {2 + 2}bar {{ baz"""
|
||||||
|
x = f"""foo {{ {2 + 2}bar {{ baz"""
|
||||||
|
|
||||||
|
# edge case: FSTRING_MIDDLE containing only whitespace should not be stripped
|
||||||
|
x = f"{a} {b}"
|
||||||
|
|
||||||
|
x = f"foo {
|
||||||
|
2 + 2
|
||||||
|
} bar baz"
|
||||||
|
|
||||||
|
x = f"foo {{ {"a {2 + 2} b"}bar {{ baz"
|
||||||
|
x = f"foo {{ {f'a {2 + 2} b'}bar {{ baz"
|
||||||
|
x = f"foo {{ {f"a {2 + 2} b"}bar {{ baz"
|
||||||
|
|
||||||
|
x = f"foo {{ {f'a {f"a {2 + 2} b"} b'}bar {{ baz"
|
||||||
|
x = f"foo {{ {f"a {f"a {2 + 2} b"} b"}bar {{ baz"
|
||||||
|
|
||||||
|
x = """foo {{ {2 + 2}bar
|
||||||
|
baz"""
|
||||||
|
|
||||||
|
|
||||||
|
x = f"""foo {{ {2 + 2}bar {{ baz"""
|
||||||
|
|
||||||
|
x = f"""foo {{ {
|
||||||
|
2 + 2
|
||||||
|
}bar {{ baz"""
|
||||||
|
|
||||||
|
|
||||||
|
x = f"""foo {{ {
|
||||||
|
2 + 2
|
||||||
|
}bar
|
||||||
|
baz"""
|
||||||
|
|
||||||
|
x = f"""foo {{ a
|
||||||
|
foo {2 + 2}bar {{ baz
|
||||||
|
|
||||||
|
x = f"foo {{ {
|
||||||
|
2 + 2 # comment
|
||||||
|
}bar"
|
||||||
|
|
||||||
|
{{ baz
|
||||||
|
|
||||||
|
}} buzz
|
||||||
|
|
||||||
|
{print("abc" + "def"
|
||||||
|
)}
|
||||||
|
abc"""
|
||||||
|
|
||||||
|
# edge case: end triple quotes at index zero
|
||||||
|
f"""foo {2+2} bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
f' \' {f"'"} \' '
|
||||||
|
f" \" {f'"'} \" "
|
||||||
|
|
||||||
|
x = f"a{2+2:=^72}b"
|
||||||
|
x = f"a{2+2:x}b"
|
||||||
|
|
||||||
|
rf"foo"
|
||||||
|
rf"{foo}"
|
||||||
|
|
||||||
|
f"{x:{y}d}"
|
||||||
|
|
||||||
|
x = f"a{2+2:=^{x}}b"
|
||||||
|
x = f"a{2+2:=^{foo(x+y**2):something else}}b"
|
||||||
|
x = f"a{2+2:=^{foo(x+y**2):something else}one more}b"
|
||||||
|
f"{(abc:=10)}"
|
||||||
|
|
||||||
|
f"This is a really long string, but just make sure that you reflow fstrings {
|
||||||
|
2+2:d
|
||||||
|
}"
|
||||||
|
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
|
||||||
|
|
||||||
|
f"{2+2=}"
|
||||||
|
f"{2+2 = }"
|
||||||
|
f"{ 2 + 2 = }"
|
||||||
|
|
||||||
|
f"""foo {
|
||||||
|
datetime.datetime.now():%Y
|
||||||
|
%m
|
||||||
|
%d
|
||||||
|
}"""
|
||||||
|
|
||||||
|
f"{
|
||||||
|
X
|
||||||
|
!r
|
||||||
|
}"
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"xxxxxxxxxxxIncorrect --line-ranges format, expect START-END, found"
|
||||||
|
f" {lines_str!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \
|
||||||
|
got {escape}"
|
||||||
|
|
||||||
|
x = f"\N{GREEK CAPITAL LETTER DELTA} \N{SNOWMAN} {x}"
|
||||||
|
rf"\{{\}}"
|
||||||
|
|
||||||
|
f"""
|
||||||
|
WITH {f'''
|
||||||
|
{1}_cte AS ()'''}
|
||||||
|
"""
|
@ -229,8 +229,34 @@ file_input
|
|||||||
LPAR
|
LPAR
|
||||||
'('
|
'('
|
||||||
arglist
|
arglist
|
||||||
STRING
|
fstring
|
||||||
"f'{indent}{_type}'"
|
FSTRING_START
|
||||||
|
"f'"
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'indent'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'_type'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
FSTRING_END
|
||||||
|
"'"
|
||||||
|
/fstring
|
||||||
COMMA
|
COMMA
|
||||||
','
|
','
|
||||||
argument
|
argument
|
||||||
@ -370,8 +396,34 @@ file_input
|
|||||||
LPAR
|
LPAR
|
||||||
'('
|
'('
|
||||||
arglist
|
arglist
|
||||||
STRING
|
fstring
|
||||||
"f'{indent}/{_type}'"
|
FSTRING_START
|
||||||
|
"f'"
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'indent'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
'/'
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'_type'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
FSTRING_END
|
||||||
|
"'"
|
||||||
|
/fstring
|
||||||
COMMA
|
COMMA
|
||||||
','
|
','
|
||||||
argument
|
argument
|
||||||
@ -494,8 +546,34 @@ file_input
|
|||||||
LPAR
|
LPAR
|
||||||
'('
|
'('
|
||||||
arglist
|
arglist
|
||||||
STRING
|
fstring
|
||||||
"f'{indent}{_type}'"
|
FSTRING_START
|
||||||
|
"f'"
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'indent'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
NAME
|
||||||
|
'_type'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
FSTRING_END
|
||||||
|
"'"
|
||||||
|
/fstring
|
||||||
COMMA
|
COMMA
|
||||||
','
|
','
|
||||||
argument
|
argument
|
||||||
@ -557,8 +635,36 @@ file_input
|
|||||||
LPAR
|
LPAR
|
||||||
'('
|
'('
|
||||||
arglist
|
arglist
|
||||||
STRING
|
fstring
|
||||||
"f' {node.prefix!r}'"
|
FSTRING_START
|
||||||
|
"f'"
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
' '
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
power
|
||||||
|
NAME
|
||||||
|
'node'
|
||||||
|
trailer
|
||||||
|
DOT
|
||||||
|
'.'
|
||||||
|
NAME
|
||||||
|
'prefix'
|
||||||
|
/trailer
|
||||||
|
/power
|
||||||
|
BANG
|
||||||
|
'!'
|
||||||
|
NAME
|
||||||
|
'r'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
FSTRING_END
|
||||||
|
"'"
|
||||||
|
/fstring
|
||||||
COMMA
|
COMMA
|
||||||
','
|
','
|
||||||
argument
|
argument
|
||||||
@ -613,8 +719,36 @@ file_input
|
|||||||
LPAR
|
LPAR
|
||||||
'('
|
'('
|
||||||
arglist
|
arglist
|
||||||
STRING
|
fstring
|
||||||
"f' {node.value!r}'"
|
FSTRING_START
|
||||||
|
"f'"
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
' '
|
||||||
|
fstring_replacement_field
|
||||||
|
LBRACE
|
||||||
|
'{'
|
||||||
|
power
|
||||||
|
NAME
|
||||||
|
'node'
|
||||||
|
trailer
|
||||||
|
DOT
|
||||||
|
'.'
|
||||||
|
NAME
|
||||||
|
'value'
|
||||||
|
/trailer
|
||||||
|
/power
|
||||||
|
BANG
|
||||||
|
'!'
|
||||||
|
NAME
|
||||||
|
'r'
|
||||||
|
RBRACE
|
||||||
|
'}'
|
||||||
|
/fstring_replacement_field
|
||||||
|
FSTRING_MIDDLE
|
||||||
|
''
|
||||||
|
FSTRING_END
|
||||||
|
"'"
|
||||||
|
/fstring
|
||||||
COMMA
|
COMMA
|
||||||
','
|
','
|
||||||
argument
|
argument
|
||||||
|
@ -343,12 +343,11 @@ def test_detect_debug_f_strings(self) -> None:
|
|||||||
features = black.get_features_used(root)
|
features = black.get_features_used(root)
|
||||||
self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
|
self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
|
||||||
|
|
||||||
# We don't yet support feature version detection in nested f-strings
|
|
||||||
root = black.lib2to3_parse(
|
root = black.lib2to3_parse(
|
||||||
"""f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
|
"""f"heard a rumour that { f'{1+1=}' } ... seems like it could be true" """
|
||||||
)
|
)
|
||||||
features = black.get_features_used(root)
|
features = black.get_features_used(root)
|
||||||
self.assertNotIn(black.Feature.DEBUG_F_STRINGS, features)
|
self.assertIn(black.Feature.DEBUG_F_STRINGS, features)
|
||||||
|
|
||||||
@patch("black.dump_to_file", dump_to_stderr)
|
@patch("black.dump_to_file", dump_to_stderr)
|
||||||
def test_string_quotes(self) -> None:
|
def test_string_quotes(self) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user