Skip to content

Commit 2886808

Browse files
committed
Issue #11297: Add collections.ChainMap()
1 parent 0bb02cc commit 2886808

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

‎Doc/library/collections.rst‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Python's general purpose built-in containers, :class:`dict`, :class:`list`,
2323
===================== ====================================================================
2424
:func:`namedtuple` factory function for creating tuple subclasses with named fields
2525
:class:`deque` list-like container with fast appends and pops on either end
26+
:class:`ChainMap` dict-like class for creating a single view of multiple mappings
2627
:class:`Counter` dict subclass for counting hashable objects
2728
:class:`OrderedDict` dict subclass that remembers the order entries were added
2829
:class:`defaultdict` dict subclass that calls a factory function to supply missing values
@@ -37,6 +38,119 @@ Python's general purpose built-in containers, :class:`dict`, :class:`list`,
3738
as well.
3839

3940

41+
:class:`ChainMap` objects
42+
-------------------------
43+
44+
A :class:`ChainMap` class is provided for quickly linking a number of mappings
45+
so they can be treated as a single unit. It is often much faster than creating
46+
a new dictionary and running multiple :meth:`~dict.update` calls.
47+
48+
The class can be used to simulate nested scopes and is useful in templating.
49+
50+
.. class:: ChainMap(*maps)
51+
52+
A :class:`ChainMap` groups multiple dicts or other mappings together to
53+
create a single, updateable view. If no *maps* are specified, a single empty
54+
dictionary is provided so that a new chain always has at least one mapping.
55+
56+
The underlying mappings are stored in a list. That list is public and can
57+
accessed or updated using the *maps* attribute. There is no other state.
58+
59+
Lookups search the underlying mappings successively until a key is found. In
60+
contrast, writes, updates, and deletions only operate on the first mapping.
61+
62+
A class:`ChainMap` incorporates the underlying mappings by reference. So, if
63+
one of the underlying mappings gets updated, those changes will be reflected
64+
in class:`ChainMap`.
65+
66+
All of the usual dictionary methods are supported. In addition, there is a
67+
*maps* attribute, a method for creating new subcontexts, and a property for
68+
accessing all but the first mapping:
69+
70+
.. attribute:: maps
71+
72+
A user updateable list of mappings. The list is ordered from
73+
first-searched to last-searched. It is the only stored state and can
74+
modified to change which mappings are searched. The list should
75+
always contain at least one mapping.
76+
77+
.. method:: new_child()
78+
79+
Returns a new :class:`ChainMap` containing a new :class:`dict` followed by
80+
all of the maps in the current instance. A call to ``d.new_child()`` is
81+
equivalent to: ``ChainMap({}, *d.maps)``. This method is used for
82+
creating subcontexts that can be updated without altering values in any
83+
of the parent mappings.
84+
85+
.. attribute:: parents()
86+
87+
Returns a new :class:`ChainMap` containing all of the maps in the current
88+
instance except the first one. This is useful for skipping the first map
89+
in the search. The use-cases are similar to those for the
90+
:keyword:`nonlocal` keyword used in :term:`nested scopes <nested scope>`.
91+
The use-cases also parallel those for the builtin :func:`super` function.
92+
A reference to ``d.parents`` is equivalent to: ``ChainMap(*d.maps[1:])``.
93+
94+
.. versionadded:: 3.3
95+
96+
Example of simulating Python's internal lookup chain::
97+
98+
import __builtin__
99+
pylookup = ChainMap(locals(), globals(), vars(__builtin__))
100+
101+
Example of letting user specified values take precedence over environment
102+
variables which in turn take precedence over default values::
103+
104+
import os, argparse
105+
defaults = {'color': 'red', 'user': guest}
106+
parser = argparse.ArgumentParser()
107+
parser.add_argument('-u', '--user')
108+
parser.add_argument('-c', '--color')
109+
user_specified = vars(parser.parse_args())
110+
combined = ChainMap(user_specified, os.environ, defaults)
111+
112+
Example patterns for using the :class:`ChainMap` class to simulate nested
113+
contexts::
114+
115+
c = ChainMap() Create root context
116+
d = c.new_child() Create nested child context
117+
e = c.new_child() Child of c, independent from d
118+
e.maps[0] Current context dictionary -- like Python's locals()
119+
e.maps[-1] Root context -- like Python's globals()
120+
e.parents Enclosing context chain -- like Python's nonlocals
121+
122+
d['x'] Get first key in the chain of contexts
123+
d['x'] = 1 Set value in current context
124+
del['x'] Delete from current context
125+
list(d) All nested values
126+
k in d Check all nested values
127+
len(d) Number of nested values
128+
d.items() All nested items
129+
dict(d) Flatten into a regular dictionary
130+
131+
.. seealso::
132+
133+
* The `MultiContext class
134+
<http://svn.enthought.com/svn/enthought/CodeTools/trunk/enthought/contexts/multi_context.py>`_
135+
in the Enthought `CodeTools package
136+
<https://github.com/enthought/codetools>`_\ has options to support
137+
writing to any mapping in the chain.
138+
139+
* Django's `Context class
140+
<http://code.djangoproject.com/browser/django/trunk/django/template/context.py>`_
141+
for templating is a read-only chain of mappings. It also features
142+
pushing and popping of contexts similar to the
143+
:meth:`~collections.ChainMap.new_child` method and the
144+
:meth:`~collections.ChainMap.parents` property.
145+
146+
* The `Nested Contexts recipe
147+
<http://code.activestate.com/recipes/577434/>`_ has options to control
148+
whether writes and other mutations apply only to the first mapping or to
149+
any mapping in the chain.
150+
151+
* A `greatly simplified read-only version of Chainmap
152+
<http://code.activestate.com/recipes/305268/>`_\.
153+
40154
:class:`Counter` objects
41155
------------------------
42156

