diff options
author | Bryce Guinta <bryce.paul.guinta@gmail.com> | 2018-03-05 20:59:51 -0700 |
---|---|---|
committer | Bryce Guinta <bryce.paul.guinta@gmail.com> | 2018-03-11 00:57:53 -0700 |
commit | fbcf8a03ed6f9160c2780c6627fe76b66f4ad0c5 (patch) | |
tree | 432c033c9ae81a54e66fe7022fcc9d62aba96b4b | |
parent | 7b0f007b7fe149f1202de572b6016f4c106fcb71 (diff) | |
download | astroid-git-fbcf8a03ed6f9160c2780c6627fe76b66f4ad0c5.tar.gz |
Implement inference for len builtin
Close #112
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | astroid/brain/brain_builtin_inference.py | 24 | ||||
-rw-r--r-- | astroid/helpers.py | 42 | ||||
-rw-r--r-- | astroid/tests/unittest_brain.py | 139 |
4 files changed, 209 insertions, 0 deletions
@@ -65,6 +65,10 @@ Change log for the astroid package (used to be astng) Close #1699 + * Implement inference for len builtin + + Close #112 + 2017-12-15 -- 1.6.0 diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 0f57b26e..b4174765 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -618,6 +618,29 @@ def _class_or_tuple_to_container(node, context=None): return class_container +def infer_len(node, context=None): + """Infer length calls + + :param nodes.Call node: len call to infer + :param context.InferenceContext: node context + :rtype nodes.Const: + """ + call = arguments.CallSite.from_call(node) + if call.keyword_arguments: + raise UseInferenceDefault( + "TypeError: len() must take no keyword arguments") + if len(call.positional_arguments) != 1: + raise UseInferenceDefault( + "TypeError: len() must take exactly one argument " + "({len}) given".format(len=len(call.positional_arguments))) + [argument_node] = call.positional_arguments + try: + return nodes.Const(helpers.object_len(argument_node)) + except (AstroidTypeError, InferenceError) as exc: + raise UseInferenceDefault(str(exc)) from exc + + + # Builtins inference register_builtin_transform(infer_bool, 'bool') register_builtin_transform(infer_super, 'super') @@ -633,6 +656,7 @@ register_builtin_transform(infer_type, 'type') register_builtin_transform(infer_slice, 'slice') register_builtin_transform(infer_isinstance, 'isinstance') register_builtin_transform(infer_issubclass, 'issubclass') +register_builtin_transform(infer_len, 'len') # Infer object.__new__ calls MANAGER.register_transform( diff --git a/astroid/helpers.py b/astroid/helpers.py index 7a6175cf..cd472af2 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -222,3 +222,45 @@ def class_instance_as_index(node): except exceptions.InferenceError: pass return None + + +def object_len(node, context=None): + """Infer length of given node object + + :param Union[nodes.ClassDef, nodes.Instance] node: + :param node: Node to infer length of + + :raises AstroidTypeError: If an invalid node is returned + from __len__ method or no __len__ method exists + :raises InferenceError: If the given node cannot be inferred + or if multiple nodes are inferred + :rtype int: Integer length of node + """ + inferred_node = safe_infer(node, context=context) + if inferred_node is None or inferred_node is util.Uninferable: + raise exceptions.InferenceError(node=node) + if inferred_node.qname() in ('builtins.str', 'builtins.bytes'): + return len(inferred_node.value) + if isinstance(inferred_node, (nodes.List, nodes.Set, nodes.Tuple, nodes.FrozenSet)): + return len(inferred_node.elts) + if isinstance(inferred_node, nodes.Dict): + return len(inferred_node.items) + try: + node_type = object_type(inferred_node, context=context) + len_call = next(node_type.igetattr("__len__", context=context)) + except exceptions.AttributeInferenceError: + raise exceptions.AstroidTypeError( + "object of type '{}' has no len()" + .format(len_call.pytype())) + + try: + result_of_len = next(len_call.infer_call_result(node, context)) + # Remove StopIteration catch when #507 is fixed + except StopIteration: + raise exceptions.InferenceError(node=node) + if (isinstance(result_of_len, nodes.Const) and result_of_len.pytype() == "builtins.int"): + return result_of_len.value + else: + raise exceptions.AstroidTypeError( + "'{}' object cannot be interpreted as an integer" + .format(result_of_len)) diff --git a/astroid/tests/unittest_brain.py b/astroid/tests/unittest_brain.py index d261e6ca..1408abd2 100644 --- a/astroid/tests/unittest_brain.py +++ b/astroid/tests/unittest_brain.py @@ -1092,5 +1092,144 @@ def _get_result(code): return _get_result_node(code).as_string() +class TestLenBuiltinInference: + def test_len_list(self): + # Uses .elts + node = astroid.extract_node(""" + len(['a','b','c']) + """) + node = next(node.infer()) + assert node.as_string() == '3' + assert isinstance(node, nodes.Const) + + def test_len_tuple(self): + node = astroid.extract_node(""" + len(('a','b','c')) + """) + node = next(node.infer()) + assert node.as_string() == '3' + + def test_len_var(self): + # Make sure argument is inferred + node = astroid.extract_node(""" + a = [1,2,'a','b','c'] + len(a) + """) + node = next(node.infer()) + assert node.as_string() == '5' + + def test_len_dict(self): + # Uses .items + node = astroid.extract_node(""" + a = {'a': 1, 'b': 2} + len(a) + """) + node = next(node.infer()) + assert node.as_string() == '2' + + def test_len_set(self): + node = astroid.extract_node(""" + len({'a'}) + """) + inferred_node = next(node.infer()) + assert inferred_node.as_string() == '1' + + def test_len_object(self): + """Test len with objects that implement the len protocol""" + node = astroid.extract_node(""" + class A: + def __len__(self): + return 57 + len(A()) + """) + inferred_node = next(node.infer()) + assert inferred_node.as_string() == '57' + + def test_len_class_with_metaclass(self): + """Make sure proper len method is located""" + cls_node, inst_node = astroid.extract_node(""" + class F2(type): + def __new__(cls, name, bases, attrs): + return super().__new__(cls, name, bases, {}) + def __len__(self): + return 57 + class F(metaclass=F2): + def __len__(self): + return 4 + len(F) #@ + len(F()) #@ + """) + assert next(cls_node.infer()).as_string() == '57' + assert next(inst_node.infer()).as_string() == '4' + + def test_len_object_failure(self): + """If taking the length of a class, do not use an instance method""" + node = astroid.extract_node(""" + class F: + def __len__(self): + return 57 + len(F) + """) + with pytest.raises(astroid.InferenceError): + next(node.infer()) + + def test_len_string(self): + node = astroid.extract_node(""" + len("uwu") + """) + assert next(node.infer()).as_string() == "3" + + def test_len_generator_failure(self): + node = astroid.extract_node(""" + def gen(): + yield 'a' + yield 'b' + len(gen()) + """) + with pytest.raises(astroid.InferenceError): + next(node.infer()) + + def test_len_failure_missing_variable(self): + node = astroid.extract_node(""" + len(a) + """) + with pytest.raises(astroid.InferenceError): + next(node.infer()) + + def test_len_bytes(self): + node = astroid.extract_node(""" + len(b'uwu') + """) + assert next(node.infer()).as_string() == '3' + + + @pytest.mark.xfail(reason="Can't retrieve subclassed type value ") + def test_int_subclass_result(self): + """I am unable to figure out the value of an + object which subclasses int""" + node = astroid.extract_node(""" + class IntSubclass(int): + pass + + class F: + def __len__(self): + return IntSubclass(5) + len(F()) + """) + assert next(node.infer()).as_string() == '5' + + + @pytest.mark.xfail(reason="Can't use list special astroid fields") + def test_int_subclass_argument(self): + """I am unable to access the length of a object which + subclasses list""" + node = astroid.extract_node(""" + class ListSubclass(list): + pass + len(ListSubclass([1,2,3,4,4])) + """) + assert next(node.infer()).as_string() == '5' + + if __name__ == '__main__': unittest.main() |