Merge lp:~james-w/linaro-image-tools/fetch-packages into lp:linaro-image-tools/11.11

Proposed by James Westby
Status: Merged
Merged at revision: 64
Proposed branch: lp:~james-w/linaro-image-tools/fetch-packages
Merge into: lp:linaro-image-tools/11.11
Prerequisite: lp:~james-w/linaro-image-tools/create-hwpack-skeleton
Diff against target: 547 lines (+510/-0)
4 files modified
hwpack/packages.py (+165/-0)
hwpack/testing.py (+112/-0)
hwpack/tests/__init__.py (+1/-0)
hwpack/tests/test_packages.py (+232/-0)
To merge this branch: bzr merge lp:~james-w/linaro-image-tools/fetch-packages
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle (community) Approve
Review via email: mp+34355@code.launchpad.net

Description of the change

Hi,

Here's the result of my investigations in to how we can fetch packages.

It's small code which I like. It's currently limited to fetching just the
specified packages, as we don't really know what we want there yet. I'm
assuming that if we need to then python-apt can allow us to resolve
dependencies too.

I have two issues with this change, the first being the possible licensing
impact of using python-apt. It is GPL2+ licensed, which I believe would
mean that this code would need to be too, which means getting a TSC
exemption, unless that has already been done for linaro-image-tools.

The second is the growing amount of code in testing.py with no tests.
Should I be testing that code too?

Thanks,

James

To post a comment you must log in.
84. By James Westby

Use apt rather than bzrlib, with our own method to avoid the stdout output.

85. By James Westby

Remove the unneeded ugly code.

86. By James Westby

Merged create-hwpack-skeleton into fetch-packages.

87. By James Westby

Use a class as the return value such that we can return name, version and filename.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Revision history for this message
James Westby (james-w) wrote :

> FWIW about the licensing, http://bazaar.launchpad.net/~linaro-maintainers
> /linaro-image-tools/linaro-image-tools/revision/44 put linaro-media-create
> under GPLv3.

I just saw that too. I guess that means we will have no problem, but I will
check with Loïc.

Thanks,

James

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

As for the code, I think it's by and large fine, bearing in mind that I don't know how python-apt works.

I don't know if the stuff in testing.py needs tests, but it certainly needs documentation. I also wonder if AptSource should be called AptSourceFixture or something.

DummyProgress could use a docstring. After all that effort going to silencing python-apt, some kind of progress is likely to be desired, I think... I guess this can be carefully added back in later.

fetch_packages doesn't document its return type correctly any more.

All else looks good.

review: Approve
88. By James Westby

Add some documentation for the Package object.

89. By James Westby

Make Package in to DummyFetchedPackage to share an interface.

90. By James Westby

Add more documentation to testing.py and rename AptSource to AptSourceFixture

91. By James Westby

