diff --git a/ChangeLog b/ChangeLog index 0091e20eb..be83426c9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -13,6 +13,10 @@ What's New in astroid 3.2.4? ============================ Release date: TBA +* Avoid reporting unary/binary op type errors when inference is ambiguous. + + Closes #2467 + What's New in astroid 3.2.3? diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 22bb4da81..bfb5e3c23 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -1381,24 +1381,26 @@ def postinit(self, target: Name | Attribute | Subscript, value: NodeNG) -> None: See astroid/protocols.py for actual implementation. """ - def type_errors(self, context: InferenceContext | None = None): + def type_errors( + self, context: InferenceContext | None = None + ) -> list[util.BadBinaryOperationMessage]: """Get a list of type errors which can occur during inference. Each TypeError is represented by a :class:`BadBinaryOperationMessage` , which holds the original exception. - :returns: The list of possible type errors. - :rtype: list(BadBinaryOperationMessage) + If any inferred result is uninferable, an empty list is returned. """ + bad = [] try: - results = self._infer_augassign(context=context) - return [ - result - for result in results - if isinstance(result, util.BadBinaryOperationMessage) - ] + for result in self._infer_augassign(context=context): + if result is util.Uninferable: + raise InferenceError + if isinstance(result, util.BadBinaryOperationMessage): + bad.append(result) except InferenceError: return [] + return bad def get_children(self): yield self.target @@ -1497,24 +1499,26 @@ def postinit(self, left: NodeNG, right: NodeNG) -> None: self.left = left self.right = right - def type_errors(self, context: InferenceContext | None = None): + def type_errors( + self, context: InferenceContext | None = None + ) -> list[util.BadBinaryOperationMessage]: """Get a list of type errors which can occur during inference. Each TypeError is represented by a :class:`BadBinaryOperationMessage`, which holds the original exception. - :returns: The list of possible type errors. - :rtype: list(BadBinaryOperationMessage) + If any inferred result is uninferable, an empty list is returned. """ + bad = [] try: - results = self._infer_binop(context=context) - return [ - result - for result in results - if isinstance(result, util.BadBinaryOperationMessage) - ] + for result in self._infer_binop(context=context): + if result is util.Uninferable: + raise InferenceError + if isinstance(result, util.BadBinaryOperationMessage): + bad.append(result) except InferenceError: return [] + return bad def get_children(self): yield self.left @@ -4262,24 +4266,26 @@ def __init__( def postinit(self, operand: NodeNG) -> None: self.operand = operand - def type_errors(self, context: InferenceContext | None = None): + def type_errors( + self, context: InferenceContext | None = None + ) -> list[util.BadUnaryOperationMessage]: """Get a list of type errors which can occur during inference. Each TypeError is represented by a :class:`BadUnaryOperationMessage`, which holds the original exception. - :returns: The list of possible type errors. - :rtype: list(BadUnaryOperationMessage) + If any inferred result is uninferable, an empty list is returned. """ + bad = [] try: - results = self._infer_unaryop(context=context) - return [ - result - for result in results - if isinstance(result, util.BadUnaryOperationMessage) - ] + for result in self._infer_unaryop(context=context): + if result is util.Uninferable: + raise InferenceError + if isinstance(result, util.BadUnaryOperationMessage): + bad.append(result) except InferenceError: return [] + return bad def get_children(self): yield self.operand diff --git a/tests/test_inference.py b/tests/test_inference.py index ec8fc71b6..65e662097 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -2743,6 +2743,15 @@ def __radd__(self, other): error = errors[0] self.assertEqual(str(error), expected_value) + def test_binary_type_errors_partially_uninferable(self) -> None: + def patched_infer_binop(context): + return iter([util.BadBinaryOperationMessage(None, None, None), Uninferable]) + + binary_op_node = extract_node("0 + 0") + binary_op_node._infer_binop = patched_infer_binop + errors = binary_op_node.type_errors() + self.assertEqual(errors, []) + def test_unary_type_errors(self) -> None: ast_nodes = extract_node( """ @@ -2810,6 +2819,15 @@ def test_unary_type_errors_for_non_instance_objects(self) -> None: self.assertEqual(len(errors), 1) self.assertEqual(str(errors[0]), "bad operand type for unary ~: slice") + def test_unary_type_errors_partially_uninferable(self) -> None: + def patched_infer_unary_op(context): + return iter([util.BadUnaryOperationMessage(None, None, "msg"), Uninferable]) + + unary_op_node = extract_node("~0") + unary_op_node._infer_unaryop = patched_infer_unary_op + errors = unary_op_node.type_errors() + self.assertEqual(errors, []) + def test_bool_value_recursive(self) -> None: pairs = [ ("{}", False), @@ -3528,6 +3546,15 @@ def __radd__(self, other): return NotImplemented self.assertIsInstance(inferred, Instance) self.assertEqual(inferred.name, "B") + def test_augop_type_errors_partially_uninferable(self) -> None: + def patched_infer_augassign(context) -> None: + return iter([util.BadBinaryOperationMessage(None, None, None), Uninferable]) + + aug_op_node = extract_node("__name__ += 'test'") + aug_op_node._infer_augassign = patched_infer_augassign + errors = aug_op_node.type_errors() + self.assertEqual(errors, []) + def test_string_interpolation(self): ast_nodes = extract_node( """