Skip to content

Commit

Permalink
Merge b198470 into c2676a8
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-maier committed Jul 23, 2020
2 parents c2676a8 + b198470 commit a5ac80c
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 72 deletions.
161 changes: 91 additions & 70 deletions nocasedict/_nocasedict.py
Expand Up @@ -31,9 +31,9 @@
import warnings
from collections import OrderedDict
try:
from collections.abc import Mapping
from collections.abc import Mapping, MutableMapping
except ImportError:
from collections import Mapping
from collections import Mapping, MutableMapping
try:
from collections.abc import UserDict
except ImportError:
Expand Down Expand Up @@ -67,7 +67,7 @@ def _real_key(key):
return None


class NocaseDict(object):
class NocaseDict(MutableMapping):
# pylint: disable=line-too-long
"""
A case-insensitive, ordered, and case-preserving, dictionary.
Expand Down Expand Up @@ -109,27 +109,24 @@ def __init__(self, *args, **kwargs):
"""
Parameters:
*args : An optional single positional argument that must be a
:term:`py:mapping`, an :term:`py:iterable`, or `None.`
*args : An optional single positional argument that is used to
initialize the dictionary in iteration order of the specified
object. The argument must be one of:
If not provided or provided as `None`, the dictionary initialized
from the positional arguments will be empty.
- a dictionary object, or more generally an object that supports
subscription by key and has a method ``keys()`` returning an
iterable of keys.
For mapping objects, the dictionary will be initialized with the
key-value pairs from the mapping object, preserving the iteration
order of the mapping object.
- an iterable. If a key occurs more than once (case-insensitively),
the last value for that key becomes the corresponding value in
the dictionary. Each item in the iterable must be one of:
For iterable objects, the dictionary will be initialized with the
items from the iterable in iteration order. If a key occurs more
than once (case-insensitively), the last value for that key becomes
the corresponding value 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 iterable with exactly two items. The first item is used as the
key, and the second item as the value.
* an object with a ``name`` attribute. The value of the ``name``
attribute is used as the key, and the object itself as the value.
* an object with a ``name`` attribute. The value of the ``name``
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 initialized from the positional argument.
Expand All @@ -143,11 +140,15 @@ def __init__(self, *args, **kwargs):
To summarize, only the following types of init arguments allow defining
the order of items in the new dictionary across all Python versions
supported by this package: Passing an iterable, an ordered mapping, or
`None` as a single positional argument, and passing at most one keyword
supported by this package: Passing an iterable or an ordered mapping
as a single positional argument, and passing at most one keyword
argument. A :exc:`py:UserWarning` will be issued if the provided
arguments cause the order of provided items not to be preserved in the
new dictionary.
arguments cause the order of provided items not to be preserved.
Raises:
TypeError: Key does not have a ``lower()`` method.
TypeError: Expected at most 1 positional argument, got {n}.
ValueError: Cannot unpack positional argument item #{i}.
"""

# The internal dictionary, with lower case keys. An item in this dict
Expand All @@ -158,21 +159,19 @@ def __init__(self, *args, **kwargs):
if args:
if len(args) > 1:
raise TypeError(
"NocaseDict expected at most 1 argument, got {n}".
"Expected at most 1 positional argument, got {n}".
format(n=len(args)))
arg = args[0]
if isinstance(arg, (NocaseDict, Mapping, UserDict)):
# It is a mapping/dictionary.
# pylint: disable=unidiomatic-typecheck
if type(arg) is dict and ODict is not dict:
warnings.warn(
"Initializing a NocaseDict object from type {t} before "
"Python 3.7 is not guaranteed to preserve order "
"of items".format(t=type(arg)),
"Before Python 3.7, initializing a NocaseDict object "
"from a dict object is not guaranteed to preserve the "
"order of its items",
UserWarning, stacklevel=2)
self.update(arg)
elif arg is None:
pass
else:
# The following raises TypeError if not iterable
for i, item in enumerate(arg):
Expand All @@ -186,8 +185,8 @@ def __init__(self, *args, **kwargs):
key, value = item
except ValueError as exc:
value_error = ValueError(
"Cannot unpack NocaseDict init item #{i} of "
"type {t} into key, value: {exc}".
"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..'
raise value_error
Expand All @@ -197,9 +196,9 @@ def __init__(self, *args, **kwargs):
if kwargs:
if len(kwargs) > 1 and ODict is not dict:
warnings.warn(
"Initializing a NocaseDict object from keyword arguments "
"before Python 3.7 is not guaranteed to preserve order "
"of items",
"Before Python 3.7, initializing a NocaseDict object from "
"more than one keyword argument is not guaranteed to "
"preserve their order",
UserWarning, stacklevel=2)
self.update(kwargs)

Expand Down Expand Up @@ -300,7 +299,7 @@ def get(self, key, default=None):
except KeyError:
return default

def setdefault(self, key, default):
def setdefault(self, key, default=None):
"""
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
Expand Down Expand Up @@ -388,52 +387,74 @@ def __repr__(self):
return "{0.__class__.__name__}({{{1}}})".format(self, items_str)

