diff --git a/Lib/ast.py b/Lib/ast.py index 7a43581c0e6ce6..00e4cb53b75daf 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -62,20 +62,23 @@ def literal_eval(node_or_string): node_or_string = parse(node_or_string, mode='eval') if isinstance(node_or_string, Expression): node_or_string = node_or_string.body - def _raise_malformed_node(node): - raise ValueError(f'malformed node or string: {node!r}') - def _convert_num(node): + def _raise_malformed_node(node, context = None): + message = "malformed node or string" + if context is not None and isinstance(node, AST): + message += f" in {context} context" + raise ValueError(f'{message}: {node!r}') + def _convert_num(node, context): if not isinstance(node, Constant) or type(node.value) not in (int, float, complex): - _raise_malformed_node(node) + _raise_malformed_node(node, context) return node.value - def _convert_signed_num(node): + def _convert_signed_num(node, context): if isinstance(node, UnaryOp) and isinstance(node.op, (UAdd, USub)): - operand = _convert_num(node.operand) + operand = _convert_num(node.operand, "unary operation") if isinstance(node.op, UAdd): return + operand else: return - operand - return _convert_num(node) + return _convert_num(node, context) def _convert(node): if isinstance(node, Constant): return node.value @@ -90,18 +93,18 @@ def _convert(node): return set() elif isinstance(node, Dict): if len(node.keys) != len(node.values): - _raise_malformed_node(node) + _raise_malformed_node(node, "dictionary") return dict(zip(map(_convert, node.keys), map(_convert, node.values))) elif isinstance(node, BinOp) and isinstance(node.op, (Add, Sub)): - left = _convert_signed_num(node.left) - right = _convert_num(node.right) + left = _convert_signed_num(node.left, "binary operation") + right = _convert_num(node.right, "binary operation") if isinstance(left, (int, float)) and isinstance(right, complex): if isinstance(node.op, Add): return left + right else: return left - right - return _convert_signed_num(node) + return _convert_signed_num(node, "literal") return _convert(node_or_string) diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index 6b71adac4e4a6b..57d911e2edcbae 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -971,6 +971,22 @@ def test_literal_eval_malformed_dict_nodes(self): malformed = ast.Dict(keys=[ast.Constant(1)], values=[ast.Constant(2), ast.Constant(3)]) self.assertRaises(ValueError, ast.literal_eval, malformed) + def test_literal_eval_message(self): + tests = { + "2 * 5": "in literal context", + "[] + []": "in binary operation context", + "+''": "in unary operation context", + ast.Dict( + keys=[ast.Constant(1)], values=[] + ): "in dictionary context", + ast.BinOp( + ast.Constant(1), ast.Add(), "oops" + ): "malformed node or string: 'oops'", + } + for test, message in tests.items(): + with self.subTest(test=test, expected_message=message): + self.assertRaisesRegex(ValueError, message, ast.literal_eval, test) + def test_bad_integer(self): # issue13436: Bad error message with invalid numeric values body = [ast.ImportFrom(module='time', diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index e0bb5b56b2614f..bd2ff80e8f1b25 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -338,7 +338,10 @@ def g(): self.assertIsNone(g.__doc__) def test_literal_eval(self): - with self.assertRaisesRegex(ValueError, 'malformed node or string'): + with self.assertRaisesRegex( + ValueError, + 'malformed node or string in literal context' + ): ast.literal_eval("f'x'") def test_ast_compile_time_concat(self): diff --git a/Misc/NEWS.d/next/Library/2019-12-19-18-20-08.bpo-32888.J5XUjV.rst b/Misc/NEWS.d/next/Library/2019-12-19-18-20-08.bpo-32888.J5XUjV.rst new file mode 100644 index 00000000000000..364bac26216c16 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-12-19-18-20-08.bpo-32888.J5XUjV.rst @@ -0,0 +1,2 @@ +Enhance :func:`ast.literal_eval` error messages with context information. Original +patch is written by Chris Angelico, improved and maintained by Batuhan Taskaya.