#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Data Structures
===============
Defines various data structures classes:
- :class:`ArbitraryPrecisionMapping`: A mutable mapping / *dict* like object
where numeric keys are stored with an arbitrary precision.
- :class:`Structure`: An object similar to C/C++ structured type.
- :class:`Lookup`: A *dict* sub-class acting as a lookup to retrieve keys by
values.
- :class:`CaseInsensitiveMapping`: A case insensitive mapping allowing values
retrieving from keys while ignoring the key case.
"""
from __future__ import division, unicode_literals
from collections import Mapping, MutableMapping
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2017 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'
__all__ = ['ArbitraryPrecisionMapping',
'Structure',
'Lookup',
'CaseInsensitiveMapping']
[docs]class ArbitraryPrecisionMapping(MutableMapping):
"""
Implements a mutable mapping / *dict* like object where numeric keys are
stored with an arbitrary precision.
Parameters
----------
data : dict, optional
*dict* of data to store into the mapping at initialisation.
key_decimals : int, optional
Decimals count the keys will be rounded at
Other Parameters
----------------
\**kwargs : dict, optional
Key / Value pairs to store into the mapping at initialisation.
Attributes
----------
key_decimals
Methods
-------
__setitem__
__getitem__
__delitem__
__contains__
__iter__
__len__
Examples
--------
>>> data1 = {0.1999999998: 'Nemo', 0.2000000000: 'John'}
>>> apm1 = ArbitraryPrecisionMapping(data1, key_decimals=10)
>>> # Doctests skip for Python 2.x compatibility.
>>> tuple(apm1.keys()) # doctest: +SKIP
(0.1999999998, 0.2)
>>> apm2 = ArbitraryPrecisionMapping(data1, key_decimals=7)
>>> # Doctests skip for Python 2.x compatibility.
>>> tuple(apm2.keys()) # doctest: +SKIP
(0.2,)
"""
def __init__(self, data=None, key_decimals=0, **kwargs):
self._data = dict()
self._key_decimals = None
self.key_decimals = key_decimals
self.update({} if data is None else data, **kwargs)
@property
def data(self):
"""
Property for **self.data** attribute.
Returns
-------
dict
:class:`ArbitraryPrecisionMapping` data structure.
Warning
-------
:attr:`ArbitraryPrecisionMapping.data` is read only.
"""
return self._data
@data.setter
def data(self, value):
"""
Setter for **self.data** attribute.
Parameters
----------
value : object
Attribute value.
"""
raise AttributeError('"{0}" attribute is read only!'.format('data'))
@property
def key_decimals(self):
"""
Property for **self._key_decimals** private attribute.
Returns
-------
unicode
self._key_decimals.
"""
return self._key_decimals
@key_decimals.setter
def key_decimals(self, value):
"""
Setter for **self._key_decimals** private attribute.
Parameters
----------
value : unicode
Attribute value.
"""
if value is not None:
assert isinstance(value, int), (
'"{0}" attribute: "{1}" is not a "int" instance!').format(
'key_decimals', value)
self._key_decimals = value
def _round(self, item):
"""
Rounds given item if numeric.
Parameters
----------
item : object
Attribute.
Parameters
----------
item : object
Attribute.
Notes
-----
- Reimplements the :meth:`MutableMapping.__setitem__` method.
"""
try:
return round(item, self._key_decimals)
except TypeError:
return item
[docs] def __setitem__(self, item, value):
"""
Sets given item (rounded if numeric) with given value.
Parameters
----------
item : object
Attribute.
value : object
Value.
Notes
-----
- Reimplements the :meth:`MutableMapping.__setitem__` method.
"""
self._data[self._round(item)] = value
[docs] def __getitem__(self, item):
"""
Returns the value of given item (rounded if numeric).
Parameters
----------
item : unicode
Item (rounded if numeric) name.
Returns
-------
object
Item value.
Notes
-----
- Reimplements the :meth:`MutableMapping.__getitem__` method.
"""
return self._data[self._round(item)]
[docs] def __delitem__(self, item):
"""
Deletes the item (rounded if numeric) with given value.
Parameters
----------
item : unicode
Item (rounded if numeric) name.
Notes
-----
- Reimplements the :meth:`MutableMapping.__delitem__` method.
"""
del self._data[self._round(item)]
[docs] def __contains__(self, item):
"""
Returns if the mapping contains given item (rounded if numeric).
Parameters
----------
item : unicode
Item (rounded if numeric) name.
Returns
-------
bool
Is item in mapping.
Notes
-----
- Reimplements the :meth:`MutableMapping.__contains__` method.
"""
return self._round(item) in self._data
[docs] def __iter__(self):
"""
Iterates over the items (rounded if numeric) names in the mapping.
Returns
-------
generator
Item names.
Notes
-----
- Reimplements the :meth:`MutableMapping.__iter__` method.
"""
return iter(self._data)
[docs] def __len__(self):
"""
Returns the items count.
Returns
-------
int
Items count.
Notes
-----
- Reimplements the :meth:`MutableMapping.__iter__` method.
"""
return len(self._data)
[docs]class Structure(dict):
"""
Defines an object similar to C/C++ structured type.
Other Parameters
----------------
\*args : list, optional
Arguments.
\**kwargs : dict, optional
Key / Value pairs.
Methods
-------
__getattr__
__setattr__
__delattr__
update
References
----------
.. [1] Mansencal, T. (n.d.). Structure. Retrieved from
https://github.com/KelSolaar/Foundations/\
blob/develop/foundations/data_structures.py
Examples
--------
>>> person = Structure(first_name='Doe', last_name='John', gender='male')
>>> # Doctests skip for Python 2.x compatibility.
>>> person.first_name # doctest: +SKIP
'Doe'
>>> sorted(person.keys())
['first_name', 'gender', 'last_name']
>>> # Doctests skip for Python 2.x compatibility.
>>> person['gender'] # doctest: +SKIP
'male'
"""
def __init__(self, *args, **kwargs):
dict.__init__(self, **kwargs)
self.__dict__.update(**kwargs)
[docs] def __getattr__(self, attribute):
"""
Returns given attribute value.
Parameters
----------
attribute : unicode
Attribute name.
Notes
-----
- Reimplements the :meth:`dict.__getattr__` method.
Returns
-------
object
Attribute value.
Raises
------
AttributeError
If the attribute is not defined.
"""
try:
return dict.__getitem__(self, attribute)
except KeyError:
raise AttributeError('"{0}" object has no attribute "{1}"'.format(
self.__class__.__name__, attribute))
[docs] def __setattr__(self, attribute, value):
"""
Sets both key and sibling attribute with given value.
Parameters
----------
attribute : object
Attribute.
value : object
Value.
Notes
-----
- Reimplements the :meth:`dict.__setattr__` method.
"""
dict.__setitem__(self, attribute, value)
object.__setattr__(self, attribute, value)
__setitem__ = __setattr__
[docs] def __delattr__(self, attribute):
"""
Deletes both key and sibling attribute.
Parameters
----------
attribute : object
Attribute.
Notes
-----
- Reimplements the :meth:`dict.__delattr__` method.
"""
dict.__delitem__(self, attribute)
object.__delattr__(self, attribute)
__delitem__ = __delattr__
[docs] def update(self, *args, **kwargs):
"""
Updates both keys and sibling attributes.
Other Parameters
----------------
\*args : list, optional
Arguments.
\**kwargs : dict, optional
Keywords arguments.
Notes
-----
- Reimplements the :meth:`dict.update` method.
"""
dict.update(self, *args, **kwargs)
self.__dict__.update(*args, **kwargs)
[docs]class Lookup(dict):
"""
Extends *dict* type to provide a lookup by value(s).
Methods
-------
first_key_from_value
keys_from_value
References
----------
.. [2] Mansencal, T. (n.d.). Lookup. Retrieved from
https://github.com/KelSolaar/Foundations/\
blob/develop/foundations/data_structures.py
Examples
--------
>>> person = Lookup(first_name='Doe', last_name='John', gender='male')
>>> person.first_key_from_value('Doe')
'first_name'
>>> persons = Lookup(John='Doe', Jane='Doe', Luke='Skywalker')
>>> sorted(persons.keys_from_value('Doe'))
['Jane', 'John']
"""
[docs] def first_key_from_value(self, value):
"""
Gets the first key with given value.
Parameters
----------
value : object
Value.
Returns
-------
object
Key.
"""
for key, data in self.items():
if data == value:
return key
[docs] def keys_from_value(self, value):
"""
Gets the keys with given value.
Parameters
----------
value : object
Value.
Returns
-------
object
Keys.
"""
return [key for key, data in self.items() if data == value]
[docs]class CaseInsensitiveMapping(MutableMapping):
"""
Implements a case-insensitive mutable mapping / *dict* object.
Allows values retrieving from keys while ignoring the key case.
The keys are expected to be unicode or string-like objects supporting the
:meth:`str.lower` method.
Parameters
----------
data : dict
*dict* of data to store into the mapping at initialisation.
Other Parameters
----------------
\**kwargs : dict, optional
Key / Value pairs to store into the mapping at initialisation.
Methods
-------
__setitem__
__getitem__
__delitem__
__contains__
__iter__
__len__
__eq__
__ne__
__repr__
copy
lower_items
Warning
-------
The keys are expected to be unicode or string-like objects.
References
----------
.. [3] Reitz, K. (n.d.). CaseInsensitiveDict. Retrieved from
https://github.com/kennethreitz/requests/\
blob/v1.2.3/requests/structures.py#L37
Examples
--------
>>> methods = CaseInsensitiveMapping({'McCamy': 1, 'Hernandez': 2})
>>> methods['mccamy']
1
"""
def __init__(self, data=None, **kwargs):
self._data = dict()
self.update({} if data is None else data, **kwargs)
@property
def data(self):
"""
Property for **self.data** attribute.
Returns
-------
dict
:class:`ArbitraryPrecisionMapping` data structure.
Warning
-------
:attr:`ArbitraryPrecisionMapping.data` is read only.
"""
return self._data
@data.setter
def data(self, value):
"""
Setter for **self.data** attribute.
Parameters
----------
value : object
Attribute value.
"""
raise AttributeError('"{0}" attribute is read only!'.format('data'))
[docs] def __setitem__(self, item, value):
"""
Sets given item with given value.
The item is stored as lower in the mapping while the original name and
its value are stored together as the value in a *tuple*:
{"item.lower()": ("item", value)}
Parameters
----------
item : object
Attribute.
value : object
Value.
Notes
-----
- Reimplements the :meth:`MutableMapping.__setitem__` method.
"""
self._data[item.lower()] = (item, value)
[docs] def __getitem__(self, item):
"""
Returns the value of given item.
The item value is retrieved using its lower name in the mapping.
Parameters
----------
item : unicode
Item name.
Returns
-------
object
Item value.
Notes
-----
- Reimplements the :meth:`MutableMapping.__getitem__` method.
"""
return self._data[item.lower()][1]
[docs] def __delitem__(self, item):
"""
Deletes the item with given name.
The item is deleted from the mapping using its lower name.
Parameters
----------
item : unicode
Item name.
Notes
-----
- Reimplements the :meth:`MutableMapping.__delitem__` method.
"""
del self._data[item.lower()]
[docs] def __contains__(self, item):
"""
Returns if the mapping contains given item.
Parameters
----------
item : unicode
Item name.
Returns
-------
bool
Is item in mapping.
Notes
-----
- Reimplements the :meth:`MutableMapping.__contains__` method.
"""
return item.lower() in self._data
[docs] def __iter__(self):
"""
Iterates over the items names in the mapping.
The item names returned are the original input ones.
Returns
-------
generator
Item names.
Notes
-----
- Reimplements the :meth:`MutableMapping.__iter__` method.
"""
return (item for item, value in self._data.values())
[docs] def __len__(self):
"""
Returns the items count.
Returns
-------
int
Items count.
Notes
-----
- Reimplements the :meth:`MutableMapping.__iter__` method.
"""
return len(self._data)
[docs] def __eq__(self, item):
"""
Returns the equality with given object.
Parameters
----------
item
Object item.
Returns
-------
bool
Equality.
Notes
-----
- Reimplements the :meth:`MutableMapping.__eq__` method.
"""
if isinstance(item, Mapping):
item = CaseInsensitiveMapping(item)
else:
return NotImplemented
return dict(self.lower_items()) == dict(item.lower_items())
[docs] def __ne__(self, item):
"""
Returns the inequality with given object.
Parameters
----------
item
Object item.
Returns
-------
bool
Inequality.
Notes
-----
- Reimplements the :meth:`MutableMapping.__ne__` method.
"""
return not (self == item)
[docs] def __repr__(self):
"""
Returns the mapping representation with the original item names.
Returns
-------
unicode
Mapping representation.
Notes
-----
- Reimplements the :meth:`MutableMapping.__repr__` method.
"""
return '{0}({1})'.format(self.__class__.__name__, dict(self.items()))
[docs] def copy(self):
"""
Returns a copy of the mapping.
Returns
-------
CaseInsensitiveMapping
Mapping copy.
Notes
-----
- The :class:`CaseInsensitiveMapping` class copy returned is a simple
*copy* not a *deepcopy*.
"""
return CaseInsensitiveMapping(self._data.values())
[docs] def lower_items(self):
"""
Iterates over the lower items names.
Returns
-------
generator
Lower item names.
"""
return ((item, value[1]) for (item, value) in self._data.items())