Merge lp:~james-w/pkgme-devportal/better-api into lp:pkgme-devportal
- better-api
- Merge into trunk
Proposed by
James Westby
Status: | Merged |
---|---|
Approved by: | James Westby |
Approved revision: | 109 |
Merged at revision: | 108 |
Proposed branch: | lp:~james-w/pkgme-devportal/better-api |
Merge into: | lp:pkgme-devportal |
Diff against target: |
442 lines (+99/-129) 6 files modified
devportalbinary/binary.py (+3/-2) devportalbinary/metadata.py (+39/-43) devportalbinary/pdf.py (+5/-4) devportalbinary/tests/test_binary.py (+2/-4) devportalbinary/tests/test_metadata.py (+44/-65) devportalbinary/tests/test_pdf.py (+6/-11) |
To merge this branch: | bzr merge lp:~james-w/pkgme-devportal/better-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Lange | Approve | ||
Review via email: mp+123788@code.launchpad.net |
Commit message
Move the want stuff to class methods and split some of the code out.
Description of the change
Hi,
This moves want() to be a classmethod, because it's part of a thing you
do before you use a backend. Maybe it should be moved from the class altogether.
It also moves some logic to a more generic function, and then uses that function.
It also drops some unused code (we never used the code to load a single field
from a metadata file, we just load the whole thing and use it as a dict.)
Thanks,
James
To post a comment you must log in.
- 109. By James Westby
-
Better docstring. Thanks jml.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'devportalbinary/binary.py' |
2 | --- devportalbinary/binary.py 2012-09-11 15:21:46 +0000 |
3 | +++ devportalbinary/binary.py 2012-09-11 16:56:39 +0000 |
4 | @@ -405,8 +405,9 @@ |
5 | package_name = self.get_package_name(metadata) |
6 | return guess_extra_debian_rules_targets(package_name, self.path) |
7 | |
8 | - def want_with_metadata(self, metadata): |
9 | - if list(iter_binaries(self.path)): |
10 | + @classmethod |
11 | + def want_with_metadata(cls, path, metadata): |
12 | + if list(iter_binaries(path)): |
13 | return { |
14 | 'score': 10, |
15 | 'reason': 'Has ELF binaries and a metadata file'} |
16 | |
17 | === modified file 'devportalbinary/metadata.py' |
18 | --- devportalbinary/metadata.py 2012-09-11 15:21:46 +0000 |
19 | +++ devportalbinary/metadata.py 2012-09-11 16:56:39 +0000 |
20 | @@ -92,30 +92,6 @@ |
21 | } |
22 | |
23 | |
24 | -_no_field = object() |
25 | -def get_metadata(field_name=None, default=_no_field, path=None): |
26 | - """Return the value of ``field_name`` in metadata. |
27 | - |
28 | - :param field_name: The field to look up. If None, then return all the |
29 | - metadata as a dict. |
30 | - :param default: If provided, then this value will be returned if |
31 | - 'field_name' is not present in the metadata. |
32 | - :param path: The path to the metadata file, if unspecified, look for |
33 | - ``MetadataBackend.METADATA_FILE`` in the current working directory. |
34 | - :return: The value of the field. |
35 | - """ |
36 | - if path is None: |
37 | - path = MetadataBackend.METADATA_FILE |
38 | - with open(path) as f: |
39 | - metadata = json.load(f) |
40 | - if field_name is None: |
41 | - return metadata |
42 | - value = metadata.get(field_name, default) |
43 | - if value is _no_field: |
44 | - raise KeyError(field_name) |
45 | - return value |
46 | - |
47 | - |
48 | def get_install_basedir(package_name): |
49 | return '/opt/%s' % package_name |
50 | |
51 | @@ -210,6 +186,25 @@ |
52 | return int(w) |
53 | |
54 | |
55 | +def load_json(path): |
56 | + """Load JSON from `path`. |
57 | + |
58 | + :param path: the path to load the JSON from. |
59 | + :return: (json, err) where json is the deserialized contents of the file, |
60 | + or None if it didn't exist or didn't contain json. If json is |
61 | + None then err will be a description of why it couldn't be loaded. |
62 | + """ |
63 | + if not os.path.exists(path): |
64 | + return None, 'No metadata file' |
65 | + try: |
66 | + with open(path) as f: |
67 | + metadata = json.load(f) |
68 | + except ValueError: |
69 | + # Not a JSON file. |
70 | + return None, 'Metadata file is not valid JSON' |
71 | + return metadata, None |
72 | + |
73 | + |
74 | class MetadataBackend(object): |
75 | """A backend that is mostly powered by metadata from MyApps.""" |
76 | |
77 | @@ -428,7 +423,12 @@ |
78 | """Get the version of the package.""" |
79 | return metadata.get(self.SUGGESTED_VERSION, None) |
80 | |
81 | - def get_metadata(self): |
82 | + @classmethod |
83 | + def get_metadata_path(cls, path): |
84 | + return os.path.join(path, cls.METADATA_FILE) |
85 | + |
86 | + @classmethod |
87 | + def get_metadata(cls, path): |
88 | """Get the metadata for this backend. |
89 | |
90 | Looks for the metadata in a file called ``METADATA_FILE`` in the |
91 | @@ -436,7 +436,10 @@ |
92 | |
93 | :return: A dict of metadata. |
94 | """ |
95 | - return get_metadata(path=self.metadata_path) |
96 | + metadata, err = load_json(cls.get_metadata_path(path)) |
97 | + if metadata is None: |
98 | + raise AssertionError(err) |
99 | + return metadata |
100 | |
101 | def get_info(self, metadata): |
102 | """Return a dict of InfoElements given 'metadata'. |
103 | @@ -484,22 +487,16 @@ |
104 | ExtraFiles, metadata, info[PackageName]) |
105 | return info |
106 | |
107 | - def has_metadata_file(self): |
108 | - """True if a metadata file exists.""" |
109 | - return os.path.exists(self.metadata_path) |
110 | - |
111 | - def want(self): |
112 | + @classmethod |
113 | + def want(cls, path): |
114 | """How relevant this backend is.""" |
115 | - if not self.has_metadata_file(): |
116 | - return {'score': 0, 'reason': 'No metadata file'} |
117 | - try: |
118 | - metadata = self.get_metadata() |
119 | - except ValueError: |
120 | - # Not a JSON file. |
121 | - return {'score': 0, 'reason': 'Metadata file is not valid JSON'} |
122 | - return self.want_with_metadata(metadata) |
123 | + metadata, err = load_json(cls.get_metadata_path(path)) |
124 | + if err is not None: |
125 | + return {'score': 0, 'reason': err} |
126 | + return cls.want_with_metadata(path, metadata) |
127 | |
128 | - def want_with_metadata(self, metadata): |
129 | + @classmethod |
130 | + def want_with_metadata(self, path, metadata): |
131 | """How relevant this backend is, after metadata has been found. |
132 | |
133 | Specific backends should override this. |
134 | @@ -509,16 +506,15 @@ |
135 | |
136 | def make_want_fn(backend_cls): |
137 | def metadata_want(path): |
138 | - backend = backend_cls(path) |
139 | - return backend.want() |
140 | + return backend_cls.want(path) |
141 | metadata_want.__name__ = '{}_want'.format(backend_cls.__name__) |
142 | return backend_script(metadata_want) |
143 | |
144 | |
145 | def make_all_info_fn(backend_cls): |
146 | def metadata_all_info(path): |
147 | + metadata = backend_cls.get_metadata(path) |
148 | backend = backend_cls(path) |
149 | - metadata = backend.get_metadata() |
150 | info = backend.get_info(metadata) |
151 | return convert_info(info) |
152 | metadata_all_info.__name__ = '{}_all_info'.format(backend_cls.__name__) |
153 | |
154 | === modified file 'devportalbinary/pdf.py' |
155 | --- devportalbinary/pdf.py 2012-09-11 15:21:46 +0000 |
156 | +++ devportalbinary/pdf.py 2012-09-11 16:56:39 +0000 |
157 | @@ -46,14 +46,15 @@ |
158 | return None |
159 | return '/usr/bin/xdg-open /opt/%s/%s' % (package_name, pdf_filename) |
160 | |
161 | - def want_with_metadata(self, metadata): |
162 | + @classmethod |
163 | + def want_with_metadata(cls, path, metadata): |
164 | # XXX: This implies that we're doing exact string matching on the |
165 | # paths in the metadata and the results of os.listdir. We actually |
166 | # want to exclude the icons if they are the same file, not if the |
167 | # strings happen to be equal. |
168 | - excluded_files = set([self.METADATA_FILE, 'debian']) |
169 | - excluded_files |= set(metadata.get(self.ICONS, {}).values()) |
170 | - files = os.listdir(self.path) |
171 | + excluded_files = set([cls.METADATA_FILE, 'debian']) |
172 | + excluded_files |= set(metadata.get(cls.ICONS, {}).values()) |
173 | + files = os.listdir(path) |
174 | files = [f for f in files if f not in excluded_files] |
175 | # By default, we don't want it and give no reason. Sane default in |
176 | # case of buggy code below. |
177 | |
178 | === modified file 'devportalbinary/tests/test_binary.py' |
179 | --- devportalbinary/tests/test_binary.py 2012-09-06 20:04:30 +0000 |
180 | +++ devportalbinary/tests/test_binary.py 2012-09-11 16:56:39 +0000 |
181 | @@ -293,20 +293,18 @@ |
182 | # top-level. |
183 | path = self.useFixture(MetadataFixture({})).path |
184 | self.useFixture(BinaryFileFixture(path)) |
185 | - backend = self.make_backend(path) |
186 | self.assertEqual( |
187 | {'score': 10, |
188 | 'reason': 'Has ELF binaries and a metadata file'}, |
189 | - backend.want()) |
190 | + BinaryBackend.want(path)) |
191 | |
192 | def test_want_with_just_metadata(self): |
193 | # If something just has a metadata file but no binaries it is |
194 | # not wanted |
195 | path = self.useFixture(MetadataFixture({})).path |
196 | - backend = self.make_backend(path) |
197 | self.assertEqual( |
198 | {'score': 0, |
199 | - 'reason': 'No ELF binaries found.'}, backend.want()) |
200 | + 'reason': 'No ELF binaries found.'}, BinaryBackend.want(path)) |
201 | |
202 | def test_description(self): |
203 | # The binary backend uses the package description that's in the |
204 | |
205 | === modified file 'devportalbinary/tests/test_metadata.py' |
206 | --- devportalbinary/tests/test_metadata.py 2012-09-11 15:21:46 +0000 |
207 | +++ devportalbinary/tests/test_metadata.py 2012-09-11 16:56:39 +0000 |
208 | @@ -37,14 +37,13 @@ |
209 | make_tree, |
210 | ) |
211 | |
212 | -from devportalbinary.backend import convert_info |
213 | from devportalbinary.metadata import ( |
214 | format_install_map, |
215 | get_desktop_file, |
216 | get_install_basedir, |
217 | get_install_file, |
218 | get_install_map, |
219 | - get_metadata, |
220 | + load_json, |
221 | make_all_info_fn, |
222 | make_want_fn, |
223 | MetadataBackend, |
224 | @@ -80,10 +79,42 @@ |
225 | def get_executable(self, package_name): |
226 | return 'executable:' + package_name |
227 | |
228 | - def want_with_metadata(self, metadata): |
229 | + @classmethod |
230 | + def want_with_metadata(self, path, metadata): |
231 | return {'score': 20, 'reason': "I'm a dummy", 'metadata': metadata} |
232 | |
233 | |
234 | +class TestLoadJson(TestCase): |
235 | + |
236 | + def test_load_json_all_fields(self): |
237 | + metadata = { |
238 | + 'foo': self.getUniqueString(), |
239 | + 'bar': self.getUniqueInteger(), |
240 | + } |
241 | + path = self.useFixture(MetadataFixture(metadata)).metadata_path |
242 | + found_metadata, err = load_json(path) |
243 | + self.assertEqual(metadata, found_metadata) |
244 | + self.assertEqual(None, err) |
245 | + |
246 | + def test_load_json_not_present(self): |
247 | + # load_json returns None and a description when the file is |
248 | + # missing |
249 | + filename = 'metdata.json' |
250 | + path = self.useFixture(FileTree({})).join(filename) |
251 | + found_metadata, err = load_json(path) |
252 | + self.assertEqual(None, found_metadata) |
253 | + self.assertEqual('No metadata file', err) |
254 | + |
255 | + def test_load_json_not_json(self): |
256 | + # load_json returns None and a description when the file |
257 | + # does not contain json |
258 | + filename = 'metdata.json' |
259 | + path = self.useFixture(FileTree({filename: {}})).join(filename) |
260 | + found_metadata, err = load_json(path) |
261 | + self.assertEqual(None, found_metadata) |
262 | + self.assertEqual('Metadata file is not valid JSON', err) |
263 | + |
264 | + |
265 | class MetadataTests(BackendTests): |
266 | |
267 | BACKEND = DummyBackend |
268 | @@ -101,57 +132,6 @@ |
269 | def test_metadata_file(self): |
270 | self.assertEqual('devportal-metadata.json', MetadataBackend.METADATA_FILE) |
271 | |
272 | - def test_get_metadata_field_present(self): |
273 | - # get_metadata returns the metadata value of the requested field. |
274 | - metadata = { |
275 | - 'foo': self.getUniqueString(), |
276 | - 'bar': self.getUniqueInteger(), |
277 | - } |
278 | - path = self.useFixture(MetadataFixture(metadata)).metadata_path |
279 | - foo = get_metadata('foo', path=path) |
280 | - self.assertEqual(metadata['foo'], foo) |
281 | - |
282 | - def test_get_metadata_default_file(self): |
283 | - # By default, get_metadata looks for the METADATA_FILE in the current |
284 | - # working directory. |
285 | - metadata = { |
286 | - 'foo': self.getUniqueString(), |
287 | - 'bar': self.getUniqueInteger(), |
288 | - } |
289 | - path = self.useFixture(MetadataFixture(metadata)).path |
290 | - self.addCleanup(os.chdir, os.getcwd()) |
291 | - os.chdir(path) |
292 | - foo = get_metadata('foo') |
293 | - self.assertEqual(metadata['foo'], foo) |
294 | - |
295 | - def test_get_metadata_field_not_present_default_provided(self): |
296 | - # get_metadata returns the provided default value if the field is not |
297 | - # present in the metadata. |
298 | - metadata = {} |
299 | - path = self.useFixture(MetadataFixture(metadata)).metadata_path |
300 | - default = object() |
301 | - foo = get_metadata('foo', default, path=path) |
302 | - self.assertIs(default, foo) |
303 | - |
304 | - def test_get_metadata_field_not_present_no_default(self): |
305 | - # get_metadata raises an exception if the field isn't there and no |
306 | - # default was provided. |
307 | - metadata = {} |
308 | - field = self.getUniqueString() |
309 | - path = self.useFixture(MetadataFixture(metadata)).metadata_path |
310 | - e = self.assertRaises(KeyError, get_metadata, field, path=path) |
311 | - self.assertEqual(repr(field), str(e)) |
312 | - |
313 | - def test_get_metadata_all_fields(self): |
314 | - # get_metadata returns the metadata value of the requested field. |
315 | - metadata = { |
316 | - 'foo': self.getUniqueString(), |
317 | - 'bar': self.getUniqueInteger(), |
318 | - } |
319 | - path = self.useFixture(MetadataFixture(metadata)).metadata_path |
320 | - found_metadata = get_metadata(path=path) |
321 | - self.assertEqual(metadata, found_metadata) |
322 | - |
323 | def test_get_extra_files_install_file(self): |
324 | # ExtraFiles includes an install file generated by get_install_file. |
325 | package_name = self.getUniqueString() |
326 | @@ -408,32 +388,30 @@ |
327 | |
328 | def test_want_with_no_metadata(self): |
329 | # If there's no metadata file, we don't want it. |
330 | - backend = self.make_backend() |
331 | + path = self.useFixture(TempDir()).path |
332 | self.assertEqual( |
333 | {'score': 0, 'reason': 'No metadata file'}, |
334 | - backend.want()) |
335 | + DummyBackend.want(path)) |
336 | |
337 | def test_want_with_invalid_json(self): |
338 | # If there's a metadata file, but it's not valid JSON, we don't want |
339 | # it. |
340 | - backend = self.make_backend() |
341 | - make_tree(backend.path, from_rough_spec( |
342 | - [(MetadataBackend.METADATA_FILE, 'not valid json')])) |
343 | + path = self.useFixture(FileTree(from_rough_spec( |
344 | + [(MetadataBackend.METADATA_FILE, 'not valid json')]))).path |
345 | self.assertEqual( |
346 | {'score': 0, 'reason': 'Metadata file is not valid JSON'}, |
347 | - backend.want()) |
348 | + DummyBackend.want(path)) |
349 | |
350 | def test_want_with_valid_metadata(self): |
351 | # If there's a metadata file containing valid JSON, we delegate to the |
352 | # backend's want_with_metadata method. See |
353 | # DummyBackend.want_with_metadata. |
354 | - backend = self.make_backend() |
355 | metadata = {'foo': 'bar'} |
356 | - make_tree(backend.path, from_rough_spec( |
357 | - [(MetadataBackend.METADATA_FILE, json.dumps(metadata))])) |
358 | + path = self.useFixture(FileTree(from_rough_spec( |
359 | + [(MetadataBackend.METADATA_FILE, json.dumps(metadata))]))).path |
360 | self.assertEqual( |
361 | {'score': 20, 'reason': "I'm a dummy", 'metadata': metadata}, |
362 | - backend.want()) |
363 | + DummyBackend.want(path)) |
364 | |
365 | |
366 | class DesktopFileTests(TestCase): |
367 | @@ -661,7 +639,8 @@ |
368 | backend = DummyBackend(path) |
369 | output = StringIO() |
370 | want(path, output) |
371 | - self.assertEqual(backend.want(), json.loads(output.getvalue())) |
372 | + self.assertEqual(DummyBackend.want(path), |
373 | + json.loads(output.getvalue())) |
374 | |
375 | def test_names_fn(self): |
376 | want = make_want_fn(DummyBackend) |
377 | |
378 | === modified file 'devportalbinary/tests/test_pdf.py' |
379 | --- devportalbinary/tests/test_pdf.py 2012-08-23 12:05:12 +0000 |
380 | +++ devportalbinary/tests/test_pdf.py 2012-09-11 16:56:39 +0000 |
381 | @@ -27,27 +27,24 @@ |
382 | # If we detect the metadata file and a pdf, then we score 20. |
383 | filename = self.getUniqueString() + ".pdf" |
384 | path = self.make_tree([VALID_METADATA_FILE, filename]) |
385 | - backend = PdfBackend(path) |
386 | - self.assertEqual({'score': 20, 'reason': None}, backend.want()) |
387 | + self.assertEqual({'score': 20, 'reason': None}, PdfBackend.want(path)) |
388 | |
389 | def test_want_with_single_non_pdf(self): |
390 | # If we detect a metadata file and a single other file that's not a |
391 | # PDF, we score 0. |
392 | filename = self.getUniqueString() + ".not-pdf" |
393 | path = self.make_tree([VALID_METADATA_FILE, filename]) |
394 | - backend = PdfBackend(path) |
395 | self.assertEqual( |
396 | {'score': 0, |
397 | 'reason': 'File is not a PDF: %r' % (filename,), |
398 | - }, backend.want()) |
399 | + }, PdfBackend.want(path)) |
400 | |
401 | def test_want_with_just_metadata(self): |
402 | # If we detect just the metadata file then we score 0. |
403 | path = self.make_tree([VALID_METADATA_FILE]) |
404 | - backend = PdfBackend(path) |
405 | self.assertEqual( |
406 | {'score': 0, 'reason': 'No files found, just metadata'}, |
407 | - backend.want()) |
408 | + PdfBackend.want(path)) |
409 | |
410 | def test_want_with_extra_files(self): |
411 | # If we detect other files then we score 0. This is so that |
412 | @@ -56,19 +53,17 @@ |
413 | filename = self.getUniqueString() + ".pdf" |
414 | other_filename = self.getUniqueString() + ".foo" |
415 | path = self.make_tree([VALID_METADATA_FILE, filename, other_filename]) |
416 | - backend = PdfBackend(path) |
417 | self.assertEqual( |
418 | {'score': 0, 'reason': 'More files than just a PDF: %r' % |
419 | (sorted([filename, other_filename]),)}, |
420 | - backend.want()) |
421 | + PdfBackend.want(path)) |
422 | |
423 | def test_want_with_debian_dir(self): |
424 | # If the other file is a debian dir then we score 20 still. |
425 | # This just makes local testing of the backend easier. |
426 | filename = self.getUniqueString() + ".pdf" |
427 | path = self.make_tree([VALID_METADATA_FILE, filename, 'debian/']) |
428 | - backend = PdfBackend(path) |
429 | - self.assertEqual({'score': 20, 'reason': None}, backend.want()) |
430 | + self.assertEqual({'score': 20, 'reason': None}, PdfBackend.want(path)) |
431 | |
432 | def test_want_with_icons(self): |
433 | icon_file = self.getUniqueString() + '.png' |
434 | @@ -77,7 +72,7 @@ |
435 | path = self.make_tree([ |
436 | (MetadataBackend.METADATA_FILE, metadata), icon_file, filename]) |
437 | backend = PdfBackend(path) |
438 | - self.assertEqual({'score': 20, 'reason': None}, backend.want()) |
439 | + self.assertEqual({'score': 20, 'reason': None}, PdfBackend.want(path)) |
440 | |
441 | def test_architecture(self): |
442 | # The pdf backend set architecture to all |
Looks great. Dovetails nicely with what I'm doing.
load_json needs a proper docstring before landing though.
Thanks
jml