diff options
author | Bryce Guinta <bryce.paul.guinta@gmail.com> | 2018-01-20 10:10:29 -0700 |
---|---|---|
committer | Bryce Guinta <bryce.paul.guinta@gmail.com> | 2018-03-02 21:59:08 -0700 |
commit | b604b267f4876298d142f165713d6be1bd77790e (patch) | |
tree | 1d396f094a4c4bd05e02eaa23cb38dd72b7de800 | |
parent | 9b5aa979effadfe55be602f7e157368316a373be (diff) | |
download | astroid-git-b604b267f4876298d142f165713d6be1bd77790e.tar.gz |
Fix duplicate values in inferred dictionary node from dictionary unpacking
-rw-r--r-- | ChangeLog | 6 | ||||
-rw-r--r-- | astroid/inference.py | 28 | ||||
-rw-r--r-- | astroid/tests/.unittest_inference.py.swp | bin | 176128 -> 0 bytes | |||
-rw-r--r-- | astroid/tests/unittest_inference.py | 24 |
4 files changed, 56 insertions, 2 deletions
@@ -50,6 +50,12 @@ Change log for the astroid package (used to be astng) Close #98 + * Stop duplicate nodes with the same key values + from appearing in dictionaries from dictionary + unpacking + + Close PyCQA/pylint#1843 + 2017-12-15 -- 1.6.0 diff --git a/astroid/inference.py b/astroid/inference.py index 5c83ed5e..37b1b2bb 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -89,6 +89,29 @@ def infer_map(self, context=None): yield new_seq +def _update_with_replacement(lhs_dict, rhs_dict): + """Delete nodes that equate to duplicate keys + + Since an astroid node doesn't 'equal' another node with the same value, + this function uses the as_string method to make sure duplicate keys + don't get through + + Note that both the key and the value are astroid nodes + + Fixes issue with DictUnpack causing duplicte keys + in inferred Dict items + + :param dict(nodes.NodeNG, nodes.NodeNG) lhs_dict: Dictionary to 'merge' nodes into + :param dict(nodes.NodeNG, nodes.NodeNG) rhs_dict: Dictionary with nodes to pull from + :return dict(nodes.NodeNG, nodes.NodeNG): merged dictionary of nodes + """ + combined_dict = itertools.chain(lhs_dict.items(), rhs_dict.items()) + # Overwrite keys which have the same string values + string_map = {key.as_string(): (key, value) for key, value in combined_dict} + # Return to dictionary + return dict(string_map.values()) + + def _infer_map(node, context): """Infer all values based on Dict.items""" values = {} @@ -100,14 +123,15 @@ def _infer_map(node, context): if not isinstance(double_starred, nodes.Dict): raise exceptions.InferenceError(node=node, context=context) - values.update(_infer_map(double_starred, context)) + unpack_items = _infer_map(double_starred, context) + values = _update_with_replacement(values, unpack_items) else: key = helpers.safe_infer(name, context=context) value = helpers.safe_infer(value, context=context) if any(elem in (None, util.Uninferable) for elem in (key, value)): raise exceptions.InferenceError(node=node, context=context) - values[key] = value + values = _update_with_replacement(values, {key: value}) return values diff --git a/astroid/tests/.unittest_inference.py.swp b/astroid/tests/.unittest_inference.py.swp Binary files differdeleted file mode 100644 index 4274c129..00000000 --- a/astroid/tests/.unittest_inference.py.swp +++ /dev/null diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index f50fa7ec..98a1b284 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -1925,6 +1925,30 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): node = extract_node(code) self.assertInferDict(node, expected_value) + @test_utils.require_version('3.5') + def test_dict_inference_unpack_repeated_key(self): + """Make sure astroid does not infer repeated keys in a dictionary + + Regression test for https://github.com/PyCQA/pylint/issues/1843 + """ + code = """ + base = {'data': 0} + new = {**base, 'data': 1} #@ + new2 = {'data': 1, **base} #@ # Make sure overwrite works + a = 'd' + 'ata' + b3 = {**base, a: 3} #@ Make sure keys are properly inferred + b4 = {a: 3, **base} #@ + """ + ast = extract_node(code) + final_values = ( + "{'data': 1}", + "{'data': 0}", + "{'data': 3}", + "{'data': 0}", + ) + for node, final_value in zip(ast, final_values): + assert node.targets[0].inferred()[0].as_string() == final_value + def test_dict_invalid_args(self): invalid_values = [ 'dict(*1)', |