Merge lp:~jml/pkgme/reason-for-want into lp:pkgme

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
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.

Revision history for this message
James Westby (james-w) wrote :

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

review: Approve
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()

Subscribers

People subscribed via source and target branches