Merge lp:~lifeless/launchpad/decoratedresultset into lp:launchpad

Proposed by Robert Collins on 2010-09-15
Status: Merged
Approved by: Robert Collins on 2010-09-15
Approved revision: no longer in the source branch.
Merged at revision: 11555
Proposed branch: lp:~lifeless/launchpad/decoratedresultset
Merge into: lp:launchpad
Diff against target: 207 lines (+92/-34)
2 files modified
lib/canonical/launchpad/components/decoratedresultset.py (+34/-8)
lib/canonical/launchpad/doc/decoratedresultset.txt (+58/-26)
To merge this branch: bzr merge lp:~lifeless/launchpad/decoratedresultset
Reviewer Review Type Date Requested Status
Jonathan Lange (community) 2010-09-15 Approve on 2010-09-15
Review via email: mp+35507@code.launchpad.net

Commit Message

Make it possible to get slice data out of a DecoratedResultSet.

Description of the Change

Make it possible to get slice data out of a DecoratedResultSet. This is useful for 'Bug.indexed_messages'.

To post a comment you must log in.
Jonathan Lange (jml) wrote :

 * Indent "slice_info=False" to after the paren above it so that "slice_info" aligns with "self".
 * The docstring refers to "result_decoratora", it should say "result_decorator"
 * Having a boolean called "slice_info" and an actual slice object called "slice_info" is a bit confusing. If you can think of a better name for either, please change it.

Other than that, looks good. Tweak.

