Skip to content
Snippets Groups Projects
Commit fbcf8a03 authored by Bryce Guinta's avatar Bryce Guinta
Browse files

Implement inference for len builtin

Close #112
parent 7b0f007b
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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')
Loading
Loading
@@ -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(
Loading
Loading
Loading
Loading
@@ -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))
Loading
Loading
@@ -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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment