diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 72dd43bfa0a..f409536b6fb 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -23,6 +23,7 @@ Python's general purpose built-in containers, :class:`dict`, :class:`list`, ===================== ==================================================================== :func:`namedtuple` factory function for creating tuple subclasses with named fields :class:`deque` list-like container with fast appends and pops on either end +:class:`ChainMap` dict-like class for creating a single view of multiple mappings :class:`Counter` dict subclass for counting hashable objects :class:`OrderedDict` dict subclass that remembers the order entries were added :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`, as well. +:class:`ChainMap` objects +------------------------- + +A :class:`ChainMap` class is provided for quickly linking a number of mappings +so they can be treated as a single unit. It is often much faster than creating +a new dictionary and running multiple :meth:`~dict.update` calls. + +The class can be used to simulate nested scopes and is useful in templating. + +.. class:: ChainMap(*maps) + + A :class:`ChainMap` groups multiple dicts or other mappings together to + create a single, updateable view. If no *maps* are specified, a single empty + dictionary is provided so that a new chain always has at least one mapping. + + The underlying mappings are stored in a list. That list is public and can + accessed or updated using the *maps* attribute. There is no other state. + + Lookups search the underlying mappings successively until a key is found. In + contrast, writes, updates, and deletions only operate on the first mapping. + + A class:`ChainMap` incorporates the underlying mappings by reference. So, if + one of the underlying mappings gets updated, those changes will be reflected + in class:`ChainMap`. + + All of the usual dictionary methods are supported. In addition, there is a + *maps* attribute, a method for creating new subcontexts, and a property for + accessing all but the first mapping: + + .. attribute:: maps + + A user updateable list of mappings. The list is ordered from + first-searched to last-searched. It is the only stored state and can + modified to change which mappings are searched. The list should + always contain at least one mapping. + + .. method:: new_child() + + 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 + creating subcontexts that can be updated without altering values in any + of the parent mappings. + + .. attribute:: parents() + + Returns a new :class:`ChainMap` containing all of the maps in the current + instance except the first one. This is useful for skipping the first map + in the search. The use-cases are similar to those for the + :keyword:`nonlocal` keyword used in :term:`nested scopes `. + The use-cases also parallel those for the builtin :func:`super` function. + A reference to ``d.parents`` is equivalent to: ``ChainMap(*d.maps[1:])``. + + .. versionadded:: 3.3 + + Example of simulating Python's internal lookup chain:: + + import __builtin__ + pylookup = ChainMap(locals(), globals(), vars(__builtin__)) + + Example of letting user specified values take precedence over environment + variables which in turn take precedence over default values:: + + import os, argparse + defaults = {'color': 'red', 'user': guest} + parser = argparse.ArgumentParser() + parser.add_argument('-u', '--user') + parser.add_argument('-c', '--color') + user_specified = vars(parser.parse_args()) + combined = ChainMap(user_specified, os.environ, defaults) + + Example patterns for using the :class:`ChainMap` class to simulate nested + contexts:: + + c = ChainMap() Create root context + d = c.new_child() Create nested child context + e = c.new_child() Child of c, independent from d + e.maps[0] Current context dictionary -- like Python's locals() + e.maps[-1] Root context -- like Python's globals() + e.parents Enclosing context chain -- like Python's nonlocals + + d['x'] Get first key in the chain of contexts + d['x'] = 1 Set value in current context + del['x'] Delete from current context + list(d) All nested values + k in d Check all nested values + len(d) Number of nested values + d.items() All nested items + dict(d) Flatten into a regular dictionary + + .. seealso:: + + * The `MultiContext class + `_ + in the Enthought `CodeTools package + `_\ has options to support + writing to any mapping in the chain. + + * Django's `Context class + `_ + for templating is a read-only chain of mappings. It also features + pushing and popping of contexts similar to the + :meth:`~collections.ChainMap.new_child` method and the + :meth:`~collections.ChainMap.parents` property. + + * The `Nested Contexts recipe + `_ has options to control + whether writes and other mutations apply only to the first mapping or to + any mapping in the chain. + + * A `greatly simplified read-only version of Chainmap + `_\. + :class:`Counter` objects ------------------------ diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 4317535e873..aab5aeece01 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -636,7 +636,7 @@ class Counter(dict): ### ChainMap (helper for configparser and string.Template) ######################################################################## -class _ChainMap(MutableMapping): +class ChainMap(MutableMapping): ''' A ChainMap groups multiple dicts (or other mappings) together to create a single, updateable view. diff --git a/Lib/configparser.py b/Lib/configparser.py index 4e3af5f43ea..15fc26677ec 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -120,7 +120,7 @@ ConfigParser -- responsible for parsing a list of """ from collections.abc import MutableMapping -from collections import OrderedDict as _default_dict, _ChainMap +from collections import OrderedDict as _default_dict, ChainMap as _ChainMap import functools import io import itertools diff --git a/Lib/string.py b/Lib/string.py index 2bc5d008036..d4f9cd93554 100644 --- a/Lib/string.py +++ b/Lib/string.py @@ -46,7 +46,7 @@ def capwords(s, sep=None): #################################################################### import re as _re -from collections import _ChainMap +from collections import ChainMap class _TemplateMetaclass(type): pattern = r""" @@ -100,7 +100,7 @@ class Template(metaclass=_TemplateMetaclass): if not args: mapping = kws elif kws: - mapping = _ChainMap(kws, args[0]) + mapping = ChainMap(kws, args[0]) else: mapping = args[0] # Helper function for .sub() @@ -126,7 +126,7 @@ class Template(metaclass=_TemplateMetaclass): if not args: mapping = kws elif kws: - mapping = _ChainMap(kws, args[0]) + mapping = ChainMap(kws, args[0]) else: mapping = args[0] # Helper function for .sub() diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 35fe5ff19d2..5c73d7816bc 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -11,7 +11,7 @@ import keyword import re import sys from collections import UserDict -from collections import _ChainMap as ChainMap +from collections import ChainMap from collections.abc import Hashable, Iterable, Iterator from collections.abc import Sized, Container, Callable from collections.abc import Set, MutableSet @@ -21,7 +21,7 @@ from collections.abc import ByteString ################################################################################ -### _ChainMap (helper class for configparser and the string module) +### ChainMap (helper class for configparser and the string module) ################################################################################ class TestChainMap(unittest.TestCase): diff --git a/Misc/NEWS b/Misc/NEWS index d3d813c39cf..30fa398ab9e 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -35,6 +35,8 @@ Core and Builtins Library ------- +- Issue #11297: Add collections.ChainMap(). + - Issue #10755: Add the posix.fdlistdir() function. Patch by Ross Lagerwall. - Issue #4761: Add the *at() family of functions (openat(), etc.) to the posix