Merge lp:~jml/pkgme/reason-for-want into lp:pkgme
- reason-for-want
- Merge into trunk
Proposed by
Jonathan Lange
Status: | Merged |
---|---|
Approved by: | Jonathan Lange |
Approved revision: | 120 |
Merged at revision: | 105 |
Proposed branch: | lp:~jml/pkgme/reason-for-want |
Merge into: | lp:pkgme |
Diff against target: |
438 lines (+191/-33) 7 files modified
doc/backends/index.txt (+8/-1) pkgme/backend.py (+73/-17) pkgme/backends/dummy/want (+1/-1) pkgme/backends/python/want (+2/-1) pkgme/tests/test_backend.py (+99/-4) pkgme/tests/test_python_backend.py (+3/-4) pkgme/tests/test_vala_backend.py (+5/-5) |
To merge this branch: | bzr merge lp:~jml/pkgme/reason-for-want |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby | Approve | ||
Review via email: mp+103917@code.launchpad.net |
Commit message
Allow backends to provide a reason for wanting or not wanting to package a project.
Description of the change
Sometimes, when using pkgme on a project that you are *certain* should be packageable, pkgme says that there are no eligible backends. In those cases, it's really difficult to figure out why a backend has decided that it can't package something.
This patch allows backends to specify an optional reason in their 'want' script. It does this by allowing 'want' to output a JSON dict instead of just an integer. The JSON dict must have a 'score' key and may have a 'reason' key. The value of 'reason' is displayed in NoEligibleBackends if provided.
To post a comment you must log in.
lp:~jml/pkgme/reason-for-want
updated
- 119. By Jonathan Lange
-
Clean up XXXs that have since been fixed.
lp:~jml/pkgme/reason-for-want
updated
- 120. By Jonathan Lange
-
Handle empty want scripts too.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/backends/index.txt' |
2 | --- doc/backends/index.txt 2011-11-16 16:28:02 +0000 |
3 | +++ doc/backends/index.txt 2012-04-27 17:22:19 +0000 |
4 | @@ -46,7 +46,7 @@ |
5 | the generic Rails backend when appropriate. |
6 | |
7 | Assigning all the scores up-front isn't a good idea though, so we use the following |
8 | -guildelines: |
9 | +guidelines: |
10 | |
11 | * 0 - no information can be provided about the project (e.g. a Ruby backend with a Python project). |
12 | * 10 - some information can be provided, but the backend is generic (e.g. Ruby backend). |
13 | @@ -106,6 +106,13 @@ |
14 | current directory the root of the project, and should return a number on stdout that is |
15 | how well the backend will work for the project (see `Selecting a backend`_). |
16 | |
17 | +The ``want`` script can also return a JSON array instead of a number. This |
18 | +array **must** have a ``score`` key with a number that is how well the backend |
19 | +will work for the project. It may also have a ``reason`` key with a text |
20 | +value that explains why this score was given. This reason will be shown when |
21 | +``pkgme`` cannot find any eligible backends for a project. Ideally, such |
22 | +reasons are short and have no formatting. |
23 | + |
24 | After implementing that script you have a choice of how to provide the rest of the |
25 | information. |
26 | |
27 | |
28 | === modified file 'pkgme/backend.py' |
29 | --- pkgme/backend.py 2012-02-01 18:04:19 +0000 |
30 | +++ pkgme/backend.py 2012-04-27 17:22:19 +0000 |
31 | @@ -1,3 +1,4 @@ |
32 | +import json |
33 | import os |
34 | import sys |
35 | |
36 | @@ -120,12 +121,31 @@ |
37 | return self.name |
38 | |
39 | def want(self, path): |
40 | + """How much this backend wants to be *the* backend. |
41 | + |
42 | + :return: (score, reason). 'reason' can be None for older backends. |
43 | + """ |
44 | raise NotImplementedError(self.want) |
45 | |
46 | def get_info(self, path): |
47 | raise NotImplementedError(self.get_info) |
48 | |
49 | |
50 | +class WantError(Exception): |
51 | + |
52 | + def __init__(self, name, basepath, error, data=None): |
53 | + self.name = name |
54 | + self.basepath = basepath |
55 | + self.error = error |
56 | + self.data = data |
57 | + message = ( |
58 | + "Backend %s (%s) %s from '%s' script" |
59 | + % (name, basepath, error, ExternalHelpersBackend.WANT_SCRIPT_NAME)) |
60 | + if data is not None: |
61 | + message = "%s: %r" % (message, data) |
62 | + super(WantError, self).__init__(message) |
63 | + |
64 | + |
65 | class ExternalHelpersBackend(Backend): |
66 | |
67 | WANT_SCRIPT_NAME = "want" |
68 | @@ -137,6 +157,39 @@ |
69 | def describe(self): |
70 | return "%s (%s)" % (self.name, self.basepath) |
71 | |
72 | + def _parse_want_output(self, output): |
73 | + result = output.strip() |
74 | + # ints and dicts can be parsed as JSON. |
75 | + try: |
76 | + result = json.loads(result) |
77 | + except ValueError: |
78 | + raise WantError( |
79 | + self.name, self.basepath, |
80 | + "returned invalid score", str(result)) |
81 | + try: |
82 | + # If it's just an int, return that with no reason. |
83 | + return int(result), None |
84 | + except TypeError: |
85 | + # Do we have a score? Is 'result' even a dict? |
86 | + try: |
87 | + score = result['score'] |
88 | + except KeyError: |
89 | + raise WantError( |
90 | + self.name, self.basepath, |
91 | + "did not return a score", result) |
92 | + except TypeError: |
93 | + raise WantError( |
94 | + self.name, self.basepath, |
95 | + "returned invalid score", result) |
96 | + # Is the score an integer? |
97 | + try: |
98 | + score = int(score) |
99 | + except ValueError: |
100 | + raise WantError( |
101 | + self.name, self.basepath, |
102 | + "returned non-integer score", result['score']) |
103 | + return score, result.get('reason', None) |
104 | + |
105 | def want(self, path): |
106 | try: |
107 | out = run_script(self.basepath, self.WANT_SCRIPT_NAME, path) |
108 | @@ -149,16 +202,7 @@ |
109 | "Backend %s (%s) has no usable '%s' script: %s" |
110 | % (self.name, self.basepath, self.WANT_SCRIPT_NAME, |
111 | str(e))) |
112 | - result = out.strip() |
113 | - try: |
114 | - score = int(result) |
115 | - except ValueError: |
116 | - raise AssertionError( |
117 | - "Backend %s (%s) returned non-integer score from " |
118 | - "'%s' script: %s" |
119 | - % (self.name, self.basepath, |
120 | - self.WANT_SCRIPT_NAME, str(result))) |
121 | - return score |
122 | + return self._parse_want_output(out) |
123 | |
124 | def get_info(self, path): |
125 | cls = MultipleExternalHelpersInfo |
126 | @@ -171,7 +215,7 @@ |
127 | |
128 | class StaticBackend(Backend): |
129 | |
130 | - def __init__(self, name, score, info, expected_path=None): |
131 | + def __init__(self, name, score, info, expected_path=None, reason=None): |
132 | """Create a StaticBackend. |
133 | |
134 | :param name: The name of the backend. |
135 | @@ -179,20 +223,22 @@ |
136 | call. (But see expected_path). |
137 | :param info: A fixed dict of packaging information always returned in |
138 | response to a 'want' call. (But see expected_path). |
139 | - :expected_path: If provided, then only return 'score' and 'info' if |
140 | - the backend is being called for this path. |
141 | + :param expected_path: If provided, then only return 'score' and |
142 | + 'info' if the backend is being called for this path. |
143 | + :param reason: An optional reason for the score we're giving. |
144 | """ |
145 | super(StaticBackend, self).__init__(name) |
146 | self.score = score |
147 | self.info = info |
148 | self.expected_path = expected_path |
149 | + self.reason = reason |
150 | |
151 | def want(self, path): |
152 | if self.expected_path is not None: |
153 | assert self.expected_path == path, ( |
154 | "Called with cwd of %s, expected %s" |
155 | % (path, self.expected_path)) |
156 | - return self.score |
157 | + return self.score, self.reason |
158 | |
159 | def get_info(self, path): |
160 | if self.expected_path is not None: |
161 | @@ -216,7 +262,14 @@ |
162 | |
163 | def __init__(self, path, tried_backends, |
164 | disallowed_backends=None): |
165 | - backend_names = sorted([b.name for b in tried_backends]) |
166 | + backend_names = [] |
167 | + for backend, reason in tried_backends: |
168 | + if reason: |
169 | + name = '%s (%s)' % (backend.name, reason) |
170 | + else: |
171 | + name = backend.name |
172 | + backend_names.append(name) |
173 | + backend_names = sorted(backend_names) |
174 | msg = ("No eligible backends for %s. Tried %s" |
175 | % (path, ', '.join(backend_names))) |
176 | if disallowed_backends is not None: |
177 | @@ -270,15 +323,18 @@ |
178 | raise NoBackend() |
179 | disallowed, backends = self._split_disallowed_backends(self.backends) |
180 | eligible = [] |
181 | + ineligible = [] |
182 | for backend in backends: |
183 | - score = backend.want(path) |
184 | + score, reason = backend.want(path) |
185 | trace.debug(' %s scored %s' % (backend.name, score)) |
186 | if score > 0: |
187 | eligible.append((score, backend)) |
188 | + else: |
189 | + ineligible.append((backend, reason)) |
190 | if not eligible: |
191 | # FIXME: some info about the problem. Bug 809423. |
192 | raise NoEligibleBackend( |
193 | - path, backends, |
194 | + path, ineligible, |
195 | disallowed_backends=disallowed) |
196 | return sorted( |
197 | eligible, key=lambda (score, backend): (-score, backend.name)) |
198 | |
199 | === modified file 'pkgme/backends/dummy/want' |
200 | --- pkgme/backends/dummy/want 2011-12-07 21:03:08 +0000 |
201 | +++ pkgme/backends/dummy/want 2012-04-27 17:22:19 +0000 |
202 | @@ -3,5 +3,5 @@ |
203 | if [ -e is_pkgme_test ]; then |
204 | echo 100 |
205 | else |
206 | - echo 0 |
207 | + echo '{"score": 0, "reason": "\"is_pkgme_test\" does not exist"}' |
208 | fi |
209 | |
210 | === modified file 'pkgme/backends/python/want' |
211 | --- pkgme/backends/python/want 2011-11-25 01:50:29 +0000 |
212 | +++ pkgme/backends/python/want 2012-04-27 17:22:19 +0000 |
213 | @@ -1,5 +1,6 @@ |
214 | #!/usr/bin/env python |
215 | |
216 | +import json |
217 | import os |
218 | |
219 | |
220 | @@ -7,4 +8,4 @@ |
221 | if os.path.isfile("setup.py"): |
222 | print 20 |
223 | else: |
224 | - print 0 |
225 | + print json.dumps({'score': 0, 'reason': "Couldn't find a setup.py"}) |
226 | |
227 | === modified file 'pkgme/tests/test_backend.py' |
228 | --- pkgme/tests/test_backend.py 2012-02-01 14:38:48 +0000 |
229 | +++ pkgme/tests/test_backend.py 2012-04-27 17:22:19 +0000 |
230 | @@ -17,6 +17,7 @@ |
231 | NoEligibleBackend, |
232 | StaticBackend, |
233 | StaticLoader, |
234 | + WantError, |
235 | ) |
236 | from pkgme.project_info import ( |
237 | MultipleExternalHelpersInfo, |
238 | @@ -61,7 +62,71 @@ |
239 | script = "#!/bin/sh\necho 10\n" |
240 | tempdir = self.make_want_script(script) |
241 | backend = ExternalHelpersBackend(self.getUniqueString(), tempdir.path) |
242 | - self.assertEqual(10, backend.want(tempdir.path)) |
243 | + self.assertEqual((10, None), backend.want(tempdir.path)) |
244 | + |
245 | + def test_empty_want_script(self): |
246 | + script = "#!/bin/sh\n" |
247 | + tempdir = self.make_want_script(script) |
248 | + backend = ExternalHelpersBackend(self.getUniqueString(), tempdir.path) |
249 | + e = self.assertRaises(WantError, backend.want, tempdir.path) |
250 | + self.assertEqual( |
251 | + "Backend %s (%s) returned invalid score from 'want' script: ''" |
252 | + % (backend.name, tempdir.path), |
253 | + str(e)) |
254 | + |
255 | + def test_want_script_with_reason(self): |
256 | + reason = self.getUniqueString() |
257 | + script = ( |
258 | + "#!/usr/bin/python\n" |
259 | + "import json\n" |
260 | + "print json.dumps({'score': 10, 'reason': %r})" |
261 | + % (reason,)) |
262 | + tempdir = self.make_want_script(script) |
263 | + backend = ExternalHelpersBackend(self.getUniqueString(), tempdir.path) |
264 | + self.assertEqual((10, reason), backend.want(tempdir.path)) |
265 | + |
266 | + def test_want_script_with_reason_and_bad_score(self): |
267 | + reason = self.getUniqueString() |
268 | + script = ( |
269 | + "#!/usr/bin/python\n" |
270 | + "import json\n" |
271 | + "print json.dumps({'score': 'foo', 'reason': '%s'})" |
272 | + % (reason,)) |
273 | + name = self.getUniqueString() |
274 | + tempdir = self.make_want_script(script) |
275 | + backend = ExternalHelpersBackend(name, tempdir.path) |
276 | + e = self.assertRaises(WantError, backend.want, tempdir.path) |
277 | + self.assertEqual( |
278 | + "Backend %s (%s) returned non-integer score from '%s' " |
279 | + "script: u'foo'" |
280 | + % (name, tempdir.path, ExternalHelpersBackend.WANT_SCRIPT_NAME), |
281 | + str(e)) |
282 | + |
283 | + def test_want_script_with_no_score(self): |
284 | + reason = self.getUniqueString() |
285 | + script = ( |
286 | + "#!/usr/bin/python\n" |
287 | + "import json\n" |
288 | + "print json.dumps({'reason': '%s'})" |
289 | + % (reason,)) |
290 | + name = self.getUniqueString() |
291 | + tempdir = self.make_want_script(script) |
292 | + backend = ExternalHelpersBackend(name, tempdir.path) |
293 | + e = self.assertRaises(WantError, backend.want, tempdir.path) |
294 | + self.assertEqual( |
295 | + "Backend %s (%s) did not return a score from '%s' script: %s" |
296 | + % (name, tempdir.path, ExternalHelpersBackend.WANT_SCRIPT_NAME, |
297 | + "{u'reason': u'%s'}" % (reason,)), |
298 | + str(e)) |
299 | + |
300 | + def test_want_script_with_missing_reason(self): |
301 | + script = ( |
302 | + "#!/usr/bin/python\n" |
303 | + "import json\n" |
304 | + "print json.dumps({'score': 10})") |
305 | + tempdir = self.make_want_script(script) |
306 | + backend = ExternalHelpersBackend(self.getUniqueString(), tempdir.path) |
307 | + self.assertEqual((10, None), backend.want(tempdir.path)) |
308 | |
309 | def test_missing_want_script(self): |
310 | tempdir = self.useFixture(TempdirFixture()) |
311 | @@ -92,12 +157,27 @@ |
312 | tempdir = self.make_want_script(script) |
313 | name = self.getUniqueString() |
314 | backend = ExternalHelpersBackend(name, tempdir.path) |
315 | - e = self.assertRaises(AssertionError, backend.want, tempdir.path) |
316 | + e = self.assertRaises(WantError, backend.want, tempdir.path) |
317 | self.assertEqual( |
318 | - "Backend %s (%s) returned non-integer score from '%s' script: foo" |
319 | + "Backend %s (%s) returned invalid score from '%s' script: 'foo'" |
320 | % (name, tempdir.path, ExternalHelpersBackend.WANT_SCRIPT_NAME), |
321 | str(e)) |
322 | |
323 | + def test_want_script_returns_list(self): |
324 | + script = ( |
325 | + "#!/usr/bin/python\n" |
326 | + "import json\n" |
327 | + "print json.dumps([1, 2, 3])") |
328 | + tempdir = self.make_want_script(script) |
329 | + name = self.getUniqueString() |
330 | + backend = ExternalHelpersBackend(name, tempdir.path) |
331 | + e = self.assertRaises(WantError, backend.want, tempdir.path) |
332 | + self.assertEqual( |
333 | + ("Backend %s (%s) returned invalid score " |
334 | + "from '%s' script: [1, 2, 3]" |
335 | + % (name, tempdir.path, ExternalHelpersBackend.WANT_SCRIPT_NAME)), |
336 | + str(e)) |
337 | + |
338 | def test_want_script_run_in_correct_dir(self): |
339 | cwd_tempdir = self.useFixture(TempdirFixture()) |
340 | script = """#!%s |
341 | @@ -110,7 +190,7 @@ |
342 | backend_tempdir = self.make_want_script(script) |
343 | backend = ExternalHelpersBackend( |
344 | self.getUniqueString(), backend_tempdir.path) |
345 | - self.assertEqual(10, backend.want(cwd_tempdir.path)) |
346 | + self.assertEqual((10, None), backend.want(cwd_tempdir.path)) |
347 | |
348 | def test_get_info_with_multiple(self): |
349 | helpers_path = self.getUniqueString() |
350 | @@ -165,6 +245,21 @@ |
351 | selector = BackendSelector(backends) |
352 | self.assertRaises(NoEligibleBackend, selector.get_info, path) |
353 | |
354 | + def test_reasons_for_ineligbility(self): |
355 | + backend1 = StaticBackend('no-reason', 0, None) |
356 | + backend2 = StaticBackend( |
357 | + 'a', 0, None, reason=self.getUniqueString()) |
358 | + backend3 = StaticBackend( |
359 | + 'b', 0, None, reason=self.getUniqueString()) |
360 | + backends = [backend1, backend2, backend3] |
361 | + selector = BackendSelector(backends) |
362 | + e = self.assertRaises( |
363 | + NoEligibleBackend, selector.get_info, '/path') |
364 | + self.assertEqual( |
365 | + 'No eligible backends for /path. Tried a (%s), b (%s), ' |
366 | + 'no-reason' % (backend2.reason, backend3.reason), |
367 | + str(e)) |
368 | + |
369 | def test_selects_first_name_in_tie(self): |
370 | info1 = self.getUniqueString() |
371 | info2 = self.getUniqueString() |
372 | |
373 | === modified file 'pkgme/tests/test_python_backend.py' |
374 | --- pkgme/tests/test_python_backend.py 2011-11-25 01:52:28 +0000 |
375 | +++ pkgme/tests/test_python_backend.py 2012-04-27 17:22:19 +0000 |
376 | @@ -1,5 +1,3 @@ |
377 | -import os |
378 | - |
379 | from fixtures import TestWithFixtures |
380 | from testtools import TestCase |
381 | |
382 | @@ -16,12 +14,13 @@ |
383 | tempdir = self.useFixture(TempdirFixture()) |
384 | tempdir.touch('setup.py') |
385 | backend = ExternalHelpersBackend("python", backend_dir) |
386 | - self.assertEqual(20, backend.want(tempdir.path)) |
387 | + self.assertEqual((20, None), backend.want(tempdir.path)) |
388 | |
389 | def test_want_without_setup_py(self): |
390 | tempdir = self.useFixture(TempdirFixture()) |
391 | backend = ExternalHelpersBackend("python", backend_dir) |
392 | - self.assertEqual(0, backend.want(tempdir.path)) |
393 | + self.assertEqual( |
394 | + (0, "Couldn't find a setup.py"), backend.want(tempdir.path)) |
395 | |
396 | def test_all_info(self): |
397 | tempdir = self.useFixture(TempdirFixture()) |
398 | |
399 | === modified file 'pkgme/tests/test_vala_backend.py' |
400 | --- pkgme/tests/test_vala_backend.py 2011-07-15 09:22:47 +0000 |
401 | +++ pkgme/tests/test_vala_backend.py 2012-04-27 17:22:19 +0000 |
402 | @@ -47,31 +47,31 @@ |
403 | self.tempdir.create_file("configure.ac", "") |
404 | self.tempdir.create_file("main.vala", "") |
405 | backend = self.get_backend() |
406 | - self.assertEqual(20, backend.want(self.tempdir.path)) |
407 | + self.assertEqual((20, None), backend.want(self.tempdir.path)) |
408 | |
409 | def test_want_one_deep(self): |
410 | self.tempdir.create_file("configure.ac", "") |
411 | self.tempdir.create_file(os.path.join("one", "main.vala"), "") |
412 | backend = self.get_backend() |
413 | - self.assertEqual(20, backend.want(self.tempdir.path)) |
414 | + self.assertEqual((20, None), backend.want(self.tempdir.path)) |
415 | |
416 | def test_want_two_deep(self): |
417 | self.tempdir.create_file("configure.ac", "") |
418 | self.tempdir.create_file( |
419 | os.path.join("one", "two", "main.vala"), "") |
420 | backend = self.get_backend() |
421 | - self.assertEqual(0, backend.want(self.tempdir.path)) |
422 | + self.assertEqual((0, None), backend.want(self.tempdir.path)) |
423 | |
424 | def test_want_without_configure_ac(self): |
425 | self.tempdir.create_file("main.vala", "") |
426 | backend = self.get_backend() |
427 | - self.assertEqual(0, backend.want(self.tempdir.path)) |
428 | + self.assertEqual((0, None), backend.want(self.tempdir.path)) |
429 | |
430 | def test_want_with_configure_in(self): |
431 | self.tempdir.create_file("configure.in", "") |
432 | self.tempdir.create_file("main.vala", "") |
433 | backend = self.get_backend() |
434 | - self.assertEqual(20, backend.want(self.tempdir.path)) |
435 | + self.assertEqual((20, None), backend.want(self.tempdir.path)) |
436 | |
437 | def test_architecture(self): |
438 | info = self.get_info() |
Hi,
291 + def test_want_ script_ with_missing_ score(self) :
should be ..._missing_reason I think?
Aside from that they only change I might make would be to extract a
method for running a python script with a given want response as
that is repeated in a few tests. I'm happy for it to land without
that change though.
Thanks,
James