#
# (C) Copyright 2003-2007 Hewlett-Packard Development Company, L.P.
# (C) Copyright 2006-2007 Novell, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Original authors, until 2007:
# Tim Potter <tpot@hp.com>
# Martin Pool <mbp@hp.com>
# Bart Whiteley <bwhiteley@suse.de>
#
# Authors since 2014:
# Andreas Maier <andreas.r.maier@gmx.de>
# Karl Schopmeyer <k.schopmeyer@swbell.net>
#
"""
The only class exposed by this package is :class:`nocasedict.NocaseDict`.
"""
from __future__ import print_function, absolute_import
import sys
import os
import warnings
from collections import OrderedDict
from collections.abc import MutableMapping, KeysView, ValuesView, ItemsView
from typing import Any, AnyStr, NoReturn, Optional, Iterator, Tuple, Dict
from ._utils import _stacklevel_above_nocasedict
__all__ = ['NocaseDict']
# Starting with Python 3.7, the standard dict is guaranteed to be ordered.
# Note: In CPython, that already happened in 3.6, but it was not guaranteed
# for all implementations.
# pylint: disable=invalid-name
if sys.version_info[0:2] >= (3, 7):
_ODICT_TYPE = dict
else:
_ODICT_TYPE = OrderedDict
Key = Optional[AnyStr]
# This env var is set when building the docs. It causes the methods
# that are supposed to exist only in a particular Python version, not to be
# removed, so they appear in the docs.
BUILDING_DOCS = os.environ.get('BUILDING_DOCS', False)
# Used as default value for parameters to detect that they have not been
# specified as an argument. Idea from CPython's datetime.timezone.
_OMITTED = object()
class _DictView:
# pylint: disable=too-few-public-methods
"""
Base class for directory views, with common methods.
"""
def __init__(self, dct):
self._dict = dct
def __len__(self):
return len(self._dict)
def __contains__(self, x):
return x in iter(self)
def __reversed__(self):
return reversed(list(iter(self)))
def __repr__(self):
return "{t}({d!r})".format(
t=self.__class__.__name__, d=self._dict)
class dict_keys(_DictView, KeysView):
# pylint: disable=too-few-public-methods
"""
Dictionary values view.
"""
def __iter__(self):
# pylint: disable=protected-access
data = self._dict._data
for k in data:
yield data[k][0]
class dict_values(_DictView, ValuesView):
# pylint: disable=too-few-public-methods
"""
Dictionary values view.
"""
def __iter__(self):
# pylint: disable=protected-access
data = self._dict._data
for k in data:
yield data[k][1]
class dict_items(_DictView, ItemsView):
# pylint: disable=too-few-public-methods
"""
Dictionary items view.
"""
def __iter__(self):
# pylint: disable=protected-access
data = self._dict._data
for k in data:
yield data[k]
[docs]class NocaseDict(MutableMapping):
"""
A case-insensitive and case-preserving ordered dictionary.
The dictionary is case-insensitive: When items of the dictionary are
looked up by key or keys are compared by the dictionary, that is done
case-insensitively. The case-insensitivity is defined by performing the
lookup or comparison on the result of the :meth:`__casefold__` method on
the key value.
`None` is allowed as a key value and will not be case folded.
The dictionary is case-preserving: When keys are returned, they have
the lexical case that was originally specified when adding or updating the
item.
The dictionary is ordered: The dictionary maintains the order in which items
were added for all Python versions supported by this package. This is
consistent with the ordering behavior of the built-in :class:`py:dict`
class starting with Python 3.7.
The :class:`~nocasedict.NocaseDict` class is derived from the abstract base
class :class:`py:collections.abc.MutableMapping` and not from the
:class:`py:dict` class, because of the unique implementation of
:class:`~nocasedict.NocaseDict`, which maintains a single dictionary with
the casefolded keys, and values that are tuples (original key, value).
This supports key based lookup with a single dictionary lookup.
Users that need to test whether an object is a dictionary should do that
with ``isinstance(obj, Mapping)`` or ``isinstance(obj, MutableMapping)``.
The provided key and value objects will be referenced from the
dictionary without being copied, consistent with the built-in
:class:`py:dict` class.
Except for the case-insensitivity of its keys, the
:class:`~nocasedict.NocaseDict` class behaves like the built-in
:class:`py:dict` class starting with Python 3.7 (where it is guaranteed to
be ordered), so its documentation applies completely.
The :class:`~nocasedict.NocaseDict` class itself provides no added
functionality compared to the built-in :class:`py:dict` class.
This package provides mixin classes for adding functionality:
* :class:`~nocasedict.HashableMixin` mixin class: Adds case-insensitive
hashability.
* :func:`~nocasedict.KeyableByMixin` mixin generator function: Adds ability
to get the key from an attribute of the value object.
Example of usage::
from nocasedict import NocaseDict
dict1 = NocaseDict({'Alpha': 1, 'Beta': 2})
print(dict1['ALPHA']) # Lookup by key is case-insensitive
# 1
print(dict1) # Access of keys is case-preserving
# NocaseDict({'Alpha': 1, 'Beta': 2})
"""
# Methods not implemented:
#
# * __getattribute__(self, name): The method inherited from object is used;
# no reason to have a different implementation.
#
# * __sizeof__(self): The method inherited from object is used.
# TODO(issue #37): Clarify the rules for implementing __sizeof__().
def __init__(self, *args, **kwargs) -> None:
"""
Parameters:
*args : An optional single positional argument representing key-value
pairs to initialize the dictionary from, in iteration order of the
specified object. The argument must be one of:
- a dictionary object, or more specifically an object that has a
method ``keys()`` providing iteration through the keys and that
supports subscription by key (e.g. ``ncd[key]``) for accessing
the values.
- an iterable. If a key occurs more than once (case-insensitively),
the last item for that key becomes the corresponding item in
the dictionary. Each item in the iterable must be one of:
* an iterable with exactly two items. The first item is used as
the key, and the second item as the value.
* an object with a key attribute, if the
:func:`~nocasedict.KeyableByMixin` mixin generator function is
used. The value of the key attribute is used as the key, and
the object itself as the value.
**kwargs : Optional keyword arguments representing key-value pairs to
add to the dictionary after being initialized from the positional
argument.
If a key being added is already present (case-insensitively) from
the positional argument, key and value will be updated from the
keyword argument.
Before Python 3.7, the order of keyword arguments as specified in
the call to the method was not guaranteed to be preserved for the
method implementation, so passing more than one keyword argument
may have resulted in arbitrary order of items in the dictionary.
To summarize, only the following types of init arguments are guaranteed
to preserve the order of provided items after having been added to the
new dictionary, across all Python versions supported by this package:
* Passing an iterable as a single positional argument, and passing at
most one keyword argument.
* Passing an ordered dictionary/mapping as a single positional
argument, and passing at most one keyword argument.
A :exc:`py:UserWarning` will be issued if the order of provided items
in the arguments is not guaranteed to be preserved.
Examples for initializing::
from nocasedict import NocaseDict
dict1 = NocaseDict({'Alpha': 1, 'Beta': 2})
dict2 = NocaseDict(dict1)
dict3 = NocaseDict([('Alpha', 1), ('Beta', 2)])
dict4 = NocaseDict((('Alpha', 1), ('Beta', 2)))
dict5 = NocaseDict(Alpha=1, Beta=2)
dict6 = NocaseDict(dict1, BETA=3)
Raises:
TypeError: Expected at most 1 positional argument, got {n}.
ValueError: Cannot unpack positional argument item #{i}.
"""
# The internal dictionary, with casefolded keys. An item in this dict
# is the tuple (original key, value).
self._data: Dict[Key, Any] = _ODICT_TYPE()
self.update(*args, **kwargs)
def _casefolded_key(self, key: Key) -> Key:
"""
This method returns the casefolded key and handles the case of key
being `None`.
"""
if key is None:
return None
return self.__casefold__(key)
[docs] @staticmethod
def __casefold__(key: AnyStr) -> AnyStr:
"""
This method implements the case-insensitive behavior of the class.
It returns a case-insensitive form of the input key by calling a
"casefold method" on the key. The input key will not be `None`.
The casefold method called by this method is :meth:`py:str.casefold`.
If that method does not exist on the key value (e.g. because it is a
byte string), :meth:`py:bytes.lower` is called, for compatibility with
earlier versions of the package.
This method can be overridden by users in order to change the
case-insensitive behavior of the class.
See :ref:`Overriding the default casefold method` for details.
Parameters:
key (AnyStr): Input key. Will not be `None`.
Returns:
AnyStr: Case-insensitive form of the input key.
Raises:
AttributeError: The key does not have the casefold method.
"""
try:
return key.casefold() # type: ignore
except AttributeError:
# Probably a byte string, fall back to lower()
return key.lower()
# Basic accessor and setter methods
[docs] def __getitem__(self, key: Key) -> Any:
"""
Return the value of the item with an existing key (looked up
case-insensitively).
Invoked when using e.g.: ``value = ncd[key]``
Raises:
AttributeError: The key does not have the casefold method.
KeyError: Key does not exist (case-insensitively).
"""
k = self._casefolded_key(key)
try:
return self._data[k][1]
except KeyError:
key_error = KeyError("Key {0!r} not found".format(key))
key_error.__cause__ = None # Suppress 'During handling..'
raise key_error # pylint: disable=raise-missing-from
[docs] def __setitem__(self, key: Key, value: Any) -> None:
"""
Update the value of the item with an existing key (looked up
case-insensitively), or if an item with the key does not exist, add an
item with the specified key and value.
Invoked when using e.g.: ``ncd[key] = value``
Raises:
AttributeError: The key does not have the casefold method.
"""
k = self._casefolded_key(key)
self._data[k] = (key, value)
[docs] def __delitem__(self, key: Key) -> None:
"""
Delete the item with an existing key (looked up case-insensitively).
Invoked when using: ``del ncd[key]``
Raises:
AttributeError: The key does not have the casefold method.
KeyError: Key does not exist (case-insensitively).
"""
k = self._casefolded_key(key)
try:
del self._data[k]
except KeyError:
key_error = KeyError("Key {0!r} not found".format(key))
key_error.__cause__ = None # Suppress 'During handling..'
raise key_error # pylint: disable=raise-missing-from
[docs] def __len__(self) -> int:
"""
Return the number of items in the dictionary.
Invoked when using: ``len(ncd)``
"""
return len(self._data)
[docs] def __contains__(self, key: Any) -> bool:
"""
Return a boolean indicating whether the dictionary contains an item
with the key (looked up case-insensitively).
Invoked when using: ``key in ncd``
Raises:
AttributeError: The key does not have the casefold method.
"""
k = self._casefolded_key(key)
return k in self._data
[docs] def __reversed__(self) -> Iterator[Any]:
"""
Return an iterator for the reversed iteration order of the dictionary.
Invoked when using: ``reversed[ncd]``
"""
# Implementing __reversed__() is necessary because the fall back of
# reversed() to using len() and __getitem__() (the sequence protocol")
# requires that the object is a sequence, and relying on the fallback
# for dicts results in TypeError.
return reversed(self.keys())
[docs] @classmethod
def fromkeys(cls, iterable, value=None) -> 'NocaseDict':
"""
Return a new :class:`NocaseDict` object with keys from the specified
iterable of keys, and values all set to the specified value.
Raises:
AttributeError: The key does not have the casefold method.
"""
return cls([(key, value) for key in iterable])
[docs] def get(self, key: Key, default=None) -> Any:
"""
Return the value of the item with an existing key (looked up
case-insensitively), or if the key does not exist, a default value.
Raises:
AttributeError: The key does not have the casefold method.
"""
try:
return self[key]
except KeyError:
return default
[docs] def pop(self, key: Key, default=_OMITTED) -> Any:
"""
Remove the item with the specified key if it exists (looked up
case-insensitively), and return its value.
If an item with the key does not exist, the default value is returned
if specified, otherwise :exc:`py:KeyError` is raised.
Raises:
KeyError: Key does not exist (case-insensitively) and no default was
specified.
"""
k = self._casefolded_key(key)
try:
return self._data.pop(k)[1]
except KeyError:
if default is not _OMITTED:
return default
raise
[docs] def popitem(self) -> Tuple[Key, Any]:
"""
Remove the last dictionary item (in iteration order) and return it as a
tuple (key, value).
The last item in iteration order is the last item that was added to the
dictionary.
Raises:
KeyError: Dictionary is empty.
"""
return self._data.popitem()[1]
[docs] def setdefault(self, key: Key, default=None) -> Any:
"""
If an item with the key (looked up case-insensitively) does not exist,
add an item with that key and the specified default value, and return
the value of the item with the key.
Raises:
AttributeError: The key does not have the casefold method.
"""
if key not in self:
self[key] = default
return self[key]
# Iteration methods
[docs] def keys(self) -> dict_keys:
# pylint: disable=line-too-long
"""
Return a view on the dictionary keys (in the original lexical case) in
dictionary iteration order.
See
`Dictionary View Objects <https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects>`_ for details about view objects.
""" # noqa: E501
# pylint: enable=line-too-long
return dict_keys(self)
[docs] def keys_nocase(self) -> KeysView:
# pylint: disable=line-too-long
"""
Return a view on the casefolded dictionary keys in dictionary iteration
order.
See
`Dictionary View Objects <https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects>`_ for details about view objects.
""" # noqa: E501
# pylint: enable=line-too-long
return self._data.keys()
[docs] def values(self) -> dict_values:
# pylint: disable=line-too-long
"""
Return a view on the dictionary values in dictionary iteration order.
See
`Dictionary View Objects <https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects>`_ for details about view objects.
""" # noqa: E501
# pylint: enable=line-too-long
return dict_values(self)
[docs] def items(self) -> dict_items:
# pylint: disable=line-too-long
"""
Return a view on the dictionary items in dictionary iteration order,
where each item is a tuple of its key (in the original lexical case)
and its value.
See
`Dictionary View Objects on <https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects>`_ for details about view objects.
""" # noqa: E501
# pylint: enable=line-too-long
return dict_items(self)
[docs] def __iter__(self) -> Iterator[Key]:
"""
Return an iterator through the dictionary keys (in the original lexical
case) in dictionary iteration order.
Invoked when using: ``for key in ncd``
"""
for k in self._data:
yield self._data[k][0]
# Other stuff
[docs] def __repr__(self) -> str:
"""
Return a string representation of the dictionary that is suitable for
debugging.
The order of items is in dictionary iteration order, and the keys are
in the original lexical case.
Invoked when using e.g.: ``repr(ncd)``
"""
items = ["{0!r}: {1!r}".format(key, value)
for key, value in self.items()]
items_str = ', '.join(items)
return "{0.__class__.__name__}({{{1}}})".format(self, items_str)
[docs] def update(self, *args, **kwargs) -> None:
# pylint: disable=arguments-differ,signature-differs
# Note: The signature in Python 3 is: update(self, other=(), /, **kwds)
# Since the / marker cannot be used in Python 2, the *args
# approach has the same effect, i.e. to ensure that the
# parameter can only be specified as a keyword argument.
"""
Update the dictionary from key/value pairs.
If a key is already present in the dictionary (looked up
case-insensitively), its key and value is updated (without affecting
its position in the dictionary iteration order). Otherwise, an item
with the key and value is added to the dictionary.
The provided key and value objects will be referenced from the
dictionary without being copied, consistent with the built-in
:class:`py:dict` class.
Parameters:
*args : An optional single positional argument representing key-value
pairs to update the dictionary from, in iteration order of the
specified object. The argument must be one of:
- a dictionary object, or more specifically an object that has a
method ``keys()`` providing iteration through the keys and that
supports subscription by key for accessing the values.
- an iterable. If a key occurs more than once (case-insensitively),
the last item for that key becomes the corresponding item in
the dictionary. Each item in the iterable must be one of:
* an iterable with exactly two items. The first item is used as
the key, and the second item as the value.
* an object with a key attribute, if the
:func:`~nocasedict.KeyableByMixin` mixin generator function is
used. The value of the key attribute is used as the key, and
the object itself as the value.
**kwargs : Optional keyword arguments representing key-value pairs to
update the dictionary from, after having processed the positional
argument.
Before Python 3.7, the order of keyword arguments as specified in
the call to the method was not guaranteed to be preserved for the
method implementation, so passing more than one keyword argument
may have resulted in arbitrary order of items in the dictionary.
Raises:
AttributeError: The key does not have the casefold method.
TypeError: Expected at most 1 positional argument, got {n}.
ValueError: Cannot unpack positional argument item #{i}.
"""
if args:
if len(args) > 1:
raise TypeError(
"Expected at most 1 positional argument, got {n}".
format(n=len(args)))
other = args[0]
try:
# Try mapping / dictionary
for key in other.keys():
self[key] = other[key]
# pylint: disable=unidiomatic-typecheck
if type(other) is dict and _ODICT_TYPE is not dict and \
len(other.keys()) > 1:
warnings.warn(
"Before Python 3.7, initializing or updating a "
"NocaseDict object from a dict object with more than "
"one item is not guaranteed to preserve the order of "
"its items",
UserWarning,
stacklevel=_stacklevel_above_nocasedict())
except AttributeError:
# Expecting an iterable
# Try whether KeyableByMixin() was used
key_attr = getattr(
self, 'nocasedict_KeyableByMixin_key_attr', None)
# The following raises TypeError if not iterable:
for i, item in enumerate(other):
if key_attr and hasattr(item, key_attr):
# Is a keyable object
key = getattr(item, key_attr)
value = item
else:
# Expecting key, value pair
try:
key, value = item
except ValueError as exc:
value_error = ValueError(
"Cannot unpack positional argument item #{i} "
"of type {t} into key, value: {exc}".
format(i=i, t=type(item), exc=exc))
value_error.__cause__ = None # Suppress 'During..'
# pylint: disable=raise-missing-from
raise value_error
self[key] = value
for key, val in kwargs.items():
self[key] = val
if len(kwargs) > 1 and _ODICT_TYPE is not dict:
warnings.warn(
"Before Python 3.7, initializing or updating a NocaseDict "
"object from more than one keyword argument is not guaranteed "
"to preserve their order",
UserWarning,
stacklevel=_stacklevel_above_nocasedict())
[docs] def clear(self) -> None:
"""
Remove all items from the dictionary.
"""
self._data.clear()
[docs] def copy(self) -> 'NocaseDict':
"""
Return a copy of the dictionary.
This is a middle-deep copy; the copy is independent of the original in
all attributes that have mutable types except for:
* The values in the dictionary
Note that the Python functions :func:`py:copy.copy` and
:func:`py:copy.deepcopy` can be used to create completely shallow or
completely deep copies of objects of this class.
"""
result = NocaseDict()
result._data = self._data.copy() # pylint: disable=protected-access
return result
[docs] def __eq__(self, other: Any) -> bool:
"""
Return a boolean indicating whether the dictionary and the other
dictionary are equal, by matching items (case-insensitively) based on
their keys, and then comparing the values of matching items for
equality.
The other dictionary may be a :class:`NocaseDict` object or any other
mapping. In all cases, the matching of keys takes place
case-insensitively.
Invoked when using e.g.: ``ncd == other``
Raises:
AttributeError: The key does not have the casefold method.
"""
# Issue #1062: Could compare hash values for better performance
for key, self_value in self.items():
if key not in other:
return False
other_value = other[key]
try:
if not self_value == other_value:
return False
except TypeError:
return False # not comparable -> considered not equal
return len(self) == len(other)
[docs] def __ne__(self, other: Any) -> bool:
"""
Return a boolean indicating whether the dictionary and the other
dictionary are not equal, by negating the equality test.
The other dictionary may be a :class:`NocaseDict` object or any other
mapping. In all cases, the matching of keys takes place
case-insensitively.
Invoked when using e.g.: ``ncd != other``
Raises:
AttributeError: The key does not have the casefold method.
"""
return not self == other
def _raise_ordering_not_supported(self, other: Any, op: str) -> NoReturn:
"""
Function to raise a TypeError indicating that ordering of this class
is not supported.
"""
raise TypeError(
"'{}' not supported between instances of '{}' and '{}'".
format(op, type(self), type(other)))
def __lt__(self, other: Any) -> NoReturn:
self._raise_ordering_not_supported(other, '<')
def __gt__(self, other: Any) -> NoReturn:
self._raise_ordering_not_supported(other, '>')
def __ge__(self, other: Any) -> NoReturn:
self._raise_ordering_not_supported(other, '>=')
def __le__(self, other: Any) -> NoReturn:
self._raise_ordering_not_supported(other, '<=')