# -*- coding: utf-8 -*-

# haggis: a library of general purpose utilities
#
# Copyright (C) 2019  Joseph R. Fox-Rabinovitz <jfoxrabinovitz at gmail dot com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# Author: Joseph Fox-Rabinovitz <jfoxrabinovitz at gmail dot com>
# Version: 13 Apr 2019: Initial Coding



"""
Implementations based on some of the recipes provided in the Python
documentation, and other sources like Stack Overflow.
"""


__all__ = [
    'all_combinations', 'all_nsc', 'any_nsc', 'consume', 'dict_merge',
    'dict_select', 'grouper', 'immutable', 'islast', 'is_ordered_subset',
    'lenumerate', 'mapping_context', 'option_lookup', 'shift_left',
    'KeyedSingleton', 'RecursiveDict', 'Namespace',
]


from collections import deque
from collections.abc import Mapping
from functools import reduce
from itertools import chain, combinations, islice, repeat
from operator import index, or_, and_
from warnings import warn

from .exceptions import ErrorTransform


def immutable(allow_properties=True,
              docstring='Forbids write access to class attributes.'):
    """
    Create a class decorator that sets a pre-defined version of
    :py:meth:`~object.__setattr__` that forbids write access to the
    attributes of a class.

    Access to properties may be allowed through `allow_properties`.
    The default setting of :py:obj:`True` does not guarantee that
    properties will be acessible, just that the parent implementation
    of :py:meth:`~object.__setattr__` will be responsible for handling
    the access.

    The messages of exceptions that are raised are based on the type of
    access that is requested. They mimic the default message that appear
    from most Python classes.

    Parameters
    ----------
    allow_properties : bool
        Whether or not to make an exception for properties. If
        :py:obj:`True`, setting of properties will be delegated to the
        parent's :py:meth:`~object.__setattr__`.
    docstring : str
        The string to associate with :py:meth:`~object.__setattr__`\ 's
        `__doc__` attibute.

    Return
    ------
    decorator : callable
        A class decorator that inserts a :py:meth:`~object.__setattr__`
        function into the decorated class.

    Notes
    -----
    Checking for properties based on the following Stack Overflow post:
    https://stackoverflow.com/a/46101204/2988730. See also the
    discussion following the answer itself.

    MRO issues with properly calling :py:meth:`~object.__setattr__` on
    properties are discussed in great detail in the answers and
    discussion surrounding https://stackoverflow.com/q/46121637/2988730.
    """
    def make_setattr(owner):
        if allow_properties:
            def __setattr__(self, name, value):
                typ = type(self)
                prop = getattr(typ, name, None)
                if isinstance(prop, property):
                    if owner is None:
                        return prop.__set__(self, value)
                    return super(owner, self).__setattr__(name, value)

                if hasattr(self, name):
                    msg = 'readonly attribute'
                else:
                    msg = '{!r} object has no attribute {!r}'.format(
                        type(self).__name__, name
                    )
                raise AttributeError(msg)
        else:
            def __setattr__(self, name, value):
                if hasattr(self, name):
                    msg = 'readonly attribute'
                else:
                    msg = '{!r} object has no attribute {!r}'.format(
                        type(self).__name__, name
                    )
                raise AttributeError(msg)

        if docstring:
            __setattr__.__doc__ = docstring

        return __setattr__

    def decorator(cls):
        """
        A class decorator that assigns a special
        :py:meth:`~object.__setattr__` method to forbid attribute
        modification.
        """
        cls.__setattr__ = make_setattr(cls)
        return cls

    return decorator


def consume(iterator, n=None):
    """
    Consume an iterator entirely, or advance `n` steps ahead.

    This function is based pretty much exactly on the similarly named
    recipe in the documentation at
    https://docs.python.org/3/library/itertools.html#itertools-recipes
    """
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)


