diff options
author | Claudiu Popa <cpopa@cloudbasesolutions.com> | 2015-06-28 01:36:47 +0300 |
---|---|---|
committer | Claudiu Popa <cpopa@cloudbasesolutions.com> | 2015-06-28 01:36:47 +0300 |
commit | dac2473052d249c6cf29a5a79d774ca1968cbe17 (patch) | |
tree | cf78b527c524ddee78de6856d2ca0b788e587407 | |
parent | 1676d10e0254c99ff6b415c817c7d9114c202344 (diff) | |
download | astroid-dac2473052d249c6cf29a5a79d774ca1968cbe17.tar.gz |
Add support for retrieving TypeErrors for binary arithmetic operations and augmented assignments.
The change is similar to what was added for UnaryOps: a new method
called *type_errors* for both AugAssign and BinOp, which can be used
to retrieve type errors occurred during inference. Also, a new
exception object was added, BinaryOperationError.
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | astroid/exceptions.py | 13 | ||||
-rw-r--r-- | astroid/inference.py | 50 | ||||
-rw-r--r-- | astroid/node_classes.py | 41 | ||||
-rw-r--r-- | astroid/tests/unittest_inference.py | 83 |
5 files changed, 175 insertions, 19 deletions
@@ -194,6 +194,13 @@ Change log for the astroid package (used to be astng) * Improve the inference of binary arithmetic operations (normal and augmented). + * Add support for retrieving TypeErrors for binary arithmetic operations. + + The change is similar to what was added for UnaryOps: a new method + called *type_errors* for both AugAssign and BinOp, which can be used + to retrieve type errors occurred during inference. Also, a new + exception object was added, BinaryOperationError. + 2015-03-14 -- 1.3.6 diff --git a/astroid/exceptions.py b/astroid/exceptions.py index f6f0269..9b18d85 100644 --- a/astroid/exceptions.py +++ b/astroid/exceptions.py @@ -90,3 +90,16 @@ class UnaryOperationError(OperationError): operand_type = self.operand.name msg = "bad operand type for unary {}: {}" return msg.format(self.op, operand_type) + + +class BinaryOperationError(OperationError): + """Object which describes type errors for BinOps.""" + + def __init__(self, left_type, op, right_type): + self.left_type = left_type + self.right_type = right_type + self.op = op + + def __str__(self): + msg = "unsupported operand type(s) for {}: {!r} and {!r}" + return msg.format(self.op, self.left_type.name, self.right_type.name) diff --git a/astroid/inference.py b/astroid/inference.py index 94871ce..c23f7a8 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -29,7 +29,8 @@ from astroid.manager import AstroidManager from astroid.exceptions import ( AstroidError, InferenceError, NoDefault, NotFoundError, UnresolvableName, - UnaryOperationError + UnaryOperationError, + BinaryOperationError, ) from astroid.bases import (YES, Instance, InferenceContext, _infer_stmts, copy_context, path_wrapper, @@ -357,6 +358,17 @@ nodes.BoolOp._infer = _infer_boolop # UnaryOp, BinOp and AugAssign inferences +def _filter_operation_errors(self, infer_callable, context, error): + for result in infer_callable(self, context): + if isinstance(result, error): + # For the sake of .infer(), we don't care about operation + # errors, which is the job of pylint. So return something + # which shows that we can't infer the result. + yield YES + else: + yield result + + def _infer_unaryop(self, context=None): """Infer what an UnaryOp should return when evaluated.""" for operand in self.operand.infer(context): @@ -402,14 +414,8 @@ def _infer_unaryop(self, context=None): @path_wrapper def infer_unaryop(self, context=None): """Infer what an UnaryOp should return when evaluated.""" - for result in _infer_unaryop(self, context): - if isinstance(result, UnaryOperationError): - # For the sake of .infer(), we don't care about operation - # errors, which is the job of pylint. So return something - # which shows that we can't infer the result. - yield YES - else: - yield result + return _filter_operation_errors(self, _infer_unaryop, + context, UnaryOperationError) nodes.UnaryOp._infer_unaryop = _infer_unaryop nodes.UnaryOp._infer = raise_if_nothing_infered(infer_unaryop) @@ -579,11 +585,13 @@ def _infer_binary_operation(left, right, op, context, flow_factory): return # TODO(cpopa): yield a BinaryOperationError here, # since the operation is not supported - yield YES + yield BinaryOperationError(left_type, op, right_type) def _infer_binop(self, context): """Binary operation inferrence logic.""" + if context is None: + context = InferenceContext() left = self.left right = self.right op = self.op @@ -612,14 +620,19 @@ def _infer_binop(self, context): yield result +@path_wrapper def infer_binop(self, context=None): - return _infer_binop(self, context) + return _filter_operation_errors(self, _infer_binop, + context, BinaryOperationError) -nodes.BinOp._infer = yes_if_nothing_infered(path_wrapper(infer_binop)) +nodes.BinOp._infer_binop = _infer_binop +nodes.BinOp._infer = yes_if_nothing_infered(infer_binop) -def infer_augassign(self, context=None): +def _infer_augassign(self, context=None): """Inferrence logic for augmented binary operations.""" + if context is None: + context = InferenceContext() op = self.op for lhs in self.target.infer_lhs(context=context): @@ -646,7 +659,16 @@ def infer_augassign(self, context=None): yield result -nodes.AugAssign._infer = path_wrapper(infer_augassign) +@path_wrapper +def infer_augassign(self, context=None): + return _filter_operation_errors(self, _infer_augassign, + context, BinaryOperationError) + +nodes.AugAssign._infer_augassign = _infer_augassign +nodes.AugAssign._infer = infer_augassign + +# End of binary operation inference. + def infer_arguments(self, context=None): name = context.lookupname diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 8d40471..312c095 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -23,7 +23,10 @@ import sys import six from logilab.common.decorators import cachedproperty -from astroid.exceptions import NoDefault, UnaryOperationError, InferenceError +from astroid.exceptions import ( + NoDefault, UnaryOperationError, + InferenceError, BinaryOperationError +) from astroid.bases import (NodeNG, Statement, Instance, InferenceContext, _infer_stmts, YES, BUILTINS) from astroid.mixins import (BlockRangeMixIn, AssignTypeMixin, @@ -412,6 +415,24 @@ class AugAssign(Statement, AssignTypeMixin): target = None value = None + # This is set by inference.py + def _infer_augassign(self, context=None): + raise NotImplementedError + + def type_errors(self, context=None): + """Return a list of TypeErrors which can occur during inference. + + Each TypeError is represented by a :class:`BinaryOperationError`, + which holds the original exception. + """ + try: + results = self._infer_augassign(context=context) + return [result for result in results + if isinstance(result, BinaryOperationError)] + except InferenceError: + return [] + + class Backquote(NodeNG): """class representing a Backquote node""" _astroid_fields = ('value',) @@ -423,6 +444,24 @@ class BinOp(NodeNG): left = None right = None + # This is set by inference.py + def _infer_binop(self, context=None): + raise NotImplementedError + + def type_errors(self, context=None): + """Return a list of TypeErrors which can occur during inference. + + Each TypeError is represented by a :class:`BinaryOperationError`, + which holds the original exception. + """ + try: + results = self._infer_binop(context=context) + return [result for result in results + if isinstance(result, BinaryOperationError)] + except InferenceError: + return [] + + class BoolOp(NodeNG): """class representing a BoolOp node""" _astroid_fields = ('values',) diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index c506121..7cc2ed5 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -1964,6 +1964,81 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): inferred = next(bad_node.infer()) self.assertEqual(inferred, YES) + def test_binary_op_type_errors(self): + ast_nodes = test_utils.extract_node(''' + import collections + 1 + "a" #@ + 1 - [] #@ + 1 * {} #@ + 1 / collections #@ + 1 ** (lambda x: x) #@ + {} * {} #@ + {} - {} #@ + {} | {} #@ + {} >> {} #@ + [] + () #@ + () + [] #@ + [] * 2.0 #@ + () * 2.0 #@ + 2.0 >> 2.0 #@ + class A(object): pass + class B(object): pass + A() + B() #@ + class A1(object): + def __add__(self): return NotImplemented + A1() + A1() #@ + class A(object): + def __add__(self, other): return NotImplemented + class B(object): + def __radd__(self, other): return NotImplemented + A() + B() #@ + class Parent(object): + pass + class Child(Parent): + def __add__(self, other): return NotImplemented + Child() + Parent() #@ + class A(object): + def __add__(self): return NotImplemented + class B(A): + def __radd__(self, other): + return NotImplemented + A() + B() #@ + # Augmented + f = 1 + f+=A() #@ + x = 1 + x+=[] #@ + ''') + msg = "unsupported operand type(s) for {op}: {lhs!r} and {rhs!r}" + expected = [ + msg.format(op="+", lhs="int", rhs="str"), + msg.format(op="-", lhs="int", rhs="list"), + msg.format(op="*", lhs="int", rhs="dict"), + msg.format(op="/", lhs="int", rhs="module"), + msg.format(op="**", lhs="int", rhs="function"), + msg.format(op="*", lhs="dict", rhs="dict"), + msg.format(op="-", lhs="dict", rhs="dict"), + msg.format(op="|", lhs="dict", rhs="dict"), + msg.format(op=">>", lhs="dict", rhs="dict"), + msg.format(op="+", lhs="list", rhs="tuple"), + msg.format(op="+", lhs="tuple", rhs="list"), + msg.format(op="*", lhs="list", rhs="float"), + msg.format(op="*", lhs="tuple", rhs="float"), + msg.format(op=">>", lhs="float", rhs="float"), + msg.format(op="+", lhs="A", rhs="B"), + msg.format(op="+", lhs="A1", rhs="A1"), + msg.format(op="+", lhs="A", rhs="B"), + msg.format(op="+", lhs="Child", rhs="Parent"), + msg.format(op="+", lhs="A", rhs="B"), + msg.format(op="+=", lhs="int", rhs="A"), + msg.format(op="+=", lhs="int", rhs="list"), + ] + for node, expected_value in zip(ast_nodes, expected): + errors = node.type_errors() + self.assertEqual(len(errors), 1) + error = errors[0] + self.assertEqual(str(error), expected_value) + def test_unary_type_errors(self): ast_nodes = test_utils.extract_node(''' import collections @@ -2177,7 +2252,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): class B(object): def __radd__(self, other): return other - A() + B() # + A() + B() #@ ''') inferred = next(node.infer()) self.assertIsInstance(inferred, Instance) @@ -2191,7 +2266,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): class B(object): def __radd__(self, other): return other - A() + B() # + A() + B() #@ ''') inferred = next(node.infer()) self.assertIsInstance(inferred, Instance) @@ -2202,7 +2277,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): class A(object): pass class B(object): pass - A() + B() # + A() + B() #@ ''') inferred = next(node.infer()) self.assertEqual(inferred, YES) @@ -2213,7 +2288,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): def __add__(self, other): return NotImplemented class B(object): def __radd__(self, other): return NotImplemented - A() + B() # + A() + B() #@ ''') inferred = next(node.infer()) self.assertEqual(inferred, YES) |