Merge lp:~james-w/pkgme-devportal/better-api into lp:pkgme-devportal

Proposed by James Westby
Status: Merged
Approved by: James Westby
Approved revision: no longer in the source branch.
Merged at revision: 107
Proposed branch: lp:~james-w/pkgme-devportal/better-api
Merge into: lp:pkgme-devportal
Diff against target: 465 lines (+138/-105)
10 files modified
devportalbinary/backend.py (+7/-18)
devportalbinary/backends/binary/all_info (+3/-3)
devportalbinary/backends/binary/want (+3/-3)
devportalbinary/backends/pdf/all_info (+3/-3)
devportalbinary/backends/pdf/want (+3/-3)
devportalbinary/binary.py (+9/-1)
devportalbinary/metadata.py (+22/-0)
devportalbinary/pdf.py (+9/-1)
devportalbinary/tests/test_backend.py (+40/-73)
devportalbinary/tests/test_metadata.py (+39/-0)
To merge this branch: bzr merge lp:~james-w/pkgme-devportal/better-api
Reviewer Review Type Date Requested Status
Jonathan Lange Approve
Review via email: mp+123772@code.launchpad.net

Commit message

Add a more direct API for error handling and serialization of backends.

Description of the change

Hi,

Here's the start of an attempt to straighten out the APIs in pkgme-devportal.

This makes backend_script more directly reflect the API of a backend script
(though perhaps the path could be implicit as it's defined to be $PWD for
backends). It takes a function and just wraps it in the error handling
and serialization.

Next it removes the want() and all_info() functions because they are
specific to a particular way of writing a backend. In fact they move
to metadata.py and become function generators, which take a MetadataBackend
subclass and wrap it with the necessary stuff.

The rest is just changes to match this change.

I felt that this was a better organization, but it's not where I'll be stopping.
The stuff in metadata.py still assumes too much, and the MetadataBackend
class still does too much, but now I can change that to fit a better API
than before.

Thanks,

James

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Looks good to me.

I'm not sure why make_all_info_fn and make_want_fn wrap their returns in backend_script. Maybe that should be done in the scripts themselves? It doesn't really matter.

I look forward to seeing where this goes.

Thanks,
jml

review: Approve
107. By James Westby

