summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <cpopa@cloudbasesolutions.com>2015-06-28 01:36:47 +0300
committerClaudiu Popa <cpopa@cloudbasesolutions.com>2015-06-28 01:36:47 +0300
commitdac2473052d249c6cf29a5a79d774ca1968cbe17 (patch)
treecf78b527c524ddee78de6856d2ca0b788e587407
parent1676d10e0254c99ff6b415c817c7d9114c202344 (diff)
downloadastroid-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--ChangeLog7
-rw-r--r--astroid/exceptions.py13
-rw-r--r--astroid/inference.py50
-rw-r--r--astroid/node_classes.py41
-rw-r--r--astroid/tests/unittest_inference.py83
5 files changed, 175 insertions, 19 deletions
diff --git a/ChangeLog b/ChangeLog
index 8879c08..70e2399 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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)