summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryce Guinta <bryce.paul.guinta@gmail.com>2018-03-05 20:59:51 -0700
committerBryce Guinta <bryce.paul.guinta@gmail.com>2018-03-11 00:57:53 -0700
commitfbcf8a03ed6f9160c2780c6627fe76b66f4ad0c5 (patch)
tree432c033c9ae81a54e66fe7022fcc9d62aba96b4b
parent7b0f007b7fe149f1202de572b6016f4c106fcb71 (diff)
downloadastroid-git-fbcf8a03ed6f9160c2780c6627fe76b66f4ad0c5.tar.gz
Implement inference for len builtin
Close #112
-rw-r--r--ChangeLog4
-rw-r--r--astroid/brain/brain_builtin_inference.py24
-rw-r--r--astroid/helpers.py42
-rw-r--r--astroid/tests/unittest_brain.py139
4 files changed, 209 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 278d6ed2..1d5002a3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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()