diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 6ef114e6779..ea4a31154f7 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -76,14 +76,19 @@ The class can be used to simulate nested scopes and is useful in templating. be modified to change which mappings are searched. The list should always contain at least one mapping. - .. method:: new_child() + .. method:: new_child(m=None) - Returns a new :class:`ChainMap` containing a new :class:`dict` followed by - all of the maps in the current instance. A call to ``d.new_child()`` is - equivalent to: ``ChainMap({}, *d.maps)``. This method is used for + Returns a new :class:`ChainMap` containing a new map followed by + all of the maps in the current instance. If ``m`` is specified, + it becomes the new map at the front of the list of mappings; if not + specified, an empty dict is used, so that a call to ``d.new_child()`` + is equivalent to: ``ChainMap({}, *d.maps)``. This method is used for creating subcontexts that can be updated without altering values in any of the parent mappings. + .. versionchanged:: 3.4 + The optional ``m`` parameter was added. + .. attribute:: parents Property returning a new :class:`ChainMap` containing all of the maps in diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 53083e4bd64..0612e1f6105 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -821,9 +821,14 @@ def copy(self): __copy__ = copy - def new_child(self): # like Django's Context.push() - 'New ChainMap with a new dict followed by all previous maps.' - return self.__class__({}, *self.maps) + def new_child(self, m=None): # like Django's Context.push() + ''' + New ChainMap with a new map followed by all previous maps. If no + map is provided, an empty dict is used. + ''' + if m is None: + m = {} + return self.__class__(m, *self.maps) @property def parents(self): # like Django's Context.pop() diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 8850e8bc136..dfad78e0056 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -112,6 +112,38 @@ def test_dict_coercion(self): self.assertEqual(dict(d), dict(a=1, b=2, c=30)) self.assertEqual(dict(d.items()), dict(a=1, b=2, c=30)) + def test_new_child(self): + 'Tests for changes for issue #16613.' + c = ChainMap() + c['a'] = 1 + c['b'] = 2 + m = {'b':20, 'c': 30} + d = c.new_child(m) + self.assertEqual(d.maps, [{'b':20, 'c':30}, {'a':1, 'b':2}]) # check internal state + self.assertIs(m, d.maps[0]) + + # Use a different map than a dict + class lowerdict(dict): + def __getitem__(self, key): + if isinstance(key, str): + key = key.lower() + return dict.__getitem__(self, key) + def __contains__(self, key): + if isinstance(key, str): + key = key.lower() + return dict.__contains__(self, key) + + c = ChainMap() + c['a'] = 1 + c['b'] = 2 + m = lowerdict(b=20, c=30) + d = c.new_child(m) + self.assertIs(m, d.maps[0]) + for key in 'abc': # check contains + self.assertIn(key, d) + for k, v in dict(a=1, B=20, C=30, z=100).items(): # check get + self.assertEqual(d.get(k, 100), v) + ################################################################################ ### Named Tuples