[r=james-w] Add a more direct API for error handling and serialization of backends.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'devportalbinary/backend.py'
2--- devportalbinary/backend.py 2012-08-28 11:02:45 +0000
3+++ devportalbinary/backend.py 2012-09-11 15:29:20 +0000
4@@ -2,8 +2,8 @@
5 # GNU Affero General Public License version 3 (see the file LICENSE).
6
7 __all__ = [
8- 'all_info',
9- 'want',
10+ 'backend_script',
11+ 'convert_info',
12 ]
13
14 from functools import wraps
15@@ -21,7 +21,7 @@
16 BASE_USER_ERROR = PkgmeError
17
18
19-def _convert_info(info):
20+def convert_info(info):
21 return dict((element.name, data) for element, data in info.items())
22
23
24@@ -35,17 +35,18 @@
25 error handling. It takes care of converting the data to JSON and printing
26 it out.
27
28- :param get_data: A function that returns a JSON-serializable object.
29+ :param get_data: A function that returns a JSON-serializable object
30+ when passed a path to inspect.
31 :return: A function that can be invoked as the 'main' of a backend script.
32 """
33 @wraps(get_data)
34- def _wrapper(backend, output_stream=None, error_stream=None):
35+ def _wrapper(path, output_stream=None, error_stream=None):
36 if output_stream is None:
37 output_stream = sys.stdout
38 if error_stream is None:
39 error_stream = sys.stderr
40 try:
41- info = get_data(backend)
42+ info = get_data(path)
43 except BASE_USER_ERROR, e:
44 error_stream.write(str(e))
45 error_stream.write('\n')
46@@ -54,15 +55,3 @@
47 json.dump(info, output_stream)
48 return 0
49 return _wrapper
50-
51-
52-@backend_script
53-def all_info(backend):
54- metadata = backend.get_metadata()
55- info = backend.get_info(metadata)
56- return _convert_info(info)
57-
58-
59-@backend_script
60-def want(backend):
61- return backend.want()
62
63=== modified file 'devportalbinary/backends/binary/all_info'
64--- devportalbinary/backends/binary/all_info 2012-08-27 22:34:59 +0000
65+++ devportalbinary/backends/binary/all_info 2012-09-11 15:29:20 +0000
66@@ -2,11 +2,11 @@
67 # Copyright 2011 Canonical Ltd. This software is licensed under the
68 # GNU Affero General Public License version 3 (see the file LICENSE).
69
70+import os
71 import sys
72
73-from devportalbinary.backend import all_info
74-from devportalbinary.binary import BinaryBackend
75+from devportalbinary.binary import all_info
76
77
78 if __name__ == '__main__':
79- sys.exit(all_info(BinaryBackend()))
80+ sys.exit(all_info(os.getcwd()))
81
82=== modified file 'devportalbinary/backends/binary/want'
83--- devportalbinary/backends/binary/want 2012-08-27 22:34:59 +0000
84+++ devportalbinary/backends/binary/want 2012-09-11 15:29:20 +0000
85@@ -2,11 +2,11 @@
86 # Copyright 2011 Canonical Ltd. This software is licensed under the
87 # GNU Affero General Public License version 3 (see the file LICENSE).
88
89+import os
90 import sys
91
92-from devportalbinary.backend import want
93-from devportalbinary.binary import BinaryBackend
94+from devportalbinary.binary import want
95
96
97 if __name__ == '__main__':
98- sys.exit(want(BinaryBackend()))
99+ sys.exit(want(os.getcwd()))
100
101=== modified file 'devportalbinary/backends/pdf/all_info'
102--- devportalbinary/backends/pdf/all_info 2012-08-27 22:34:59 +0000
103+++ devportalbinary/backends/pdf/all_info 2012-09-11 15:29:20 +0000
104@@ -2,11 +2,11 @@
105 # Copyright 2011 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108+import os
109 import sys
110
111-from devportalbinary.backend import all_info
112-from devportalbinary.pdf import PdfBackend
113+from devportalbinary.pdf import all_info
114
115
116 if __name__ == '__main__':
117- sys.exit(all_info(PdfBackend()))
118+ sys.exit(all_info(os.getcwd()))
119
120=== modified file 'devportalbinary/backends/pdf/want'
121--- devportalbinary/backends/pdf/want 2012-08-27 22:34:59 +0000
122+++ devportalbinary/backends/pdf/want 2012-09-11 15:29:20 +0000
123@@ -2,11 +2,11 @@
124 # Copyright 2011 Canonical Ltd. This software is licensed under the
125 # GNU Affero General Public License version 3 (see the file LICENSE).
126
127+import os
128 import sys
129
130-from devportalbinary.backend import want
131-from devportalbinary.pdf import PdfBackend
132+from devportalbinary.pdf import want
133
134
135 if __name__ == '__main__':
136- sys.exit(want(PdfBackend()))
137+ sys.exit(want(os.getcwd()))
138
139=== modified file 'devportalbinary/binary.py'
140--- devportalbinary/binary.py 2012-08-30 14:30:41 +0000
141+++ devportalbinary/binary.py 2012-09-11 15:29:20 +0000
142@@ -44,7 +44,11 @@
143
144 from devportalbinary.configuration import load_configuration
145 from devportalbinary.database import PackageDatabase
146-from devportalbinary.metadata import MetadataBackend
147+from devportalbinary.metadata import (
148+ make_all_info_fn,
149+ make_want_fn,
150+ MetadataBackend,
151+ )
152
153
154 OVERRIDE_DH_STRIP_TEMPLATE = """
155@@ -408,3 +412,7 @@
156 'reason': 'Has ELF binaries and a metadata file'}
157 else:
158 return {'score': 0, 'reason': 'No ELF binaries found.'}
159+
160+
161+want = make_want_fn(BinaryBackend)
162+all_info = make_all_info_fn(BinaryBackend)
163
164=== modified file 'devportalbinary/metadata.py'
165--- devportalbinary/metadata.py 2012-08-27 22:02:42 +0000
166+++ devportalbinary/metadata.py 2012-09-11 15:29:20 +0000
167@@ -46,6 +46,10 @@
168 )
169 from pkgme.project_info import DictInfo
170
171+from .backend import (
172+ backend_script,
173+ convert_info,
174+ )
175 from .utils import get_latest_stable_ubuntu_distroseries
176
177
178@@ -501,3 +505,21 @@
179 Specific backends should override this.
180 """
181 raise NotImplementedError(self.want_with_metadata)
182+
183+
184+def make_want_fn(backend_cls):
185+ def metadata_want(path):
186+ backend = backend_cls(path)
187+ return backend.want()
188+ metadata_want.__name__ = '{}_want'.format(backend_cls.__name__)
189+ return backend_script(metadata_want)
190+
191+
192+def make_all_info_fn(backend_cls):
193+ def metadata_all_info(path):
194+ backend = backend_cls(path)
195+ metadata = backend.get_metadata()
196+ info = backend.get_info(metadata)
197+ return convert_info(info)
198+ metadata_all_info.__name__ = '{}_all_info'.format(backend_cls.__name__)
199+ return backend_script(metadata_all_info)
200
201=== modified file 'devportalbinary/pdf.py'
202--- devportalbinary/pdf.py 2012-08-23 12:09:47 +0000
203+++ devportalbinary/pdf.py 2012-09-11 15:29:20 +0000
204@@ -3,7 +3,11 @@
205
206 import os
207
208-from devportalbinary.metadata import MetadataBackend
209+from devportalbinary.metadata import (
210+ make_all_info_fn,
211+ make_want_fn,
212+ MetadataBackend,
213+ )
214
215
216 class PdfBackend(MetadataBackend):
217@@ -66,3 +70,7 @@
218 else:
219 reason = 'More files than just a PDF: %r' % (sorted(files),)
220 return {'score': score, 'reason': reason}
221+
222+
223+want = make_want_fn(PdfBackend)
224+all_info = make_all_info_fn(PdfBackend)
225
226=== modified file 'devportalbinary/tests/test_backend.py'
227--- devportalbinary/tests/test_backend.py 2012-08-28 11:02:45 +0000
228+++ devportalbinary/tests/test_backend.py 2012-09-11 15:29:20 +0000
229@@ -2,6 +2,7 @@
230 # GNU Affero General Public License version 3 (see the file LICENSE).
231
232 import json
233+import os
234 from StringIO import StringIO
235
236 from fixtures import MonkeyPatch
237@@ -9,11 +10,10 @@
238 from testtools import TestCase
239
240 from devportalbinary.backend import (
241- all_info,
242+ backend_script,
243 BASE_USER_ERROR,
244- _convert_info,
245+ convert_info,
246 USER_ERROR_RETURN_CODE,
247- want,
248 )
249
250
251@@ -26,8 +26,8 @@
252 def get_metadata(self):
253 return self._metadata
254
255- def get_info(self, metadata):
256- return {PackageName: metadata}
257+ def get_info(self, path):
258+ return {PackageName.name: self._metadata}
259
260 def want(self):
261 return self._want
262@@ -46,29 +46,29 @@
263 def get_metadata(self):
264 return {}
265
266- def get_info(self, metadata):
267+ def get_info(self, path):
268 raise self._raise()
269
270 def want(self):
271 raise self._raise()
272
273
274-class TestDumpJSON(TestCase):
275+class TestConvertInfo(TestCase):
276
277 def test_convert(self):
278 package_name = self.getUniqueString()
279 info = {PackageName: package_name}
280 self.assertEqual(
281- {PackageName.name: package_name}, _convert_info(info))
282-
283-
284-class TestAllInfo(TestCase):
285-
286- def test_all_info(self):
287+ {PackageName.name: package_name}, convert_info(info))
288+
289+
290+class TestBackendScript(TestCase):
291+
292+ def test_backend_script(self):
293 metadata = {'foo': 'bar'}
294 backend = DummyBackend(None, metadata)
295 output = StringIO()
296- all_info(backend, output)
297+ backend_script(backend.get_info)(os.getcwd(), output)
298 self.assertEqual(
299 {PackageName.name: metadata}, json.loads(output.getvalue()))
300
301@@ -77,7 +77,7 @@
302 self.useFixture(MonkeyPatch('sys.stdout', stream))
303 metadata = {'foo': 'bar'}
304 backend = DummyBackend(None, metadata)
305- all_info(backend)
306+ backend_script(backend.get_info)(os.getcwd())
307 self.assertEqual(
308 {PackageName.name: metadata}, json.loads(stream.getvalue()))
309
310@@ -85,61 +85,28 @@
311 metadata = {'foo': 'bar'}
312 backend = DummyBackend(None, metadata)
313 output = StringIO()
314- result = all_info(backend, output)
315- self.assertEqual(0, result)
316-
317- def test_normal_error_raises(self):
318- output = StringIO()
319- backend = BrokenBackend(RuntimeError, "Told you so")
320- self.assertRaises(RuntimeError, all_info, backend, output)
321-
322- def test_user_error_logs(self):
323- output = StringIO()
324- error = StringIO()
325- backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
326- result = all_info(backend, output, error)
327- self.assertEqual(USER_ERROR_RETURN_CODE, result)
328- self.assertEqual('', output.getvalue())
329- self.assertEqual("Told you so\n", error.getvalue())
330-
331- def test_error_in_get_metatada(self):
332- output = StringIO()
333- error = StringIO()
334- backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
335- backend.get_metadata = backend._raise
336- result = all_info(backend, output, error)
337- self.assertEqual(USER_ERROR_RETURN_CODE, result)
338- self.assertEqual('', output.getvalue())
339- self.assertEqual("Told you so\n", error.getvalue())
340-
341-
342-class TestWant(TestCase):
343-
344- def test_default_to_stdout(self):
345- stream = StringIO()
346- self.useFixture(MonkeyPatch('sys.stdout', stream))
347- want_reason = {'score': 10, 'reason': 'reason'}
348- backend = DummyBackend(want_reason, None)
349- want(backend)
350- self.assertEqual(want_reason, json.loads(stream.getvalue()))
351-
352- def test_return_zero_on_success(self):
353- want_reason = {'score': 10, 'reason': 'reason'}
354- backend = DummyBackend(want_reason, None)
355- output = StringIO()
356- result = want(backend, output)
357- self.assertEqual(0, result)
358-
359- def test_normal_error_raises(self):
360- output = StringIO()
361- backend = BrokenBackend(RuntimeError, "Told you so")
362- self.assertRaises(RuntimeError, want, backend, output)
363-
364- def test_user_error_logs(self):
365- output = StringIO()
366- error = StringIO()
367- backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
368- result = want(backend, output, error)
369- self.assertEqual(USER_ERROR_RETURN_CODE, result)
370- self.assertEqual('', output.getvalue())
371- self.assertEqual("Told you so\n", error.getvalue())
372+ result = backend_script(backend.get_info)(os.getcwd(), output)
373+ self.assertEqual(0, result)
374+
375+ def test_normal_error_raises(self):
376+ output = StringIO()
377+ backend = BrokenBackend(RuntimeError, "Told you so")
378+ self.assertRaises(RuntimeError, backend_script(backend.get_info),
379+ os.getcwd(), output)
380+
381+ def test_user_error_logs(self):
382+ output = StringIO()
383+ error = StringIO()
384+ backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
385+ result = backend_script(backend.get_info)(os.getcwd(), output, error)
386+ self.assertEqual(USER_ERROR_RETURN_CODE, result)
387+ self.assertEqual('', output.getvalue())
388+ self.assertEqual("Told you so\n", error.getvalue())
389+
390+ def test_passes_path(self):
391+ output = StringIO()
392+ expected_path = self.getUniqueString()
393+ def return_path(path):
394+ return path
395+ result = backend_script(return_path)(expected_path, output)
396+ self.assertEqual(expected_path, json.loads(output.getvalue()))
397
398=== modified file 'devportalbinary/tests/test_metadata.py'
399--- devportalbinary/tests/test_metadata.py 2012-08-27 22:02:42 +0000
400+++ devportalbinary/tests/test_metadata.py 2012-09-11 15:29:20 +0000
401@@ -1,6 +1,7 @@
402 # Copyright 2011-2012 Canonical Ltd. This software is licensed under the
403 # GNU Affero General Public License version 3 (see the file LICENSE).
404
405+from cStringIO import StringIO
406 import json
407 import os
408
409@@ -36,6 +37,7 @@
410 make_tree,
411 )
412
413+from devportalbinary.backend import convert_info
414 from devportalbinary.metadata import (
415 format_install_map,
416 get_desktop_file,
417@@ -43,6 +45,8 @@
418 get_install_file,
419 get_install_map,
420 get_metadata,
421+ make_all_info_fn,
422+ make_want_fn,
423 MetadataBackend,
424 )
425 from devportalbinary.testing import (
426@@ -649,4 +653,39 @@
427 get_install_basedir(package_name))
428
429
430+class TestMakeWantFn(TestCase):
431+
432+ def test_calls_want(self):
433+ want = make_want_fn(DummyBackend)
434+ path = self.getUniqueString()
435+ backend = DummyBackend(path)
436+ output = StringIO()
437+ want(path, output)
438+ self.assertEqual(backend.want(), json.loads(output.getvalue()))
439+
440+ def test_names_fn(self):
441+ want = make_want_fn(DummyBackend)
442+ self.assertEqual('DummyBackend_want', want.__name__)
443+
444+
445+class TestMakeAllInfoFn(TestCase):
446+
447+ def test_calls_get_info(self):
448+ class VeryDummyBackend(MetadataBackend):
449+ def get_info(self, metadata):
450+ return {PackageName: metadata}
451+ expected_metadata = dict(foo="bar")
452+ all_info = make_all_info_fn(VeryDummyBackend)
453+ path = self.useFixture(MetadataFixture(expected_metadata)).path
454+ backend = DummyBackend(path)
455+ output = StringIO()
456+ all_info(path, output)
457+ self.assertEqual({PackageName.name: expected_metadata},
458+ json.loads(output.getvalue()))
459+
460+ def test_names_fn(self):
461+ all_info = make_all_info_fn(DummyBackend)
462+ self.assertEqual('DummyBackend_all_info', all_info.__name__)
463+
464+
465 # XXX: Assuming icon name in desktop == package name

Subscribers

People subscribed via source and target branches