Merge lp:~leonardr/lazr.batchnavigator/build-url-without-length into lp:lazr.batchnavigator

Proposed by Leonard Richardson on 2010-08-18
Status: Merged
Merged at revision: 36
Proposed branch: lp:~leonardr/lazr.batchnavigator/build-url-without-length
Merge into: lp:lazr.batchnavigator
Diff against target: 196 lines (+63/-16)
4 files modified
src/lazr/batchnavigator/NEWS.txt (+3/-1)
src/lazr/batchnavigator/README.txt (+58/-13)
src/lazr/batchnavigator/_batchnavigator.py (+1/-1)
src/lazr/batchnavigator/z3batching/batch.py (+1/-1)
To merge this branch: bzr merge lp:~leonardr/lazr.batchnavigator/build-url-without-length
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) code 2010-08-18 Approve on 2010-08-19
Review via email: mp+33045@code.launchpad.net

Description of the Change

A previous branch changed the Batch object to avoid calling len() unless it was absolutely necessary, since len() can be inefficient. This branch changes the BatchNavigator object to avoid calling len() unless absolutely necessary. (The places where it's absolutely necessary: getting the link to the final batch, and determining whether the header string is plural or not. The plural determination could probably be optimized further: whenever any batch data is present, we know whether there's one item in the batch or whether there's more. But this isn't what I'm trying to optimize just now.)

I use the same trick to test this as Benji used to test his changes to the Batch object. I define a simple subclass of the 'list' type that raises a RuntimeError when __len__ is called, and use it throughout the existing tests (except where noted).

To post a comment you must log in.
37. By Leonard Richardson on 2010-08-19

Give up trying to optimize .heading after realizing that it doesn't matter, since .heading is always invoked immediately after an explicit determination of length.

38. By Leonard Richardson on 2010-08-19

Updated NEWS.

Guilherme Salgado (salgado) wrote :

This looks very nice

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/batchnavigator/NEWS.txt'
2--- src/lazr/batchnavigator/NEWS.txt 2010-08-18 11:38:56 +0000
3+++ src/lazr/batchnavigator/NEWS.txt 2010-08-19 14:26:43 +0000
4@@ -2,12 +2,14 @@
5 NEWS for lazr.batchnavigator
6 ============================
7
8-1.2.2 (Unreleased)
9+1.2.2 (2010-08-19)
10 ==================
11
12 - Make len() cheap to call when the current batch is the last (or
13 only) batch.
14
15+- Avoid calling len() when generating navigator URLs.
16+
17 1.2.1 (2010-08-12)
18 ==================
19
20
21=== modified file 'src/lazr/batchnavigator/README.txt'
22--- src/lazr/batchnavigator/README.txt 2010-07-21 19:49:40 +0000
23+++ src/lazr/batchnavigator/README.txt 2010-08-19 14:26:43 +0000
24@@ -64,15 +64,38 @@
25
26 You can create a batch and a batch navigator like so:
27
28+ >>> safe_reindeer = reindeer
29+ >>> safe_reindeer_batch_navigator = BatchNavigator(
30+ ... safe_reindeer, build_request(), size=3)
31+
32+An important feature of lazr.batchnavigator is its reluctance to
33+invoke len() on an underlying data set. len() can be an expensive
34+operation that provides little benefit, so this library tries hard to
35+avoid calling len() unless it's absolutely necessary. To show this
36+off, we'll define a subclass of Python's list type that explodes when
37+len() is invoked on it.
38+
39+ >>> class ListWithExplosiveLen(list):
40+ ... """A list subclass that doesn't like its len() being called."""
41+ ... def __len__(self):
42+ ... raise RuntimeError
43+
44+Unless otherwise stated, we will use this list exclusively throughout
45+this test, to verify that len() is never called unless we want it to
46+be.
47+
48+ >>> explosive_reindeer = ListWithExplosiveLen(reindeer)
49 >>> reindeer_batch_navigator = BatchNavigator(
50- ... reindeer, build_request(), size=3)
51+ ... explosive_reindeer, build_request(), size=3)
52
53-The BatchNavigator implements IBatchNavigator.
54+The BatchNavigator implements IBatchNavigator. We need to use the
55+'safe' batch navigator here, because verifyObject probes all methods
56+of the object it's passed, including __len__.
57
58 >>> from zope.interface.verify import verifyObject
59 >>> from lazr.batchnavigator.interfaces import IBatchNavigator
60
61- >>> verifyObject(IBatchNavigator, reindeer_batch_navigator)
62+ >>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator)
63 True
64
65 The BatchNavigator class provides IBatchNavigatorFactory. This can be used
66@@ -97,7 +120,12 @@
67 ''
68 >>> reindeer_batch_navigator.nextBatchURL()
69 'http://www.example.com/foo?start=3'
70- >>> reindeer_batch_navigator.lastBatchURL()
71+
72+There's no way to get the URL to the final batch without knowing the
73+length of the entire list, so we'll use the safe batch navigator to
74+demonstrate lastBatchURL():
75+
76+ >>> safe_reindeer_batch_navigator.lastBatchURL()
77 'http://www.example.com/foo?start=6'
78
79 The next link will be empty when there are no further results:
80@@ -130,6 +158,9 @@
81 ... 'batch': '3'})
82 >>> reindeer_batch_navigator_with_qs = BatchNavigator(
83 ... reindeer, request, size=3)
84+ >>> safe_reindeer_batch_navigator_with_qs = BatchNavigator(
85+ ... safe_reindeer, request, size=3)
86+
87
88 In this case, we created the BatchNavigator with a default size of '3' and
89 the request is asking exactly that number of items per batch, and thus, we
90@@ -141,12 +172,16 @@
91 'http://www.example.com/foo?fnorb=bar&start=0'
92 >>> reindeer_batch_navigator_with_qs.nextBatchURL()
93 'http://www.example.com/foo?fnorb=bar&start=6'
94- >>> reindeer_batch_navigator_with_qs.lastBatchURL()
95+
96+(Again, there's no way to get the last batch without knowing the size
97+of the entire list.)
98+
99+ >>> safe_reindeer_batch_navigator_with_qs.lastBatchURL()
100 'http://www.example.com/foo?fnorb=bar&start=6'
101
102 You can ask for the links for each of the result pages:
103
104- >>> links = reindeer_batch_navigator.batchPageURLs()
105+ >>> links = safe_reindeer_batch_navigator.batchPageURLs()
106 >>> for link in links:
107 ... label, url = link.items()[0]
108 ... print label, url
109@@ -281,7 +316,7 @@
110 ...
111 >>> sm = getSiteManager()
112 >>> sm.registerAdapter(ExampleAdapter)
113- >>> example = ExampleResultSet(reindeer)
114+ >>> example = ExampleResultSet(safe_reindeer)
115 >>> example_batch_navigator = BatchNavigator(
116 ... example, build_request(), size=3)
117 >>> example_batch_navigator.currentBatch().total()
118@@ -417,16 +452,19 @@
119 parameter called "absent," but it's not passed in our ongoing page
120 request.
121
122- >>> navigator_with_parameters = BatchNavigator(
123- ... reindeer, request_with_parameters, size=3,
124- ... transient_parameters=['quiet', 'absent'])
125+ >>> def build_navigator(list):
126+ ... return BatchNavigator(
127+ ... list, request_with_parameters, size=3,
128+ ... transient_parameters=['quiet', 'absent'])
129+ >>> navigator_with_parameters = build_navigator(reindeer)
130+ >>> safe_navigator_with_parameters = build_navigator(safe_reindeer)
131
132 Of these three parameters, only "noisy" recurs in the links produced by
133 the batch navigator.
134
135 >>> navigator_with_parameters.nextBatchURL()
136 'http://www.example.com/foo?noisy=HELLO&start=3'
137- >>> navigator_with_parameters.lastBatchURL()
138+ >>> safe_navigator_with_parameters.lastBatchURL()
139 'http://www.example.com/foo?noisy=HELLO&start=6'
140
141 The transient parameter is omitted, and the one that was never passed in
142@@ -440,7 +478,7 @@
143 BatchNavigator's heading property contains a description of the objects
144 for display.
145
146- >>> navigator.heading
147+ >>> safe_reindeer_batch_navigator.heading
148 'results'
149
150 There is a special case for when there is only one item in the batch,
151@@ -450,11 +488,18 @@
152 >>> navigator.heading
153 'result'
154
155+(Accessing .heading causes len() to be called on the underlying list,
156+which is why we have to use the safe batch navigator. In theory, this
157+could be optimized, but there's no real point, since the heading is
158+invariably preceded by the actual length of the underlying list,
159+eg. "10 results". Since len() is called anyway, and its value is
160+cached, a second len() won't hurt performance.)
161+
162 The heading can be set by passing a singular and a plural version of
163 the heading. The batch navigation will return the appropriate
164 header based on the total items in the batch.
165
166- >>> navigator = BatchNavigator(reindeer, request=request)
167+ >>> navigator = BatchNavigator(safe_reindeer, request=request)
168 >>> navigator.setHeadings('bug', 'bugs')
169 >>> navigator.heading
170 'bugs'
171
172=== modified file 'src/lazr/batchnavigator/_batchnavigator.py'
173--- src/lazr/batchnavigator/_batchnavigator.py 2010-05-05 03:36:31 +0000
174+++ src/lazr/batchnavigator/_batchnavigator.py 2010-08-19 14:26:43 +0000
175@@ -185,7 +185,7 @@
176
177 def generateBatchURL(self, batch):
178 url = ""
179- if not batch:
180+ if batch is None:
181 return url
182
183 qs = self.getCleanQueryString()
184
185=== modified file 'src/lazr/batchnavigator/z3batching/batch.py'
186--- src/lazr/batchnavigator/z3batching/batch.py 2010-08-18 11:38:56 +0000
187+++ src/lazr/batchnavigator/z3batching/batch.py 2010-08-19 14:26:43 +0000
188@@ -196,7 +196,7 @@
189
190 def firstBatch(self):
191 return _Batch(self.list, 0, size=self.size,
192- _listlength=self.listlength)
193+ _listlength=self._listlength)
194
195 def lastBatch(self):
196 # Return the last possible batch for this dataset, at the

Subscribers

People subscribed via source and target branches