Skip to content

Commit e4d9349

Browse files
authored
Merge pull request #1154 from PyCQA/py312-fstring-format-spec
3.12: format specs are not an error
2 parents 0aca13d + 6fddf73 commit e4d9349

File tree

3 files changed

+37
-20
lines changed

3 files changed

+37
-20
lines changed

pycodestyle.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@
150150
DUNDER_REGEX = re.compile(r"^__([^\s]+)__(?::\s*[a-zA-Z.0-9_\[\]\"]+)? = ")
151151
BLANK_EXCEPT_REGEX = re.compile(r"except\s*:")
152152

153+
if sys.version_info >= (3, 12):
154+
FSTRING_START = tokenize.FSTRING_START
155+
FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE
156+
FSTRING_END = tokenize.FSTRING_END
157+
else:
158+
FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1
159+
153160
_checks = {'physical_line': {}, 'logical_line': {}, 'tree': {}}
154161

155162

@@ -494,7 +501,7 @@ def missing_whitespace_after_keyword(logical_line, tokens):
494501

495502

496503
@register_check
497-
def missing_whitespace(logical_line):
504+
def missing_whitespace(logical_line, tokens):
498505
r"""Each comma, semicolon or colon should be followed by whitespace.
499506
500507
Okay: [a, b]
@@ -508,20 +515,31 @@ def missing_whitespace(logical_line):
508515
E231: foo(bar,baz)
509516
E231: [{'a':'b'}]
510517
"""
511-
line = logical_line
512-
for index in range(len(line) - 1):
513-
char = line[index]
514-
next_char = line[index + 1]
515-
if char in ',;:' and next_char not in WHITESPACE:
516-
before = line[:index]
517-
if char == ':' and before.count('[') > before.count(']') and \
518-
before.rfind('{') < before.rfind('['):
519-
continue # Slice syntax, no space required
520-
if char == ',' and next_char in ')]':
521-
continue # Allow tuple with only one element: (3,)
522-
if char == ':' and next_char == '=' and sys.version_info >= (3, 8):
523-
continue # Allow assignment expression
524-
yield index, "E231 missing whitespace after '%s'" % char
518+
brace_stack = []
519+
for tok in tokens:
520+
if tok.type == tokenize.OP and tok.string in {'[', '(', '{'}:
521+
brace_stack.append(tok.string)
522+
elif tok.type == FSTRING_START:
523+
brace_stack.append('f')
524+
elif brace_stack:
525+
if tok.type == tokenize.OP and tok.string in {']', ')', '}'}:
526+
brace_stack.pop()
527+
elif tok.type == FSTRING_END:
528+
brace_stack.pop()
529+
530+
if tok.type == tokenize.OP and tok.string in {',', ';', ':'}:
531+
next_char = tok.line[tok.end[1]:tok.end[1] + 1]
532+
if next_char not in WHITESPACE and next_char not in '\r\n':
533+
# slice
534+
if tok.string == ':' and brace_stack[-1:] == ['[']:
535+
continue
536+
# 3.12+ fstring format specifier
537+
elif tok.string == ':' and brace_stack[-2:] == ['f', '{']:
538+
continue
539+
# tuple (and list for some reason?)
540+
elif tok.string == ',' and next_char in ')]':
541+
continue
542+
yield tok.end, f'E231 missing whitespace after {tok.string!r}'
525543

526544

527545
@register_check
@@ -2010,10 +2028,7 @@ def build_tokens_line(self):
20102028
continue
20112029
if token_type == tokenize.STRING:
20122030
text = mute_string(text)
2013-
elif (
2014-
sys.version_info >= (3, 12) and
2015-
token_type == tokenize.FSTRING_MIDDLE
2016-
):
2031+
elif token_type == FSTRING_MIDDLE:
20172032
text = 'x' * len(text)
20182033
if prev_row:
20192034
(start_row, start_col) = start

testsuite/E12.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def example_issue254():
366366
# more stuff
367367
)
368368
)
369-
#: E701:1:8 E122:2:1 E203:4:8 E128:5:1
369+
#: E701:1:8 E231:1:9 E122:2:1 E203:4:8 E128:5:1
370370
if True:\
371371
print(True)
372372

testsuite/python312.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ def g[T: str, U: int](x: T, y: U) -> dict[T, U]:
2727
} {f'{other} {thing}'}'
2828
#: E201:1:4 E202:1:17
2929
f'{ an_error_now }'
30+
#: Okay
31+
f'{x:02x}'

0 commit comments

Comments
 (0)