Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support files with type comment syntax errors #3594

Merged
merged 11 commits into from Mar 19, 2023
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -29,6 +29,8 @@

<!-- Changes to the parser or to version autodetection -->

- Added support for formatting files with invalid type comments (#3594)

### Performance

<!-- Changes that improve Black's performance. -->
Expand Down
26 changes: 19 additions & 7 deletions src/black/parsing.py
Expand Up @@ -148,24 +148,29 @@ def lib2to3_unparse(node: Node) -> str:


def parse_single_version(
src: str, version: Tuple[int, int]
src: str, version: Tuple[int, int], *, type_comments: bool
) -> Union[ast.AST, ast3.AST]:
filename = "<unknown>"
# typed-ast is needed because of feature version limitations in the builtin ast 3.8>
if sys.version_info >= (3, 8) and version >= (3,):
return ast.parse(src, filename, feature_version=version, type_comments=True)
return ast.parse(
src, filename, feature_version=version, type_comments=type_comments
)

if _IS_PYPY:
# PyPy 3.7 doesn't support type comment tracking which is not ideal, but there's
# not much we can do as typed-ast won't work either.
if sys.version_info >= (3, 8):
return ast3.parse(src, filename, type_comments=True)
return ast3.parse(src, filename, type_comments=type_comments)
else:
return ast3.parse(src, filename)
else:
# Typed-ast is guaranteed to be used here and automatically tracks type
# comments separately.
return ast3.parse(src, filename, feature_version=version[1])
if type_comments:
# Typed-ast is guaranteed to be used here and automatically tracks type
# comments separately.
return ast3.parse(src, filename, feature_version=version[1])
else:
return ast.parse(src, filename)


def parse_ast(src: str) -> Union[ast.AST, ast3.AST]:
Expand All @@ -175,11 +180,18 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST]:
first_error = ""
for version in sorted(versions, reverse=True):
try:
return parse_single_version(src, version)
return parse_single_version(src, version, type_comments=True)
except SyntaxError as e:
if not first_error:
first_error = str(e)

# Try to parse without type comments
for version in sorted(versions, reverse=True):
try:
return parse_single_version(src, version, type_comments=False)
except SyntaxError:
pass

raise SyntaxError(first_error)


Expand Down
11 changes: 11 additions & 0 deletions tests/data/type_comments/type_comment_syntax_error.py
@@ -0,0 +1,11 @@
def foo(
# type: Foo
x): pass

# output

def foo(
# type: Foo
x,
):
pass
7 changes: 7 additions & 0 deletions tests/test_format.py
Expand Up @@ -190,3 +190,10 @@ def test_power_op_newline() -> None:
# requires line_length=0
source, expected = read_data("miscellaneous", "power_op_newline")
assert_format(source, expected, mode=black.Mode(line_length=0))


def test_type_comment_syntax_error() -> None:
"""Test that black is able to format python code with type comment syntax errors."""
source, expected = read_data("type_comments", "type_comment_syntax_error")
assert_format(source, expected)
black.assert_equivalent(source, expected)