def dict_merge(parent, child, keys=None, exclude=None, key=None):
    """
    Filter a parent dictionary and override it with values from a
    child, if supplied as a mapping.

    Parameters
    ----------
    parent : dict
        The base mapping to get the keys from.
    child : dict, value or None
        If a mapping type, `child[key]` will be the main value and the
        remaining keys will override the values obtained from `parent`.
        Otherwise, `child` will be the main value. If :py:obj:`None`,
        treated as missing entirely.
    keys : iterable or None
        An iterable of the keys to extract from `parent` and `child`, if
        it is a mapping. Should not contain `key`. :py:obj:`None` means
        to use all the keys of `parent`.
    exclude : container or None
        Keys to omit from the final result, even if they are present in
        `keys`.
    key :
        The name of the main value in `parent`, if it is not overriden
        by `child`. If `child` is a mapping, either it or `parent` must
        contain this key. Otherwise, the value of child is the value of
        the key.

    Return
    ------
    main: value (opt)
        The value of the main key, only returned if `key` is not
        :py:obj:`None`. If `child` is a mapping type, either it or
        `parent` will provide the key. Otherwise, it will be `child`
        itself.
    selection:
        The selected values of `parent`, possibly overriden by `child`
        if it is a mapping.
    """
    selection = dict_select(parent, keys, exclude)
    cmap = isinstance(child, Mapping)

    if cmap:
        selection.update(dict_select(child, keys, exclude))

    if key is None:
        return selection

    if child is None:
        value = parent[key]
    elif cmap:
        value = child[key] if key in child else parent[key]
    else:
        value = child

    return value, selection


def dict_select(dic, keys=None, exclude=None, extra='ignore'):
    """
    Filter a dictionary so only the specified keys are present.

    Parameters
    ----------
    dic : dict
        The dictionarty to filter.
    keys : iterable or None
        The keys to include in the output. Another dictionary may be
        used since it iterates over its keys by default. :py:obj:`None`
        means to use all keys.
    exclude : container or None
        The keys to exclude. Anything that supports the `in` operator is
        valid here. `exclude` takes precedence over `keys`: no keys in
        `exclude` will be present in the output, even if they are
        present in `keys`.
    extra : {'ignore', 'err', 'warn'}
        How to handle members of `dic` that are neither in `keys` nor
        explicitly listed in `exclude`:

        ignore :
            Skip over extra keys.
        err :
            Issue an error if invalid keys are found.
        warn :
            Issue a warning if extra keys are found.

        Values other than `'ignore'` will compare sets of keys.

    Return
    ------
    selection : dict
        A new `dict` object, even if `keys` is a superset of the actual
        keys found in `dic`.

    Notes
    -----
    The default behavior is just to make a copy of `dic`.
    """
    input_keys = set(dic.keys())
    select_keys = input_keys if keys is None else set(keys)
    exclude_keys = set() if exclude is None else set(exclude)

    if extra != 'ignore':
        extra_keys = (input_keys - select_keys) - exclude_keys
        extra_fmt = ', '.join(str(k) for k in extra_keys)
        message = 'Found extra keys: {}'.format(extra_fmt)
        if extra == 'warn':
            warn(message)
        elif extra == 'err':
            raise KeyError(message)
        else:
            raise ValueError('Invalid value of `extra`: "{}"'.format(extra))

    selection = (input_keys & select_keys) - exclude_keys

    return {k: dic[k] for k in selection}


def grouper(iterable, n, tail=True, fillvalue=None):
    """
    Gather an iterable into chunks or blocks of fixed length.

    Based on ``grouper`` from :ref:`itertools-recipes`. The main
    difference is that this implementation allows the final chunk to
    contain fewer elements. This version also returns lazy iterables for
    the inner chunks, unlike the ``grouper`` recipe which returns
    chunks.

    By default, the iterator assumes that all of the elements of a group
    have been consumed by the caller. If this is not the case, the next
    group will start with the un-consumed segment. An easy way to
    guarantee proper consumption is to use :py:func:`consume`.

    Inspired by https://stackoverflow.com/a/23926929/2988730.

    Parameters
    ----------
    iterable : iterable
        The iterable to split.
    n : int
        The size of the chunks to split into.
    tail : bool
        If :py:obj:`True`, the final group may contain fewer than `n`
        elements. If :py:obj:`False`, `fillvalue` will be used to pad
        the last group if it turns out to be shorter than `n`.
    fillvalue :
        The item to pad the final chunk with if `tail` is
        :py:obj:`False` and the iterable's length is not a multiple of
        `n`.

    Yields
    ------
    group :
        An iterable containing the next `n` or fewer elements of
        `iterable`.

    Notes
    -----
    This uses the updated generator protocol as per
    https://stackoverflow.com/a/45605358/2988730, meaning that this
    function theoretically requires Python 3.5+.
    """
    iterable = iter(iterable)
    n = index(n)
    while True:
        try:
            peek = next(iterable)
        except StopIteration:
            return
        it = iterable if tail else chain(iterable, repeat(fillvalue))
        yield chain((peek,), islice(it, n - 1))


