Merge lp:~leonardr/lazr.restful/bleed-through-stack into lp:lazr.restful

Proposed by Leonard Richardson on 2010-01-13
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/bleed-through-stack
Merge into: lp:lazr.restful
Diff against target: 348 lines (+321/-0)
2 files modified
src/lazr/restful/docs/utils.txt (+204/-0)
src/lazr/restful/utils.py (+117/-0)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/bleed-through-stack
Reviewer Review Type Date Requested Status
Jonathan Lange (community) 2010-01-13 Approve on 2010-01-14
Review via email: mp+17298@code.launchpad.net
To post a comment you must log in.
Leonard Richardson (leonardr) wrote :

This branch introduces the BleedThroughDict, a data structure I'll be using in my multi-version web service code to keep track of which Python annotations apply to which versions of the web service.

The basic idea behind the BleedThroughDict: it's a stack of dictionaries that acts like a single dictionary. I can push a dictionary onto the stack containing annotations for version n, then another dictionary containing annotations for version n+1. Any annotations defined in version n but not in version n+1 will 'bleed through', and version n+1 will inherit their values.

I couldn't find a similar data structure already existing, but if there is one in an easily accessible utility library I'm happy to use it instead.

I did not implement all of dict's methods, only the ones I think I'll need for the multi-version code.

97. By Leonard Richardson on 2010-01-13

Removed probably unnecessary substack code.

Jonathan Lange (jml) wrote :

Hey Leonard,

I can't find much to comment on in this branch -- the test coverage is great, the docs read well and the code is clean.

I don't know of any other general data abstractions like this, but would note that it's got similarities to the layered configuration approach we have in lazr.config. Perhaps there's code that can be shared there.

Some thoughts:
  - Although 'append' and 'pop' rhyme nicely with the list methods, I think I would prefer 'push' to 'append', since it makes the stack nature clear. Change at your discretion.
  - 'missing' is private, so it should probably be prefixed with an underscore.
  - You know your needs best, but I reckon you'll want an __iter__ and maybe keys() & values().

Minor formatting issues:
  - in _get_from_top_of_stack, there should be spaces around the minus operator in the 'range' call.
  - in the doc, 'my_new_dict = { 1 : 2 }' should read 'my_new_dict = {1: 2}'

cheers,
jml

review: Approve
Leonard Richardson (leonardr) wrote :

I filed bug 507399 to see if lazr.config can use BleedThroughDict.

98. By Leonard Richardson on 2010-01-14

