diff options
author | Claudiu Popa <cpopa@cloudbasesolutions.com> | 2015-06-27 18:43:00 +0300 |
---|---|---|
committer | Claudiu Popa <cpopa@cloudbasesolutions.com> | 2015-06-27 18:43:00 +0300 |
commit | 1da9318dd2a0b0d87ac51021cc9dce69a4dd524f (patch) | |
tree | b66f15f220425205ba968d8889f15c9e67eb554e | |
parent | 024109e7c955265bd6e1841063f30192ab5de0d9 (diff) | |
download | astroid-1da9318dd2a0b0d87ac51021cc9dce69a4dd524f.tar.gz |
Improve the inference of binary arithmetic operations (normal and augmented)
This patch completely changes the way how binary and augmented operations are
inferred, trying to be as compatible as possible with the semantics
from the language reference.
-rw-r--r-- | ChangeLog | 3 | ||||
-rw-r--r-- | astroid/inference.py | 277 | ||||
-rw-r--r-- | astroid/protocols.py | 129 | ||||
-rw-r--r-- | astroid/tests/unittest_inference.py | 425 | ||||
-rw-r--r-- | pylintrc | 2 |
5 files changed, 700 insertions, 136 deletions
@@ -191,6 +191,9 @@ Change log for the astroid package (used to be astng) * Add helpers.is_supertype and helpers.is_subtype, two functions for checking if an object is a super/sub type of another. + * Improve the inference of binary arithmetic operations (normal + and augmented). + 2015-03-14 -- 1.3.6 diff --git a/astroid/inference.py b/astroid/inference.py index 94ecd7e..47eaa44 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -18,10 +18,13 @@ """this module contains a set of functions to handle inference on astroid trees """ +import functools import itertools import operator +from astroid import helpers from astroid import nodes +from astroid import protocols from astroid.manager import AstroidManager from astroid.exceptions import ( AstroidError, InferenceError, NoDefault, @@ -30,10 +33,7 @@ from astroid.exceptions import ( ) from astroid.bases import (YES, Instance, InferenceContext, _infer_stmts, copy_context, path_wrapper, - raise_if_nothing_infered) -from astroid.protocols import ( - _arguments_infer_argname, - BIN_OP_METHOD, UNARY_OP_METHOD) + raise_if_nothing_infered, yes_if_nothing_infered) MANAGER = AstroidManager() @@ -309,7 +309,7 @@ def _infer_unaryop(self, context=None): # The operand doesn't support this operation. yield UnaryOperationError(operand, self.op, exc) except AttributeError as exc: - meth = UNARY_OP_METHOD[self.op] + meth = protocols.UNARY_OP_METHOD[self.op] if meth is None: # `not node`. Determine node's boolean # value and negate its result, unless it is @@ -412,42 +412,246 @@ def _infer_boolop(self, context=None): nodes.BoolOp._infer = _infer_boolop -def _infer_binop(operator, operand1, operand2, context, failures=None): - if operand1 is YES: - yield operand1 - return - try: - for valnode in operand1.infer_binary_op(operator, operand2, context): - yield valnode - except AttributeError: +# BinOp and AugAssign inferences + +def _is_not_implemented(const): + """Check if the given const node is NotImplemented.""" + return isinstance(const, nodes.Const) and const.value is NotImplemented + + +def _invoke_binop_inference(instance, op, other, context, method_name): + """Invoke binary operation inference on the given instance.""" + method = instance.getattr(method_name)[0] + return instance.infer_binary_op(op, other, context, method) + +def _aug_op(instance, op, other, context, reverse=False): + """Get an inference callable for an augmented binary operation.""" + method_name = protocols.AUGMENTED_OP_METHOD[op] + return functools.partial(_invoke_binop_inference, + instance=instance, + op=op, other=other, + context=context, + method_name=method_name) + +def _bin_op(instance, op, other, context, reverse=False): + """Get an inference callable for a normal binary operation. + + If *reverse* is True, then the reflected method will be used instead. + """ + if reverse: + method_name = protocols.REFLECTED_BIN_OP_METHOD[op] + else: + method_name = protocols.BIN_OP_METHOD[op] + return functools.partial(_invoke_binop_inference, + instance=instance, + op=op, other=other, + context=context, + method_name=method_name) + + +def _get_binop_contexts(context, left, right): + """Get contexts for binary operations. + + This will return two inferrence contexts, the first one + for x.__op__(y), the other one for y.__rop__(x), where + only the arguments are inversed. + """ + # The order is important, since the first one should be + # left.__op__(right). + for arg in (right, left): + new_context = context.clone() + new_context.callcontext = CallContext( + [arg], starargs=None, dstarargs=None) + new_context.boundnode = None + yield new_context + +def _same_type(type1, type2): + """Check if type1 is the same as type2.""" + return type1.qname() == type2.qname() + + +def _get_binop_flow(left, left_type, op, right, right_type, + context, reverse_context): + """Get the flow for binary operations. + + The rules are a bit messy: + + * if left and right have the same type, then only one + method will be called, left.__op__(right) + * if left and right are unrelated typewise, then first + left.__op__(right) is tried and if this does not exist + or returns NotImplemented, then right.__rop__(left) is tried. + * if left is a subtype of right, then only left.__op__(right) + is tried. + * if left is a supertype of right, then right.__rop__(left) + is first tried and then left.__op__(right) + """ + if _same_type(left_type, right_type): + methods = [_bin_op(left, op, right, context)] + elif helpers.is_subtype(left_type, right_type): + methods = [_bin_op(left, op, right, context)] + elif helpers.is_supertype(left_type, right_type): + methods = [_bin_op(right, op, left, reverse_context, reverse=True), + _bin_op(left, op, right, context)] + else: + methods = [_bin_op(left, op, right, context), + _bin_op(right, op, left, reverse_context, reverse=True)] + return methods + + +def _get_aug_flow(left, left_type, aug_op, right, right_type, + context, reverse_context): + """Get the flow for augmented binary operations. + + The rules are a bit messy: + + * if left and right have the same type, then left.__augop__(right) + is first tried and then left.__op__(right). + * if left and right are unrelated typewise, then + left.__augop__(right) is tried, then left.__op__(right) + is tried and then right.__rop__(left) is tried. + * if left is a subtype of right, then left.__augop__(right) + is tried and then left.__op__(right). + * if left is a supertype of right, then left.__augop__(right) + is tried, then right.__rop__(left) and then + left.__op__(right) + """ + op = aug_op.strip("=") + if _same_type(left_type, right_type): + methods = [_aug_op(left, aug_op, right, context), + _bin_op(left, op, right, context)] + elif helpers.is_subtype(left_type, right_type): + methods = [_aug_op(left, aug_op, right, context), + _bin_op(left, op, right, context)] + elif helpers.is_supertype(left_type, right_type): + methods = [_aug_op(left, aug_op, right, context), + _bin_op(right, op, left, reverse_context, reverse=True), + _bin_op(left, op, right, context)] + else: + methods = [_aug_op(left, aug_op, right, context), + _bin_op(left, op, right, context), + _bin_op(right, op, left, reverse_context, reverse=True)] + return methods + + +def _infer_binary_operation(left, right, op, context, flow_factory): + """Infer a binary operation between a left operand and a right operand + + This is used by both normal binary operations and augmented binary + operations, the only difference is the flow factory used. + """ + + context, reverse_context = _get_binop_contexts(context, left, right) + left_type = helpers.object_type(left) + right_type = helpers.object_type(right) + methods = flow_factory(left, left_type, op, right, right_type, + context, reverse_context) + for method in methods: try: - # XXX just suppose if the type implement meth, returned type - # will be the same - operand1.getattr(BIN_OP_METHOD[operator]) - yield operand1 - except Exception: # pylint: disable=broad-except - if failures is None: + results = list(method()) + except AttributeError: + continue + except NotFoundError: + continue + except InferenceError: + yield YES + return + else: + if any(result is YES for result in results): yield YES - else: - failures.append(operand1) + return + + # TODO(cpopa): since the inferrence engine might return + # more values than are actually possible, we decide + # to return YES if we have union types. + if all(map(_is_not_implemented, results)): + continue + not_implemented = sum(1 for result in results + if _is_not_implemented(result)) + if not_implemented and not_implemented != len(results): + # Can't decide yet what this is, not yet though. + yield YES + return + + for result in results: + yield result + return + # TODO(cpopa): yield a BinaryOperationError here, + # since the operation is not supported + yield YES + + +def _infer_binop(self, context): + """Binary operation inferrence logic.""" + left = self.left + right = self.right + op = self.op + + for lhs in left.infer(context=context): + if lhs is YES: + # Don't know how to process this. + yield YES + return + + # TODO(cpopa): if we have A() * A(), trying to infer + # the rhs with the same context will result in an + # inferrence error, so we create another context for it. + # This is a bug which should be fixed in InferenceContext at some point. + rhs_context = context.clone() + rhs_context.path = set() + for rhs in right.infer(context=rhs_context): + if rhs is YES: + # Don't know how to process this. + yield YES + return + + results = _infer_binary_operation(lhs, rhs, op, + context, _get_binop_flow) + for result in results: + yield result + def infer_binop(self, context=None): - failures = [] - for lhs in self.left.infer(context): - for val in _infer_binop(self.op, lhs, self.right, context, failures): - yield val - for lhs in failures: - for rhs in self.right.infer(context): - for val in _infer_binop(self.op, rhs, lhs, context): - yield val -nodes.BinOp._infer = path_wrapper(infer_binop) + return _infer_binop(self, context) + +nodes.BinOp._infer = yes_if_nothing_infered(path_wrapper(infer_binop)) +def infer_augassign(self, context=None): + """Inferrence logic for augmented binary operations.""" + op = self.op + + for lhs in self.target.infer_lhs(context=context): + if lhs is YES: + # Don't know how to process this. + yield YES + return + + # TODO(cpopa): if we have A() * A(), trying to infer + # the rhs with the same context will result in an + # inferrence error, so we create another context for it. + # This is a bug which should be fixed in InferenceContext at some point. + rhs_context = context.clone() + rhs_context.path = set() + for rhs in self.value.infer(context=rhs_context): + if rhs is YES: + # Don't know how to process this. + yield YES + return + + results = _infer_binary_operation(lhs, rhs, op, + context, _get_aug_flow) + for result in results: + yield result + + +nodes.AugAssign._infer = path_wrapper(infer_augassign) + def infer_arguments(self, context=None): name = context.lookupname if name is None: raise InferenceError() - return _arguments_infer_argname(self, name, context) + return protocols._arguments_infer_argname(self, name, context) nodes.Arguments._infer = infer_arguments @@ -463,17 +667,6 @@ def infer_ass(self, context=None): nodes.AssName._infer = path_wrapper(infer_ass) nodes.AssAttr._infer = path_wrapper(infer_ass) -def infer_augassign(self, context=None): - failures = [] - for lhs in self.target.infer_lhs(context): - for val in _infer_binop(self.op, lhs, self.value, context, failures): - yield val - for lhs in failures: - for rhs in self.value.infer(context): - for val in _infer_binop(self.op, rhs, lhs, context): - yield val -nodes.AugAssign._infer = path_wrapper(infer_augassign) - # no infer method on DelName and DelAttr (expected InferenceError) diff --git a/astroid/protocols.py b/astroid/protocols.py index d9d36ae..2cc0626 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -23,6 +23,8 @@ import collections import operator import sys +import six + from astroid.exceptions import InferenceError, NoDefault, NotFoundError from astroid.node_classes import unpack_infer from astroid.bases import ( @@ -34,10 +36,18 @@ from astroid.bases import ( from astroid.nodes import const_factory from astroid import nodes + +def _reflected_name(name): + return "__r" + name[2:] + +def _augmented_name(name): + return "__i" + name[2:] + + _CONTEXTLIB_MGR = 'contextlib.contextmanager' BIN_OP_METHOD = {'+': '__add__', '-': '__sub__', - '/': '__div__', + '/': '__div__' if six.PY2 else '__truediv__', '//': '__floordiv__', '*': '__mul__', '**': '__power__', @@ -50,6 +60,15 @@ BIN_OP_METHOD = {'+': '__add__', '@': '__matmul__' } +REFLECTED_BIN_OP_METHOD = { + key: _reflected_name(value) + for (key, value) in BIN_OP_METHOD.items() +} +AUGMENTED_OP_METHOD = { + key + "=": _augmented_name(value) + for (key, value) in BIN_OP_METHOD.items() +} + UNARY_OP_METHOD = {'+': '__pos__', '-': '__neg__', '~': '__invert__', @@ -74,9 +93,7 @@ nodes.Set.infer_unary_op = lambda self, op: _infer_unary_op(set(self.elts), op) nodes.Const.infer_unary_op = lambda self, op: _infer_unary_op(self.value, op) nodes.Dict.infer_unary_op = lambda self, op: _infer_unary_op(dict(self.items), op) - - -# binary operations ########################################################### +# Binary operations BIN_OP_IMPL = {'+': lambda a, b: a + b, '-': lambda a, b: a - b, @@ -98,81 +115,53 @@ if sys.version_info >= (3, 5): for _KEY, _IMPL in list(BIN_OP_IMPL.items()): BIN_OP_IMPL[_KEY + '='] = _IMPL -def const_infer_binary_op(self, operator, other, context): - for other in other.infer(context): - if isinstance(other, nodes.Const): +def const_infer_binary_op(self, operator, other, context, _): + not_implemented = nodes.Const(NotImplemented) + if isinstance(other, nodes.Const): + try: + impl = BIN_OP_IMPL[operator] try: - impl = BIN_OP_IMPL[operator] + yield const_factory(impl(self.value, other.value)) + except Exception: # pylint: disable=broad-except + # ArithmeticError is not enough: float >> float is a TypeError + yield not_implemented + except TypeError: + yield not_implemented + else: + yield not_implemented - try: - yield const_factory(impl(self.value, other.value)) - except Exception: # pylint: disable=broad-except - # ArithmeticError is not enough: float >> float is a TypeError - # TODO : let pylint know about the problem - pass - except TypeError: - # XXX log TypeError - continue - elif other is YES: - yield other - else: - try: - for val in other.infer_binary_op(operator, self, context): - yield val - except AttributeError: - yield YES nodes.Const.infer_binary_op = yes_if_nothing_infered(const_infer_binary_op) -def tl_infer_binary_op(self, operator, other, context): - for other in other.infer(context): - if isinstance(other, self.__class__) and operator == '+': - node = self.__class__() - elts = [n for elt in self.elts for n in elt.infer(context) - if not n is YES] - elts += [n for elt in other.elts for n in elt.infer(context) - if not n is YES] - node.elts = elts - yield node - elif isinstance(other, nodes.Const) and operator == '*': - if not isinstance(other.value, int): - yield YES - continue - node = self.__class__() - elts = [n for elt in self.elts for n in elt.infer(context) - if not n is YES] * other.value - node.elts = elts - yield node - elif isinstance(other, Instance) and not isinstance(other, nodes.Const): - yield YES - # XXX else log TypeError +def tl_infer_binary_op(self, operator, other, context, method): + not_implemented = nodes.Const(NotImplemented) + if isinstance(other, self.__class__) and operator == '+': + node = self.__class__() + elts = [n for elt in self.elts for n in elt.infer(context) + if not n is YES] + elts += [n for elt in other.elts for n in elt.infer(context) + if not n is YES] + node.elts = elts + yield node + elif isinstance(other, nodes.Const) and operator == '*': + if not isinstance(other.value, int): + yield not_implemented + return + + node = self.__class__() + elts = [n for elt in self.elts for n in elt.infer(context) + if not n is YES] * other.value + node.elts = elts + yield node + else: + yield not_implemented + nodes.Tuple.infer_binary_op = yes_if_nothing_infered(tl_infer_binary_op) nodes.List.infer_binary_op = yes_if_nothing_infered(tl_infer_binary_op) -def dict_infer_binary_op(self, operator, other, context): - for other in other.infer(context): - if isinstance(other, Instance) and isinstance(other._proxied, nodes.Class): - yield YES - # XXX else log TypeError -nodes.Dict.infer_binary_op = yes_if_nothing_infered(dict_infer_binary_op) - -def instance_infer_binary_op(self, operator, other, context): - try: - methods = self.getattr(BIN_OP_METHOD[operator]) - except (NotFoundError, KeyError): - # Unknown operator - yield YES - else: - for method in methods: - if not isinstance(method, nodes.Function): - continue - for result in method.infer_call_result(self, context): - if result is not YES: - yield result - # We are interested only in the first infered method, - # don't go looking in the rest of the methods of the ancestors. - break +def instance_infer_binary_op(self, operator, other, context, method): + return method.infer_call_result(self, context) Instance.infer_binary_op = yes_if_nothing_infered(instance_infer_binary_op) diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index f63aca6..c506121 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -883,26 +883,65 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = builder.string_build('a = "*" * 40', __name__, __file__) self._test_const_infered(ast['a'], "*" * 40) - def test_binary_op_bitand(self): + def test_binary_op_int_bitand(self): ast = builder.string_build('a = 23&20', __name__, __file__) self._test_const_infered(ast['a'], 23&20) - def test_binary_op_bitor(self): + def test_binary_op_int_bitor(self): ast = builder.string_build('a = 23|8', __name__, __file__) self._test_const_infered(ast['a'], 23|8) - def test_binary_op_bitxor(self): + def test_binary_op_int_bitxor(self): ast = builder.string_build('a = 23^9', __name__, __file__) self._test_const_infered(ast['a'], 23^9) - def test_binary_op_shiftright(self): + def test_binary_op_int_shiftright(self): ast = builder.string_build('a = 23 >>1', __name__, __file__) self._test_const_infered(ast['a'], 23>>1) - def test_binary_op_shiftleft(self): + def test_binary_op_int_shiftleft(self): ast = builder.string_build('a = 23 <<1', __name__, __file__) self._test_const_infered(ast['a'], 23<<1) + def test_binary_op_other_type(self): + ast_nodes = test_utils.extract_node(''' + class A: + def __add__(self, other): + return other + 42 + A() + 1 #@ + 1 + A() #@ + ''') + first = next(ast_nodes[0].infer()) + self.assertIsInstance(first, nodes.Const) + self.assertEqual(first.value, 43) + + second = next(ast_nodes[1].infer()) + self.assertEqual(second, YES) + + def test_binary_op_other_type_using_reflected_operands(self): + ast_nodes = test_utils.extract_node(''' + class A(object): + def __radd__(self, other): + return other + 42 + A() + 1 #@ + 1 + A() #@ + ''') + first = next(ast_nodes[0].infer()) + self.assertEqual(first, YES) + + second = next(ast_nodes[1].infer()) + self.assertIsInstance(second, nodes.Const) + self.assertEqual(second.value, 43) + + def test_binary_op_reflected_and_not_implemented_is_type_error(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __radd__(self, other): return NotImplemented + + 1 + A() #@ + ''') + first = next(ast_node.infer()) + self.assertEqual(first, YES) def test_binary_op_list_mul(self): for code in ('a = [[]] * 2', 'a = 2 * [[]]'): @@ -924,7 +963,6 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(len(infered), 1) self.assertEqual(infered[0], YES) - def test_binary_op_tuple_add(self): ast = builder.string_build('a = (1,) + (2,)', __name__, __file__) infered = list(ast['a'].infer()) @@ -1197,34 +1235,22 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): def test_list_inference(self): """#20464""" code = ''' - import optparse - + from unknown import Unknown A = [] B = [] def test(): xyz = [ - "foobar=%s" % options.ca, + Unknown ] + A + B - - if options.bind is not None: - xyz.append("bind=%s" % options.bind) return xyz - def main(): - global options - - parser = optparse.OptionParser() - (options, args) = parser.parse_args() - Z = test() ''' ast = test_utils.build_module(code, __name__) - infered = list(ast['Z'].infer()) - self.assertEqual(len(infered), 1, infered) - self.assertIsInstance(infered[0], Instance) - self.assertIsInstance(infered[0]._proxied, nodes.Class) - self.assertEqual(infered[0]._proxied.name, 'list') + infered = next(ast['Z'].infer()) + self.assertIsInstance(infered, nodes.List) + self.assertEqual(len(infered.elts), 0) def test__new__(self): code = ''' @@ -2128,6 +2154,359 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): inferred = next(node.infer()) self.assertEqual(inferred.value, expected) + def test_binop_same_types(self): + ast_nodes = test_utils.extract_node(''' + class A(object): + def __add__(self, other): + return 42 + 1 + 1 #@ + 1 - 1 #@ + "a" + "b" #@ + A() + A() #@ + ''') + expected_values = [2, 0, "ab", 42] + for node, expected in zip(ast_nodes, expected_values): + inferred = next(node.infer()) + self.assertIsInstance(inferred, nodes.Const) + self.assertEqual(inferred.value, expected) + + def test_binop_different_types_reflected_only(self): + node = test_utils.extract_node(''' + class A(object): + pass + class B(object): + def __radd__(self, other): + return other + A() + B() # + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_binop_different_types_normal_not_implemented_and_reflected(self): + node = test_utils.extract_node(''' + class A(object): + def __add__(self, other): + return NotImplemented + class B(object): + def __radd__(self, other): + return other + A() + B() # + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_binop_different_types_no_method_implemented(self): + node = test_utils.extract_node(''' + class A(object): + pass + class B(object): pass + A() + B() # + ''') + inferred = next(node.infer()) + self.assertEqual(inferred, YES) + + def test_binop_different_types_reflected_and_normal_not_implemented(self): + node = test_utils.extract_node(''' + class A(object): + def __add__(self, other): return NotImplemented + class B(object): + def __radd__(self, other): return NotImplemented + A() + B() # + ''') + inferred = next(node.infer()) + self.assertEqual(inferred, YES) + + def test_binop_subtype(self): + node = test_utils.extract_node(''' + class A(object): pass + class B(A): + def __add__(self, other): return other + B() + A() #@ + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_binop_subtype_implemented_in_parent(self): + node = test_utils.extract_node(''' + class A(object): + def __add__(self, other): return other + class B(A): pass + B() + A() #@ + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_binop_subtype_not_implemented(self): + node = test_utils.extract_node(''' + class A(object): + pass + class B(A): + def __add__(self, other): return NotImplemented + B() + A() #@ + ''') + inferred = next(node.infer()) + self.assertEqual(inferred, YES) + + def test_binop_supertype(self): + node = test_utils.extract_node(''' + class A(object): + pass + class B(A): + def __radd__(self, other): + return other + A() + B() #@ + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_binop_supertype_rop_not_implemented(self): + node = test_utils.extract_node(''' + class A(object): + def __add__(self, other): + return other + class B(A): + def __radd__(self, other): + return NotImplemented + A() + B() #@ + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'B') + + def test_binop_supertype_both_not_implemented(self): + node = test_utils.extract_node(''' + class A(object): + def __add__(self): return NotImplemented + class B(A): + def __radd__(self, other): + return NotImplemented + A() + B() #@ + ''') + inferred = next(node.infer()) + self.assertEqual(inferred, YES) + + def test_binop_inferrence_errors(self): + ast_nodes = test_utils.extract_node(''' + from unknown import Unknown + class A(object): + def __add__(self, other): return NotImplemented + class B(object): + def __add__(self, other): return Unknown + A() + Unknown #@ + Unknown + A() #@ + B() + A() #@ + A() + B() #@ + ''') + for node in ast_nodes: + self.assertEqual(next(node.infer()), YES) + + def test_binop_ambiguity(self): + ast_nodes = test_utils.extract_node(''' + class A(object): + def __add__(self, other): + if isinstance(other, B): + return NotImplemented + if type(other) is type(self): + return 42 + return NotImplemented + class B(A): pass + class C(object): + def __radd__(self, other): + if isinstance(other, B): + return 42 + return NotImplemented + A() + B() #@ + B() + A() #@ + A() + C() #@ + C() + A() #@ + ''') + for node in ast_nodes: + self.assertEqual(next(node.infer()), YES) + + def test_aug_op_same_type_not_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return NotImplemented + A() + A() #@ + ''') + self.assertEqual(next(ast_node.infer()), YES) + + def test_aug_op_same_type_aug_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return other + f = A() + f += A() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_aug_op_same_type_aug_not_implemented_normal_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return 42 + f = A() + f += A() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, nodes.Const) + self.assertEqual(inferred.value, 42) + + def test_aug_op_subtype_both_not_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return NotImplemented + class B(A): + pass + b = B() + b+=A() #@ + ''') + self.assertEqual(next(ast_node.infer()), YES) + + def test_aug_op_subtype_aug_op_is_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return 42 + class B(A): + pass + b = B() + b+=A() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, nodes.Const) + self.assertEqual(inferred.value, 42) + + def test_aug_op_subtype_normal_op_is_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __add__(self, other): return 42 + class B(A): + pass + b = B() + b+=A() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, nodes.Const) + self.assertEqual(inferred.value, 42) + + def test_aug_different_types_no_method_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): pass + class B(object): pass + f = A() + f += B() #@ + ''') + self.assertEqual(next(ast_node.infer()), YES) + + def test_aug_different_types_augop_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return other + class B(object): pass + f = A() + f += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'B') + + def test_aug_different_types_aug_not_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return other + class B(object): pass + f = A() + f += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'B') + + def test_aug_different_types_aug_not_implemented_rop_fallback(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return NotImplemented + class B(object): + def __radd__(self, other): return other + f = A() + f += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_augop_supertypes_none_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): pass + class B(object): pass + a = A() + a += B() #@ + ''') + self.assertEqual(next(ast_node.infer()), YES) + + def test_augop_supertypes_not_implemented_returned_for_all(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return NotImplemented + class B(object): + def __add__(self, other): return NotImplemented + a = A() + a += B() #@ + ''') + self.assertEqual(next(ast_node.infer()), YES) + + def test_augop_supertypes_augop_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return other + class B(A): pass + a = A() + a += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'B') + + def test_augop_supertypes_reflected_binop_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + class B(A): + def __radd__(self, other): return other + a = A() + a += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'A') + + def test_augop_supertypes_normal_binop_implemented(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __iadd__(self, other): return NotImplemented + def __add__(self, other): return other + class B(A): + def __radd__(self, other): return NotImplemented + + a = A() + a += B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred.name, 'B') + class GetattrTest(unittest.TestCase): @@ -98,7 +98,7 @@ disable=invalid-name,protected-access,no-self-use,unused-argument, too-many-return-statements,redefined-outer-name,undefined-variable,
too-many-locals,method-hidden,duplicate-code,attribute-defined-outside-init,
fixme,missing-docstring,too-many-lines,too-many-statements,undefined-loop-variable,
- unpacking-non-sequence,import-error,no-name-in-module,bad-builtin
+ unpacking-non-sequence,import-error,no-name-in-module,bad-builtin,too-many-arguments
[BASIC]
|