Skip to content

Commit

Permalink
Improve AST safety parsing error message (#2304)
Browse files Browse the repository at this point in the history
Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
  • Loading branch information
felix-hilden and hramezani committed Jul 13, 2021
1 parent 2946d3b commit 91773b8
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 54 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -20,6 +20,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
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+ ;)
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

0 comments on commit 91773b8

Please sign in to comment.