def islast(iterable):
    """
    An iterator similar to the builtin :py:func:`enumerate`, except that
    instead of the index, it returns a boolean flag indicating if the
    current element is the last one.

    Based on the recipe at http://stackoverflow.com/a/2429118/2988730.
    Saw http://stackoverflow.com/a/2429260/2988730 after writing this.
    """
    iterable = iter(iterable)
    item = next(iterable)
    for peek in iterable:
        yield False, item
        item = peek
    yield True, item


def lenumerate(iterable, seq_type=None):
    """
    A generator that returns the length of each element of `iterable` in
    addition to the element itself.

    Each element must be an iterable itself. `seq_type` controls the
    type of sequence the elements will be turned into to get the length.
    If `None`, elements are assumed to have a `len` and will not be
    altered. Otherwise, `seq_type` must be a callable that accepts one
    iterable argument and returns a valid sequence, like
    :py:class:`list`, :py:class:`tuple`, :py:class:`str` or
    :py:class:`bytearray`.

    Specifying `seq_type` is roughly equivalent to calling
    ``lenumerate(map(seq_type, iterable))``. It guarantees consumption
    of all the elements of the sub-iterables. This could be important
    for something like :py:func:`grouper`.

    The name is a portmanteau of :py:func:`len` and
    :py:func:`enumerate`.
    """
    if seq_type is None:
        seq_type = lambda x: x
    for item in iterable:
        item = seq_type(item)
        yield len(item), item


def is_ordered_subset(a, b):
    """
    Check if iterable `a` is an ordered subset of iterable `b`.

    An ordered subset means that elements can be selected from `b`
    while preserving order in such a way as to get `a`.

    For example,::

        0, 1, 2

    is an ordered subset of::

        0, 2, 1, 3, 2

    but::

        0, 3, 1

    is not.

    Source: https://stackoverflow.com/a/11820887/2988730
    """
    it = iter(b)
    return all(x in it for x in a)


def all_combinations(sequence, start=0, stop=None):
    """
    Generate all combinations of a sequence with all possible lengths.

    The smallest length is `start`, which defaults to zero. The largest
    is `stop`, which defaults to `len(sequence)`. `stop` is inclusive.

    To use an iterable with no :py:func:`len`, specify `stop` manually.

    Example::

        all_combinations([1, 2, 3])

    generates::

        (,), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)
    """
    if stop is None:
        stop = len(sequence)
    for n in range(start, stop + 1):
        for combo in combinations(sequence, n):
            yield combo


class KeyedSingleton(type):
    """
    A meta-class for genralized singleton initialization that returns an
    existing object based on an input key.

    The key is expected to be the first argument to the class
    constructor.

    Inspired by https://stackoverflow.com/a/8665179/2988730 and
    https://stackoverflow.com/a/31723324/2988730.

    I added my own, improved answer at
    https://stackoverflow.com/a/45175660/2988730
    """
    def __init__(cls, *args, **kwargs):
        """
        Initializes the class with an empty dictionary of instances.

        Arguments are `name`, `bases`, and `attributes`, as usual.
        """
        super().__init__(*args, **kwargs)
        cls._instances = {}

    def __call__(cls, *args, **kwargs):
        """
        The constructor/initializer require at least one argument since
        the first argument is the singleton key.

        If an instance was already created with the requested key, it is
        returned without being re-allocated or re-initialized. 
        """
        if not args:
            raise ValueError(
                'Instance key must be first argument in constructor'
            )
        key = args[0]
        if key in cls._instances:
            instance = cls._instances[key]
        else:
            instance = super().__call__(*args, **kwargs)
            cls._instances[key] = instance
        return instance

    def reset(cls):
        """
        Resets/clears the class's registry so that new isntances will be
        constructed for further calls to `cls`.
        """
        cls._instances.clear()


