#
# typedict.py - Provides the TypeDict class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`TypeDict` class, a type-aware dictionary.
"""
from collections import abc
[docs]class TypeDict(object):
"""A type-aware dictionary.
The purpose of the ``TypeDict`` is to allow value lookup using either
classes or instances as keys. The ``TypeDict`` can be used in the same way
that you would use a regular ``dict``, but the ``get`` and ``__getitem__``
methods have some extra functionality.
**Easy to understand example**
Let's say we have a class with some properties::
import fsleyes_widgets.utils.typedict as td
class Animal(object):
isMammal = True
numLegs = 4
And we want to associate some tooltips with those properties::
tooltips = td.TypeDict({
'Animal.isMammal' : 'Set this to True for mammals, '
'False for reptiles.',
'Animal.numLegs' : 'The nuber of legs on this animal.'
})
Because we used a ``TypeDict``, we can now look up those tooltips
in a number of ways::
a = Animal()
# Lookup by string (equivalent to a normal dict lookup)
tt = tooltips['Animal.isMammal']
# Lookup by class
tt = tooltips[Animal, 'isMammal']
# Lookup by instance
tt = tooltips[a, 'isMammal']
This functionality also works across class hierarchies::
class Cat(Animal):
numYoutubeHits = 10
tooltips = td.TypeDict({
'Animal.isMammal' : 'Set this to True for mammals, '
'False for reptiles.',
'Animal.numLegs' : 'The nuber of legs on this animal.',
'Cat.numYoutubeHits' : 'Number of youtube videos this cat '
'has starred in.'
})
c = Cat()
isMammalTooltip = tooltips[Cat, 'isMammal']
numLegsTooltip = tooltips[c, 'numLegs']
youtubeHitsTooltip = tooltips[c, 'numYoutubeHits']
# Class-hierachy-aware TypeDict lookups only
# work when you pass in an instance/class as
# the key - the following will result in a
# KeyError:
t = tooltips['Cat.numLegs']
The :meth:`get` method has some extra functionality for working with
class hierarchies::
tooltips = td.TypeDict({
'Animal.isMammal' : 'Set this to True for mammals, '
'False for reptiles.',
'Animal.numLegs' : 'The nuber of legs on this animal.',
'Cat.numLegs' : 'This will be equal to four for all cats, '
'but could be less for disabled cats, '
'or more for lucky cats.',
'Cat.numYoutubeHits' : 'Number of youtube videos this cat '
'has starred in.'
})
print tooltips.get((c, 'numLegs'))
# 'This will be equal to four for all cats, but could '
# 'be less for disabled cats, or more for lucky cats.'
print tooltips.get((c, 'numLegs'), allhits=True)
# ['This will be equal to four for all cats, but could '
# 'be less for disabled cats, or more for lucky cats.',
# 'The nuber of legs on this animal.']
print tooltips.get((c, 'numLegs'), allhits=True, bykey=True)
# {('Animal', 'numLegs'): 'The nuber of legs on this animal.',
# ('Cat', 'numLegs'): 'This will be equal to four for all cats, '
# 'but could be less for disabled cats, or '
# 'more for lucky cats.'}
**Boring technical description**
The ``TypeDict`` is a custom dictionary which allows classes or class
instances to be used as keys for value lookups, but internally transforms
any class/instance keys into strings. Tuple keys are supported. Value
assignment with class/instance keys is not supported. All keys are
transformed via the :meth:`tokenifyKey` method before being internally
used and/or stored.
If a class/instance is passed in as a key, and there is no value
associated with that class, a search is performed on all of the base
classes of that class to see if any values are present for them.
"""
def __init__(self, initial=None):
"""Create a ``TypeDict``.
:arg initial: Dictionary containing initial values.
"""
if initial is None:
initial = {}
self.__dict = {}
for k, v in initial.items():
self[k] = v
def __str__(self):
return self.__dict.__str__()
def __repr__(self):
return self.__dict.__repr__()
def __len__(self):
return len(self.__dict)
[docs] def keys(self):
return self.__dict.keys()
[docs] def values(self):
return self.__dict.values()
[docs] def items(self):
return self.__dict.items()
def __setitem__(self, key, value):
self.__dict[self.tokenifyKey(key)] = value
[docs] def tokenifyKey(self, key):
"""Turns a dictionary key, which may have been specified as a
string, or a combination of strings and types, into a tuple.
"""
if isinstance(key, str):
if '.' in key: return tuple(key.split('.'))
else: return key
if isinstance(key, abc.Sequence):
tKeys = map(self.tokenifyKey, key)
key = []
for tk in tKeys:
if isinstance(tk, str): key.append(tk)
elif isinstance(tk, abc.Sequence): key += list(tk)
else: key.append(tk)
return tuple(key)
return key
[docs] def get(self, key, default=None, allhits=False, bykey=False, exact=False):
"""Retrieve the value associated with the given key. If
no value is present, return the specified ``default`` value,
which itself defaults to ``None``.
If the specified key contains a class or instance, and the ``exact``
argument is ``False`` (the default), the entire class hierarchy is
searched, and the first value present for the class, or any base
class, are returned. If ``exact is True`` and no value exists
for the specific class, the ``default`` is returned.
If ``exact is False`` and the ``allhits`` argument evaluates to
``True``, the entire class hierarchy is searched, and all values
present for the class, and any base class, are returned as a sequence.
If ``allhits`` is ``True`` and the ``bykey`` parameter is also
set to ``True``, a dictionary is returned rather than a sequence,
where the dictionary contents are the subset of this dictionary,
containing the keys which equated to the given key, and their
corresponding values.
"""
try: return self.__getitem__(key, allhits, bykey, exact)
except KeyError: return default
def __getitem__(self, key, allhits=False, bykey=False, exact=False):
origKey = key
key = self.tokenifyKey(key)
bases = []
# Make the code a bit easier by
# treating non-tuple keys as tuples
if not isinstance(key, tuple):
key = tuple([key])
newKey = []
# Transform any class/instance elements into
# their string representation (the class name)
for elem in key:
if isinstance(elem, type):
newKey.append(elem.__name__)
bases .append(elem.__bases__)
elif not isinstance(elem, (str, int)):
newKey.append(elem.__class__.__name__)
bases .append(elem.__class__.__bases__)
else:
newKey.append(elem)
bases .append(None)
if exact:
bases = []
key = newKey
keys = []
hits = []
while True:
# If the key was not a tuple turn
# it back into a single element key
# for the lookup
if len(key) == 1: lKey = key[0]
else: lKey = tuple(key)
val = self.__dict.get(lKey, None)
# We've found a value for the key
if val is not None:
# If allhits is false, just return the value
if not allhits: return val
# Otherwise, accumulate the value, and keep
# searching
else:
hits.append(val)
if bykey:
keys.append(lKey)
# No more base classes to search for - there
# really is no value associated with this key
elif all([b is None for b in bases]):
raise KeyError(key)
# Search through the base classes to see
# if a value is present for one of them
for i, (elem, elemBases) in enumerate(zip(key, bases)):
if elemBases is None:
continue
# test each of the base classes
# of the current tuple element
for elemBase in elemBases:
newKey = list(key)
newKey[i] = elemBase
if len(newKey) == 1: newKey = newKey[0]
else: newKey = tuple(newKey)
try:
newVal = self.__getitem__(newKey, allhits, bykey)
except KeyError:
continue
if not allhits:
return newVal
else:
if bykey:
newKeys, newVals = zip(*newVal.items())
keys.extend(newKeys)
hits.extend(newVals)
else:
hits.extend(newVal)
# No value for any base classes either
if len(hits) == 0:
raise KeyError(origKey)
# if bykey is true, return a dict
# containing the values and their
# corresponding keys
if bykey:
return dict(zip(keys, hits))
# otherwise just return the
# list of matched values
else:
return hits