‎Lib/collections/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ def __and__(self, other):
636636
### ChainMap (helper for configparser and string.Template)
637637
########################################################################
638638

639-
class _ChainMap(MutableMapping):
639+
class ChainMap(MutableMapping):
640640
''' A ChainMap groups multiple dicts (or other mappings) together
641641
to create a single, updateable view.
642642

‎Lib/configparser.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
"""
121121

122122
from collections.abc import MutableMapping
123-
from collections import OrderedDict as _default_dict, _ChainMap
123+
from collections import OrderedDict as _default_dict, ChainMap as _ChainMap
124124
import functools
125125
import io
126126
import itertools

‎Lib/string.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def capwords(s, sep=None):
4646

4747
####################################################################
4848
import re as _re
49-
from collections import _ChainMap
49+
from collections import ChainMap
5050

5151
class _TemplateMetaclass(type):
5252
pattern = r"""
@@ -100,7 +100,7 @@ def substitute(self, *args, **kws):
100100
if not args:
101101
mapping = kws
102102
elif kws:
103-
mapping = _ChainMap(kws, args[0])
103+
mapping = ChainMap(kws, args[0])
104104
else:
105105
mapping = args[0]
106106
# Helper function for .sub()
@@ -126,7 +126,7 @@ def safe_substitute(self, *args, **kws):
126126
if not args:
127127
mapping = kws
128128
elif kws:
129-
mapping = _ChainMap(kws, args[0])
129+
mapping = ChainMap(kws, args[0])
130130
else:
131131
mapping = args[0]
132132
# Helper function for .sub()

‎Lib/test/test_collections.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import re
1212
import sys
1313
from collections import UserDict
14-
from collections import _ChainMap as ChainMap
14+
from collections import ChainMap
1515
from collections.abc import Hashable, Iterable, Iterator
1616
from collections.abc import Sized, Container, Callable
1717
from collections.abc import Set, MutableSet
@@ -21,7 +21,7 @@
2121

2222

2323
################################################################################
24-
### _ChainMap (helper class for configparser and the string module)
24+
### ChainMap (helper class for configparser and the string module)
2525
################################################################################
2626

2727
class TestChainMap(unittest.TestCase):

‎Misc/NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Core and Builtins
3535
Library
3636
-------
3737

38+
- Issue #11297: Add collections.ChainMap().
39+
3840
- Issue #10755: Add the posix.fdlistdir() function. Patch by Ross Lagerwall.
3941

4042
- Issue #4761: Add the *at() family of functions (openat(), etc.) to the posix

0 commit comments

Comments
 (0)