class RecursiveDict(dict):
    """
    Mapping that allows recursive lookup of keys.

    Lookup can be controlled by specifying a key type and/or a value
    type. A value suitable for recursion that is not found as a key
    raises a :py:exc:`KeyError`, while a value of the wrong type raises
    a :py:exc:`TypeError`. Requesting a key of the wrong type raises a
    :py:exc:`TypeError` as well.

    Only lookup operations are different from :py:class:`dict`\ :
    :py:meth:`__getitem__`, :py:meth:`setdefault` and :py:meth:`get`.
    :py:meth:`~dict.pop` and :py:meth:`~dict.popitem` are not modified.
    Full control is provided through the unaltered setter operations.

    Here is an example of how :py:attr:`value_type` affects the lookup::

        d = RecursiveDict({'a': 1, 'b': 'a'})
        print(d.setdefault('d', 'c'))  # Prints "c"

    vs::

        d = RecursiveDict({'a': 1, 'b': 'a'}, value_type=int)
        print(d.setdefault('d', 'c'))  # Raises "KeyError: 'c'"

    .. py:attribute:: key_type

       A class object indicating the type that may be used as a key.
       :py:obj:`None` indicates "any". The default is :py:class:`str`.
       Values of this type indicate recursive lookup.

    .. py:attribute:: value_type

       A class object indicating the type that may be used as a value.
       :py:obj:`None`, the default, indicates "any" type.
    """
    __slots__ = ('key_type', 'value_type')

    def __init__(self, *args, key_type=str, value_type=None, **kwargs):
        """
        Construct a new mapping with the specified key and value types.

        All normal :py:class:`dict` constructor arguments are accepted.

        `key_type` and `value_type` may be classes, tuples of classes,
        or :py:obj:`None`. The latter case is equivalent to "anything
        goes". The dictionary will accept keys and values of the wrong
        type, but will raise an error on lookup, so it is best to leave
        either `key_type` or `value_type` as :py:obj:`None`.

        If both types are constrained and there is overlap between them,
        `key_type` always takes precedence: a value that can be a
        `key_type` is always looked up as a key. Only when the lookup
        fails is the value returned.

        If `key_type` is constrained, but `value_type` is not, lookup
        continues until a :py:exc:`KeyError` is raised or a
        non-\ `key_type` value is encountered. Similarly, if only
        `value_type` is constrained, lookup continues until a
        :py:exc:`KeyError` is raised or a value of the requested type
        is found.

        There is no constraint on either (both are `None`), recursion
        will stop only when a value is not present in the dictionary as
        a key.
        """
        self.key_type = key_type
        self.value_type = value_type
        super().__init__(*args, **kwargs)

    def __getitem__(self, key):
        """
        Recursively fetch an item from this dictionary.
        """
        return self._conditional_get(key, iskey=True)[1]

    def setdefault(self, key, default=None):
        """
        Set the key to the specified value if not found, return the
        value for the key.

        The default value is set exactly as specified, but the return
        value is dereferenced.
        """
        if key in self:
            return self[key]
        self[key] = default
        return self._conditional_get(default)[1]

    def get(self, key, default=None):
        """
        Retrieve a fully dereferenced value for `key` if present, or
        `default` if not.

        `default` gets dereferenced if the key is not present.
        """
        if key in self:
            return self[key]
        return self._conditional_get(default)[1]

    def final_key(self, key):
        """
        In a recursive reference, retreive the final key that actually
        contains the value mapping.

        For example, given::

            x = RecursiveDict(key_type=str, value_type=int)
            x['a'] = 'b'
            x['b'] = 'c'
            x['c'] = 1

        The result of :py:meth:`final_key` on any of the keys defined
        above would be ``'c'``.

        If the final key does not contain a value, it will still be
        returned. This means that if the dictionary above were to have
        ``x['c'] = 'd'`` (a broken recursion), :py:meth:`final_key`
        would return ``'c'`` regardless.

        Passing in a missing key will raise a :py:exc:`KeyError` as
        usual.
        """
        return self._conditional_get(key, iskey=True, err=False)[0]

    def _conditional_get(self, obj, iskey=False, err=True):
        """
        Check if the object is a possible key, and correctly retrieve
        the dereferenced value for it.

        If the value is a key type, but is not a valid key, it must also
        be a valid value type to avoid raising a :py:exc:`KeyError`.

        Parameters
        ----------
        obj :
            The object to search for.
        iskey : bool
            :py:obj:`True` if `obj` is required to be a key on the
            first iteration. Otherwise it represents a value (which may
            be a recursive key).
        err : bool
            Whether or not to raise an error if the key is not found.
        """
        key = obj if iskey else None
        while self._check_key_or_value_type(obj, iskey=iskey):
            try:
                new = super().__getitem__(obj)
            except KeyError:
                if err and (iskey or
                            not self._check_value_type(obj, err=False)):
                    raise
                break
            else:
                key = obj
                obj = new
            iskey = False
        return key, obj

    def _check_key_type(self, key, err=True):
        """
        Verify that the selected object is a valid key.

        Return the status, or raise a :py:exc:`TypeError` if `err` is
        :py:obj:`True`.
        """
        if self.key_type is None or isinstance(key, self.key_type):
            return True
        if err:
            raise TypeError('{} not allowed for keys'.format(type(key)))
        return False

    def _check_value_type(self, value, err=True):
        """
        Verify that the selected object is a valid value.

        Return the status, or raise a :py:exc:`TypeError` if `err` is
        :py:obj:`True`.
        """
        if self.value_type is None or isinstance(value, self.value_type):
            return True
        if err:
            raise TypeError('{} not allowed in mapping'.format(type(value)))
        return False

    def _check_key_or_value_type(self, obj, iskey):
        """
        Check if the object is of a proper key or value type, depending
        on the iteration count.

        If `iskey` is :py:obj:`True`, the object must be a key. All
        subsequent iterations return :py:obj:`True` if the object is a
        key, :py:obj:`False` if it is a valid value, and raise an error
        if it is neither.
        """
        check = self._check_key_type(obj, err=iskey)
        if check:
            return True
        return not self._check_value_type(obj, err=True)


