"""Implementation file of the :any:`dkey` module."""
from warnings import warn as _warn
_warning_types = {'developer': DeprecationWarning, 'end user': FutureWarning}
_DEFAULT = object()
[docs]class deprecate_keys(dict):
"""Wrapper for dicts that allows to set certain keys as deprecated."""
[docs] def __init__(self, dictionary, *args):
"""
Construct the wrapper class.
Internally, the items are stored in the same order as the given dictionary (CPython >=3.6).
The deprecated old keys that were replaced with new ones are positioned
just before their respective new keys.
Parameters
----------
dictionary: dict
The dictionary to wrap
*args
Zero or more keys that should show deprecation warnings.
Use :any:`dkey.dkey` for each key.
"""
super().__init__()
self._key_mappings = {}
_key_mappings_new = {}
for mapping in args:
self._key_mappings[mapping['old key']] = mapping
_key_mappings_new[mapping['new key']] = mapping
if not mapping['new key'] in dictionary:
raise ValueError(f'The new key `{mapping["new key"]}` which should replace the '
+f'old key `{mapping["old key"]}` is not in the given dict.')
for key, value in dictionary.items():
try:
mapping = _key_mappings_new[key]
if not (mapping['old key'] == mapping['new key']):
super().__setitem__(mapping['old key'], value)
except KeyError:
# Not deprecated.
pass
super().__setitem__(key, value)
[docs] def __eq__(self, other):
"""
Return `True` if identical to `other`.
Warns if either this or `other` contains deprecated keys.
Parameters
----------
other
The other object to compare this one to
Returns
-------
bool
True if they are equal, False otherwise
Warns
-----
CustomWarning
Warns with the warnings stored for all contained keys.
"""
for mapping in self._key_mappings.values():
deprecate_keys._warn_deprecation(mapping)
try:
for mapping in other._key_mappings.values():
deprecate_keys._warn_deprecation(mapping)
except AttributeError:
pass
return super().__eq__(other)
[docs] def __ne__(self, other):
"""
Return `True` if not identical to `other`.
Warns if either this or `other` contains deprecated keys.
Parameters
----------
other
The other object to compare this one to
Returns
-------
bool
True if they are not equal, False otherwise
Warns
-----
CustomWarning
Warns with the warnings stored for all contained keys.
"""
for mapping in self._key_mappings.values():
deprecate_keys._warn_deprecation(mapping)
try:
for mapping in other._key_mappings.values():
deprecate_keys._warn_deprecation(mapping)
except AttributeError:
pass
return super().__ne__(other)
[docs] def __getitem__(self, key):
"""
Get the value of the item of the given key `key`.
Warns if the given key is deprecated. If the key does not exist, it will behave
the same as a normal dict, i.e. it will raise a :any:`KeyError`.
Parameters
----------
key
The key for which to return the value
Returns
-------
value
The value stored for the given key
Raises
------
KeyError
If the key is not found
Warns
-----
CustomWarning
Warns with the warning stored for the given key if the key is deprecated.
"""
self._check_deprecated(key)
return super().__getitem__(key)
[docs] def __setitem__(self, key, value):
"""
Set the value of the item of the given key `key` to `value`.
Warns if the given key is deprecated.
Parameters
----------
key
The key under which to store the given value
value
The value to store
Warns
-----
CustomWarning
Warns with the warning stored for the given key if the key is deprecated.
Further access to the given key will not spawn additional warnings.
"""
if self._check_deprecated(key):
del self._key_mappings[key]
super().__setitem__(key, value)
[docs] def __delitem__(self, key):
"""
Remove the item with key `key` and its associated value.
Warns if the given key is deprecated.
Parameters
----------
key
The key of the item which to remove.
Raises
------
KeyError
Raises a :any:`KeyError` if the given key does not exist
Warns
-----
CustomWarning
Warns with the warning stored for the given key if the key is deprecated.
Further access to the given key will not spawn additional warnings.
"""
self.pop(key)
[docs] def __contains__(self, key):
"""
Return `True` if the given key `key` is in this dict, else `False`.
Warns if the given key is deprecated.
Parameters
----------
key
The key to search in this dict.
Returns
-------
IsInDict : bool
`True` if the given key is in this dict. `False`, otherwise.
Warns
-----
CustomWarning
Warns with the warning stored for the given key if the key is deprecated.
"""
if self._check_deprecated(key):
return True
else:
return super().__contains__(key)
[docs] def __iter__(self):
"""
Return an iterator over the keys of the dictionary.
Warns for each deprecated item accessed.
Returns
-------
Iterator : iterator
An iterator over the wrapped dict
Warns
-----
CustomWarning
Warns whenever a deprecated item in the dict is returned. Each item
will warn with its set warning type and message.
"""
for key in iter(super().keys()):
self._check_deprecated(key)
yield key
[docs] def clear(self):
"""
Remove all entries from the dict.
Will also remove all deprecation warnings and all keys.
"""
self._key_mappings = dict()
super().clear()
[docs] def copy(self):
"""
Return a shallow copy of this wrapped dict.
Will return a wrapped dict that, as the original, will warn
with the deprecation warnings set for this dict.
Returns
-------
copy : deprecate_keys
A shallow copy of the underlying dict and a shallow
copy of the underlying deprecation key structure.
"""
output = deprecate_keys(super())
output._key_mappings = self._key_mappings.copy()
return output
[docs] def get(self, key, default=None):
"""
Get the value stored under `key` or `default` if this key doesn't exist.
This function is the same as :any:`deprecate_keys.__getitem__` except that
it does not throw for non existing keys, but returns the given default
value instead.
Parameters
----------
key
The key for which to return the value
default, optional
The value to return, if the given key is not stored in the dict. Defaults
to :any:`None` if not given.
Returns
-------
value
The value stored for `key` or `default` if `key` is not in the dict.
"""
try:
return self.__getitem__(key)
except KeyError:
return default
[docs] def pop(self, key, default=_DEFAULT):
"""
Remove and return the item with key `key`.
If the key is not in the dict and no default value is set,
this function will raise an exception. If a default value is
given, this value will be returned instead.
Parameters
----------
key
The key to pop
default : optional
The value to return if the given `key` is not in the dict.
If non is given, an exception is raised instead.
Returns
-------
value
The value of the key given or the given default value, or
if no default value is given, an exception is raised.
Raises
------
KeyError
If `key` is not in the dict and no default value is given,
this function raises a :any:`KeyError`.
Warns
-----
CustomWarning
Warns if the popped item is a deprecated key. The deprecation
information for this key is removed.
"""
if self._check_deprecated(key):
del self._key_mappings[key]
if default is _DEFAULT:
return super().pop(key)
else:
return super().pop(key, default)
[docs] def popitem(self):
"""
Pop an item from the dict.
This function has the same behaviour as :any:`dict.popitem`. Therefore,
for Python < 3.6 this function will pop an arbitrary item, whereas for
Python >= 3.6 this function will raise the last item added to this dict.
If the dict is empty, this function will raise a :any:`KeyError`.
Returns
-------
key
The key popped
value
The associated value of the popped key
Raises
------
KeyError
If the dict is empty.
Warns
-----
CustomWarning
Warns if the popped item is a deprecated key. The deprecation
information for this key is removed.
"""
item = super().popitem()
if self._check_deprecated(item[0]):
del self._key_mappings[item[0]]
return item
[docs] def items(self):
"""
Return a new view of the dictionary's items: iterator of `(key, value)` pairs.
Works the same as the plain :any:`dict.items` function except that
it warns about all deprecated keys contained before returning.
Returns
-------
dict_items
Basically an iterator over `(key, value)` pairs of the dict. For more information
about dict views see: `dictionary view <https://docs.python.org/3/library/stdtypes.html#dict-views>`_
Warns
-----
CustomWarning
Warns for each deprecated item in the dictionary before returning.
"""
for mapping in self._key_mappings.values():
self._warn_deprecation(mapping)
return super().items()
[docs] def values(self):
"""
Return a new view of the dictionary's values.
Works the same as the plain :any:`dict.values` function except that
it warns about all deprecated keys associated with returned values before returning
the view.
Returns
-------
dict_values
Basically an iterator over the contained values of the dict. For more information
about dict views see: `dictionary view <https://docs.python.org/3/library/stdtypes.html#dict-views>`_
Warns
-----
CustomWarning
Warns for each deprecated item in the dictionary before returning.
"""
for mapping in self._key_mappings.values():
self._warn_deprecation(mapping)
return super().values()
[docs] def keys(self):
"""
Return a new view of the dictionary's keys.
Works the same as the plain :any:`dict.keys` function except that
it warns about all deprecated keys before returning the view.
Returns
-------
dict_keys
Basically an iterator over the contained keys of the dict. For more information
about dict views see: `dictionary view <https://docs.python.org/3/library/stdtypes.html#dict-views>`_
Warns
-----
CustomWarning
Warns for each deprecated item in the dictionary before returning.
"""
for mapping in self._key_mappings.values():
self._warn_deprecation(mapping)
return super().keys()
[docs] def __len__(self):
"""
Return the number of items in the dict.
Will raise warnings, if the dict contains deprecated values. One for
each deprecated value.
Returns
-------
int
The number of items in the dict
Warns
-----
CustomWarning
Warns for each deprecated item in the dictionary before returning.
"""
for mapping in self._key_mappings.values():
self._warn_deprecation(mapping)
return super().__len__()
def _check_deprecated(self, key):
"""
Check if the given key is deprecated and warn if it is.
Warns using the warning type and message stored with the key and returns True.
Otherwise it returns False and does not warn.
Parameters
----------
key
The key to look up in the dict of deprecated keys
Returns
-------
deprecated : bool
Whether the key is deprecated or not
"""
try:
mapping = self._key_mappings[key]
self._warn_deprecation(mapping)
return True
except KeyError:
return False
@staticmethod
def _warn_deprecation(mapping):
"""
Warn with the given deprecated key mapping.
Uses the default Python :any:`warnings.warn` function
extracting the `'warning message'` and `'warning type'`
from the given mapping (dict).
Parameters
----------
mapping: dict
Dict that needs to contain the two keys `'warning message'`,
which should be a :any:`str`, and `'warning type'` which needs
to be a valid subclass of :any:`Exception`.
Warns
-----
CustomWarning
Warns with the given message and warning type.
"""
_warn(mapping['warning message'], mapping['warning type'])
[docs]def dkey(*args, deprecated_in=None, removed_in=None, details=None, warning_type='developer'):
"""
Convert a key into a deprecation lookup dict.
To use the :any:`dkey.deprecate_keys` function it is easiest to generate
its input with this function. This function generates:
- A key removed deprecation warning object if one key is provided
- A key replaced deprecation warning object if two keys are provided
Parameters
----------
*args
One or two keys. If one key is passed, it is assumed that this
key will be removed in the future. If two keys are passed, it is
assumed that the second key is the replacement for the first one.
deprecated_in : str, optional
Version in which this key was deprecated. If given, will appear in the
warning message.
removed_in : str, optional
Version in which this key will be removed and will no longer work. If given,
will appear in the warning message.
details : str, optional
Will remove the default final sentence (do no longer use, or use `xxx` from now on).
warning_type : {'developer', 'end user', ArbitraryWarning}, optional
The warning type to use when the old key is accessed
By default, deprecation warnings are intended for developers only which
means a any:`DeprecationWarning` is used which isn't shown to end users.
If it should be shown to end users, this can be done by passing 'end user'
which will raise :any:`FutureWarning`. If you want to use your custom warning
type this is also possible.
.. note:: Your custom warning must work with :any:`warnings.warn`
Returns
-------
dict
A dict that can be used as a deprecated key input for :any:`dkey.deprecate_keys`.
Raises
------
ValueError
If zero or more than two keys are passed to this function.
"""
if len(args) == 0:
raise ValueError('No key given')
elif len(args) > 2:
raise ValueError(f'More than three keys were given ({len(args)}). Maximum allowed: 2.')
old_key = args[0]
if len(args) == 1:
new_key = old_key
replace = False
else:
new_key = args[1]
replace = True
message = f'Key `{old_key}` is deprecated'
if deprecated_in:
message += f' since version {deprecated_in}'
message += '.'
if removed_in:
message += f' It will be removed in version {removed_in}.'
if details is None:
if replace:
details = f'Use `{new_key}` from now on.'
else:
details = 'It shouldn\'t be used anymore.'
message += ' ' + details
try:
warning_type = _warning_types[warning_type]
except KeyError:
pass
return {'old key': old_key, 'new key': new_key, 'warning message': message, 'warning type': warning_type}