Response to feedback.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/docs/utils.txt'
2--- src/lazr/restful/docs/utils.txt 2010-01-11 14:38:47 +0000
3+++ src/lazr/restful/docs/utils.txt 2010-01-14 10:11:11 +0000
4@@ -1,6 +1,210 @@
5 Various utility functions
6 *************************
7
8+BleedThroughDict
9+================
10+
11+This class is a stack of named dicts that can be treated as a single
12+dict. Values from dicts lower on the stack will "bleed through" to the
13+top, unless blocked by a value from a dict higher on the stack.
14+
15+A BleedThroughDict starts out empty and unusable. It can't be treated
16+as a dict because there are no real dicts in the stack.
17+
18+ >>> from lazr.restful.utils import BleedThroughDict
19+ >>> stack = BleedThroughDict()
20+
21+ >>> stack.is_empty
22+ True
23+
24+ >>> stack.dict_names
25+ []
26+
27+ >>> sorted(stack.items())
28+ []
29+
30+ >>> stack.pop()
31+ Traceback (most recent call last):
32+ ...
33+ IndexError: pop from empty list
34+
35+ >>> stack['key'] = 'value'
36+ Traceback (most recent call last):
37+ ...
38+ IndexError: Stack is empty
39+
40+ >>> stack['key']
41+ Traceback (most recent call last):
42+ ...
43+ KeyError: 'key'
44+ >>> print stack.get('key')
45+ None
46+ >>> print stack.get('key', 'default')
47+ default
48+
49+ >>> del stack['key']
50+ Traceback (most recent call last):
51+ ...
52+ IndexError: Stack is empty
53+
54+To use a BleedThroughDict you must push a named dict onto the
55+stack. Now it acts just like a regular dict.
56+
57+ >>> stack.push('dict #1')
58+ >>> stack['key'] = 'value'
59+ >>> print stack['key']
60+ value
61+ >>> print stack.get('key', 'default')
62+ value
63+
64+ >>> sorted(stack.items())
65+ [('key', 'value')]
66+
67+You can pop a named dict off a stack: you'll get back a tuple
68+(name, dict, opaque). (See below for what 'opaque' means.)
69+
70+ >>> print stack.pop()
71+ ('dict #1', {'key': 'value'}, False)
72+
73+ >>> stack.push('dict #1')
74+ >>> stack['key'] = 'value'
75+ >>> stack['key2'] = 'value2'
76+
77+Push a second named dict onto the stack, and things start to get
78+interesting.
79+
80+ >>> stack.push('dict #2')
81+ >>> print stack['key']
82+ value
83+ >>> print stack['key2']
84+ value2
85+
86+Setting a value will overwrite any preexisting value...
87+
88+ >>> stack['key'] = "A different value"
89+ >>> print stack['key']
90+ A different value
91+ >>> sorted(stack.items())
92+ [('key', 'A different value'), ('key2', 'value2')]
93+
94+But removing the new value will restore the old value.
95+
96+ >>> del stack['key']
97+ >>> print stack['key']
98+ value
99+ >>> sorted(stack.items())
100+ [('key', 'value'), ('key2', 'value2')]
101+
102+You can only delete a value that's present in a dict at the top of the
103+stack.
104+
105+ >>> print stack['key']
106+ value
107+ >>> del stack['key']
108+ Traceback (most recent call last):
109+ ...
110+ KeyError: 'key'
111+
112+Passing in a dictionary
113+-----------------------
114+
115+By default. the 'push' method pushess an empty named dictionary onto
116+the stack. You can pass in a dictionary object as well as a name, and
117+a copy of that dictionary will be used instead.
118+
119+ >>> stack[1]
120+ Traceback (most recent call last):
121+ ...
122+ KeyError: 1
123+
124+ >>> my_new_dict = {1: 2}
125+ >>> stack.push('dict of numbers', my_new_dict)
126+ >>> stack[1]
127+ 2
128+ >>> stack[1] = 10
129+
130+Since the BleedThroughDict makes a copy, the original dictionary is
131+not affected by changes to the BleedthroughStack.
132+
133+ >>> stack[1]
134+ 10
135+ >>> my_new_dict[1]
136+ 2
137+
138+Opaque dictionaries
139+-------------------
140+
141+When you push a dictionary onto the stack you can declare it to be
142+opaque. Values from dictionaries lower in the stack will not bleed
143+through an opaque dictionary.
144+
145+ >>> print stack['key']
146+ value
147+
148+ >>> stack.push("An opaque dictionary", opaque=True)
149+
150+The dictionaries that define 'key' and 'key2' are still there...
151+
152+ >>> stack.dict_names
153+ ['dict #1', 'dict #2', 'dict of numbers', 'An opaque dictionary']
154+
155+...but 'key' and 'key2' are no longer accessible.
156+
157+ >>> sorted(stack.items())
158+ []
159+
160+ >>> stack['key']
161+ Traceback (most recent call last):
162+ ...
163+ KeyError: 'key'
164+
165+ >>> stack['key2']
166+ Traceback (most recent call last):
167+ ...
168+ KeyError: 'key2'
169+
170+ >>> stack['key'] = 'Brand new value'
171+ >>> print stack['key']
172+ Brand new value
173+
174+If you pop the opaque dictionary off the stack...
175+
176+ >>> print stack.pop()
177+ ('An opaque dictionary', {'key': 'Brand new value'}, True)
178+
179+...'key' and 'key2' are visible again.
180+
181+ >>> sorted(stack.items())
182+ [(1, 10), ('key', 'value'), ('key2', 'value2')]
183+
184+You can push a transparent dictionary on top of an opaque dictionary.
185+Values from the opaque dictionary will still be visible, but values
186+from below the opaque dictionary will not become visible.
187+
188+ >>> stack.push("An opaque dictionary", opaque=True)
189+ >>> stack['key'] = 'Another value'
190+
191+ >>> stack.push("A transparent dictionary")
192+
193+ >>> sorted(stack.items())
194+ [('key', 'Another value')]
195+
196+ >>> print stack['key']
197+ Another value
198+
199+ >>> stack['key2']
200+ Traceback (most recent call last):
201+ ...
202+ KeyError: 'key2'
203+
204+And of course you can push another opaque dictionary onto the stack,
205+and stop values from bleeding through any further.
206+
207+ >>> stack.push("Another opaque dictionary", opaque=True)
208+
209+ >>> sorted(stack.items())
210+ []
211+
212 implement_from_dict
213 ===================
214
215
216=== modified file 'src/lazr/restful/utils.py'
217--- src/lazr/restful/utils.py 2010-01-11 14:38:47 +0000
218+++ src/lazr/restful/utils.py 2010-01-14 10:11:11 +0000
219@@ -4,6 +4,7 @@
220
221 __metaclass__ = type
222 __all__ = [
223+ 'BleedThroughDict',
224 'camelcase_to_underscore_separated',
225 'get_current_browser_request',
226 'implement_from_dict',
227@@ -28,6 +29,122 @@
228
229 missing = object()
230
231+
232+class BleedThroughDict(object):
233+ """A stack of named dictionaries that acts as a single dictionary.
234+
235+ If a key can't be found in the dictionary on top of the stack,
236+ this class will look in the dictionary just below the top of the
237+ stack, then the dictionary below that, and so on--until it
238+ encounters the end of the stack or a dictionary that was defined
239+ as 'opaque'.
240+ """
241+ missing = object()
242+
243+ def __init__(self):
244+ """Initialize the bleed-through dictionary."""
245+ self.stack = []
246+
247+ def push(self, name, dictionary=None, opaque=False):
248+ """Pushes a dictionary onto the stack.
249+
250+ :arg name: The name of the dictionary to push.
251+ :arg dictionary: The contents of the dictionary to push.
252+ :arg opaque: If True, values will not 'bleed through' from
253+ dictionaries lower on the stack.
254+ """
255+ if name is None:
256+ self.stack.append(name)
257+ return
258+ if dictionary is None:
259+ dictionary = {}
260+ self.stack.append((name, dict(dictionary), opaque))
261+
262+ def pop(self):
263+ """Pop a tuple representing a dictionary from the stack.
264+
265+ :return: A 3-tuple (name, dict, opaque). 'dict' is the
266+ contents of the dictionary. 'opaque' is whether or not the
267+ dictionary was defined as opaque. This 3-tuple can be passed
268+ in as the *args to append().
269+ """
270+ return self.stack.pop()
271+
272+ @property
273+ def dict_names(self):
274+ """Return the names of dictionaries in the stack.
275+
276+ The names are useful for passing into substack().
277+ """
278+ return [item[0] for item in self.stack if item is not None]
279+
280+ @property
281+ def is_empty(self):
282+ """Is the stack empty?"""
283+ return len(self.stack) == 0
284+
285+ def __getitem__(self, key):
286+ """Look up an item somewhere in the stack."""
287+ if self.is_empty:
288+ raise KeyError(key)
289+ value = self._get_from_top_of_stack(key)
290+ if value is missing:
291+ raise KeyError(key)
292+ return value
293+
294+ def get(self, key, default=None):
295+ """Look up an item somewhere in the stack, with default fallback."""
296+ if self.is_empty:
297+ return default
298+ value = self._get_from_top_of_stack(key)
299+ if value is missing:
300+ return default
301+ return value
302+
303+ def items(self):
304+ """Return a merged view of the items in the dictionary."""
305+ # Find the last opaque dictionary in the stack. We'll get our
306+ # values from it and every subsequent dictionary. If there are
307+ # no opaque dictionaries, we'll get our values from the entire
308+ # stack.
309+ dictionaries = self.stack
310+ for i in range(len(self.stack)-1, -1, -1):
311+ name, dictionary, opaque = self.stack[i]
312+ if opaque:
313+ dictionaries = self.stack[i:]
314+ break
315+ final_dictionary = {}
316+ for name, dictionary, opaque in dictionaries:
317+ final_dictionary.update(dictionary)
318+ return final_dictionary.items()
319+
320+ def __setitem__(self, key, value):
321+ """Set a value in the dict at the top of the stack."""
322+ if self.is_empty:
323+ raise IndexError("Stack is empty")
324+ self.stack[-1][1][key] = value
325+
326+ def __delitem__(self, key):
327+ """Delete a value from the dict at the top of the stack."""
328+ if self.is_empty:
329+ raise IndexError("Stack is empty")
330+ del self.stack[-1][1][key]
331+
332+ def _get_from_top_of_stack(self, key):
333+ """Try to find the given key, starting from the top of the stack.
334+
335+ If the key is not present in the dict at the top of the stack,
336+ this method tries the dict beneath it, and so on to the bottom.
337+ """
338+ value = missing
339+ for i in range(len(self.stack) - 1, -1, -1):
340+ name, dictionary, opaque = self.stack[i]
341+ value = dictionary.get(key, missing)
342+ if opaque or value is not missing:
343+ break
344+ return value
345+
346+
347 def implement_from_dict(class_name, interface, values, superclass=object):
348 """Return a class that implements an interface's attributes.
349

Subscribers

People subscribed via source and target branches