def update(self, *args, **kwargs):
# 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 an item for a key exists
in the dictionary (looked up case-insensitively), its value is updated.
If an item for a key does not exist, it is added to the dictionary.
Update the dictionary from key/value pairs.
If an item for a key exists in the dictionary (looked up
case-insensitively), its value is updated. If an item for a key does
not exist, it is added to the dictionary.
The provided key and values are stored in the dictionary without
being copied.
The provided key and value objects will be referenced from the
dictionary without being copied.
Each positional argument can be:
Parameters:
*args : An optional single positional argument that must be one of:
* an object with a method `items()` that returns an
:term:`py:iterable` of tuples containing key and value.
- a dictionary object, or more generally an object that supports
subscription by key and has a method ``keys()`` returning an
iterable of keys.
* an object without such a method, that is an :term:`py:iterable` of
tuples containing key and value.
- an iterable. Each item in the iterable must be an iterable with
exactly two items. The first item is used as the key, and the
second item as the value. If a key occurs more than once
(case-insensitively), the last value for that key becomes the
corresponding value in the dictionary.
Each keyword argument is a key/value pair.
**kwargs : Optional keyword arguments representing key-value pairs to
add to the dictionary.
The updates are performed first for the positional arguments in the
iteration order of their iterables, and then for the keyword arguments.
Starting with Python 3.7, the order of keyword arguments as
specified by the caller is preserved.
Starting with Python 3.7, the order of keyword arguments as specified
by the caller is preserved and is used to update the dictionary items
in that order.
If a key being added is already present (case-insensitively) from
the positional argument, the value from the keyword argument
replaces the value from the positional argument.
Raises:
TypeError: Key does not have a ``lower()`` method.
TypeError: Expected at most 1 positional argument, got {n}.
ValueError: Cannot unpack positional argument item #{i}.
"""
for mapping in args:
if hasattr(mapping, 'items'):
items = mapping.items()
else:
items = mapping
for i, item in enumerate(items):
try:
key, value = item
except ValueError as exc:
value_error = ValueError(
"Cannot unpack NocaseDict update item #{i} of "
"type {t} into key, value: {exc}".
format(i=i, t=type(item), exc=exc))
value_error.__cause__ = None # Suppress 'During handling..'
raise value_error
self[key] = value
for key, value in kwargs.items():
self[key] = value
if args:
if len(args) > 1:
raise TypeError(
"Expected at most 1 positional argument, got {n}".
format(n=len(args)))
other = args[0]
try:
for key in other.keys():
self[key] = other[key]
except AttributeError:
for i, item in enumerate(other):
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..'
raise value_error
self[key] = value

for key in kwargs:
self[key] = kwargs[key]

def clear(self):
"""
Expand Down
14 changes: 12 additions & 2 deletions tests/unittest/test_nocasedict.py
Expand Up @@ -87,14 +87,14 @@ def __hash__(self):
None, None, True
),
(
"Empty dict from None as positional arg",
"Empty dict from None as positional arg (not iterable)",
dict(
init_args=(None,),
init_kwargs={},
exp_dict=OrderedDict(),
verify_order=True,
),
None, None, not TEST_AGAINST_DICT
TypeError, None, True
),
(
"Empty dict from empty list as positional arg",
Expand Down Expand Up @@ -1603,6 +1603,16 @@ def test_NocaseDict_repr(testcase, obj):
),
None, None, True
),
(
"Empty dict, with two positional arguments (too many args)",
dict(
obj=NocaseDict(),
args=[dict(a=1), dict(b=2)],
kwargs={},
exp_obj=None,
),
TypeError, None, True
),
(
"Empty dict, with integer key in update args (no lower / success)",
dict(
Expand Down

0 comments on commit a5ac80c

Please sign in to comment.