Skip to content

Commit fcaa8bf

Browse files
author
Friday
committed
Fix spurious E231 for nested f-string format specifiers
When a replacement field appears inside an f-string format spec (e.g. `f"{x:0.{digits:d}f}"`), the `:` in the nested `{digits:d}` is a format specifier, not a dict colon. Previously pycodestyle reported a false-positive E231 on it because the brace-stack pattern `['f', '{']` only matched the top-level replacement field. Track `FSTRING_MIDDLE` / `TSTRING_MIDDLE` tokens: when `{` is opened immediately after one of these tokens it is a nested replacement field inside a format spec. Push `'F'` (instead of `'{'`) onto the brace stack, then suppress E231 for `:` when the top of the stack is `'F'`. Fixes #1241.
1 parent 8b53059 commit fcaa8bf

File tree

2 files changed

+27
-1
lines changed

2 files changed

+27
-1
lines changed

pycodestyle.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,9 +874,17 @@ def missing_whitespace(logical_line, tokens):
874874
prev_text = prev_end = None
875875
operator_types = (tokenize.OP, tokenize.NAME)
876876
brace_stack = []
877+
_after_fstring_middle = False
877878
for token_type, text, start, end, line in tokens:
878879
if token_type == tokenize.OP and text in {'[', '(', '{'}:
879-
brace_stack.append(text)
880+
# After FSTRING_MIDDLE/TSTRING_MIDDLE, `{` opens a nested
881+
# replacement field inside a format spec. Mark it with 'F'
882+
# so the `:` in e.g. f"{x:0.{digits:d}f}" is recognised as
883+
# a format-specifier colon, not a dict colon.
884+
if _after_fstring_middle and text == '{': # pragma: >=3.12 cover
885+
brace_stack.append('F')
886+
else:
887+
brace_stack.append(text)
880888
elif token_type == FSTRING_START: # pragma: >=3.12 cover
881889
brace_stack.append('f')
882890
elif token_type == TSTRING_START: # pragma: >=3.14 cover
@@ -897,6 +905,16 @@ def missing_whitespace(logical_line, tokens):
897905
):
898906
brace_stack.pop()
899907

908+
# Track whether the previous meaningful token was FSTRING_MIDDLE
909+
# or TSTRING_MIDDLE (i.e. we just entered a format spec region).
910+
if ( # pragma: >=3.12 cover
911+
token_type == FSTRING_MIDDLE or
912+
token_type == TSTRING_MIDDLE
913+
):
914+
_after_fstring_middle = True
915+
elif token_type not in SKIP_COMMENTS:
916+
_after_fstring_middle = False
917+
900918
if token_type in SKIP_COMMENTS:
901919
continue
902920

@@ -912,6 +930,9 @@ def missing_whitespace(logical_line, tokens):
912930
# 3.14+ tstring format specifier
913931
elif text == ':' and brace_stack[-2:] == ['t', '{']: # pragma: >=3.14 cover # noqa: E501
914932
pass
933+
# nested replacement field in f/t-string format spec
934+
elif text == ':' and brace_stack[-1:] == ['F']: # pragma: >=3.12 cover # noqa: E501
935+
pass
915936
# tuple (and list for some reason?)
916937
elif text == ',' and next_char in ')]':
917938
pass

testing/data/python312.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ def g[T: str, U: int](x: T, y: U) -> dict[T, U]:
2929
f'{ an_error_now }'
3030
#: Okay
3131
f'{x:02x}'
32+
#: Okay
33+
# nested replacement field in format spec (issue #1241)
34+
f"{x:0.{digits:d}f}"
35+
#: Okay
36+
f'{value:{fill}{align}{width}.{precision}f}'

0 commit comments

Comments
 (0)