Merge lp:~lifeless/launchpad/bug-722956 into lp:launchpad
- bug-722956
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Robert Collins | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 12489 | ||||
Proposed branch: | lp:~lifeless/launchpad/bug-722956 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
606 lines (+159/-55) 20 files modified
lib/lp/app/doc/tales.txt (+2/-0) lib/lp/app/widgets/suggestion.py (+3/-1) lib/lp/code/browser/branchlisting.py (+3/-3) lib/lp/code/feed/branch.py (+1/-1) lib/lp/code/interfaces/branch.py (+11/-3) lib/lp/code/interfaces/branchcollection.py (+4/-1) lib/lp/code/interfaces/branchnamespace.py (+5/-2) lib/lp/code/interfaces/hasbranches.py (+3/-1) lib/lp/code/interfaces/seriessourcepackagebranch.py (+7/-0) lib/lp/code/model/branch.py (+29/-13) lib/lp/code/model/branchcollection.py (+49/-9) lib/lp/code/model/branchnamespace.py (+1/-1) lib/lp/code/model/hasbranches.py (+2/-2) lib/lp/code/model/seriessourcepackagebranch.py (+6/-1) lib/lp/code/model/tests/test_branchset.py (+26/-12) lib/lp/code/model/tests/test_branchtarget.py (+1/-1) lib/lp/code/tests/test_helpers.py (+2/-1) lib/lp/code/vocabularies/branch.py (+1/-1) lib/lp/codehosting/branchdistro.py (+2/-1) lib/lp/codehosting/scanner/mergedetection.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~lifeless/launchpad/bug-722956 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins (community) | Approve | ||
Stuart Bishop (community) | Needs Information | ||
Review via email: mp+51481@code.launchpad.net |
Commit message
[r=lifeless]
Description of the change
The API for /branches is nearly guaranteed to have branches from many different contexts, and we were doing late evaluation on many attributes trying to render the collection. This fixes 2 of those cases and lays the ground work to fix more when the page becomes an issue again.
Robert Collins (lifeless) wrote : | # |
On Mon, Feb 28, 2011 at 9:27 PM, Stuart Bishop
<email address hidden> wrote:
> Review: Needs Information
> The new eager_load parameter is exposed to the webservice. What happens when web service requests use this parameter? This seems a quick hack rather than an API improvement.
Its not exposed to the webservice. The getBranches parameters in the
web service is controlled via the @operation_
IHasBranches. The variant that I have enabled eager loading for is
only exposed as a collection in the web service: setting a default
parameter lets the method be used in code, and used as a collection in
the webservice, with the default being what the webservice needs.
> I suspect we need a new method (that is not exposed to the webservice) rather than adding an optional parameter to the existing method. Or cast the result set to an eager result set (results = IEagerResultSet
I don't think any of those are needed, because my change isn't exposed
as part of the web service contract; its invisible.
> Its annoying that the seemingly arbitrary and undocumented ordering in associatedSuite
I will comment on both sides of the eager load to point this out.
Thanks for the review,
Rob
Robert Collins (lifeless) wrote : | # |
I've addressed Stuarts comments; approving to make ec2land happy.
Preview Diff
1 | === modified file 'lib/lp/app/doc/tales.txt' |
2 | --- lib/lp/app/doc/tales.txt 2011-02-03 17:59:50 +0000 |
3 | +++ lib/lp/app/doc/tales.txt 2011-03-01 04:23:03 +0000 |
4 | @@ -498,6 +498,8 @@ |
5 | |
6 | >>> login_person(fooix.owner, LaunchpadTestRequest()) |
7 | >>> fooix.development_focus.branch = branch |
8 | + >>> from lp.services.propertycache import clear_property_cache |
9 | + >>> clear_property_cache(branch) |
10 | >>> print test_tales("branch/fmt:bzr-link", branch=branch) |
11 | <a href=".../~eric/fooix/bar" class="sprite branch">lp://dev/fooix</a> |
12 | |
13 | |
14 | === modified file 'lib/lp/app/widgets/suggestion.py' |
15 | --- lib/lp/app/widgets/suggestion.py 2011-02-08 15:43:30 +0000 |
16 | +++ lib/lp/app/widgets/suggestion.py 2011-03-01 04:23:03 +0000 |
17 | @@ -231,7 +231,9 @@ |
18 | collection = branch.target.collection.targetedBy(logged_in_user, |
19 | since) |
20 | collection = collection.visibleByUser(logged_in_user) |
21 | - branches = collection.getBranches().config(distinct=True) |
22 | + # May actually need some eager loading, but the API isn't fine grained |
23 | + # yet. |
24 | + branches = collection.getBranches(eager_load=False).config(distinct=True) |
25 | target_branches = list(branches.config(limit=5)) |
26 | # If there is a development focus branch, make sure it is always |
27 | # shown, and as the first item. |
28 | |
29 | === modified file 'lib/lp/code/browser/branchlisting.py' |
30 | --- lib/lp/code/browser/branchlisting.py 2011-02-11 15:09:53 +0000 |
31 | +++ lib/lp/code/browser/branchlisting.py 2011-03-01 04:23:03 +0000 |
32 | @@ -650,7 +650,7 @@ |
33 | if lifecycle_status is not None: |
34 | collection = collection.withLifecycleStatus(*lifecycle_status) |
35 | collection = collection.visibleByUser(self.user) |
36 | - return collection.getBranches().order_by( |
37 | + return collection.getBranches(eager_load=False).order_by( |
38 | self._listingSortToOrderBy(self.sort_by)) |
39 | |
40 | @property |
41 | @@ -809,7 +809,7 @@ |
42 | if lifecycle_status is not None: |
43 | collection = collection.withLifecycleStatus(*lifecycle_status) |
44 | collection = collection.visibleByUser(self.user) |
45 | - return collection.getBranches().order_by( |
46 | + return collection.getBranches(eager_load=False).order_by( |
47 | self._branch_order) |
48 | |
49 | |
50 | @@ -1501,7 +1501,7 @@ |
51 | # We're only interested in active branches. |
52 | collection = self.getBranchCollection().withLifecycleStatus( |
53 | *DEFAULT_BRANCH_STATUS_IN_LISTING) |
54 | - for branch in collection.getBranches(): |
55 | + for branch in collection.getBranches(eager_load=False): |
56 | branches.setdefault(branch.distroseries, []).append(branch) |
57 | return branches |
58 | |
59 | |
60 | === modified file 'lib/lp/code/feed/branch.py' |
61 | --- lib/lp/code/feed/branch.py 2010-08-24 10:45:57 +0000 |
62 | +++ lib/lp/code/feed/branch.py 2011-03-01 04:23:03 +0000 |
63 | @@ -163,7 +163,7 @@ |
64 | from lp.code.model.branch import Branch |
65 | collection = self._getCollection().visibleByUser( |
66 | None).withLifecycleStatus(*DEFAULT_BRANCH_STATUS_IN_LISTING) |
67 | - branches = collection.getBranches() |
68 | + branches = collection.getBranches(eager_load=False) |
69 | branches.order_by( |
70 | Desc(Branch.date_last_modified), |
71 | Asc(Branch.target_suffix), |
72 | |
73 | === modified file 'lib/lp/code/interfaces/branch.py' |
74 | --- lib/lp/code/interfaces/branch.py 2011-02-28 14:08:54 +0000 |
75 | +++ lib/lp/code/interfaces/branch.py 2011-03-01 04:23:03 +0000 |
76 | @@ -708,7 +708,10 @@ |
77 | """ |
78 | |
79 | def associatedSuiteSourcePackages(): |
80 | - """Return the suite source packages that this branch is linked to.""" |
81 | + """Return the suite source packages that this branch is linked to. |
82 | + |
83 | + :return: A list of suite source packages ordered by pocket. |
84 | + """ |
85 | |
86 | def branchLinks(): |
87 | """Return a sorted list of ICanHasLinkedBranch objects. |
88 | @@ -1286,8 +1289,13 @@ |
89 | """ |
90 | |
91 | @collection_default_content() |
92 | - def getBranches(limit=50): |
93 | - """Return a collection of branches.""" |
94 | + def getBranches(limit=50, eager_load=True): |
95 | + """Return a collection of branches. |
96 | + |
97 | + :param eager_load: If True (the default because this is used in the |
98 | + web service and it needs the related objects to create links) eager |
99 | + load related objects (products, code imports etc). |
100 | + """ |
101 | |
102 | |
103 | class IBranchListingQueryOptimiser(Interface): |
104 | |
105 | === modified file 'lib/lp/code/interfaces/branchcollection.py' |
106 | --- lib/lp/code/interfaces/branchcollection.py 2011-01-20 00:06:17 +0000 |
107 | +++ lib/lp/code/interfaces/branchcollection.py 2011-03-01 04:23:03 +0000 |
108 | @@ -57,13 +57,16 @@ |
109 | of individuals and teams that own branches in this collection. |
110 | """ |
111 | |
112 | - def getBranches(): |
113 | + def getBranches(eager_load=False): |
114 | """Return a result set of all branches in this collection. |
115 | |
116 | The returned result set will also join across the specified tables as |
117 | defined by the arguments to this function. These extra tables are |
118 | joined specificly to allow the caller to sort on values not in the |
119 | Branch table itself. |
120 | + |
121 | + :param eager_load: If True trigger eager loading of all the related |
122 | + objects in the collection. |
123 | """ |
124 | |
125 | def getMergeProposals(statuses=None, for_branches=None, |
126 | |
127 | === modified file 'lib/lp/code/interfaces/branchnamespace.py' |
128 | --- lib/lp/code/interfaces/branchnamespace.py 2010-08-20 20:31:18 +0000 |
129 | +++ lib/lp/code/interfaces/branchnamespace.py 2011-03-01 04:23:03 +0000 |
130 | @@ -52,8 +52,11 @@ |
131 | a given prefix, use createBranchWithPrefix. |
132 | """ |
133 | |
134 | - def getBranches(): |
135 | - """Return the branches in this namespace.""" |
136 | + def getBranches(eager_load=False): |
137 | + """Return the branches in this namespace. |
138 | + |
139 | + :param eager_load: If True eager load related data for the branches. |
140 | + """ |
141 | |
142 | def getBranchName(name): |
143 | """Get the potential unique name for a branch called 'name'. |
144 | |
145 | === modified file 'lib/lp/code/interfaces/hasbranches.py' |
146 | --- lib/lp/code/interfaces/hasbranches.py 2010-10-27 07:15:02 +0000 |
147 | +++ lib/lp/code/interfaces/hasbranches.py 2011-03-01 04:23:03 +0000 |
148 | @@ -60,13 +60,15 @@ |
149 | @operation_returns_collection_of(Interface) # Really IBranch. |
150 | @export_read_operation() |
151 | def getBranches(status=None, visible_by_user=None, |
152 | - modified_since=None): |
153 | + modified_since=None, eager_load=False): |
154 | """Returns all branches with the given lifecycle status. |
155 | |
156 | :param status: A list of statuses to filter with. |
157 | :param visible_by_user: Normally the user who is asking. |
158 | :param modified_since: If set, filters the branches being returned |
159 | to those that have been modified since the specified date/time. |
160 | + :param eager_load: If True load related objects for the whole |
161 | + collection. |
162 | :returns: A list of `IBranch`. |
163 | """ |
164 | |
165 | |
166 | === modified file 'lib/lp/code/interfaces/seriessourcepackagebranch.py' |
167 | --- lib/lp/code/interfaces/seriessourcepackagebranch.py 2010-08-20 20:31:18 +0000 |
168 | +++ lib/lp/code/interfaces/seriessourcepackagebranch.py 2011-03-01 04:23:03 +0000 |
169 | @@ -65,6 +65,13 @@ |
170 | :return: An `IResultSet` of `ISeriesSourcePackageBranch` objects. |
171 | """ |
172 | |
173 | + def findForBranches(branches): |
174 | + """Get the links to source packages from a branch. |
175 | + |
176 | + :param branches: A an iterable of `IBranch`. |
177 | + :return: An `IResultSet` of `ISeriesSourcePackageBranch` objects. |
178 | + """ |
179 | + |
180 | def findForSourcePackage(sourcepackage): |
181 | """Get the links to branches from a source package. |
182 | |
183 | |
184 | === modified file 'lib/lp/code/model/branch.py' |
185 | --- lib/lp/code/model/branch.py 2011-02-27 19:45:44 +0000 |
186 | +++ lib/lp/code/model/branch.py 2011-03-01 04:23:03 +0000 |
187 | @@ -142,6 +142,7 @@ |
188 | from lp.services.job.interfaces.job import JobStatus |
189 | from lp.services.job.model.job import Job |
190 | from lp.services.mail.notificationrecipientset import NotificationRecipientSet |
191 | +from lp.services.propertycache import cachedproperty |
192 | |
193 | |
194 | class Branch(SQLBase, BzrIdentityMixin): |
195 | @@ -724,13 +725,19 @@ |
196 | DeleteCodeImport(self.code_import)() |
197 | Store.of(self).flush() |
198 | |
199 | + @cachedproperty |
200 | + def _associatedProductSeries(self): |
201 | + """Helper for eager loading associatedProductSeries.""" |
202 | + # This is eager loaded by BranchCollection.getBranches. |
203 | + # Imported here to avoid circular import. |
204 | + from lp.registry.model.productseries import ProductSeries |
205 | + return Store.of(self).find( |
206 | + ProductSeries, |
207 | + ProductSeries.branch == self) |
208 | + |
209 | def associatedProductSeries(self): |
210 | """See `IBranch`.""" |
211 | - # Imported here to avoid circular import. |
212 | - from lp.registry.model.productseries import ProductSeries |
213 | - return Store.of(self).find( |
214 | - ProductSeries, |
215 | - ProductSeries.branch == self) |
216 | + return self._associatedProductSeries |
217 | |
218 | def getProductSeriesPushingTranslations(self): |
219 | """See `IBranch`.""" |
220 | @@ -740,14 +747,21 @@ |
221 | ProductSeries, |
222 | ProductSeries.translations_branch == self) |
223 | |
224 | - def associatedSuiteSourcePackages(self): |
225 | - """See `IBranch`.""" |
226 | + @cachedproperty |
227 | + def _associatedSuiteSourcePackages(self): |
228 | + """Helper for associatedSuiteSourcePackages.""" |
229 | + # This is eager loaded by BranchCollection.getBranches. |
230 | series_set = getUtility(IFindOfficialBranchLinks) |
231 | - # Order by the pocket to get the release one first. |
232 | + # Order by the pocket to get the release one first. If changing this be |
233 | + # sure to also change BranchCollection.getBranches. |
234 | links = series_set.findForBranch(self).order_by( |
235 | SeriesSourcePackageBranch.pocket) |
236 | return [link.suite_sourcepackage for link in links] |
237 | |
238 | + def associatedSuiteSourcePackages(self): |
239 | + """See `IBranch`.""" |
240 | + return self._associatedSuiteSourcePackages |
241 | + |
242 | # subscriptions |
243 | def subscribe(self, person, notification_level, max_diff_lines, |
244 | code_review_level, subscribed_by): |
245 | @@ -1311,7 +1325,8 @@ |
246 | branches = all_branches.visibleByUser( |
247 | visible_by_user).withLifecycleStatus(*lifecycle_statuses) |
248 | branches = branches.withBranchType( |
249 | - BranchType.HOSTED, BranchType.MIRRORED).scanned().getBranches() |
250 | + BranchType.HOSTED, BranchType.MIRRORED).scanned().getBranches( |
251 | + eager_load=False) |
252 | branches.order_by( |
253 | Desc(Branch.date_last_modified), Desc(Branch.id)) |
254 | if branch_count is not None: |
255 | @@ -1327,7 +1342,7 @@ |
256 | branches = all_branches.visibleByUser( |
257 | visible_by_user).withLifecycleStatus(*lifecycle_statuses) |
258 | branches = branches.withBranchType( |
259 | - BranchType.IMPORTED).scanned().getBranches() |
260 | + BranchType.IMPORTED).scanned().getBranches(eager_load=False) |
261 | branches.order_by( |
262 | Desc(Branch.date_last_modified), Desc(Branch.id)) |
263 | if branch_count is not None: |
264 | @@ -1341,7 +1356,8 @@ |
265 | """See `IBranchSet`.""" |
266 | all_branches = getUtility(IAllBranches) |
267 | branches = all_branches.withLifecycleStatus( |
268 | - *lifecycle_statuses).visibleByUser(visible_by_user).getBranches() |
269 | + *lifecycle_statuses).visibleByUser(visible_by_user).getBranches( |
270 | + eager_load=False) |
271 | branches.order_by( |
272 | Desc(Branch.date_created), Desc(Branch.id)) |
273 | if branch_count is not None: |
274 | @@ -1360,10 +1376,10 @@ |
275 | """See `IBranchSet`.""" |
276 | return getUtility(IBranchLookup).getByUrls(urls) |
277 | |
278 | - def getBranches(self, limit=50): |
279 | + def getBranches(self, limit=50, eager_load=True): |
280 | """See `IBranchSet`.""" |
281 | anon_branches = getUtility(IAllBranches).visibleByUser(None) |
282 | - branches = anon_branches.scanned().getBranches() |
283 | + branches = anon_branches.scanned().getBranches(eager_load=eager_load) |
284 | branches.order_by( |
285 | Desc(Branch.date_last_modified), Desc(Branch.id)) |
286 | branches.config(limit=limit) |
287 | |
288 | === modified file 'lib/lp/code/model/branchcollection.py' |
289 | --- lib/lp/code/model/branchcollection.py 2011-01-27 14:02:38 +0000 |
290 | +++ lib/lp/code/model/branchcollection.py 2011-03-01 04:23:03 +0000 |
291 | @@ -23,20 +23,26 @@ |
292 | from zope.component import getUtility |
293 | from zope.interface import implements |
294 | |
295 | +from canonical.launchpad.components.decoratedresultset import ( |
296 | + DecoratedResultSet, |
297 | + ) |
298 | from canonical.launchpad.webapp.interfaces import ( |
299 | DEFAULT_FLAVOR, |
300 | IStoreSelector, |
301 | MAIN_STORE, |
302 | ) |
303 | from canonical.launchpad.webapp.vocabulary import CountableIterator |
304 | +from canonical.lazr.utils import safe_hasattr |
305 | +from lp.bugs.model.bug import Bug |
306 | +from lp.bugs.model.bugbranch import BugBranch |
307 | from lp.code.interfaces.branch import user_has_special_branch_access |
308 | from lp.code.interfaces.branchcollection import ( |
309 | IBranchCollection, |
310 | InvalidFilter, |
311 | ) |
312 | - |
313 | -from lp.bugs.model.bug import Bug |
314 | -from lp.bugs.model.bugbranch import BugBranch |
315 | +from lp.code.interfaces.seriessourcepackagebranch import ( |
316 | + IFindOfficialBranchLinks, |
317 | + ) |
318 | from lp.code.enums import BranchMergeProposalStatus |
319 | from lp.code.interfaces.branchlookup import IBranchLookup |
320 | from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES |
321 | @@ -55,6 +61,7 @@ |
322 | from lp.registry.model.product import Product |
323 | from lp.registry.model.sourcepackagename import SourcePackageName |
324 | from lp.registry.model.teammembership import TeamParticipation |
325 | +from lp.services.propertycache import get_property_cache |
326 | |
327 | |
328 | class GenericBranchCollection: |
329 | @@ -89,7 +96,7 @@ |
330 | |
331 | def count(self): |
332 | """See `IBranchCollection`.""" |
333 | - return self.getBranches().count() |
334 | + return self.getBranches(eager_load=False).count() |
335 | |
336 | def ownerCounts(self): |
337 | """See `IBranchCollection`.""" |
338 | @@ -136,7 +143,7 @@ |
339 | |
340 | def _getBranchIdQuery(self): |
341 | """Return a Storm 'Select' for the branch IDs in this collection.""" |
342 | - select = self.getBranches()._get_select() |
343 | + select = self.getBranches(eager_load=False)._get_select() |
344 | select.columns = (Branch.id,) |
345 | return select |
346 | |
347 | @@ -144,11 +151,43 @@ |
348 | """Return the where expressions for this collection.""" |
349 | return self._branch_filter_expressions |
350 | |
351 | - def getBranches(self): |
352 | + def getBranches(self, eager_load=False): |
353 | """See `IBranchCollection`.""" |
354 | tables = [Branch] + self._tables.values() |
355 | expressions = self._getBranchExpressions() |
356 | - return self.store.using(*tables).find(Branch, *expressions) |
357 | + resultset = self.store.using(*tables).find(Branch, *expressions) |
358 | + if not eager_load: |
359 | + return resultset |
360 | + def do_eager_load(rows): |
361 | + branch_ids = set(branch.id for branch in rows) |
362 | + if not branch_ids: |
363 | + return |
364 | + branches = dict((branch.id, branch) for branch in rows) |
365 | + caches = dict((branch.id, get_property_cache(branch)) |
366 | + for branch in rows) |
367 | + for cache in caches.values(): |
368 | + if not safe_hasattr(cache, '_associatedProductSeries'): |
369 | + cache._associatedProductSeries = [] |
370 | + if not safe_hasattr(cache, '_associatedSuiteSourcePackages'): |
371 | + cache._associatedSuiteSourcePackages = [] |
372 | + # associatedProductSeries |
373 | + # Imported here to avoid circular import. |
374 | + from lp.registry.model.productseries import ProductSeries |
375 | + for productseries in self.store.find( |
376 | + ProductSeries, |
377 | + ProductSeries.branchID.is_in(branch_ids)): |
378 | + cache = caches[productseries.branchID] |
379 | + cache._associatedProductSeries.append(productseries) |
380 | + # associatedSuiteSourcePackages |
381 | + series_set = getUtility(IFindOfficialBranchLinks) |
382 | + # Order by the pocket to get the release one first. If changing |
383 | + # this be sure to also change BranchCollection.getBranches. |
384 | + links = series_set.findForBranches(rows).order_by( |
385 | + SeriesSourcePackageBranch.pocket) |
386 | + for link in links: |
387 | + cache = caches[link.branchID] |
388 | + cache._associatedSuiteSourcePackages.append(link) |
389 | + return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load) |
390 | |
391 | def getMergeProposals(self, statuses=None, for_branches=None, |
392 | target_branch=None, merged_revnos=None): |
393 | @@ -364,7 +403,7 @@ |
394 | # of the unique name and sort based on relevance. |
395 | branch = self._getExactMatch(search_term) |
396 | if branch is not None: |
397 | - if branch in self.getBranches(): |
398 | + if branch in self.getBranches(eager_load=False): |
399 | return CountableIterator(1, [branch]) |
400 | else: |
401 | return CountableIterator(0, []) |
402 | @@ -397,7 +436,8 @@ |
403 | |
404 | # Get the results. |
405 | collection = self._filterBy([Branch.id.is_in(Union(*queries))]) |
406 | - results = collection.getBranches().order_by(Branch.name, Branch.id) |
407 | + results = collection.getBranches(eager_load=False).order_by( |
408 | + Branch.name, Branch.id) |
409 | return CountableIterator(results.count(), results) |
410 | |
411 | def scanned(self): |
412 | |
413 | === modified file 'lib/lp/code/model/branchnamespace.py' |
414 | --- lib/lp/code/model/branchnamespace.py 2010-10-18 02:37:53 +0000 |
415 | +++ lib/lp/code/model/branchnamespace.py 2011-03-01 04:23:03 +0000 |
416 | @@ -218,7 +218,7 @@ |
417 | name = "%s-%s" % (prefix, count) |
418 | return name |
419 | |
420 | - def getBranches(self): |
421 | + def getBranches(self, eager_load=False): |
422 | """See `IBranchNamespace`.""" |
423 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
424 | return store.find(Branch, self._getBranchesClause()) |
425 | |
426 | === modified file 'lib/lp/code/model/hasbranches.py' |
427 | --- lib/lp/code/model/hasbranches.py 2010-10-27 07:15:02 +0000 |
428 | +++ lib/lp/code/model/hasbranches.py 2011-03-01 04:23:03 +0000 |
429 | @@ -26,7 +26,7 @@ |
430 | """A mixin implementation for `IHasBranches`.""" |
431 | |
432 | def getBranches(self, status=None, visible_by_user=None, |
433 | - modified_since=None): |
434 | + modified_since=None, eager_load=False): |
435 | """See `IHasBranches`.""" |
436 | if status is None: |
437 | status = DEFAULT_BRANCH_STATUS_IN_LISTING |
438 | @@ -35,7 +35,7 @@ |
439 | collection = collection.withLifecycleStatus(*status) |
440 | if modified_since is not None: |
441 | collection = collection.modifiedSince(modified_since) |
442 | - return collection.getBranches() |
443 | + return collection.getBranches(eager_load=eager_load) |
444 | |
445 | |
446 | class HasMergeProposalsMixin: |
447 | |
448 | === modified file 'lib/lp/code/model/seriessourcepackagebranch.py' |
449 | --- lib/lp/code/model/seriessourcepackagebranch.py 2010-08-20 20:31:18 +0000 |
450 | +++ lib/lp/code/model/seriessourcepackagebranch.py 2011-03-01 04:23:03 +0000 |
451 | @@ -101,10 +101,15 @@ |
452 | |
453 | def findForBranch(self, branch): |
454 | """See `IFindOfficialBranchLinks`.""" |
455 | + return self.findForBranches([branch]) |
456 | + |
457 | + def findForBranches(self, branches): |
458 | + """See `IFindOfficialBranchLinks`.""" |
459 | + branch_ids = set(branch.id for branch in branches) |
460 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
461 | return store.find( |
462 | SeriesSourcePackageBranch, |
463 | - SeriesSourcePackageBranch.branch == branch.id) |
464 | + SeriesSourcePackageBranch.branchID.is_in(branch_ids)) |
465 | |
466 | def findForSourcePackage(self, sourcepackage): |
467 | """See `IFindOfficialBranchLinks`.""" |
468 | |
469 | === modified file 'lib/lp/code/model/tests/test_branchset.py' |
470 | --- lib/lp/code/model/tests/test_branchset.py 2010-10-04 19:50:45 +0000 |
471 | +++ lib/lp/code/model/tests/test_branchset.py 2011-03-01 04:23:03 +0000 |
472 | @@ -5,32 +5,34 @@ |
473 | |
474 | __metaclass__ = type |
475 | |
476 | -from unittest import TestLoader |
477 | +from testtools.matchers import LessThan |
478 | |
479 | +from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller |
480 | from canonical.testing.layers import DatabaseFunctionalLayer |
481 | from lp.code.interfaces.branch import IBranchSet |
482 | from lp.code.model.branch import BranchSet |
483 | -from lp.testing import TestCaseWithFactory |
484 | +from lp.testing import ( |
485 | + logout, |
486 | + TestCaseWithFactory, |
487 | + ) |
488 | +from lp.testing._webservice import QueryCollector |
489 | +from lp.testing.matchers import HasQueryCount |
490 | |
491 | |
492 | class TestBranchSet(TestCaseWithFactory): |
493 | |
494 | layer = DatabaseFunctionalLayer |
495 | |
496 | - def setUp(self): |
497 | - TestCaseWithFactory.setUp(self) |
498 | - self.branch_set = BranchSet() |
499 | - |
500 | def test_provides_IBranchSet(self): |
501 | # BranchSet instances provide IBranchSet. |
502 | - self.assertProvides(self.branch_set, IBranchSet) |
503 | + self.assertProvides(BranchSet(), IBranchSet) |
504 | |
505 | def test_getByUrls(self): |
506 | # getByUrls returns a list of branches matching the list of URLs that |
507 | # it's given. |
508 | a = self.factory.makeAnyBranch() |
509 | b = self.factory.makeAnyBranch() |
510 | - branches = self.branch_set.getByUrls( |
511 | + branches = BranchSet().getByUrls( |
512 | [a.bzr_identity, b.bzr_identity]) |
513 | self.assertEqual({a.bzr_identity: a, b.bzr_identity: b}, branches) |
514 | |
515 | @@ -38,9 +40,21 @@ |
516 | # If a branch cannot be found for a URL, then None appears in the list |
517 | # in place of the branch. |
518 | url = 'http://example.com/doesntexist' |
519 | - branches = self.branch_set.getByUrls([url]) |
520 | + branches = BranchSet().getByUrls([url]) |
521 | self.assertEqual({url: None}, branches) |
522 | |
523 | - |
524 | -def test_suite(): |
525 | - return TestLoader().loadTestsFromName(__name__) |
526 | + def test_api_branches_query_count(self): |
527 | + webservice = LaunchpadWebServiceCaller() |
528 | + collector = QueryCollector() |
529 | + collector.register() |
530 | + self.addCleanup(collector.unregister) |
531 | + # Get 'all' of the 50 branches this collection is limited to - rather |
532 | + # than the default in-test-suite pagination size of 5. |
533 | + url = "/branches?ws.size=50" |
534 | + logout() |
535 | + response = webservice.get(url, |
536 | + headers={'User-Agent': 'AnonNeedsThis'}) |
537 | + self.assertEqual(response.status, 200, |
538 | + "Got %d for url %r with response %r" % ( |
539 | + response.status, url, response.body)) |
540 | + self.assertThat(collector, HasQueryCount(LessThan(13))) |
541 | |
542 | === modified file 'lib/lp/code/model/tests/test_branchtarget.py' |
543 | --- lib/lp/code/model/tests/test_branchtarget.py 2011-01-31 02:12:30 +0000 |
544 | +++ lib/lp/code/model/tests/test_branchtarget.py 2011-03-01 04:23:03 +0000 |
545 | @@ -53,7 +53,7 @@ |
546 | # branches related to the branch target. |
547 | self.assertEqual(self.target.collection.getBranches().count(), 0) |
548 | branch = self.makeBranchForTarget() |
549 | - branches = self.target.collection.getBranches() |
550 | + branches = self.target.collection.getBranches(eager_load=False) |
551 | self.assertEqual([branch], list(branches)) |
552 | |
553 | def test_retargetBranch_packageBranch(self): |
554 | |
555 | === modified file 'lib/lp/code/tests/test_helpers.py' |
556 | --- lib/lp/code/tests/test_helpers.py 2010-10-04 19:50:45 +0000 |
557 | +++ lib/lp/code/tests/test_helpers.py 2011-03-01 04:23:03 +0000 |
558 | @@ -35,6 +35,7 @@ |
559 | self.assertIsNot(None, fooix) |
560 | # There should be one branch with one commit. |
561 | [branch] = list( |
562 | - getUtility(IAllBranches).inProduct(fooix).getBranches()) |
563 | + getUtility(IAllBranches).inProduct(fooix).getBranches( |
564 | + eager_load=False)) |
565 | self.assertEqual(1, branch.revision_count) |
566 | self.assertEqual(commit_time, branch.getTipRevision().revision_date) |
567 | |
568 | === modified file 'lib/lp/code/vocabularies/branch.py' |
569 | --- lib/lp/code/vocabularies/branch.py 2010-12-01 18:58:44 +0000 |
570 | +++ lib/lp/code/vocabularies/branch.py 2011-03-01 04:23:03 +0000 |
571 | @@ -69,7 +69,7 @@ |
572 | logged_in_user = getUtility(ILaunchBag).user |
573 | collection = self._getCollection().visibleByUser(logged_in_user) |
574 | if query is None: |
575 | - branches = collection.getBranches() |
576 | + branches = collection.getBranches(eager_load=False) |
577 | else: |
578 | branches = collection.search(query) |
579 | return CountableIterator(branches.count(), branches, self.toTerm) |
580 | |
581 | === modified file 'lib/lp/codehosting/branchdistro.py' |
582 | --- lib/lp/codehosting/branchdistro.py 2010-11-05 14:17:11 +0000 |
583 | +++ lib/lp/codehosting/branchdistro.py 2011-03-01 04:23:03 +0000 |
584 | @@ -146,7 +146,8 @@ |
585 | """ |
586 | branches = getUtility(IAllBranches) |
587 | distroseries_branches = branches.inDistroSeries(self.old_distroseries) |
588 | - return distroseries_branches.officialBranches().getBranches() |
589 | + return distroseries_branches.officialBranches().getBranches( |
590 | + eager_load=False) |
591 | |
592 | def checkConsistentOfficialPackageBranch(self, db_branch): |
593 | """Check that `db_branch` is a consistent official package branch. |
594 | |
595 | === modified file 'lib/lp/codehosting/scanner/mergedetection.py' |
596 | --- lib/lp/codehosting/scanner/mergedetection.py 2010-10-08 20:25:27 +0000 |
597 | +++ lib/lp/codehosting/scanner/mergedetection.py 2011-03-01 04:23:03 +0000 |
598 | @@ -97,7 +97,7 @@ |
599 | BranchLifecycleStatus.DEVELOPMENT, |
600 | BranchLifecycleStatus.EXPERIMENTAL, |
601 | BranchLifecycleStatus.MATURE, |
602 | - BranchLifecycleStatus.ABANDONED).getBranches() |
603 | + BranchLifecycleStatus.ABANDONED).getBranches(eager_load=False) |
604 | for branch in branches: |
605 | last_scanned = branch.last_scanned_id |
606 | # If the branch doesn't have any revisions, not any point setting |
The new eager_load parameter is exposed to the webservice. What happens when web service requests use this parameter? This seems a quick hack rather than an API improvement.
I suspect we need a new method (that is not exposed to the webservice) rather than adding an optional parameter to the existing method. Or cast the result set to an eager result set (results = IEagerResultSet (foo.getBranche s())). Or just a helper (results = foo.getBranches(); eager_load_ branches( results) ).
Its annoying that the seemingly arbitrary and undocumented ordering in associatedSuite SourcePackages needs to be duplicated in the eager load method. We should comment this code dependency for when someone wants to change it.