diff options
author | Claudiu Popa <pcmanticore@gmail.com> | 2016-03-06 21:44:45 +0000 |
---|---|---|
committer | Claudiu Popa <pcmanticore@gmail.com> | 2016-06-04 10:38:26 +0100 |
commit | 0d65b5bdc970367415dd68bdaae912cef999d16f (patch) | |
tree | 29fb5f6ce6436d3a858958a757d94ade7b4a5a53 | |
parent | e1b66de78193b910707b665f1623d67dde86ac2f (diff) | |
download | astroid-git-0d65b5bdc970367415dd68bdaae912cef999d16f.tar.gz |
dict.values, dict.keys and dict.items are properly inferred
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | astroid/brain/brain_builtin_inference.py | 14 | ||||
-rw-r--r-- | astroid/interpreter/objectmodel.py | 58 | ||||
-rw-r--r-- | astroid/objects.py | 32 | ||||
-rw-r--r-- | astroid/tests/unittest_inference.py | 14 | ||||
-rw-r--r-- | astroid/tests/unittest_object_model.py | 57 |
6 files changed, 175 insertions, 4 deletions
@@ -41,6 +41,10 @@ Change log for the astroid package (used to be astng) runtime attributes into the proper objects via a custom object model. Closes issue #81 + * dict.values, dict.keys and dict.items are properly + inferred to their corresponding type, which also + includes the proper containers for Python 3. + * Fix a crash which occurred when a method had a same name as a builtin object, decorated at the same time by that builtin object ( a property for instance) diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index 82d10d9e..4df8ef61 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -169,25 +169,31 @@ def _infer_builtin(node, context, infer_tuple = partial( _infer_builtin, klass=nodes.Tuple, - iterables=(nodes.List, nodes.Set, objects.FrozenSet), + iterables=(nodes.List, nodes.Set, objects.FrozenSet, + objects.DictItems, objects.DictKeys, + objects.DictValues), build_elts=tuple) infer_list = partial( _infer_builtin, klass=nodes.List, - iterables=(nodes.Tuple, nodes.Set, objects.FrozenSet), + iterables=(nodes.Tuple, nodes.Set, objects.FrozenSet, + objects.DictItems, objects.DictKeys, + objects.DictValues), build_elts=list) infer_set = partial( _infer_builtin, klass=nodes.Set, - iterables=(nodes.List, nodes.Tuple, objects.FrozenSet), + iterables=(nodes.List, nodes.Tuple, objects.FrozenSet, + objects.DictKeys), build_elts=set) infer_frozenset = partial( _infer_builtin, klass=objects.FrozenSet, - iterables=(nodes.List, nodes.Tuple, nodes.Set, objects.FrozenSet), + iterables=(nodes.List, nodes.Tuple, nodes.Set, objects.FrozenSet, + objects.DictKeys), build_elts=frozenset) diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index 2e8fedb7..4ad883d2 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -564,3 +564,61 @@ class ExceptionInstanceModel(InstanceModel): @property def pymessage(self): return node_classes.Const('') + + +class DictModel(ObjectModel): + + @property + def py__class__(self): + return self._instance._proxied + + def _generic_dict_attribute(self, obj, name): + """Generate a bound method that can infer the given *obj*.""" + + class DictMethodBoundMethod(astroid.BoundMethod): + def infer_call_result(self, caller, context=None): + yield obj + + meth = next(self._instance._proxied.igetattr(name)) + return DictMethodBoundMethod(proxy=meth, bound=self._instance) + + @property + def pyitems(self): + elems = [] + obj = node_classes.List(parent=self._instance) + for key, value in self._instance.items: + elem = node_classes.Tuple(parent=obj) + elem.postinit((key, value)) + elems.append(elem) + obj.postinit(elts=elems) + + if six.PY3: + from astroid import objects + obj = objects.DictItems(obj) + + return self._generic_dict_attribute(obj, 'items') + + @property + def pykeys(self): + keys = [key for (key, _) in self._instance.items] + obj = node_classes.List(parent=self._instance) + obj.postinit(elts=keys) + + if six.PY3: + from astroid import objects + obj = objects.DictKeys(obj) + + return self._generic_dict_attribute(obj, 'keys') + + @property + def pyvalues(self): + + values = [value for (_, value) in self._instance.items] + obj = node_classes.List(parent=self._instance) + obj.postinit(values) + + if six.PY3: + from astroid import objects + obj = objects.DictValues(obj) + + return self._generic_dict_attribute(obj, 'values') diff --git a/astroid/objects.py b/astroid/objects.py index 83074664..ce2c80a5 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -180,3 +180,35 @@ class ExceptionInstance(bases.Instance): """ special_attributes = util.lazy_descriptor(lambda: objectmodel.ExceptionInstanceModel()) + + +class DictInstance(bases.Instance): + """Special kind of instances for dictionaries + + This instance knows the underlying object model of the dictionaries, which means + that methods such as .values or .items can be properly inferred. + """ + + special_attributes = util.lazy_descriptor(lambda: objectmodel.DictModel()) + + +# Custom objects tailored for dictionaries, which are used to +# disambiguate between the types of Python 2 dict's method returns +# and Python 3 (where they return set like objects). +class DictItems(bases.Proxy): + __str__ = node_classes.NodeNG.__str__ + __repr__ = node_classes.NodeNG.__repr__ + + +class DictKeys(bases.Proxy): + __str__ = node_classes.NodeNG.__str__ + __repr__ = node_classes.NodeNG.__repr__ + + +class DictValues(bases.Proxy): + __str__ = node_classes.NodeNG.__str__ + __repr__ = node_classes.NodeNG.__repr__ + +# TODO: Hack to solve the circular import problem between node_classes and objects +# This is not needed in 2.0, which has a cleaner design overall +node_classes.Dict.__bases__ = (node_classes.NodeNG, DictInstance) diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index 448b5c16..208aeaee 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -1682,6 +1682,20 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertIsInstance(inferred, Instance) self.assertEqual(inferred.qname(), "{}.list".format(BUILTINS)) + def test_conversion_of_dict_methods(self): + ast_nodes = test_utils.extract_node(''' + list({1:2, 2:3}.values()) #@ + list({1:2, 2:3}.keys()) #@ + tuple({1:2, 2:3}.values()) #@ + tuple({1:2, 3:4}.keys()) #@ + set({1:2, 2:4}.keys()) #@ + ''') + self.assertInferList(ast_nodes[0], [2, 3]) + self.assertInferList(ast_nodes[1], [1, 2]) + self.assertInferTuple(ast_nodes[2], [2, 3]) + self.assertInferTuple(ast_nodes[3], [1, 3]) + self.assertInferSet(ast_nodes[4], [1, 2]) + @test_utils.require_version('3.0') def test_builtin_inference_py3k(self): code = """ diff --git a/astroid/tests/unittest_object_model.py b/astroid/tests/unittest_object_model.py index ec3f4e92..8940fd93 100644 --- a/astroid/tests/unittest_object_model.py +++ b/astroid/tests/unittest_object_model.py @@ -29,6 +29,7 @@ import astroid from astroid import exceptions from astroid import MANAGER from astroid import test_utils +from astroid import objects BUILTINS = MANAGER.astroid_cache[six.moves.builtins.__name__] @@ -501,5 +502,61 @@ class ExceptionModelTest(unittest.TestCase): next(ast_nodes[2].infer()) +class DictObjectModelTest(unittest.TestCase): + + def test__class__(self): + ast_node = test_utils.extract_node('{}.__class__') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, astroid.ClassDef) + self.assertEqual(inferred.name, 'dict') + + def test_attributes_inferred_as_methods(self): + ast_nodes = test_utils.extract_node(''' + {}.values #@ + {}.items #@ + {}.keys #@ + ''') + for node in ast_nodes: + inferred = next(node.infer()) + self.assertIsInstance(inferred, astroid.BoundMethod) + + @unittest.skipUnless(six.PY2, "needs Python 2") + def test_concrete_objects_for_dict_methods(self): + ast_nodes = test_utils.extract_node(''' + {1:1, 2:3}.values() #@ + {1:1, 2:3}.keys() #@ + {1:1, 2:3}.items() #@ + ''') + values = next(ast_nodes[0].infer()) + self.assertIsInstance(values, astroid.List) + self.assertEqual([value.value for value in values.elts], [1, 3]) + + keys = next(ast_nodes[1].infer()) + self.assertIsInstance(keys, astroid.List) + self.assertEqual([key.value for key in keys.elts], [1, 2]) + + items = next(ast_nodes[2].infer()) + self.assertIsInstance(items, astroid.List) + for expected, elem in zip([(1, 1), (2, 3)], items.elts): + self.assertIsInstance(elem, astroid.Tuple) + self.assertEqual(list(expected), [elt.value for elt in elem.elts]) + + @unittest.skipIf(six.PY2, "needs Python 3") + def test_wrapper_objects_for_dict_methods_python3(self): + ast_nodes = test_utils.extract_node(''' + {1:1, 2:3}.values() #@ + {1:1, 2:3}.keys() #@ + {1:1, 2:3}.items() #@ + ''') + values = next(ast_nodes[0].infer()) + self.assertIsInstance(values, objects.DictValues) + self.assertEqual([elt.value for elt in values.elts], [1, 3]) + keys = next(ast_nodes[1].infer()) + self.assertIsInstance(keys, objects.DictKeys) + self.assertEqual([elt.value for elt in keys.elts], [1, 2]) + items = next(ast_nodes[2].infer()) + self.assertIsInstance(items, objects.DictItems) + + if __name__ == '__main__': unittest.main() |