def shift_left(*iterables):
    """
    For a given sequence of iterables, return a sequence of iterables
    that has all the elements shifted as far left as possible.

    For example, given::

        a = [1]
        b = [2, 3]
        c = [4, 5, 6]
        d = [7, 8]

    ``a, b, c, d = map(list, shift_left(a, b, c, d))`` will rearrange
    the lists so that::

        a = [1, 3, 6]
        b = [2, 5]
        c = [4, 8]
        d = [7]

    The name ``shift_left`` comes from visulizing the inputs as columns::

        1 2 4 7         1 2 4 7
          3 5 8   ==>   3 5 8
            6           6

    The result is a list of lists.
    """
    # Get iterators
    iterables = [iter(iterable) for iterable in iterables]
    # Generate a list of tuples containing each row
    count = len(iterables)
    shifted = [[] for _ in range(count)]
    while count > 0:
        # Generate a row
        it = 0
        while it < count:
            try:
                shifted[it].append(next(iterables[it]))
            except StopIteration:
                # Remove expired iterable
                del iterables[it]
                count -= 1
            else:
                # Keep moving along row
                it += 1

    return shifted


def option_lookup(name, mapping, option, key_func=None, value_func=None,
                  err_type=ValueError, key_err=KeyError):
    """
    Perform a lookup of an option in a mapping whose keys represent
    the valid options.

    If an invalid option is selected, it an `err_type` error is raised
    (:py:exc:`ValueError` by default).

    Parameters
    ----------
    name : str
        The name of the mapping to display if an error occurs. If the
        option is a function argument, the name of the argument is a
        good name choice.
    mapping : mapping
        The mapping to perform a the lookup in.
    option : object
        The key to look up in mapping.
    key_func : callable or None
        The transformation to apply to `option` to obtain the actual
        key to look up. If :py:obj:`None` (the default), no
        transformation is done. A common choice for this argument is
        :py:meth:`str.casefold` or :py:meth:`str.lower`, for
        case-insensitive string keys.
    value_func : callable or None
        The transformation to apply to the value that is found. If
        :py:obj:`None` (the default), no transformation is done. If
        supplied, must be a callable that accepts three arguments:

        - ``option``
        - ``key``: ``option`` transformed by ``key_func``
        - ``value``: The raw value retreived from the dictionary.

    err_type : type
        The type of the :py:exc:`Exception` to raise. Default is
        :py:exc:`ValueError`.
    key_err : type or tuple[type]
        The type of error expected from a failed lookup in `mapping`.
        If multiple types are to be expected, a :py:class:`tuple` of
        types may be supplied. Defaults to :py:exc:`KeyError`.
    """
    key = option if key_func is None else key_func(option)
    with ErrorTransform(key_err, err_type, 'Invalid value for `{0}`: {1!r}',
                        name, option):
        value = mapping[key]
    if value_func is not None:
        value = value_func(option, key, value)
    return value


