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
=== added file 'hwpack/packages.py'
--- hwpack/packages.py 1970-01-01 00:00:00 +0000
+++ hwpack/packages.py 2010-09-02 15:37:46 +0000
@@ -0,0 +1,165 @@
1import os
2import shutil
3import tempfile
4
5from apt.cache import Cache
6from apt.package import FetchError
7import apt_pkg
8
9
10class DummyProgress(object):
11 """An AcquireProgress that silences all output.
12
13 This can be used to ensure that apt produces no output
14 when fetching files.
15 """
16
17 def start(self):
18 pass
19
20 def ims_hit(self, item):
21 pass
22
23 def fail(self, item):
24 pass
25
26 def fetch(self, item):
27 pass
28
29 def pulse(self, owner):
30 return True
31
32 def media_change(self):
33 return False
34
35 def stop(self):
36 pass
37
38
39class FetchedPackage(object):
40 """The result of fetching packages.
41
42 :ivar name: the name of the fetched package.
43 :type name: str
44 :ivar version: the version of the fetched package.
45 :type version: str
46 :ivar filename: the filename that the package has.
47 :type filename: str
48 :ivar content: a file that the content of the package can be read from.
49 :type content: a file-like object
50 :ivar size: the size of the package
51 :type size: int
52 :ivar md5: the hex representation of the md5sum of the contents of
53 the package.
54 :type md5: str
55 """
56
57 def __init__(self, name, version, filename, content, size, md5):
58 """Create a FetchedPackage.
59
60 See the instance variables for the arguments.
61 """
62 self.name = name
63 self.version = version
64 self.filename = filename
65 self.content = content
66 self.size = size
67 self.md5 = md5
68
69 def __eq__(self, other):
70 return (self.name == other.name
71 and self.version == other.version
72 and self.filename == other.filename
73 and self.content.read() == other.content.read()
74 and self.size == other.size
75 and self.md5 == other.md5)
76
77 def __hash__(self):
78 return hash(
79 (self.name, self.version, self.filename, self.size, self.md5))
80
81
82class PackageFetcher(object):
83 """A class to fetch packages from a defined list of sources."""
84
85 def __init__(self, sources):
86 """Create a PackageFetcher.
87
88 Once created a PackageFetcher should have its `prepare` method
89 called before use.
90
91 :param sources: a list of sources such that they can be prefixed
92 with "deb " and fed to apt.
93 :type sources: an iterable of str
94 """
95 self.sources = sources
96 self.tempdir = None
97
98 def prepare(self):
99 """Prepare a PackageFetcher for use.
100
101 Should be called before use, and after any modification to the list
102 of sources.
103 """
104 self.cleanup()
105 self.tempdir = tempfile.mkdtemp(prefix="hwpack-apt-cache-")
106 files = ["var/lib/dpkg/status",
107 ]
108 dirs = ["var/lib/dpkg",
109 "etc/apt/",
110 "var/cache/apt/archives/partial",
111 "var/lib/apt/lists/partial",
112 ]
113 for d in dirs:
114 os.makedirs(os.path.join(self.tempdir, d))
115 for fn in files:
116 with open(os.path.join(self.tempdir, fn), 'w'):
117 pass
118 sources_list = os.path.join(
119 self.tempdir, "etc", "apt", "sources.list")
120 with open(sources_list, 'w') as f:
121 for source in self.sources:
122 f.write("deb %s\n" % source)
123 self.cache = Cache(rootdir=self.tempdir, memonly=True)
124 self.cache.update()
125 self.cache.open()
126
127 def cleanup(self):
128 """Cleanup any remaining artefacts.
129
130 Should be called on all PackageFetchers when they are finished
131 with.
132 """
133 if self.tempdir is not None and os.path.exists(self.tempdir):
134 shutil.rmtree(self.tempdir)
135
136 def fetch_packages(self, packages):
137 """Fetch the files for the given list of package names.
138
139 :param packages: a list of package names to install
140 :type packages: an iterable of str
141 :return: a list of the packages that were fetched, with relevant
142 metdata and the contents of the files available.
143 :rtype: an iterable of FetchedPackages.
144 :raises KeyError: if any of the package names in the list couldn't
145 be found.
146 """
147 results = []
148 for package in packages:
149 candidate = self.cache[package].candidate
150 base = os.path.basename(candidate.filename)
151 destfile = os.path.join(self.tempdir, base)
152 acq = apt_pkg.Acquire(DummyProgress())
153 acqfile = apt_pkg.AcquireFile(
154 acq, candidate.uri, candidate.md5, candidate.size,
155 base, destfile=destfile)
156 acq.run()
157 if acqfile.status != acqfile.STAT_DONE:
158 raise FetchError(
159 "The item %r could not be fetched: %s" %
160 (acqfile.destfile, acqfile.error_text))
161 result_package = FetchedPackage(
162 candidate.package.name, candidate.version, base,
163 open(destfile), candidate.size, candidate.md5)
164 results.append(result_package)
165 return results
0166
=== modified file 'hwpack/testing.py'
--- hwpack/testing.py 2010-08-31 15:10:50 +0000
+++ hwpack/testing.py 2010-09-02 15:37:46 +0000
@@ -1,8 +1,15 @@
1from contextlib import contextmanager1from contextlib import contextmanager
2import hashlib
3import os
4import shutil
5import tempfile
2from StringIO import StringIO6from StringIO import StringIO
3import tarfile7import tarfile
48
9from testtools import TestCase
10
5from hwpack.better_tarfile import writeable_tarfile11from hwpack.better_tarfile import writeable_tarfile
12from hwpack.packages import FetchedPackage
613
714
8@contextmanager15@contextmanager
@@ -37,3 +44,108 @@
37 yield tf44 yield tf
38 finally:45 finally:
39 tf.close()46 tf.close()
47
48
49class DummyFetchedPackage(FetchedPackage):
50 """A FetchedPackage with dummy information.
51
52 See FetchedPackage for the instance variables.
53 """
54
55 def __init__(self, name, version):
56 """Create a DummyFetchedPackage.
57
58 :param name: the name of the package.
59 :type name: str
60 :param version: the version of the package.
61 :type version: str
62 """
63 self.name = name
64 self.version = version
65
66 @property
67 def filename(self):
68 return "%s_%s_all.deb" % (self.name, self.version)
69
70 @property
71 def content(self):
72 return StringIO("Content of %s" % self.filename)
73
74 @property
75 def size(self):
76 return len(self.content.read())
77
78 @property
79 def md5(self):
80 md5sum = hashlib.md5()
81 md5sum.update(self.content.read())
82 return md5sum.hexdigest()
83
84
85class AptSourceFixture(object):
86 """A fixture that provides an apt source, with packages and indices.
87
88 An apt source provides a set of package files, and a Packages file
89 that allows apt to determine the contents of the source.
90
91 :ivar sources_entry: the URI and suite to give to apt to view the
92 source (i.e. a sources.list line without the "deb" prefix
93 :type sources_entry: str
94 """
95
96 def __init__(self, packages):
97 """Create an AptSourceFixture.
98
99 :param packages: a list of packages to add to the source
100 and index.
101 :type packages: an iterable of FetchedPackages
102 """
103 self.packages = packages
104
105 def setUp(self):
106 self.rootdir = tempfile.mkdtemp(prefix="hwpack-apt-source-")
107 for package in self.packages:
108 with open(
109 os.path.join(self.rootdir, package.filename), 'wb') as f:
110 f.write(package.content.read())
111 with open(os.path.join(self.rootdir, "Packages"), 'wb') as f:
112 for package in self.packages:
113 f.write('Package: %s\n' % package.name)
114 f.write('Version: %s\n' % package.version)
115 f.write('Filename: %s\n' % package.filename)
116 f.write('Size: %d\n' % package.size)
117 f.write('Architecture: all\n')
118 f.write('MD5sum: %s\n' % package.md5)
119 f.write('\n')
120
121 def tearDown(self):
122 if os.path.exists(self.rootdir):
123 shutil.rmtree(self.rootdir)
124
125 @property
126 def sources_entry(self):
127 return "file:" + os.path.abspath(self.rootdir) +" ./"
128
129
130class TestCaseWithFixtures(TestCase):
131 """A TestCase with the ability to easily add 'fixtures'.
132
133 A fixture is an object which can be created and cleaned up, and
134 this test case knows how to manage them to ensure that they will
135 always be cleaned up at the end of the test.
136 """
137
138 def useFixture(self, fixture):
139 """Make use of a fixture, ensuring that it will be cleaned up.
140
141 Given a fixture, this method will run the `setUp` method of
142 the fixture, and ensure that its `tearDown` method will be
143 called at the end of the test, regardless of success or failure.
144
145 :param fixture: the fixture to use.
146 :type fixture: an object with setUp and tearDown methods.
147 :return: the fixture that was passed in.
148 """
149 self.addCleanup(fixture.tearDown)
150 fixture.setUp()
151 return fixture
40152
=== modified file 'hwpack/tests/__init__.py'
--- hwpack/tests/__init__.py 2010-08-31 01:05:12 +0000
+++ hwpack/tests/__init__.py 2010-09-02 15:37:46 +0000
@@ -4,6 +4,7 @@
4 module_names = ['hwpack.tests.test_config',4 module_names = ['hwpack.tests.test_config',
5 'hwpack.tests.test_better_tarfile',5 'hwpack.tests.test_better_tarfile',
6 'hwpack.tests.test_hardwarepack',6 'hwpack.tests.test_hardwarepack',
7 'hwpack.tests.test_packages',
7 'hwpack.tests.test_tarfile_matchers',8 'hwpack.tests.test_tarfile_matchers',
8 ]9 ]
9 loader = unittest.TestLoader()10 loader = unittest.TestLoader()
1011
=== added file 'hwpack/tests/test_packages.py'
--- hwpack/tests/test_packages.py 1970-01-01 00:00:00 +0000
+++ hwpack/tests/test_packages.py 2010-09-02 15:37:46 +0000
@@ -0,0 +1,232 @@
1import os
2from StringIO import StringIO
3
4from testtools import TestCase
5
6from hwpack.packages import (
7 FetchedPackage,
8 PackageFetcher,
9 )
10from hwpack.testing import (
11 AptSourceFixture,
12 DummyFetchedPackage,
13 TestCaseWithFixtures,
14 )
15
16
17class FetchedPackageTests(TestCase):
18
19 def test_attributes(self):
20 package = FetchedPackage(
21 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
22 self.assertEqual("foo", package.name)
23 self.assertEqual("1.1", package.version)
24 self.assertEqual("foo_1.1.deb", package.filename)
25 self.assertEqual("xxxx", package.content.read())
26 self.assertEqual(4, package.size)
27 self.assertEqual("aaaa", package.md5)
28
29 def test_equal(self):
30 package1 = FetchedPackage(
31 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
32 package2 = FetchedPackage(
33 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
34 self.assertEqual(package1, package2)
35
36 def test_not_equal_different_name(self):
37 package1 = FetchedPackage(
38 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
39 package2 = FetchedPackage(
40 "bar", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
41 self.assertNotEqual(package1, package2)
42
43 def test_not_equal_different_version(self):
44 package1 = FetchedPackage(
45 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
46 package2 = FetchedPackage(
47 "foo", "1.2", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
48 self.assertNotEqual(package1, package2)
49
50 def test_not_equal_different_filename(self):
51 package1 = FetchedPackage(
52 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
53 package2 = FetchedPackage(
54 "foo", "1.1", "afoo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
55 self.assertNotEqual(package1, package2)
56
57 def test_not_equal_different_content(self):
58 package1 = FetchedPackage(
59 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
60 package2 = FetchedPackage(
61 "foo", "1.1", "foo_1.1.deb", StringIO("yyyy"), 4, "aaaa")
62 self.assertNotEqual(package1, package2)
63
64 def test_not_equal_different_size(self):
65 package1 = FetchedPackage(
66 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
67 package2 = FetchedPackage(
68 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 5, "aaaa")
69 self.assertNotEqual(package1, package2)
70
71 def test_not_equal_different_md5(self):
72 package1 = FetchedPackage(
73 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
74 package2 = FetchedPackage(
75 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "bbbb")
76 self.assertNotEqual(package1, package2)
77
78 def test_equal_hash_equal(self):
79 package1 = FetchedPackage(
80 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
81 package2 = FetchedPackage(
82 "foo", "1.1", "foo_1.1.deb", StringIO("xxxx"), 4, "aaaa")
83 self.assertEqual(hash(package1), hash(package2))
84
85
86class PackageFetcherTests(TestCaseWithFixtures):
87
88 def test_cleanup_removes_tempdir(self):
89 fetcher = PackageFetcher([])
90 fetcher.prepare()
91 tempdir = fetcher.tempdir
92 fetcher.cleanup()
93 self.assertFalse(os.path.exists(tempdir))
94
95 def test_cleanup_ignores_missing_tempdir(self):
96 fetcher = PackageFetcher([])
97 fetcher.prepare()
98 tempdir = fetcher.tempdir
99 fetcher.cleanup()
100 # Check that there is no problem removing it again
101 fetcher.cleanup()
102
103 def test_cleanup_before_prepare(self):
104 fetcher = PackageFetcher([])
105 # Check that there is no problem cleaning up before we start
106 fetcher.cleanup()
107
108 def test_prepare_creates_tempdir(self):
109 fetcher = PackageFetcher([])
110 self.addCleanup(fetcher.cleanup)
111 fetcher.prepare()
112 self.assertTrue(os.path.isdir(fetcher.tempdir))
113
114 def test_prepare_creates_var_lib_dpkg_status_file(self):
115 fetcher = PackageFetcher([])
116 self.addCleanup(fetcher.cleanup)
117 fetcher.prepare()
118 self.assertEqual(
119 '',
120 open(os.path.join(
121 fetcher.tempdir, "var", "lib", "dpkg", "status")).read())
122
123 def test_prepare_creates_var_cache_apt_archives_partial_dir(self):
124 fetcher = PackageFetcher([])
125 self.addCleanup(fetcher.cleanup)
126 fetcher.prepare()
127 self.assertTrue(
128 os.path.isdir(os.path.join(
129 fetcher.tempdir, "var", "cache", "apt", "archives",
130 "partial")))
131
132 def test_prepare_creates_var_lib_apt_lists_partial_dir(self):
133 fetcher = PackageFetcher([])
134 self.addCleanup(fetcher.cleanup)
135 fetcher.prepare()
136 self.assertTrue(
137 os.path.isdir(os.path.join(
138 fetcher.tempdir, "var", "lib", "apt", "lists", "partial")))
139
140 def test_prepare_creates_etc_apt_sources_list_file(self):
141 source1 = self.useFixture(AptSourceFixture([]))
142 source2 = self.useFixture(AptSourceFixture([]))
143 fetcher = PackageFetcher(
144 [source1.sources_entry, source2.sources_entry])
145 self.addCleanup(fetcher.cleanup)
146 fetcher.prepare()
147 self.assertEqual(
148 "deb %s\ndeb %s\n" % (
149 source1.sources_entry, source2.sources_entry),
150 open(os.path.join(
151 fetcher.tempdir, "etc", "apt", "sources.list")).read())
152
153 def get_fetcher(self, sources):
154 fetcher = PackageFetcher([s.sources_entry for s in sources])
155 self.addCleanup(fetcher.cleanup)
156 fetcher.prepare()
157 return fetcher
158
159 def test_fetch_packages_not_found_because_no_sources(self):
160 fetcher = self.get_fetcher([])
161 self.assertRaises(KeyError, fetcher.fetch_packages, ["nothere"])
162
163 def test_fetch_packages_not_found_because_not_in_sources(self):
164 available_package = DummyFetchedPackage("foo", "1.0")
165 source = self.useFixture(AptSourceFixture([available_package]))
166 fetcher = self.get_fetcher([source])
167 self.assertRaises(KeyError, fetcher.fetch_packages, ["nothere"])
168
169 def test_fetch_packages_not_found_one_of_two_missing(self):
170 available_package = DummyFetchedPackage("foo", "1.0")
171 source = self.useFixture(AptSourceFixture([available_package]))
172 fetcher = self.get_fetcher([source])
173 self.assertRaises(
174 KeyError, fetcher.fetch_packages, ["foo", "nothere"])
175
176 def test_fetch_packges_fetches_no_packages(self):
177 available_package = DummyFetchedPackage("foo", "1.0")
178 source = self.useFixture(AptSourceFixture([available_package]))
179 fetcher = self.get_fetcher([source])
180 self.assertEqual(0, len(fetcher.fetch_packages([])))
181
182 def test_fetch_packges_fetches_single_package(self):
183 available_package = DummyFetchedPackage("foo", "1.0")
184 source = self.useFixture(AptSourceFixture([available_package]))
185 fetcher = self.get_fetcher([source])
186 self.assertEqual(1, len(fetcher.fetch_packages(["foo"])))
187
188 def test_fetch_packges_fetches_correct_packge(self):
189 available_package = DummyFetchedPackage("foo", "1.0")
190 source = self.useFixture(AptSourceFixture([available_package]))
191 fetcher = self.get_fetcher([source])
192 self.assertEqual(
193 available_package, fetcher.fetch_packages(["foo"])[0])
194
195 def test_fetch_packges_fetches_multiple_packages(self):
196 available_packages = [
197 DummyFetchedPackage("bar", "1.0"),
198 DummyFetchedPackage("foo", "1.0"),
199 ]
200 source = self.useFixture(AptSourceFixture(available_packages))
201 fetcher = self.get_fetcher([source])
202 self.assertEqual(2, len(fetcher.fetch_packages(["foo", "bar"])))
203
204 def test_fetch_packges_fetches_multiple_packages_correctly(self):
205 available_packages = [
206 DummyFetchedPackage("foo", "1.0"),
207 DummyFetchedPackage("bar", "1.0"),
208 ]
209 source = self.useFixture(AptSourceFixture(available_packages))
210 fetcher = self.get_fetcher([source])
211 fetched = fetcher.fetch_packages(["foo", "bar"])
212 self.assertEqual(available_packages[0], fetched[0])
213 self.assertEqual(available_packages[1], fetched[1])
214
215 def test_fetch_packages_fetches_newest(self):
216 available_packages = [
217 DummyFetchedPackage("bar", "1.0"),
218 DummyFetchedPackage("bar", "1.1"),
219 ]
220 source = self.useFixture(AptSourceFixture(available_packages))
221 fetcher = self.get_fetcher([source])
222 fetched = fetcher.fetch_packages(["bar"])
223 self.assertEqual(available_packages[1], fetched[0])
224
225 def test_fetch_packages_fetches_newest_from_multiple_sources(self):
226 old_source_packages = [DummyFetchedPackage("bar", "1.0")]
227 new_source_packages = [DummyFetchedPackage("bar", "1.1")]
228 old_source = self.useFixture(AptSourceFixture(old_source_packages))
229 new_source = self.useFixture(AptSourceFixture(new_source_packages))
230 fetcher = self.get_fetcher([old_source, new_source])
231 fetched = fetcher.fetch_packages(["bar"])
232 self.assertEqual(new_source_packages[0], fetched[0])

Subscribers

People subscribed via source and target branches