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
=== modified file 'devportalbinary/backend.py'
--- devportalbinary/backend.py 2012-08-28 11:02:45 +0000
+++ devportalbinary/backend.py 2012-09-11 15:29:20 +0000
@@ -2,8 +2,8 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = [4__all__ = [
5 'all_info',5 'backend_script',
6 'want',6 'convert_info',
7 ]7 ]
88
9from functools import wraps9from functools import wraps
@@ -21,7 +21,7 @@
21BASE_USER_ERROR = PkgmeError21BASE_USER_ERROR = PkgmeError
2222
2323
24def _convert_info(info):24def convert_info(info):
25 return dict((element.name, data) for element, data in info.items())25 return dict((element.name, data) for element, data in info.items())
2626
2727
@@ -35,17 +35,18 @@
35 error handling. It takes care of converting the data to JSON and printing35 error handling. It takes care of converting the data to JSON and printing
36 it out.36 it out.
3737
38 :param get_data: A function that returns a JSON-serializable object.38 :param get_data: A function that returns a JSON-serializable object
39 when passed a path to inspect.
39 :return: A function that can be invoked as the 'main' of a backend script.40 :return: A function that can be invoked as the 'main' of a backend script.
40 """41 """
41 @wraps(get_data)42 @wraps(get_data)
42 def _wrapper(backend, output_stream=None, error_stream=None):43 def _wrapper(path, output_stream=None, error_stream=None):
43 if output_stream is None:44 if output_stream is None:
44 output_stream = sys.stdout45 output_stream = sys.stdout
45 if error_stream is None:46 if error_stream is None:
46 error_stream = sys.stderr47 error_stream = sys.stderr
47 try:48 try:
48 info = get_data(backend)49 info = get_data(path)
49 except BASE_USER_ERROR, e:50 except BASE_USER_ERROR, e:
50 error_stream.write(str(e))51 error_stream.write(str(e))
51 error_stream.write('\n')52 error_stream.write('\n')
@@ -54,15 +55,3 @@
54 json.dump(info, output_stream)55 json.dump(info, output_stream)
55 return 056 return 0
56 return _wrapper57 return _wrapper
57
58
59@backend_script
60def all_info(backend):
61 metadata = backend.get_metadata()
62 info = backend.get_info(metadata)
63 return _convert_info(info)
64
65
66@backend_script
67def want(backend):
68 return backend.want()
6958
=== modified file 'devportalbinary/backends/binary/all_info'
--- devportalbinary/backends/binary/all_info 2012-08-27 22:34:59 +0000
+++ devportalbinary/backends/binary/all_info 2012-09-11 15:29:20 +0000
@@ -2,11 +2,11 @@
2# Copyright 2011 Canonical Ltd. This software is licensed under the2# Copyright 2011 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
44
5import os
5import sys6import sys
67
7from devportalbinary.backend import all_info8from devportalbinary.binary import all_info
8from devportalbinary.binary import BinaryBackend
99
1010
11if __name__ == '__main__':11if __name__ == '__main__':
12 sys.exit(all_info(BinaryBackend()))12 sys.exit(all_info(os.getcwd()))
1313
=== modified file 'devportalbinary/backends/binary/want'
--- devportalbinary/backends/binary/want 2012-08-27 22:34:59 +0000
+++ devportalbinary/backends/binary/want 2012-09-11 15:29:20 +0000
@@ -2,11 +2,11 @@
2# Copyright 2011 Canonical Ltd. This software is licensed under the2# Copyright 2011 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
44
5import os
5import sys6import sys
67
7from devportalbinary.backend import want8from devportalbinary.binary import want
8from devportalbinary.binary import BinaryBackend
99
1010
11if __name__ == '__main__':11if __name__ == '__main__':
12 sys.exit(want(BinaryBackend()))12 sys.exit(want(os.getcwd()))
1313
=== modified file 'devportalbinary/backends/pdf/all_info'
--- devportalbinary/backends/pdf/all_info 2012-08-27 22:34:59 +0000
+++ devportalbinary/backends/pdf/all_info 2012-09-11 15:29:20 +0000
@@ -2,11 +2,11 @@
2# Copyright 2011 Canonical Ltd. This software is licensed under the2# Copyright 2011 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
44
5import os
5import sys6import sys
67
7from devportalbinary.backend import all_info8from devportalbinary.pdf import all_info
8from devportalbinary.pdf import PdfBackend
99
1010
11if __name__ == '__main__':11if __name__ == '__main__':
12 sys.exit(all_info(PdfBackend()))12 sys.exit(all_info(os.getcwd()))
1313
=== modified file 'devportalbinary/backends/pdf/want'
--- devportalbinary/backends/pdf/want 2012-08-27 22:34:59 +0000
+++ devportalbinary/backends/pdf/want 2012-09-11 15:29:20 +0000
@@ -2,11 +2,11 @@
2# Copyright 2011 Canonical Ltd. This software is licensed under the2# Copyright 2011 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
44
5import os
5import sys6import sys
67
7from devportalbinary.backend import want8from devportalbinary.pdf import want
8from devportalbinary.pdf import PdfBackend
99
1010
11if __name__ == '__main__':11if __name__ == '__main__':
12 sys.exit(want(PdfBackend()))12 sys.exit(want(os.getcwd()))
1313
=== modified file 'devportalbinary/binary.py'
--- devportalbinary/binary.py 2012-08-30 14:30:41 +0000
+++ devportalbinary/binary.py 2012-09-11 15:29:20 +0000
@@ -44,7 +44,11 @@
4444
45from devportalbinary.configuration import load_configuration45from devportalbinary.configuration import load_configuration
46from devportalbinary.database import PackageDatabase46from devportalbinary.database import PackageDatabase
47from devportalbinary.metadata import MetadataBackend47from devportalbinary.metadata import (
48 make_all_info_fn,
49 make_want_fn,
50 MetadataBackend,
51 )
4852
4953
50OVERRIDE_DH_STRIP_TEMPLATE = """54OVERRIDE_DH_STRIP_TEMPLATE = """
@@ -408,3 +412,7 @@
408 'reason': 'Has ELF binaries and a metadata file'}412 'reason': 'Has ELF binaries and a metadata file'}
409 else:413 else:
410 return {'score': 0, 'reason': 'No ELF binaries found.'}414 return {'score': 0, 'reason': 'No ELF binaries found.'}
415
416
417want = make_want_fn(BinaryBackend)
418all_info = make_all_info_fn(BinaryBackend)
411419
=== modified file 'devportalbinary/metadata.py'
--- devportalbinary/metadata.py 2012-08-27 22:02:42 +0000
+++ devportalbinary/metadata.py 2012-09-11 15:29:20 +0000
@@ -46,6 +46,10 @@
46 )46 )
47from pkgme.project_info import DictInfo47from pkgme.project_info import DictInfo
4848
49from .backend import (
50 backend_script,
51 convert_info,
52 )
49from .utils import get_latest_stable_ubuntu_distroseries53from .utils import get_latest_stable_ubuntu_distroseries
5054
5155
@@ -501,3 +505,21 @@
501 Specific backends should override this.505 Specific backends should override this.
502 """506 """
503 raise NotImplementedError(self.want_with_metadata)507 raise NotImplementedError(self.want_with_metadata)
508
509
510def make_want_fn(backend_cls):
511 def metadata_want(path):
512 backend = backend_cls(path)
513 return backend.want()
514 metadata_want.__name__ = '{}_want'.format(backend_cls.__name__)
515 return backend_script(metadata_want)
516
517
518def make_all_info_fn(backend_cls):
519 def metadata_all_info(path):
520 backend = backend_cls(path)
521 metadata = backend.get_metadata()
522 info = backend.get_info(metadata)
523 return convert_info(info)
524 metadata_all_info.__name__ = '{}_all_info'.format(backend_cls.__name__)
525 return backend_script(metadata_all_info)
504526
=== modified file 'devportalbinary/pdf.py'
--- devportalbinary/pdf.py 2012-08-23 12:09:47 +0000
+++ devportalbinary/pdf.py 2012-09-11 15:29:20 +0000
@@ -3,7 +3,11 @@
33
4import os4import os
55
6from devportalbinary.metadata import MetadataBackend6from devportalbinary.metadata import (
7 make_all_info_fn,
8 make_want_fn,
9 MetadataBackend,
10 )
711
812
9class PdfBackend(MetadataBackend):13class PdfBackend(MetadataBackend):
@@ -66,3 +70,7 @@
66 else:70 else:
67 reason = 'More files than just a PDF: %r' % (sorted(files),)71 reason = 'More files than just a PDF: %r' % (sorted(files),)
68 return {'score': score, 'reason': reason}72 return {'score': score, 'reason': reason}
73
74
75want = make_want_fn(PdfBackend)
76all_info = make_all_info_fn(PdfBackend)
6977
=== modified file 'devportalbinary/tests/test_backend.py'
--- devportalbinary/tests/test_backend.py 2012-08-28 11:02:45 +0000
+++ devportalbinary/tests/test_backend.py 2012-09-11 15:29:20 +0000
@@ -2,6 +2,7 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import json4import json
5import os
5from StringIO import StringIO6from StringIO import StringIO
67
7from fixtures import MonkeyPatch8from fixtures import MonkeyPatch
@@ -9,11 +10,10 @@
9from testtools import TestCase10from testtools import TestCase
1011
11from devportalbinary.backend import (12from devportalbinary.backend import (
12 all_info,13 backend_script,
13 BASE_USER_ERROR,14 BASE_USER_ERROR,
14 _convert_info,15 convert_info,
15 USER_ERROR_RETURN_CODE,16 USER_ERROR_RETURN_CODE,
16 want,
17 )17 )
1818
1919
@@ -26,8 +26,8 @@
26 def get_metadata(self):26 def get_metadata(self):
27 return self._metadata27 return self._metadata
2828
29 def get_info(self, metadata):29 def get_info(self, path):
30 return {PackageName: metadata}30 return {PackageName.name: self._metadata}
3131
32 def want(self):32 def want(self):
33 return self._want33 return self._want
@@ -46,29 +46,29 @@
46 def get_metadata(self):46 def get_metadata(self):
47 return {}47 return {}
4848
49 def get_info(self, metadata):49 def get_info(self, path):
50 raise self._raise()50 raise self._raise()
5151
52 def want(self):52 def want(self):
53 raise self._raise()53 raise self._raise()
5454
5555
56class TestDumpJSON(TestCase):56class TestConvertInfo(TestCase):
5757
58 def test_convert(self):58 def test_convert(self):
59 package_name = self.getUniqueString()59 package_name = self.getUniqueString()
60 info = {PackageName: package_name}60 info = {PackageName: package_name}
61 self.assertEqual(61 self.assertEqual(
62 {PackageName.name: package_name}, _convert_info(info))62 {PackageName.name: package_name}, convert_info(info))
6363
6464
65class TestAllInfo(TestCase):65class TestBackendScript(TestCase):
6666
67 def test_all_info(self):67 def test_backend_script(self):
68 metadata = {'foo': 'bar'}68 metadata = {'foo': 'bar'}
69 backend = DummyBackend(None, metadata)69 backend = DummyBackend(None, metadata)
70 output = StringIO()70 output = StringIO()
71 all_info(backend, output)71 backend_script(backend.get_info)(os.getcwd(), output)
72 self.assertEqual(72 self.assertEqual(
73 {PackageName.name: metadata}, json.loads(output.getvalue()))73 {PackageName.name: metadata}, json.loads(output.getvalue()))
7474
@@ -77,7 +77,7 @@
77 self.useFixture(MonkeyPatch('sys.stdout', stream))77 self.useFixture(MonkeyPatch('sys.stdout', stream))
78 metadata = {'foo': 'bar'}78 metadata = {'foo': 'bar'}
79 backend = DummyBackend(None, metadata)79 backend = DummyBackend(None, metadata)
80 all_info(backend)80 backend_script(backend.get_info)(os.getcwd())
81 self.assertEqual(81 self.assertEqual(
82 {PackageName.name: metadata}, json.loads(stream.getvalue()))82 {PackageName.name: metadata}, json.loads(stream.getvalue()))
8383
@@ -85,61 +85,28 @@
85 metadata = {'foo': 'bar'}85 metadata = {'foo': 'bar'}
86 backend = DummyBackend(None, metadata)86 backend = DummyBackend(None, metadata)
87 output = StringIO()87 output = StringIO()
88 result = all_info(backend, output)88 result = backend_script(backend.get_info)(os.getcwd(), output)
89 self.assertEqual(0, result)89 self.assertEqual(0, result)
9090
91 def test_normal_error_raises(self):91 def test_normal_error_raises(self):
92 output = StringIO()92 output = StringIO()
93 backend = BrokenBackend(RuntimeError, "Told you so")93 backend = BrokenBackend(RuntimeError, "Told you so")
94 self.assertRaises(RuntimeError, all_info, backend, output)94 self.assertRaises(RuntimeError, backend_script(backend.get_info),
9595 os.getcwd(), output)
96 def test_user_error_logs(self):96
97 output = StringIO()97 def test_user_error_logs(self):
98 error = StringIO()98 output = StringIO()
99 backend = BrokenBackend(BASE_USER_ERROR, "Told you so")99 error = StringIO()
100 result = all_info(backend, output, error)100 backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
101 self.assertEqual(USER_ERROR_RETURN_CODE, result)101 result = backend_script(backend.get_info)(os.getcwd(), output, error)
102 self.assertEqual('', output.getvalue())102 self.assertEqual(USER_ERROR_RETURN_CODE, result)
103 self.assertEqual("Told you so\n", error.getvalue())103 self.assertEqual('', output.getvalue())
104104 self.assertEqual("Told you so\n", error.getvalue())
105 def test_error_in_get_metatada(self):105
106 output = StringIO()106 def test_passes_path(self):
107 error = StringIO()107 output = StringIO()
108 backend = BrokenBackend(BASE_USER_ERROR, "Told you so")108 expected_path = self.getUniqueString()
109 backend.get_metadata = backend._raise109 def return_path(path):
110 result = all_info(backend, output, error)110 return path
111 self.assertEqual(USER_ERROR_RETURN_CODE, result)111 result = backend_script(return_path)(expected_path, output)
112 self.assertEqual('', output.getvalue())112 self.assertEqual(expected_path, json.loads(output.getvalue()))
113 self.assertEqual("Told you so\n", error.getvalue())
114
115
116class TestWant(TestCase):
117
118 def test_default_to_stdout(self):
119 stream = StringIO()
120 self.useFixture(MonkeyPatch('sys.stdout', stream))
121 want_reason = {'score': 10, 'reason': 'reason'}
122 backend = DummyBackend(want_reason, None)
123 want(backend)
124 self.assertEqual(want_reason, json.loads(stream.getvalue()))
125
126 def test_return_zero_on_success(self):
127 want_reason = {'score': 10, 'reason': 'reason'}
128 backend = DummyBackend(want_reason, None)
129 output = StringIO()
130 result = want(backend, output)
131 self.assertEqual(0, result)
132
133 def test_normal_error_raises(self):
134 output = StringIO()
135 backend = BrokenBackend(RuntimeError, "Told you so")
136 self.assertRaises(RuntimeError, want, backend, output)
137
138 def test_user_error_logs(self):
139 output = StringIO()
140 error = StringIO()
141 backend = BrokenBackend(BASE_USER_ERROR, "Told you so")
142 result = want(backend, output, error)
143 self.assertEqual(USER_ERROR_RETURN_CODE, result)
144 self.assertEqual('', output.getvalue())
145 self.assertEqual("Told you so\n", error.getvalue())
146113
=== modified file 'devportalbinary/tests/test_metadata.py'
--- devportalbinary/tests/test_metadata.py 2012-08-27 22:02:42 +0000
+++ devportalbinary/tests/test_metadata.py 2012-09-11 15:29:20 +0000
@@ -1,6 +1,7 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from cStringIO import StringIO
4import json5import json
5import os6import os
67
@@ -36,6 +37,7 @@
36 make_tree,37 make_tree,
37 )38 )
3839
40from devportalbinary.backend import convert_info
39from devportalbinary.metadata import (41from devportalbinary.metadata import (
40 format_install_map,42 format_install_map,
41 get_desktop_file,43 get_desktop_file,
@@ -43,6 +45,8 @@
43 get_install_file,45 get_install_file,
44 get_install_map,46 get_install_map,
45 get_metadata,47 get_metadata,
48 make_all_info_fn,
49 make_want_fn,
46 MetadataBackend,50 MetadataBackend,
47 )51 )
48from devportalbinary.testing import (52from devportalbinary.testing import (
@@ -649,4 +653,39 @@
649 get_install_basedir(package_name))653 get_install_basedir(package_name))
650654
651655
656class TestMakeWantFn(TestCase):
657
658 def test_calls_want(self):
659 want = make_want_fn(DummyBackend)
660 path = self.getUniqueString()
661 backend = DummyBackend(path)
662 output = StringIO()
663 want(path, output)
664 self.assertEqual(backend.want(), json.loads(output.getvalue()))
665
666 def test_names_fn(self):
667 want = make_want_fn(DummyBackend)
668 self.assertEqual('DummyBackend_want', want.__name__)
669
670
671class TestMakeAllInfoFn(TestCase):
672
673 def test_calls_get_info(self):
674 class VeryDummyBackend(MetadataBackend):
675 def get_info(self, metadata):
676 return {PackageName: metadata}
677 expected_metadata = dict(foo="bar")
678 all_info = make_all_info_fn(VeryDummyBackend)
679 path = self.useFixture(MetadataFixture(expected_metadata)).path
680 backend = DummyBackend(path)
681 output = StringIO()
682 all_info(path, output)
683 self.assertEqual({PackageName.name: expected_metadata},
684 json.loads(output.getvalue()))
685
686 def test_names_fn(self):
687 all_info = make_all_info_fn(DummyBackend)
688 self.assertEqual('DummyBackend_all_info', all_info.__name__)
689
690
652# XXX: Assuming icon name in desktop == package name691# XXX: Assuming icon name in desktop == package name

Subscribers

People subscribed via source and target branches