class mapping_context:
    """
    A context manager for temporarily modifying the keys of a
    mapping.

    The context manager has a :py:meth:`delete` method that allows
    values to be removed as well. The method accepts any number of
    attribute names to delete and returns the context manager, so it can
    be used in a :ref:`with` block directly. Missing key names are
    ignored.

    All mapping values are reset to their original values when the
    manager exits. Nesting multiple instances of this context manager
    has a cumulative effect.

    The context manager is reentrant: the manager can be entered and
    exited and modified before re-entry as many times as necessary.

    Sample usage::

        d = {'a': 1, 'b': 2}
        print(d)
        with mapping_context(d, b=0, c=3):
            print(d)
            with mapping_context(d, b=1).delete('a'):
                print(d)
            print(d)
        print(d)

    Will result in::

        {'a': 1, 'b': 2}
        {'a': 1, 'b': 0, 'c': 3}
        {'b': 1, 'c': 3}
        {'b': 0, 'c': 3, 'a': 1}
        {'b': 2, 'a': 1}

    Instances have three documented attributes:

    .. py:attribute:: mapping

       The :py:class:`dict`\ -like object that this context manager
       applies to. The entire interface to the mapping is through the
       :py:meth:`getfunc` and :py:meth:`setfunc` methods, so that a
       context manager for non-mapping types can be simulated by
       subclassing.

    .. py:attribute:: updates

       A mapping of the added keys to their prior values. This mapping
       is unordered.

    .. py:attribute:: sentinel

       A special marker object guaranteed not to exists in the original
       mapping, used to mark deleted keys.
    """
    __slots__ = ('mapping', 'updates', 'sentinel')

    def __init__(self, mapping, *args, **kwargs):
        """
        Initialize a new context manager to replace the
        specified elements of the given mapping.

        Positional arguments represent mappings that contain keys and
        values to replace or add, each one in a format that would be
        accepted by :py:meth:`dict.update`.

        Keyword arguments are individual keys to update.
        """
        self.sentinel = object()
        self._init(mapping, *args, **kwargs)

    def __enter__(self):
        """
        Enter the context manager by updating the requested
        elements, return the context manager itself.
        """
        for key, value in self.updates.items():
            self.updates[key] = self.getfunc(key)
            self.setfunc(key, value)
        return self

    def __exit__(self, *args, **kwargs):
        """
        Restore the elements of the mapping.
        """
        # Convert to list to avoid modification during iteration
        for item in self.updates.items():
            self.setfunc(*item)

    def delete(self, *keys):
        """
        Remove all the keys named in ``keys`` from the mapping, but keep
        a record of them to be restored on exit.

        Return the context manager itself.
        """
        for key in keys:
            self.updates[key] = self.sentinel
        return self

    def chain(self, *args, **kwargs):
        """
        Create a chained :py:class:`mapping_context` with the same
        mapping class and :py:attr:`sentinel` object as this mapping.

        This method allows the example in the class docs to be rewritten
        as::

            d = {'a': 1, 'b': 2}
            print(d)
            with mapping_context(d, b=0, c=3) as mc:
                print(d)
                with mc.chain(b=1).delete('a'):
                    print(d)
                print(d)
            print(d)

        Returns a new :py:class:`mapping_context`.
        """
        cls = type(self)
        dc = cls.__new__(cls)
        dc.sentinel = self.sentinel
        dc._init(self.mapping, *args, **kwargs)
        return dc

    def _init(self, mapping, *args, **kwargs):
        """
        Sets ``mapping`` and records the modified and added keys
        similarly to :py:meth:`dict.update`, except that it allows
        multiple positional arguments, which may be of mixed types.
        Arguments are added in the following order:

        1. ``args`` is processed in order. Each element is added in the
           order of its iterator.
        2. ``kwargs`` is added in the order of its iterator.

        Later duplicate keys silently override earlier ones.
        """
        self.mapping = mapping
        self.updates = {}
        for arg in args:
            self.updates.update(arg)
        self.updates.update(kwargs)

    def getfunc(self, key):
        """
        A customizable function to get a single element of the mapping.

        If `key` is not present, this function must return
        :py:attr:`sentinel`.

        The default implementation works for most Python builtin mapping
        types that support a :py:meth:`~dict.get` method.
        """
        return self.mapping.get(key, self.sentinel)

    def setfunc(self, key, value):
        """
        A customizable function to set a single element of the mapping.

        If value is :py:attr:`sentinel`, the element should be deleted.

        The default implementation works for most Python builtin mapping
        types that support :py:meth:`~object.__delitem__` and
        :py:meth:`~object.__setitem__` methods.
        """
        if value is self.sentinel:
            del self.mapping[key]
        else:
            self.mapping[key] = value