review: Needs Fixing
Jonathan Lange (jml) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/components/decoratedresultset.py'
2--- lib/canonical/launchpad/components/decoratedresultset.py 2010-08-25 19:23:13 +0000
3+++ lib/canonical/launchpad/components/decoratedresultset.py 2010-09-15 10:09:44 +0000
4@@ -9,6 +9,7 @@
5 ]
6
7 from lazr.delegates import delegates
8+from storm import Undef
9 from storm.zope.interfaces import IResultSet
10 from zope.security.proxy import removeSecurityProxy
11
12@@ -33,25 +34,32 @@
13 """
14 delegates(IResultSet, context='result_set')
15
16- def __init__(self, result_set, result_decorator=None, pre_iter_hook=None):
17+ def __init__(self, result_set, result_decorator=None, pre_iter_hook=None,
18+ slice_info=False):
19 """
20 :param result_set: The original result set to be decorated.
21 :param result_decorator: The method with which individual results
22 will be passed through before being returned.
23 :param pre_iter_hook: The method to be called (with the 'result_set')
24 immediately before iteration starts.
25+ :param slice_info: If True pass information about the slice parameters
26+ to the result_decorator and pre_iter_hook. any() and similar
27+ methods will cause None to be supplied.
28 """
29 self.result_set = result_set
30 self.result_decorator = result_decorator
31 self.pre_iter_hook = pre_iter_hook
32+ self.slice_info = slice_info
33
34- def decorate_or_none(self, result):
35+ def decorate_or_none(self, result, row_index=None):
36 """Decorate a result or return None if the result is itself None"""
37 if result is None:
38 return None
39 else:
40 if self.result_decorator is None:
41 return result
42+ elif self.slice_info:
43+ return self.result_decorator(result, row_index)
44 else:
45 return self.result_decorator(result)
46
47@@ -62,7 +70,8 @@
48 """
49 new_result_set = self.result_set.copy(*args, **kwargs)
50 return DecoratedResultSet(
51- new_result_set, self.result_decorator, self.pre_iter_hook)
52+ new_result_set, self.result_decorator, self.pre_iter_hook,
53+ self.slice_info)
54
55 def config(self, *args, **kwargs):
56 """See `IResultSet`.
57@@ -79,10 +88,25 @@
58 """
59 # Execute/evaluate the result set query.
60 results = list(self.result_set.__iter__(*args, **kwargs))
61+ if self.slice_info:
62+ # Calculate slice data
63+ start = self.result_set._offset
64+ if start is Undef:
65+ start = 0
66+ stop = start + len(results)
67+ result_slice = slice(start, stop)
68 if self.pre_iter_hook is not None:
69- self.pre_iter_hook(results)
70- for value in results:
71- yield self.decorate_or_none(value)
72+ if self.slice_info:
73+ self.pre_iter_hook(results, result_slice)
74+ else:
75+ self.pre_iter_hook(results)
76+ if self.slice_info:
77+ start = result_slice.start
78+ for offset, value in enumerate(results):
79+ yield self.decorate_or_none(value, offset + start)
80+ else:
81+ for value in results:
82+ yield self.decorate_or_none(value)
83
84 def __getitem__(self, *args, **kwargs):
85 """See `IResultSet`.
86@@ -94,7 +118,8 @@
87 naked_value = removeSecurityProxy(value)
88 if IResultSet.providedBy(naked_value):
89 return DecoratedResultSet(
90- value, self.result_decorator, self.pre_iter_hook)
91+ value, self.result_decorator, self.pre_iter_hook,
92+ self.slice_info)
93 else:
94 return self.decorate_or_none(value)
95
96@@ -137,4 +162,5 @@
97 """
98 new_result_set = self.result_set.order_by(*args, **kwargs)
99 return DecoratedResultSet(
100- new_result_set, self.result_decorator, self.pre_iter_hook)
101+ new_result_set, self.result_decorator, self.pre_iter_hook,
102+ self.slice_info)
103
104=== modified file 'lib/canonical/launchpad/doc/decoratedresultset.txt'
105--- lib/canonical/launchpad/doc/decoratedresultset.txt 2009-07-23 17:51:28 +0000
106+++ lib/canonical/launchpad/doc/decoratedresultset.txt 2010-09-15 10:09:44 +0000
107@@ -1,30 +1,17 @@
108 = DecoratedResultSet =
109
110-Within Soyuz (and possibly other areas of Launchpad?) there are a
111-number of content classes which do not actually correspond directly to
112-a database table. For example, a `DistroSeriesBinaryPackage` is really
113-just a representation of a `BinaryPackageName` within a certain
114-DistroSeries. Nonetheless, this representation is presented (via views)
115-to users as a useful reference point for obtaining particular package
116-releases and/or architectures.
117-
118-A problem arises however when attempting to present a search result of
119-such content. For example, when a user searches for all packages within
120-the Ubuntu Intrepid `DistroSeries` that include the letter 'l', it is
121-actually the `DistroSeriesPackageCache` that is searched, but the
122-results need to be presented back to the browser as a set of
123-`DistroSeriesBinaryPackage`s. In the past this was achieved by using a
124-list comprehension on the complete result set to convert each result
125-into the expected DSBP. This in-memory list was then passed to the view.
126-But given that views usually paginate results, they are only interested
127-in 20 or so items at a time, so there is a huge waste of resources (DB
128-and memory primarily, but some CPU).
129-
130-The purpose of the `DecoratedResultSet` is to allow such content
131-classes to pass an un-evaluated result set to the view so that the view
132-can add limits to the query *before* it is evaluated (batching), while
133-still ensuring that when the query is evaluated the result is converted
134-into the expected content class.
135+Within Launchpad we often want to return related data for a ResultSet
136+but not have every call site know how that data is structured - they
137+should not need to know how to go about loading related Persons for
138+a BugMessage, or how to calculate whether a Person is valid. Nor do
139+we want to return a less capable object - that prohibits late slicing
140+and sorting.
141+
142+DecoratedResultSet permits some preprocessing of Storm ResultSet
143+objects at the time the query executes. This can be used to present
144+content classes which are not backed directly in the database, to
145+eager load multiple related tables and present just one in the result,
146+and so on.
147
148 First, we'll create the un-decorated result set of all distributions:
149
150@@ -38,7 +25,8 @@
151 == Creating the decorator method ==
152
153 We create a decorator method that we want to be applied to any
154-results obtained from our undecorated result set:
155+results obtained from our undecorated result set. For instance,
156+we can turn a model object into a string:
157
158 >>> def result_decorator(distribution):
159 ... return "Dist name is: %s" % distribution.name
160@@ -150,3 +138,47 @@
161 > original value : 4
162 decorated result: 8
163
164+
165+== Calculating row numbers ==
166+
167+DecoratedResultSet can inform its hooks about slice data if slice_info=True is
168+passed.
169+
170+ >>> def pre_iter(rows, slice):
171+ ... print "pre iter", len(rows), slice.start, slice.stop
172+ >>> def decorate(row, row_index):
173+ ... print "row", row.id, row_index
174+ >>> _ = result_set.order_by(Distribution.id)
175+ >>> drs = DecoratedResultSet(
176+ ... result_set, decorate, pre_iter, slice_info=True)
177+
178+We need enough rows to play with:
179+
180+ >>> drs.count()
181+ 7
182+
183+ >>> _ = list(drs[1:3])
184+ pre iter 2 1 3
185+ row 2 1
186+ row 3 2
187+
188+Half open slicing is supported too:
189+
190+ >>> _ = list(drs[:3])
191+ pre iter 3 0 3
192+ row 1 0
193+ row 2 1
194+ row 3 2
195+
196+ >>> _ = list(drs[2:])
197+ pre iter 5 2 7
198+ row 3 2
199+ row 4 3
200+ row 5 4
201+ row 7 5
202+ row 8 6
203+
204+And of course empty slices:
205+
206+ >>> _ = list(drs[3:3])
207+ pre iter 0 3 3