Tweak docstrings in hwpack.packages. Thanks Michael.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'hwpack/packages.py'
2--- hwpack/packages.py 1970-01-01 00:00:00 +0000
3+++ hwpack/packages.py 2010-09-02 15:37:46 +0000
4@@ -0,0 +1,165 @@
5+import os
6+import shutil
7+import tempfile
8+
9+from apt.cache import Cache
10+from apt.package import FetchError
11+import apt_pkg
12+
13+
14+class DummyProgress(object):
15+ """An AcquireProgress that silences all output.
16+
17+ This can be used to ensure that apt produces no output
18+ when fetching files.
19+ """
20+
21+ def start(self):
22+ pass
23+
24+ def ims_hit(self, item):
25+ pass
26+
27+ def fail(self, item):
28+ pass
29+
30+ def fetch(self, item):
31+ pass
32+
33+ def pulse(self, owner):
34+ return True
35+
36+ def media_change(self):
37+ return False
38+
39+ def stop(self):
40+ pass
41+
42+
43+class FetchedPackage(object):
44+ """The result of fetching packages.
45+
46+ :ivar name: the name of the fetched package.
47+ :type name: str
48+ :ivar version: the version of the fetched package.
49+ :type version: str
50+ :ivar filename: the filename that the package has.
51+ :type filename: str
52+ :ivar content: a file that the content of the package can be read from.
53+ :type content: a file-like object
54+ :ivar size: the size of the package
55+ :type size: int
56+ :ivar md5: the hex representation of the md5sum of the contents of
57+ the package.
58+ :type md5: str
59+ """
60+
61+ def __init__(self, name, version, filename, content, size, md5):
62+ """Create a FetchedPackage.
63+
64+ See the instance variables for the arguments.
65+ """
66+ self.name = name
67+ self.version = version
68+ self.filename = filename
69+ self.content = content
70+ self.size = size
71+ self.md5 = md5
72+
73+ def __eq__(self, other):
74+ return (self.name == other.name
75+ and self.version == other.version
76+ and self.filename == other.filename
77+ and self.content.read() == other.content.read()
78+ and self.size == other.size
79+ and self.md5 == other.md5)
80+
81+ def __hash__(self):
82+ return hash(
83+ (self.name, self.version, self.filename, self.size, self.md5))
84+
85+
86+class PackageFetcher(object):
87+ """A class to fetch packages from a defined list of sources."""
88+
89+ def __init__(self, sources):
90+ """Create a PackageFetcher.
91+
92+ Once created a PackageFetcher should have its `prepare` method
93+ called before use.
94+
95+ :param sources: a list of sources such that they can be prefixed
96+ with "deb " and fed to apt.
97+ :type sources: an iterable of str
98+ """
99+ self.sources = sources
100+ self.tempdir = None
101+
102+ def prepare(self):
103+ """Prepare a PackageFetcher for use.
104+
105+ Should be called before use, and after any modification to the list
106+ of sources.
107+ """
108+ self.cleanup()
109+ self.tempdir = tempfile.mkdtemp(prefix="hwpack-apt-cache-")
110+ files = ["var/lib/dpkg/status",
111+ ]
112+ dirs = ["var/lib/dpkg",
113+ "etc/apt/",
114+ "var/cache/apt/archives/partial",
115+ "var/lib/apt/lists/partial",
116+ ]
117+ for d in dirs:
118+ os.makedirs(os.path.join(self.tempdir, d))
119+ for fn in files:
120+ with open(os.path.join(self.tempdir, fn), 'w'):
121+ pass
122+ sources_list = os.path.join(
123+ self.tempdir, "etc", "apt", "sources.list")
124+ with open(sources_list, 'w') as f:
125+ for source in self.sources:
126+ f.write("deb %s\n" % source)
127+ self.cache = Cache(rootdir=self.tempdir, memonly=True)
128+ self.cache.update()
129+ self.cache.open()
130+
131+ def cleanup(self):
132+ """Cleanup any remaining artefacts.
133+
134+ Should be called on all PackageFetchers when they are finished
135+ with.
136+ """
137+ if self.tempdir is not None and os.path.exists(self.tempdir):
138+ shutil.rmtree(self.tempdir)
139+
140+ def fetch_packages(self, packages):
141+ """Fetch the files for the given list of package names.
142+
143+ :param packages: a list of package names to install
144+ :type packages: an iterable of str
145+ :return: a list of the packages that were fetched, with relevant
146+ metdata and the contents of the files available.
147+ :rtype: an iterable of FetchedPackages.
148+ :raises KeyError: if any of the package names in the list couldn't
149+ be found.
150+ """
151+ results = []
152+ for package in packages:
153+ candidate = self.cache[package].candidate
154+ base = os.path.basename(candidate.filename)
155+ destfile = os.path.join(self.tempdir, base)
156+ acq = apt_pkg.Acquire(DummyProgress())
157+ acqfile = apt_pkg.AcquireFile(
158+ acq, candidate.uri, candidate.md5, candidate.size,
159+ base, destfile=destfile)
160+ acq.run()
161+ if acqfile.status != acqfile.STAT_DONE:
162+ raise FetchError(
163+ "The item %r could not be fetched: %s" %
164+ (acqfile.destfile, acqfile.error_text))
165+ result_package = FetchedPackage(
166+ candidate.package.name, candidate.version, base,
167+ open(destfile), candidate.size, candidate.md5)
168+ results.append(result_package)
169+ return results
170
171=== modified file 'hwpack/testing.py'
172--- hwpack/testing.py 2010-08-31 15:10:50 +0000
173+++ hwpack/testing.py 2010-09-02 15:37:46 +0000
174@@ -1,8 +1,15 @@
175 from contextlib import contextmanager
176+import hashlib
177+import os
178+import shutil
179+import tempfile
180 from StringIO import StringIO
181 import tarfile
182
183+from testtools import TestCase
184+
185 from hwpack.better_tarfile import writeable_tarfile
186+from hwpack.packages import FetchedPackage
187
188
189 @contextmanager
190@@ -37,3 +44,108 @@
191 yield tf
192 finally:
193 tf.close()
194+
195+
196+class DummyFetchedPackage(FetchedPackage):
197+ """A FetchedPackage with dummy information.
198+
199+ See FetchedPackage for the instance variables.
200+ """
201+
202+ def __init__(self, name, version):
203+ """Create a DummyFetchedPackage.
204+
205+ :param name: the name of the package.
206+ :type name: str
207+ :param version: the version of the package.
208+ :type version: str
209+ """
210+ self.name = name
211+ self.version = version
212+
213+ @property
214+ def filename(self):
215+ return "%s_%s_all.deb" % (self.name, self.version)
216+
217+ @property
218+ def content(self):
219+ return StringIO("Content of %s" % self.filename)
220+
221+ @property
222+ def size(self):
223+ return len(self.content.read())
224+
225+ @property
226+ def md5(self):
227+ md5sum = hashlib.md5()
228+ md5sum.update(self.content.read())
229+ return md5sum.hexdigest()
230+
231+
232+class AptSourceFixture(object):
233+ """A fixture that provides an apt source, with packages and indices.
234+
235+ An apt source provides a set of package files, and a Packages file
236+ that allows apt to determine the contents of the source.
237+
238+ :ivar sources_entry: the URI and suite to give to apt to view the
239+ source (i.e. a sources.list line without the "deb" prefix
240+ :type sources_entry: str
241+ """
242+
243+ def __init__(self, packages):
244+ """Create an AptSourceFixture.
245+
246+ :param packages: a list of packages to add to the source
247+ and index.
248+ :type packages: an iterable of FetchedPackages
249+ """
250+ self.packages = packages
251+
252+ def setUp(self):
253+ self.rootdir = tempfile.mkdtemp(prefix="hwpack-apt-source-")
254+ for package in self.packages:
255+ with open(
256+ os.path.join(self.rootdir, package.filename), 'wb') as f:
257+ f.write(package.content.read())
258+ with open(os.path.join(self.rootdir, "Packages"), 'wb') as f:
259+ for package in self.packages:
260+ f.write('Package: %s\n' % package.name)
261+ f.write('Version: %s\n' % package.version)
262+ f.write('Filename: %s\n' % package.filename)
263+ f.write('Size: %d\n' % package.size)
264+ f.write('Architecture: all\n')
265+ f.write('MD5sum: %s\n' % package.md5)
266+ f.write('\n')
267+
268+ def tearDown(self):
269+ if os.path.exists(self.rootdir):
270+ shutil.rmtree(self.rootdir)
271+
272+ @property
273+ def sources_entry(self):
274+ return "file:" + os.path.abspath(self.rootdir) +" ./"
275+
276+
277+class TestCaseWithFixtures(TestCase):
278+ """A TestCase with the ability to easily add 'fixtures'.
279+
280+ A fixture is an object which can be created and cleaned up, and
281+ this test case knows how to manage them to ensure that they will
282+ always be cleaned up at the end of the test.
283+ """
284+
285+ def useFixture(self, fixture):
286+ """Make use of a fixture, ensuring that it will be cleaned up.
287+
288+ Given a fixture, this method will run the `setUp` method of
289+ the fixture, and ensure that its `tearDown` method will be
290+ called at the end of the test, regardless of success or failure.
291+
292+ :param fixture: the fixture to use.
293+ :type fixture: an object with setUp and tearDown methods.
294+ :return: the fixture that was passed in.
295+ """
296+ self.addCleanup(fixture.tearDown)
297+ fixture.setUp()
298+ return fixture
299
300=== modified file 'hwpack/tests/__init__.py'
301--- hwpack/tests/__init__.py 2010-08-31 01:05:12 +0000
302+++ hwpack/tests/__init__.py 2010-09-02 15:37:46 +0000
303@@ -4,6 +4,7 @@
304 module_names = ['hwpack.tests.test_config',
305 'hwpack.tests.test_better_tarfile',
306 'hwpack.tests.test_hardwarepack',
307+ 'hwpack.tests.test_packages',
308 'hwpack.tests.test_tarfile_matchers',
309 ]
310 loader = unittest.TestLoader()
311
312=== added file 'hwpack/tests/test_packages.py'
313--- hwpack/tests/test_packages.py 1970-01-01 00:00:00 +0000
314+++ hwpack/tests/test_packages.py 2010-09-02 15:37:46 +0000
315@@ -0,0 +1,232 @@
316+import os
317+from StringIO import StringIO
318+
319+from testtools import TestCase
320+
321+from hwpack.packages import (
322+ FetchedPackage,
323+ PackageFetcher,
324+ )
325+from hwpack.testing import (
326+ AptSourceFixture,
327+ DummyFetchedPackage,
328+ TestCaseWithFixtures,
329+ )
330+
331+
332+class FetchedPackageTests(TestCase):
333+
334+ def test_attributes(self):
335+ package = FetchedPackage(
336+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
337+ self.assertEqual("foo", package.name)
338+ self.assertEqual("1.1", package.version)
339+ self.assertEqual("foo_1.1.deb", package.filename)
340+ self.assertEqual("xxxx", package.content.read())
341+ self.assertEqual(4, package.size)
342+ self.assertEqual("aaaa", package.md5)
343+
344+ def test_equal(self):
345+ package1 = FetchedPackage(
346+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
347+ package2 = FetchedPackage(
348+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
349+ self.assertEqual(package1, package2)
350+
351+ def test_not_equal_different_name(self):
352+ package1 = FetchedPackage(
353+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
354+ package2 = FetchedPackage(
355+ "bar", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
356+ self.assertNotEqual(package1, package2)
357+
358+ def test_not_equal_different_version(self):
359+ package1 = FetchedPackage(
360+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
361+ package2 = FetchedPackage(
362+ "foo", "1.2", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
363+ self.assertNotEqual(package1, package2)
364+
365+ def test_not_equal_different_filename(self):
366+ package1 = FetchedPackage(
367+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
368+ package2 = FetchedPackage(
369+ "foo", "1.1", "afoo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
370+ self.assertNotEqual(package1, package2)
371+
372+ def test_not_equal_different_content(self):
373+ package1 = FetchedPackage(
374+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
375+ package2 = FetchedPackage(
376+ "foo", "1.1", "foo_1.1.deb", StringIO("yyyy"), 4, "aaaa")
377+ self.assertNotEqual(package1, package2)
378+
379+ def test_not_equal_different_size(self):
380+ package1 = FetchedPackage(
381+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
382+ package2 = FetchedPackage(
383+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 5, "aaaa")
384+ self.assertNotEqual(package1, package2)
385+
386+ def test_not_equal_different_md5(self):
387+ package1 = FetchedPackage(
388+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
389+ package2 = FetchedPackage(
390+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "bbbb")
391+ self.assertNotEqual(package1, package2)
392+
393+ def test_equal_hash_equal(self):
394+ package1 = FetchedPackage(
395+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
396+ package2 = FetchedPackage(
397+ "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
398+ self.assertEqual(hash(package1), hash(package2))
399+
400+
401+class PackageFetcherTests(TestCaseWithFixtures):
402+
403+ def test_cleanup_removes_tempdir(self):
404+ fetcher = PackageFetcher([])
405+ fetcher.prepare()
406+ tempdir = fetcher.tempdir
407+ fetcher.cleanup()
408+ self.assertFalse(os.path.exists(tempdir))
409+
410+ def test_cleanup_ignores_missing_tempdir(self):
411+ fetcher = PackageFetcher([])
412+ fetcher.prepare()
413+ tempdir = fetcher.tempdir
414+ fetcher.cleanup()
415+ # Check that there is no problem removing it again
416+ fetcher.cleanup()
417+
418+ def test_cleanup_before_prepare(self):
419+ fetcher = PackageFetcher([])
420+ # Check that there is no problem cleaning up before we start
421+ fetcher.cleanup()
422+
423+ def test_prepare_creates_tempdir(self):
424+ fetcher = PackageFetcher([])
425+ self.addCleanup(fetcher.cleanup)
426+ fetcher.prepare()
427+ self.assertTrue(os.path.isdir(fetcher.tempdir))
428+
429+ def test_prepare_creates_var_lib_dpkg_status_file(self):
430+ fetcher = PackageFetcher([])
431+ self.addCleanup(fetcher.cleanup)
432+ fetcher.prepare()
433+ self.assertEqual(
434+ '',
435+ open(os.path.join(
436+ fetcher.tempdir, "var", "lib", "dpkg", "status")).read())
437+
438+ def test_prepare_creates_var_cache_apt_archives_partial_dir(self):
439+ fetcher = PackageFetcher([])
440+ self.addCleanup(fetcher.cleanup)
441+ fetcher.prepare()
442+ self.assertTrue(
443+ os.path.isdir(os.path.join(
444+ fetcher.tempdir, "var", "cache", "apt", "archives",
445+ "partial")))
446+
447+ def test_prepare_creates_var_lib_apt_lists_partial_dir(self):
448+ fetcher = PackageFetcher([])
449+ self.addCleanup(fetcher.cleanup)
450+ fetcher.prepare()
451+ self.assertTrue(
452+ os.path.isdir(os.path.join(
453+ fetcher.tempdir, "var", "lib", "apt", "lists", "partial")))
454+
455+ def test_prepare_creates_etc_apt_sources_list_file(self):
456+ source1 = self.useFixture(AptSourceFixture([]))
457+ source2 = self.useFixture(AptSourceFixture([]))
458+ fetcher = PackageFetcher(
459+ [source1.sources_entry, source2.sources_entry])
460+ self.addCleanup(fetcher.cleanup)
461+ fetcher.prepare()
462+ self.assertEqual(
463+ "deb %s\ndeb %s\n" % (
464+ source1.sources_entry, source2.sources_entry),
465+ open(os.path.join(
466+ fetcher.tempdir, "etc", "apt", "sources.list")).read())
467+
468+ def get_fetcher(self, sources):
469+ fetcher = PackageFetcher([s.sources_entry for s in sources])
470+ self.addCleanup(fetcher.cleanup)
471+ fetcher.prepare()
472+ return fetcher
473+
474+ def test_fetch_packages_not_found_because_no_sources(self):
475+ fetcher = self.get_fetcher([])
476+ self.assertRaises(KeyError, fetcher.fetch_packages, ["nothere"])
477+
478+ def test_fetch_packages_not_found_because_not_in_sources(self):
479+ available_package = DummyFetchedPackage("foo", "1.0")
480+ source = self.useFixture(AptSourceFixture([available_package]))
481+ fetcher = self.get_fetcher([source])
482+ self.assertRaises(KeyError, fetcher.fetch_packages, ["nothere"])
483+
484+ def test_fetch_packages_not_found_one_of_two_missing(self):
485+ available_package = DummyFetchedPackage("foo", "1.0")
486+ source = self.useFixture(AptSourceFixture([available_package]))
487+ fetcher = self.get_fetcher([source])
488+ self.assertRaises(
489+ KeyError, fetcher.fetch_packages, ["foo", "nothere"])
490+
491+ def test_fetch_packges_fetches_no_packages(self):
492+ available_package = DummyFetchedPackage("foo", "1.0")
493+ source = self.useFixture(AptSourceFixture([available_package]))
494+ fetcher = self.get_fetcher([source])
495+ self.assertEqual(0, len(fetcher.fetch_packages([])))
496+
497+ def test_fetch_packges_fetches_single_package(self):
498+ available_package = DummyFetchedPackage("foo", "1.0")
499+ source = self.useFixture(AptSourceFixture([available_package]))
500+ fetcher = self.get_fetcher([source])
501+ self.assertEqual(1, len(fetcher.fetch_packages(["foo"])))
502+
503+ def test_fetch_packges_fetches_correct_packge(self):
504+ available_package = DummyFetchedPackage("foo", "1.0")
505+ source = self.useFixture(AptSourceFixture([available_package]))
506+ fetcher = self.get_fetcher([source])
507+ self.assertEqual(
508+ available_package, fetcher.fetch_packages(["foo"])[0])
509+
510+ def test_fetch_packges_fetches_multiple_packages(self):
511+ available_packages = [
512+ DummyFetchedPackage("bar", "1.0"),
513+ DummyFetchedPackage("foo", "1.0"),
514+ ]
515+ source = self.useFixture(AptSourceFixture(available_packages))
516+ fetcher = self.get_fetcher([source])
517+ self.assertEqual(2, len(fetcher.fetch_packages(["foo", "bar"])))
518+
519+ def test_fetch_packges_fetches_multiple_packages_correctly(self):
520+ available_packages = [
521+ DummyFetchedPackage("foo", "1.0"),
522+ DummyFetchedPackage("bar", "1.0"),
523+ ]
524+ source = self.useFixture(AptSourceFixture(available_packages))
525+ fetcher = self.get_fetcher([source])
526+ fetched = fetcher.fetch_packages(["foo", "bar"])
527+ self.assertEqual(available_packages[0], fetched[0])
528+ self.assertEqual(available_packages[1], fetched[1])
529+
530+ def test_fetch_packages_fetches_newest(self):
531+ available_packages = [
532+ DummyFetchedPackage("bar", "1.0"),
533+ DummyFetchedPackage("bar", "1.1"),
534+ ]
535+ source = self.useFixture(AptSourceFixture(available_packages))
536+ fetcher = self.get_fetcher([source])
537+ fetched = fetcher.fetch_packages(["bar"])
538+ self.assertEqual(available_packages[1], fetched[0])
539+
540+ def test_fetch_packages_fetches_newest_from_multiple_sources(self):
541+ old_source_packages = [DummyFetchedPackage("bar", "1.0")]
542+ new_source_packages = [DummyFetchedPackage("bar", "1.1")]
543+ old_source = self.useFixture(AptSourceFixture(old_source_packages))
544+ new_source = self.useFixture(AptSourceFixture(new_source_packages))
545+ fetcher = self.get_fetcher([old_source, new_source])
546+ fetched = fetcher.fetch_packages(["bar"])
547+ self.assertEqual(new_source_packages[0], fetched[0])

Subscribers

People subscribed via source and target branches