class _NamespaceOverrideContext(mapping_context):
    """
    An extension of :py:class:`mapping_context` that is tailored for
    :py:class:`Namespace`.

    This class is designed specifically for :meth:`Namespace.override`.
    """
    __slots__ = ()

    def __init__(self, namespace, **kwargs):
        """
        Initialize a new context manager to replace the
        specified elements of the given namespace.
        """
        super().__init__(namespace, **kwargs)

    def __enter__(self):
        """
        Enter the context manager and return the mapping rather than
        `self`.
        """
        return super().__enter__().mapping

    def getfunc(self, key):
        """
        Mapping getter customized for :py:class:`Namespace`
        instead of regular :py:class:`dict`.
        """
        return getattr(self.mapping, key, self.sentinel)

    def setfunc(self, key, value):
        """
        Mapping setter and deleter customized for
        :py:class:`Namespace` instead of regular
        :py:class:`dict`.
        """
        if value is self.sentinel:
            delattr(self.mapping, key)
        else:
            setattr(self.mapping, key, value)


class Namespace:
    """
    A simple namespace object.

    The class is mutable. It implements containment checks. It can be
    converted to a dictionary using :py:func:`vars`.

    This class is based heavily on :py:class:`argparse.Namespace`.
    """

    def __init__(self, **kwargs):
        """
        Create a new namespace with the specified named arguments.
        """
        self.__dict__.update(kwargs)

    def __contains__(self, name):
        """
        Check if the namespace contains `name`.
        """
        return name in self.__dict__

    def __eq__(self, other):
        """
        Check if this object is equal to another namespace.

        Two namespaces are considered equal if they are both subclasses
        of :py:class:`Namespace` and contain the same mappings.
        """
        if not isinstance(other, __class__):
            return NotImplemented
        return vars(self) == vars(other)

    def __repr__(self):
        """
        Returns a usable string representation of this namespace.
        """
        items = ', '.join('{}={!r}'.format(*item)
                          for item in vars(self).items())
        return '{}({})'.format(type(self).__name__, items)

    def override(self, **kwargs):
        """
        Returns a context manager that can be used to temporarily set
        attributes in this namespace.

        The context manager has a `delete` method that allows values
        to be removed as well. The method accepts any number of
        attribute names to delete and returns the context manager, so it
        can be used in a :ref:`with` block directly. Missing key names
        are ignored.

        The context manager is a modified version of
        :py:class:`mapping_context`, so it also has a
        :py:meth:`~mapping_context.chain` method.

        All values will be reset to their original values when the
        manager exits. All context managers returned by this method
        operate on the same object, so their effects are cumulative.

        Sample usage::

            n = Namespace(a=1, b=2)
            print(n)
            with n.override(b=0, c=3):
                print(n)
                with n.override(b=1).delete('a'):
                    print(n)
                print(n)
            print(n)

        Will result in::

            Namespace(a=1, b=2)
            Namespace(a=1, b=0, c=3)
            Namespace(b=1, c=3)
            Namespace(b=0, c=3, a=1)
            Namespace(b=2, a=1)
        """
        return _NamespaceOverrideContext(self, **kwargs)


def any_nsc(iterable):
    """
    A non-short-circuiting version of :py:func:`any`.

    Useful for situations where the side-effects of an iterator are
    useful, for example when the elements are produced by a callable in
    a generator that also logs the :py:obj:`True` elements.

    The default behavior ``any([]) == False`` is preserved.

    See https://stackoverflow.com/q/1790520/2988730 for source material.
    """
    return reduce(or_, (bool(x) for x in iterable), False)


def all_nsc(iterable):
    """
    A non-short-circuiting version of :py:func:`all`.

    Useful for situations where the side-effects of an iterator are
    useful, for example when the elements are produced by a callable in
    a generator that also logs the :py:obj:`False` elements.

    The default behavior ``all([]) == True`` is preserved.

    See https://stackoverflow.com/q/1790520/2988730 for source material.
    """
    return reduce(and_, (bool(x) for x in iterable), True)
