indexden is now optional
[oweals/karmaworld.git] / dicthelpers.py
1 """
2 Karmaworld Dictionary helpers
3 Finals Club (c) 2014
4 Author: Bryan Bonvallet
5 """
6
7 # I'd wager none of these are thread safe.
8
9 import re
10 from collections import MutableMapping
11
12
13 needs_mapping = re.compile('{[a-zA-Z_]\w*}')
14
15
16 class attrdict(object):
17     """ Access dictionary by object attributes. """
18     def __getattr__(self, attr):
19         # create call stack, if not already there, to avoid loops
20         try:
21             callstack = object.__getattribute__(self, '__callstack')
22         except AttributeError:
23             object.__setattr__(self, '__callstack', [])
24             callstack = object.__getattribute__(self, '__callstack')
25
26         try:
27             # don't call something already on the stack
28             if attr in callstack:
29                 raise KeyError()
30             # track this attribute before possibly recursing
31             callstack.append(attr)
32             retattr = self.__getitem__(attr)
33             # success, remove attr from stack
34             callstack.remove(attr)
35             return retattr
36         except KeyError:
37             # if code is here, attr is not in dict
38             try:
39                 # try to grab attr from the object
40                 retattr = super(attrdict, self).__getattribute__(attr)
41                 return retattr
42             finally:
43                 # remove stack now that the attribute succeeded or failed
44                 callstack.remove(attr)
45
46     def __setattr__(self, attr, value):
47         self.__setitem__(attr, value)
48
49     def __delattr__(self, attr):
50         self.__delitem__(attr)
51
52
53 class fallbackdict(MutableMapping, attrdict):
54     """
55     Retrieve default values from a fallback dictionary and dynamically .format
56     """
57     def __init__(self, fallback, *args, **kwargs):
58         """
59         Supply dictionary to fall back to when keys are missing.
60         Other arguments are handled as normal dictionary.
61         """
62         # dodge attrdict problems by using object.__setattr__
63         classname = type(self).__name__
64         object.__setattr__(self, '_{0}__internaldict'.format(classname), {})
65         object.__setattr__(self, '_{0}__fallback'.format(classname), fallback)
66         object.__setattr__(self, '_{0}__fetching'.format(classname), [])
67         super(fallbackdict, self).__init__(*args, **kwargs)
68
69     def __needs_format__(self, value):
70         """ Helper to determine when a string needs formatting. """
71         return hasattr(value, 'format') and needs_mapping.search(value)
72
73     def __getitem__(self, key):
74         """
75         Use this dict's value if it has one otherwise grab value from parent
76         and populate any strings with format keyword arguments.
77         """
78         indict = self.__internaldict
79         fetching = self.__fetching
80
81         local = (key in indict)
82         # this will throw a key error if the key isn't in either place
83         # which is desirable
84         value = indict[key] if local else self.__fallback[key]
85
86         if (key in fetching) or (not self.__needs_format__(value)):
87             # already seeking this key in a recursed call
88             # or it doesn't need any formatting
89             # so return as-is
90             return value
91
92         # if the code is this far, strings needs formatting
93         # **self will call this function recursively.
94         # prevent infinite recursion from e.g. d['recurse'] = '{recurse}'
95         fetching.append(key)
96         value = value.format(**self)
97         # undo infinite recursion prevention
98         fetching.remove(key)
99         return value
100
101     def __setitem__(self, key, value):
102         """
103         Set the internal dict with key/value.
104         """
105         self.__internaldict[key] = value
106
107     def __delitem__(self, key):
108         """ Delete the key from the internal dict. """
109         del self.__internaldict[key]
110
111     def __uniquekeys__(self):
112         """ Returns unique keys between this dictionary and the fallback. """
113         mykeys = set(self.__internaldict.keys())
114         fbkeys = set(self.__fallback.keys())
115         # set union: all keys from both, no repeats.
116         return (mykeys | fbkeys)
117
118     def __iter__(self):
119         """ Returns the keys in this dictionary and the fallback. """
120         return iter(self.__uniquekeys__())
121
122     def __len__(self):
123         """
124         Returns the number of keys in this dictionary and the fallback.
125         """
126         return len(self.__uniquekeys__())