Merge lp:~thumper/launchpad/blueprint-dependency-vocabulary into lp:launchpad
- blueprint-dependency-vocabulary
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Michael Hudson-Doyle |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12598 |
Proposed branch: | lp:~thumper/launchpad/blueprint-dependency-vocabulary |
Merge into: | lp:launchpad |
Prerequisite: | lp:~thumper/launchpad/blueprint-dependencies |
Diff against target: |
370 lines (+134/-156) 4 files modified
lib/lp/blueprints/vocabularies/specificationdependency.py (+65/-42) lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt (+0/-97) lib/lp/blueprints/vocabularies/tests/test_doc.py (+0/-17) lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py (+69/-0) |
To merge this branch: | bzr merge lp:~thumper/launchpad/blueprint-dependency-vocabulary |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Hudson-Doyle | Approve | ||
Review via email: mp+53192@code.launchpad.net |
Commit message
[r=mwhudson][bug=707111] Allow any blueprint to be searched for when adding dependencies.
Description of the change
Allow the vocabulary to work with any dependency.
Tim Penhey (thumper) wrote : | # |
On Tue, 15 Mar 2011 12:13:40 you wrote:
> Review: Approve
> Hi Tim,
>
> This basically looks fine. The only comments I have are:
>
> * I think the ISpecification check is a bit over the top (already said
> this on IRC), * I think _order_by.__doc__ could do with expansion (or
> deletion -- the current docstring doesn't really add anything) * I think
> "_exclude_
> (the latter sounds a bit like it returns the blocked specs) * "Not" is
> imported but not used in
> ./lib/lp/
>
> "make lint" also reports a few long lines, up to you if you care :)
Addressed, and thanks.
Preview Diff
1 | === modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py' | |||
2 | --- lib/lp/blueprints/vocabularies/specificationdependency.py 2011-03-14 21:25:44 +0000 | |||
3 | +++ lib/lp/blueprints/vocabularies/specificationdependency.py 2011-03-15 08:46:24 +0000 | |||
4 | @@ -11,12 +11,12 @@ | |||
5 | 11 | 11 | ||
6 | 12 | from operator import attrgetter | 12 | from operator import attrgetter |
7 | 13 | 13 | ||
8 | 14 | from storm.locals import SQL, Store | ||
9 | 14 | from zope.component import getUtility | 15 | from zope.component import getUtility |
10 | 15 | from zope.interface import implements | 16 | from zope.interface import implements |
11 | 16 | from zope.schema.vocabulary import SimpleTerm | 17 | from zope.schema.vocabulary import SimpleTerm |
12 | 17 | 18 | ||
15 | 18 | from canonical.database.sqlbase import quote_like | 19 | from canonical.database.sqlbase import quote |
14 | 19 | from canonical.launchpad.helpers import shortlist | ||
16 | 20 | from canonical.launchpad.webapp import ( | 20 | from canonical.launchpad.webapp import ( |
17 | 21 | canonical_url, | 21 | canonical_url, |
18 | 22 | urlparse, | 22 | urlparse, |
19 | @@ -27,9 +27,11 @@ | |||
20 | 27 | NamedSQLObjectVocabulary, | 27 | NamedSQLObjectVocabulary, |
21 | 28 | SQLObjectVocabularyBase, | 28 | SQLObjectVocabularyBase, |
22 | 29 | ) | 29 | ) |
26 | 30 | 30 | from lp.blueprints.interfaces.specification import ISpecification | |
27 | 31 | from lp.blueprints.enums import SpecificationFilter | 31 | from lp.blueprints.model.specification import ( |
28 | 32 | from lp.blueprints.model.specification import Specification | 32 | recursive_blocked_query, |
29 | 33 | Specification, | ||
30 | 34 | ) | ||
31 | 33 | from lp.registry.interfaces.pillar import IPillarNameSet | 35 | from lp.registry.interfaces.pillar import IPillarNameSet |
32 | 34 | 36 | ||
33 | 35 | 37 | ||
34 | @@ -47,10 +49,10 @@ | |||
35 | 47 | as the context, or | 49 | as the context, or |
36 | 48 | - the full URL of the spec, in which case it can be any spec at all. | 50 | - the full URL of the spec, in which case it can be any spec at all. |
37 | 49 | 51 | ||
42 | 50 | For the purposes of enumeration and searching we only consider the first | 52 | For the purposes of enumeration and searching we look at all the possible |
43 | 51 | sort of spec for now. The URL form of token only matches precisely, | 53 | specifications, but order those of the same target first. If there is an |
44 | 52 | searching only looks for specs on the current target if the search term is | 54 | associated series as well, then those are shown before other matches not |
45 | 53 | not a URL. | 55 | linked to the same series. |
46 | 54 | """ | 56 | """ |
47 | 55 | 57 | ||
48 | 56 | implements(IHugeVocabulary) | 58 | implements(IHugeVocabulary) |
49 | @@ -60,30 +62,60 @@ | |||
50 | 60 | displayname = 'Select a blueprint' | 62 | displayname = 'Select a blueprint' |
51 | 61 | step_title = 'Search' | 63 | step_title = 'Search' |
52 | 62 | 64 | ||
54 | 63 | def _is_valid_candidate(self, spec, check_target=False): | 65 | def _is_valid_candidate(self, spec): |
55 | 64 | """Is `spec` a valid candidate spec for self.context? | 66 | """Is `spec` a valid candidate spec for self.context? |
56 | 65 | 67 | ||
57 | 66 | Invalid candidates are: | 68 | Invalid candidates are: |
58 | 67 | 69 | ||
62 | 68 | * The spec that we're adding a depdency to, | 70 | * None |
63 | 69 | * Specs for a different target, and | 71 | * The spec that we're adding a depdency to |
64 | 70 | * Specs that depend on this one. | 72 | * Specs that depend on this one |
65 | 71 | 73 | ||
66 | 72 | Preventing the last category prevents loops in the dependency graph. | 74 | Preventing the last category prevents loops in the dependency graph. |
67 | 73 | """ | 75 | """ |
69 | 74 | if check_target and spec.target != self.context.target: | 76 | if spec is None or spec == self.context: |
70 | 75 | return False | 77 | return False |
77 | 76 | return spec != self.context and spec not in set(self.context.all_blocked) | 78 | return spec not in set(self.context.all_blocked) |
78 | 77 | 79 | ||
79 | 78 | def _filter_specs(self, specs, check_target=False): | 80 | def _order_by(self): |
80 | 79 | """Filter `specs` to remove invalid candidates. | 81 | """Look at the context to provide grouping. |
81 | 80 | 82 | ||
82 | 81 | See `_is_valid_candidate` for what an invalid candidate is. | 83 | If the blueprint is for a project, then matching results for that |
83 | 84 | project should be first. If the blueprint is set for a series, then | ||
84 | 85 | that series should come before others for the project. Similarly for | ||
85 | 86 | the distribution, and the series goal for the distribution. | ||
86 | 87 | |||
87 | 88 | If all else is equal, the ordering is by name, then database id as a | ||
88 | 89 | final uniqueness resolver. | ||
89 | 82 | """ | 90 | """ |
94 | 83 | # XXX intellectronica 2007-07-05: is 100 a reasonable count before | 91 | order_statements = [] |
95 | 84 | # starting to warn? | 92 | spec = self.context |
96 | 85 | return [spec for spec in shortlist(specs, 100) | 93 | if spec.product is not None: |
97 | 86 | if self._is_valid_candidate(spec, check_target)] | 94 | order_statements.append( |
98 | 95 | "(CASE Specification.product WHEN %s THEN 0 ELSE 1 END)" % | ||
99 | 96 | spec.product.id) | ||
100 | 97 | if spec.productseries is not None: | ||
101 | 98 | order_statements.append( | ||
102 | 99 | "(CASE Specification.productseries" | ||
103 | 100 | " WHEN %s THEN 0 ELSE 1 END)" % | ||
104 | 101 | spec.productseries.id) | ||
105 | 102 | elif spec.distribution is not None: | ||
106 | 103 | order_statements.append( | ||
107 | 104 | "(CASE Specification.distribution WHEN %s THEN 0 ELSE 1 END)" | ||
108 | 105 | % spec.distribution.id) | ||
109 | 106 | if spec.distroseries is not None: | ||
110 | 107 | order_statements.append( | ||
111 | 108 | "(CASE Specification.distroseries" | ||
112 | 109 | " WHEN %s THEN 0 ELSE 1 END)" % | ||
113 | 110 | spec.distroseries.id) | ||
114 | 111 | order_statements.append("Specification.name") | ||
115 | 112 | order_statements.append("Specification.id") | ||
116 | 113 | return SQL(', '.join(order_statements)) | ||
117 | 114 | |||
118 | 115 | def _exclude_blocked_query(self): | ||
119 | 116 | """Return the select statement to exclude already blocked specs.""" | ||
120 | 117 | return SQL("Specification.id not in (WITH %s select id from blocked)" | ||
121 | 118 | % recursive_blocked_query(self.context)) | ||
122 | 87 | 119 | ||
123 | 88 | def toTerm(self, obj): | 120 | def toTerm(self, obj): |
124 | 89 | if obj.target == self.context.target: | 121 | if obj.target == self.context.target: |
125 | @@ -127,7 +159,7 @@ | |||
126 | 127 | spec = self._spec_from_url(token) | 159 | spec = self._spec_from_url(token) |
127 | 128 | if spec is None: | 160 | if spec is None: |
128 | 129 | spec = self.context.target.getSpecification(token) | 161 | spec = self.context.target.getSpecification(token) |
130 | 130 | if spec and self._is_valid_candidate(spec): | 162 | if self._is_valid_candidate(spec): |
131 | 131 | return self.toTerm(spec) | 163 | return self.toTerm(spec) |
132 | 132 | raise LookupError(token) | 164 | raise LookupError(token) |
133 | 133 | 165 | ||
134 | @@ -141,27 +173,18 @@ | |||
135 | 141 | if not query: | 173 | if not query: |
136 | 142 | return CountableIterator(0, []) | 174 | return CountableIterator(0, []) |
137 | 143 | spec = self._spec_from_url(query) | 175 | spec = self._spec_from_url(query) |
139 | 144 | if spec is not None and self._is_valid_candidate(spec): | 176 | if self._is_valid_candidate(spec): |
140 | 145 | return CountableIterator(1, [spec]) | 177 | return CountableIterator(1, [spec]) |
141 | 146 | quoted_query = quote_like(query) | ||
142 | 147 | sql_query = (""" | ||
143 | 148 | (Specification.name LIKE %s OR | ||
144 | 149 | Specification.title LIKE %s OR | ||
145 | 150 | fti @@ ftq(%s)) | ||
146 | 151 | """ | ||
147 | 152 | % (quoted_query, quoted_query, quoted_query)) | ||
148 | 153 | all_specs = Specification.select(sql_query, orderBy=self._orderBy) | ||
149 | 154 | candidate_specs = self._filter_specs(all_specs, check_target=True) | ||
150 | 155 | return CountableIterator(len(candidate_specs), candidate_specs) | ||
151 | 156 | 178 | ||
156 | 157 | @property | 179 | return Store.of(self.context).find( |
157 | 158 | def _all_specs(self): | 180 | Specification, |
158 | 159 | return self.context.target.specifications( | 181 | SQL('Specification.fti @@ ftq(%s)' % quote(query)), |
159 | 160 | filter=[SpecificationFilter.ALL], prejoin_people=False) | 182 | self._exclude_blocked_query(), |
160 | 183 | ).order_by(self._order_by()) | ||
161 | 161 | 184 | ||
162 | 162 | def __iter__(self): | 185 | def __iter__(self): |
165 | 163 | return ( | 186 | # We don't ever want to iterate over everything. |
166 | 164 | self.toTerm(spec) for spec in self._filter_specs(self._all_specs)) | 187 | raise NotImplementedError() |
167 | 165 | 188 | ||
168 | 166 | def __contains__(self, obj): | 189 | def __contains__(self, obj): |
169 | 167 | return self._is_valid_candidate(obj) | 190 | return self._is_valid_candidate(obj) |
170 | 168 | 191 | ||
171 | === removed file 'lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt' | |||
172 | --- lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 2011-03-15 02:39:51 +0000 | |||
173 | +++ lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 1970-01-01 00:00:00 +0000 | |||
174 | @@ -1,97 +0,0 @@ | |||
175 | 1 | SpecificationDepCandidatesVocabulary | ||
176 | 2 | ==================================== | ||
177 | 3 | |||
178 | 4 | All blueprints that can be added as a dependency of the context | ||
179 | 5 | blueprint. | ||
180 | 6 | |||
181 | 7 | >>> from zope.schema.vocabulary import getVocabularyRegistry | ||
182 | 8 | >>> vocabulary_registry = getVocabularyRegistry() | ||
183 | 9 | |||
184 | 10 | First, we set up a product with three blueprints. | ||
185 | 11 | |||
186 | 12 | >>> specced_product = factory.makeProduct() | ||
187 | 13 | >>> spec_a = factory.makeSpecification( | ||
188 | 14 | ... name='spec-a', summary='The first spec', | ||
189 | 15 | ... product=specced_product) | ||
190 | 16 | >>> spec_b = factory.makeSpecification( | ||
191 | 17 | ... name='spec-b', summary='The second spec', | ||
192 | 18 | ... product=specced_product) | ||
193 | 19 | >>> spec_c = factory.makeSpecification( | ||
194 | 20 | ... name='spec-c', summary='The third spec', | ||
195 | 21 | ... product=specced_product) | ||
196 | 22 | >>> sorted([spec.name for spec in specced_product.specifications()]) | ||
197 | 23 | [u'spec-a', u'spec-b', u'spec-c'] | ||
198 | 24 | |||
199 | 25 | |||
200 | 26 | Iterating over the vocabulary gives all blueprints for specced_product | ||
201 | 27 | except for spec_a itself. | ||
202 | 28 | |||
203 | 29 | >>> vocab = vocabulary_registry.get( | ||
204 | 30 | ... spec_a, "SpecificationDepCandidates") | ||
205 | 31 | >>> sorted([term.value.name for term in vocab]) | ||
206 | 32 | [u'spec-b', u'spec-c'] | ||
207 | 33 | |||
208 | 34 | Blueprints for other targets are considered to be 'in' the vocabulary | ||
209 | 35 | though. | ||
210 | 36 | |||
211 | 37 | >>> unrelated_spec = factory.makeSpecification( | ||
212 | 38 | ... product=factory.makeProduct()) | ||
213 | 39 | >>> vocab = vocabulary_registry.get( | ||
214 | 40 | ... spec_a, "SpecificationDepCandidates") | ||
215 | 41 | >>> unrelated_spec in vocab | ||
216 | 42 | True | ||
217 | 43 | |||
218 | 44 | We mark spec_b as a dependency of spec_a and spec_c as a dependency of | ||
219 | 45 | spec_b. | ||
220 | 46 | |||
221 | 47 | >>> spec_a.createDependency(spec_b) | ||
222 | 48 | <SpecificationDependency at ...> | ||
223 | 49 | >>> [spec.name for spec in spec_a.dependencies] | ||
224 | 50 | [u'spec-b'] | ||
225 | 51 | |||
226 | 52 | >>> spec_b.createDependency(spec_c) | ||
227 | 53 | <SpecificationDependency at ...> | ||
228 | 54 | >>> [spec.name for spec in spec_b.dependencies] | ||
229 | 55 | [u'spec-c'] | ||
230 | 56 | |||
231 | 57 | No circular dependencies - the vocabulary excludes specifications that | ||
232 | 58 | are a dependency of the context spec. | ||
233 | 59 | |||
234 | 60 | >>> spec_a in set(spec_b.all_blocked) | ||
235 | 61 | True | ||
236 | 62 | >>> spec_b in set(spec_c.all_blocked) | ||
237 | 63 | True | ||
238 | 64 | >>> vocab = vocabulary_registry.get( | ||
239 | 65 | ... spec_c, "SpecificationDepCandidates") | ||
240 | 66 | >>> spec_a in [term.value for term in vocab] | ||
241 | 67 | False | ||
242 | 68 | |||
243 | 69 | This vocabulary provides the IHugeVocabulary interface. | ||
244 | 70 | |||
245 | 71 | >>> from canonical.launchpad.webapp.testing import verifyObject | ||
246 | 72 | >>> from canonical.launchpad.webapp.vocabulary import IHugeVocabulary | ||
247 | 73 | >>> verifyObject(IHugeVocabulary, vocab) | ||
248 | 74 | True | ||
249 | 75 | |||
250 | 76 | The search() method returns specifications within the vocabulary that | ||
251 | 77 | matches the search string. The string is matched against the name, or | ||
252 | 78 | fallbacks to a full text search. | ||
253 | 79 | |||
254 | 80 | >>> from zope.security.proxy import removeSecurityProxy | ||
255 | 81 | >>> naked_vocab = removeSecurityProxy( | ||
256 | 82 | ... vocabulary_registry.get( | ||
257 | 83 | ... spec_a, "SpecificationDepCandidates")) | ||
258 | 84 | >>> list(naked_vocab.search('spec-b')) == [spec_b] | ||
259 | 85 | True | ||
260 | 86 | >>> list(naked_vocab.search('third')) == [spec_c] | ||
261 | 87 | True | ||
262 | 88 | |||
263 | 89 | The search method uses the SQL `LIKE` operator, with the values quoted | ||
264 | 90 | appropriately. Queries conataining regual expression operators, for | ||
265 | 91 | example, will simply look for the respective characters within the | ||
266 | 92 | vocabulary's item (this used to be the cause of an OOPS, see | ||
267 | 93 | https://bugs.launchpad.net/blueprint/+bug/139385 for more | ||
268 | 94 | details). | ||
269 | 95 | |||
270 | 96 | >>> list(naked_vocab.search('*')) | ||
271 | 97 | [] | ||
272 | 98 | 0 | ||
273 | === removed file 'lib/lp/blueprints/vocabularies/tests/test_doc.py' | |||
274 | --- lib/lp/blueprints/vocabularies/tests/test_doc.py 2010-08-25 05:17:20 +0000 | |||
275 | +++ lib/lp/blueprints/vocabularies/tests/test_doc.py 1970-01-01 00:00:00 +0000 | |||
276 | @@ -1,17 +0,0 @@ | |||
277 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
278 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
279 | 3 | |||
280 | 4 | """ | ||
281 | 5 | Run the doctests. | ||
282 | 6 | """ | ||
283 | 7 | |||
284 | 8 | import os | ||
285 | 9 | |||
286 | 10 | from lp.services.testing import build_doctest_suite | ||
287 | 11 | |||
288 | 12 | |||
289 | 13 | here = os.path.dirname(os.path.realpath(__file__)) | ||
290 | 14 | |||
291 | 15 | |||
292 | 16 | def test_suite(): | ||
293 | 17 | return build_doctest_suite(here, '') | ||
294 | 18 | 0 | ||
295 | === modified file 'lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py' | |||
296 | --- lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2010-10-04 19:50:45 +0000 | |||
297 | +++ lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2011-03-15 08:46:24 +0000 | |||
298 | @@ -176,3 +176,72 @@ | |||
299 | 176 | vocab = self.getVocabularyForSpec(spec) | 176 | vocab = self.getVocabularyForSpec(spec) |
300 | 177 | term = vocab.getTermByToken(canonical_url(candidate)) | 177 | term = vocab.getTermByToken(canonical_url(candidate)) |
301 | 178 | self.assertEqual(term.token, canonical_url(candidate)) | 178 | self.assertEqual(term.token, canonical_url(candidate)) |
302 | 179 | |||
303 | 180 | def test_searchForTerms_ordering_different_targets_by_name(self): | ||
304 | 181 | # If the searched for specs are on different targets, the ordering is | ||
305 | 182 | # by name. | ||
306 | 183 | spec = self.factory.makeSpecification() | ||
307 | 184 | foo_b = self.factory.makeSpecification(name='foo-b') | ||
308 | 185 | foo_a = self.factory.makeSpecification(name='foo-a') | ||
309 | 186 | vocab = self.getVocabularyForSpec(spec) | ||
310 | 187 | results = vocab.searchForTerms('foo') | ||
311 | 188 | self.assertEqual(2, len(results)) | ||
312 | 189 | found = [item.value for item in results] | ||
313 | 190 | self.assertEqual([foo_a, foo_b], found) | ||
314 | 191 | |||
315 | 192 | def test_searchForTerms_ordering_same_product_first(self): | ||
316 | 193 | # Specs on the same product are returned first. | ||
317 | 194 | widget = self.factory.makeProduct() | ||
318 | 195 | spec = self.factory.makeSpecification(product=widget) | ||
319 | 196 | foo_b = self.factory.makeSpecification(name='foo-b', product=widget) | ||
320 | 197 | foo_a = self.factory.makeSpecification(name='foo-a') | ||
321 | 198 | vocab = self.getVocabularyForSpec(spec) | ||
322 | 199 | results = vocab.searchForTerms('foo') | ||
323 | 200 | self.assertEqual(2, len(results)) | ||
324 | 201 | found = [item.value for item in results] | ||
325 | 202 | self.assertEqual([foo_b, foo_a], found) | ||
326 | 203 | |||
327 | 204 | def test_searchForTerms_ordering_same_productseries_first(self): | ||
328 | 205 | # Specs on the same product series are returned before the same | ||
329 | 206 | # products, and those before others. | ||
330 | 207 | widget = self.factory.makeProduct() | ||
331 | 208 | spec = self.factory.makeSpecification(product=widget) | ||
332 | 209 | spec.proposeGoal(widget.development_focus, widget.owner) | ||
333 | 210 | foo_c = self.factory.makeSpecification(name='foo-c', product=widget) | ||
334 | 211 | foo_c.proposeGoal(widget.development_focus, widget.owner) | ||
335 | 212 | foo_b = self.factory.makeSpecification(name='foo-b', product=widget) | ||
336 | 213 | foo_a = self.factory.makeSpecification(name='foo-a') | ||
337 | 214 | vocab = self.getVocabularyForSpec(spec) | ||
338 | 215 | results = vocab.searchForTerms('foo') | ||
339 | 216 | self.assertEqual(3, len(results)) | ||
340 | 217 | found = [item.value for item in results] | ||
341 | 218 | self.assertEqual([foo_c, foo_b, foo_a], found) | ||
342 | 219 | |||
343 | 220 | def test_searchForTerms_ordering_same_distribution_first(self): | ||
344 | 221 | # Specs on the same distribution are returned first. | ||
345 | 222 | mint = self.factory.makeDistribution() | ||
346 | 223 | spec = self.factory.makeSpecification(distribution=mint) | ||
347 | 224 | foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint) | ||
348 | 225 | foo_a = self.factory.makeSpecification(name='foo-a') | ||
349 | 226 | vocab = self.getVocabularyForSpec(spec) | ||
350 | 227 | results = vocab.searchForTerms('foo') | ||
351 | 228 | self.assertEqual(2, len(results)) | ||
352 | 229 | found = [item.value for item in results] | ||
353 | 230 | self.assertEqual([foo_b, foo_a], found) | ||
354 | 231 | |||
355 | 232 | def test_searchForTerms_ordering_same_distroseries_first(self): | ||
356 | 233 | # Specs on the same distroseries are returned before the same | ||
357 | 234 | # distribution, and those before others. | ||
358 | 235 | mint = self.factory.makeDistribution() | ||
359 | 236 | next = self.factory.makeDistroSeries(mint) | ||
360 | 237 | spec = self.factory.makeSpecification(distribution=mint) | ||
361 | 238 | spec.proposeGoal(next, mint.owner) | ||
362 | 239 | foo_c = self.factory.makeSpecification(name='foo-c', distribution=mint) | ||
363 | 240 | foo_c.proposeGoal(next, mint.owner) | ||
364 | 241 | foo_b = self.factory.makeSpecification(name='foo-b', distribution=mint) | ||
365 | 242 | foo_a = self.factory.makeSpecification(name='foo-a') | ||
366 | 243 | vocab = self.getVocabularyForSpec(spec) | ||
367 | 244 | results = vocab.searchForTerms('foo') | ||
368 | 245 | self.assertEqual(3, len(results)) | ||
369 | 246 | found = [item.value for item in results] | ||
370 | 247 | self.assertEqual([foo_c, foo_b, foo_a], found) |
Hi Tim,
This basically looks fine. The only comments I have are:
* I think the ISpecification check is a bit over the top (already said this on IRC), blocked_ query" would be a better name than "_blocked_ subselect" (the latter sounds a bit like it returns the blocked specs) blueprints/ vocabularies/ specificationde pendency. py
* I think _order_by.__doc__ could do with expansion (or deletion -- the current docstring doesn't really add anything)
* I think "_exclude_
* "Not" is imported but not used in ./lib/lp/
"make lint" also reports a few long lines, up to you if you care :)