Skip to content

Commit

Permalink
Merge pull request #1154 from PyCQA/py312-fstring-format-spec
Browse files Browse the repository at this point in the history
3.12: format specs are not an error
  • Loading branch information
asottile committed Jun 17, 2023
2 parents 0aca13d + 6fddf73 commit e4d9349
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 20 deletions.
53 changes: 34 additions & 19 deletions pycodestyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@
DUNDER_REGEX = re.compile(r"^__([^\s]+)__(?::\s*[a-zA-Z.0-9_\[\]\"]+)? = ")
BLANK_EXCEPT_REGEX = re.compile(r"except\s*:")

if sys.version_info >= (3, 12):
FSTRING_START = tokenize.FSTRING_START
FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE
FSTRING_END = tokenize.FSTRING_END
else:
FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1

_checks = {'physical_line': {}, 'logical_line': {}, 'tree': {}}


Expand Down Expand Up @@ -494,7 +501,7 @@ def missing_whitespace_after_keyword(logical_line, tokens):


@register_check
def missing_whitespace(logical_line):
def missing_whitespace(logical_line, tokens):
r"""Each comma, semicolon or colon should be followed by whitespace.
Okay: [a, b]
Expand All @@ -508,20 +515,31 @@ def missing_whitespace(logical_line):
E231: foo(bar,baz)
E231: [{'a':'b'}]
"""
line = logical_line
for index in range(len(line) - 1):
char = line[index]
next_char = line[index + 1]
if char in ',;:' and next_char not in WHITESPACE:
before = line[:index]
if char == ':' and before.count('[') > before.count(']') and \
before.rfind('{') < before.rfind('['):
continue # Slice syntax, no space required
if char == ',' and next_char in ')]':
continue # Allow tuple with only one element: (3,)
if char == ':' and next_char == '=' and sys.version_info >= (3, 8):
continue # Allow assignment expression
yield index, "E231 missing whitespace after '%s'" % char
brace_stack = []
for tok in tokens:
if tok.type == tokenize.OP and tok.string in {'[', '(', '{'}:
brace_stack.append(tok.string)
elif tok.type == FSTRING_START:
brace_stack.append('f')
elif brace_stack:
if tok.type == tokenize.OP and tok.string in {']', ')', '}'}:
brace_stack.pop()
elif tok.type == FSTRING_END:
brace_stack.pop()

if tok.type == tokenize.OP and tok.string in {',', ';', ':'}:
next_char = tok.line[tok.end[1]:tok.end[1] + 1]
if next_char not in WHITESPACE and next_char not in '\r\n':
# slice
if tok.string == ':' and brace_stack[-1:] == ['[']:
continue
# 3.12+ fstring format specifier
elif tok.string == ':' and brace_stack[-2:] == ['f', '{']:
continue
# tuple (and list for some reason?)
elif tok.string == ',' and next_char in ')]':
continue
yield tok.end, f'E231 missing whitespace after {tok.string!r}'


@register_check
Expand Down Expand Up @@ -2010,10 +2028,7 @@ def build_tokens_line(self):
continue
if token_type == tokenize.STRING:
text = mute_string(text)
elif (
sys.version_info >= (3, 12) and
token_type == tokenize.FSTRING_MIDDLE
):
elif token_type == FSTRING_MIDDLE:
text = 'x' * len(text)
if prev_row:
(start_row, start_col) = start
Expand Down
2 changes: 1 addition & 1 deletion testsuite/E12.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def example_issue254():
# more stuff
)
)
#: E701:1:8 E122:2:1 E203:4:8 E128:5:1
#: E701:1:8 E231:1:9 E122:2:1 E203:4:8 E128:5:1
if True:\
print(True)

Expand Down
2 changes: 2 additions & 0 deletions testsuite/python312.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ def g[T: str, U: int](x: T, y: U) -> dict[T, U]:
} {f'{other} {thing}'}'
#: E201:1:4 E202:1:17
f'{ an_error_now }'
#: Okay
f'{x:02x}'

0 comments on commit e4d9349

Please sign in to comment.