From 4d9dd8fdd74eed83843f6032b3b5cc4a6d4f995f Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 10 May 2021 17:08:25 +0200 Subject: [PATCH] Provide a more useful error when parsing fails during AST safety checks (#2218) --- CHANGES.md | 1 + src/black/parsing.py | 14 +++++++++++--- tests/test_black.py | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3427fe8e391..3719ba10929 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -51,6 +51,7 @@ ### _Black_ - Refactor `src/black/__init__.py` into many files (#2206) +- Provide a more useful error when parsing fails during AST safety checks (#2218) ### Documentation diff --git a/src/black/parsing.py b/src/black/parsing.py index 8e9feea9120..587cd64b701 100644 --- a/src/black/parsing.py +++ b/src/black/parsing.py @@ -108,18 +108,23 @@ def lib2to3_unparse(node: Node) -> str: def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: filename = "" + error = None 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: + except SyntaxError as e: + if error is None: + error = str(e) continue else: for feature_version in (7, 6): try: return ast3.parse(src, filename, feature_version=feature_version) - except SyntaxError: + except SyntaxError as e: + if error is None: + error = str(e) continue if ast27.__name__ == "ast": raise SyntaxError( @@ -127,7 +132,10 @@ def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]: "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) + try: + return ast27.parse(src) + except SyntaxError as e: + raise SyntaxError(error or str(e)) def stringify_ast( diff --git a/tests/test_black.py b/tests/test_black.py index f0a14aa2da4..e7035cd7cc1 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -479,6 +479,29 @@ def test_python2_should_fail_without_optional_install(self) -> None: ) self.assertIn(msg, actual) + @pytest.mark.python2 + def test_safety_check_syntax_error(self) -> None: + source = "e = 'test'\nx = f'{'" + 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) + actual = ( + runner.stderr_bytes.decode() + .replace("\n", "") + .replace("\\n", "") + .replace("\\r", "") + .replace("\r", "") + ) + msg = ( + "cannot use --safe with this file; failed to parse source file." + " AST error message: f-string: expecting '}' (, line 2)" + ) + self.assertIn(msg, actual) + @pytest.mark.python2 @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: