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

Improve AST safety parsing error message #2304

Merged
merged 9 commits into from Jul 13, 2021
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -18,6 +18,7 @@
- Fixed option usage when using the `--code` flag (#2259)
- Do not call `uvloop.install()` when _Black_ is used as a library (#2303)
- Added `--required-version` option to require a specific version to be running (#2300)
- Provide a more useful error when parsing fails during AST safety checks (#2304)
- Fix incorrect custom breakpoint indices when string group contains fake f-strings
(#2311)
- Fix regression where `R` prefixes would be lowercased for docstrings (#2285)
Expand Down
52 changes: 30 additions & 22 deletions src/black/parsing.py
Expand Up @@ -3,7 +3,7 @@
"""
import ast
import sys
from typing import Iterable, Iterator, List, Set, Union
from typing import Iterable, Iterator, List, Set, Union, Tuple

# lib2to3 fork
from blib2to3.pytree import Node, Leaf
Expand Down Expand Up @@ -106,28 +106,36 @@ def lib2to3_unparse(node: Node) -> str:
return code


def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
def parse_single_version(
src: str, version: Tuple[int, int]
) -> Union[ast.AST, ast3.AST, ast27.AST]:
filename = "<unknown>"
if sys.version_info >= (3, 8):
# TODO: support Python 4+ ;)
for minor_version in range(sys.version_info[1], 4, -1):
try:
return ast.parse(src, filename, feature_version=(3, minor_version))
except SyntaxError:
continue
else:
for feature_version in (7, 6):
try:
return ast3.parse(src, filename, feature_version=feature_version)
except SyntaxError:
continue
if ast27.__name__ == "ast":
raise SyntaxError(
"The requested source code has invalid Python 3 syntax.\n"
"If you are trying to format Python 2 files please reinstall Black"
" with the 'python2' extra: `python3 -m pip install black[python2]`."
)
return ast27.parse(src)
# typed_ast is needed because of feature version limitations in the builtin ast
felix-hilden marked this conversation as resolved.
Show resolved Hide resolved
if sys.version_info >= (3, 8) and version >= (3,):
return ast.parse(src, filename, feature_version=version)
elif version >= (3,):
return ast3.parse(src, filename, feature_version=version[1])
elif version == (2, 7):
return ast27.parse(src)
raise AssertionError("INTERNAL ERROR: Tried parsing unsupported Python version!")


def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
# TODO: support Python 4+ ;)
felix-hilden marked this conversation as resolved.
Show resolved Hide resolved
versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]

if ast27.__name__ != "ast":
versions.append((2, 7))

first_error = ""
for version in sorted(versions, reverse=True):
try:
return parse_single_version(src, version)
except SyntaxError as e:
if not first_error:
first_error = str(e)

raise SyntaxError(first_error)


def stringify_ast(
Expand Down
32 changes: 0 additions & 32 deletions tests/test_black.py
Expand Up @@ -451,38 +451,6 @@ def test_skip_magic_trailing_comma(self) -> None:
)
self.assertEqual(expected, actual, msg)

@pytest.mark.no_python2
def test_python2_should_fail_without_optional_install(self) -> None:
if sys.version_info < (3, 8):
self.skipTest(
"Python 3.6 and 3.7 will install typed-ast to work and as such will be"
" able to parse Python 2 syntax without explicitly specifying the"
" python2 extra"
)

source = "x = 1234l"
tmp_file = Path(black.dump_to_file(source))
try:
runner = BlackRunner()
result = runner.invoke(black.main, [str(tmp_file)])
self.assertEqual(result.exit_code, 123)
finally:
os.unlink(tmp_file)
assert result.stderr_bytes is not None
actual = (
result.stderr_bytes.decode()
.replace("\n", "")
.replace("\\n", "")
.replace("\\r", "")
.replace("\r", "")
)
msg = (
"The requested source code has invalid Python 3 syntax."
"If you are trying to format Python 2 files please reinstall Black"
" with the 'python2' extra: `python3 -m pip install black[python2]`."
)
self.assertIn(msg, actual)

@pytest.mark.python2
@patch("black.dump_to_file", dump_to_stderr)
def test_python2_print_function(self) -> None:
Expand Down