summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--astroid/brain/brain_builtin_inference.py14
-rw-r--r--astroid/interpreter/objectmodel.py58
-rw-r--r--astroid/objects.py32
-rw-r--r--astroid/tests/unittest_inference.py14
-rw-r--r--astroid/tests/unittest_object_model.py57
6 files changed, 175 insertions, 4 deletions
diff --git a/ChangeLog b/ChangeLog
index bfe867a1..6e3f3438 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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()