Status: | Merged |
---|---|
Merged at revision: | 100 |
Proposed branch: | lp:~ev/apt-clone/python3 |
Merge into: | lp:apt-clone |
Diff against target: |
485 lines (+119/-83) 8 files modified
apt-clone (+10/-8) apt_clone.py (+46/-40) debian/changelog (+18/-1) tests/Makefile (+3/-1) tests/test_clone.py (+19/-11) tests/test_clone_upgrade.py (+15/-16) tests/test_in_chroot.py (+5/-3) tests/test_merge_sources.py (+3/-3) |
To merge this branch: | bzr merge lp:~ev/apt-clone/python3 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson | Approve | ||
Review via email: mp+109672@code.launchpad.net |
Commit message
Description of the change
Port to Python3.
To post a comment you must log in.
lp:~ev/apt-clone/python3
updated
- 109. By Evan
-
* Finish port to Python 3:
- Handle unicode changes.
- Do not leak file descriptors.
- Test against both Python2.7 and Python3.
- Do not use the deprecated failUnless.
- Use the io module instead of StringIO.
- Fix a failing test caused by acpi-support being in main. - 110. By Evan
-
Actually use python3 :)
lp:~ev/apt-clone/python3
updated
I've addressed the issues Colin raises as r111, r112, and r113.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Thanks, looks good to me now. I think this is Michael's upstream branch so he may want to flip the version back to 0.2.3 for upload to Debian.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'apt-clone' |
2 | --- apt-clone 2012-01-27 23:47:38 +0000 |
3 | +++ apt-clone 2012-06-12 09:23:17 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -#!/usr/bin/python |
6 | +#!/usr/bin/python3 |
7 | # Copyright (C) 2011 Canonical |
8 | # |
9 | # Authors: |
10 | @@ -17,6 +17,8 @@ |
11 | # this program; if not, write to the Free Software Foundation, Inc., |
12 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
13 | |
14 | +from __future__ import print_function |
15 | + |
16 | import argparse |
17 | from apt_clone import AptClone |
18 | |
19 | @@ -79,19 +81,19 @@ |
20 | clone = AptClone() |
21 | if args.command == "info": |
22 | info = clone.info(args.source) |
23 | - print info |
24 | + print(info) |
25 | if args.command == "clone": |
26 | clone.save_state(args.source, args.destination, |
27 | args.with_dpkg_repack, args.with_dpkg_status) |
28 | - print "not installable: %s" % ", ".join(clone.not_downloadable) |
29 | - print "version mismatch: %s" % ", ".join(clone.version_mismatch) |
30 | + print("not installable: %s" % ", ".join(clone.not_downloadable)) |
31 | + print("version mismatch: %s" % ", ".join(clone.version_mismatch)) |
32 | if not args.with_dpkg_repack: |
33 | - print "\nNote that you can use --with-dpkg-repack to include "\ |
34 | - "those packages in the clone file." |
35 | + print("\nNote that you can use --with-dpkg-repack to include " |
36 | + "those packges in the clone file.") |
37 | elif args.command == "restore": |
38 | if args.simulate: |
39 | miss = clone.simulate_restore_state(args.source) |
40 | - print "missing: %s" % ",".join(sorted(list(miss))) |
41 | + print("missing: %s" % ",".join(sorted(list(miss)))) |
42 | else: |
43 | clone.restore_state(args.source, args.destination) |
44 | elif args.command == "restore-new-distro": |
45 | @@ -111,7 +113,7 @@ |
46 | if args.simulate: |
47 | miss = clone.simulate_restore_state( |
48 | args.source, args.new_distro_codename) |
49 | - print "missing: %s" % ",".join(sorted(list(miss))) |
50 | + print("missing: %s" % ",".join(sorted(list(miss)))) |
51 | else: |
52 | clone.restore_state( |
53 | args.source, args.destination, args.new_distro_codename, protect_installed) |
54 | |
55 | === modified file 'apt_clone.py' |
56 | --- apt_clone.py 2012-01-27 17:14:05 +0000 |
57 | +++ apt_clone.py 2012-06-12 09:23:17 +0000 |
58 | @@ -16,6 +16,8 @@ |
59 | # this program; if not, write to the Free Software Foundation, Inc., |
60 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
61 | |
62 | +from __future__ import print_function |
63 | + |
64 | import apt |
65 | from apt.cache import FetchFailedException |
66 | import apt_pkg |
67 | @@ -29,7 +31,7 @@ |
68 | import tempfile |
69 | import time |
70 | |
71 | -from StringIO import StringIO |
72 | +from io import BytesIO |
73 | |
74 | if "APT_CLONE_DEBUG_RESOLVER" in os.environ: |
75 | apt_pkg.config.set("Debug::pkgProblemResolver", "1") |
76 | @@ -158,9 +160,9 @@ |
77 | 'arch' : apt_pkg.config.find("APT::Architecture") |
78 | } |
79 | # save it |
80 | - f = tempfile.NamedTemporaryFile() |
81 | + f = tempfile.NamedTemporaryFile(mode='w') |
82 | info = "\n".join(["%s: %s" % (key, value) |
83 | - for (key, value) in host_info.iteritems()]) |
84 | + for (key, value) in host_info.items()]) |
85 | f.write(info+"\n") |
86 | f.flush() |
87 | tar.add(f.name, arcname="./var/lib/apt-clone/uname") |
88 | @@ -180,9 +182,10 @@ |
89 | self.version_mismatch.add(pkg.name) |
90 | # store the installed.pkgs |
91 | tarinfo = tarfile.TarInfo("./var/lib/apt-clone/installed.pkgs") |
92 | + s = s.encode('utf-8') |
93 | tarinfo.size = len(s) |
94 | tarinfo.mtime = time.time() |
95 | - tar.addfile(tarinfo, StringIO(s)) |
96 | + tar.addfile(tarinfo, BytesIO(s)) |
97 | |
98 | def _write_state_dpkg_status(self, tar): |
99 | # store dpkg-status, this is not strictly needed as installed.pkgs |
100 | @@ -232,11 +235,11 @@ |
101 | self.commands.repack_deb(pkgname, tdir) |
102 | tar.add(tdir, arcname="./var/lib/apt-clone/debs") |
103 | shutil.rmtree(tdir) |
104 | - #print tdir |
105 | + #print(tdir) |
106 | |
107 | # detect prefix |
108 | def _detect_tarprefix(self, tar): |
109 | - #print tar.getnames() |
110 | + #print(tar.getnames()) |
111 | if tar.getnames()[-1].startswith("./"): |
112 | self.TARPREFIX = "./" |
113 | else: |
114 | @@ -313,7 +316,7 @@ |
115 | self._detect_tarprefix(tar) |
116 | |
117 | if not os.path.exists(targetdir): |
118 | - print "Dir '%s' does not exist, need to bootstrap first" % targetdir |
119 | + print("Dir '%s' does not exist, need to bootstrap first" % targetdir) |
120 | distro = self._get_info_distro(statefile) |
121 | self.commands.debootstrap(targetdir, distro) |
122 | |
123 | @@ -401,7 +404,7 @@ |
124 | # the actiongroup will help libapt to speed up the following loop |
125 | with cache.actiongroup(): |
126 | for line in f.readlines(): |
127 | - line = line.strip() |
128 | + line = line.strip().decode('utf-8') |
129 | if line.startswith("#") or line == "": |
130 | continue |
131 | (name, version, auto) = line.split() |
132 | @@ -416,7 +419,7 @@ |
133 | if cache.broken_count > 0: |
134 | resolver.resolve() |
135 | if not cache[name].marked_install: |
136 | - raise SystemError, "pkg %s not marked upgrade" % name |
137 | + raise SystemError("pkg %s not marked upgrade" % name) |
138 | else: |
139 | # normal mode, this assume the system is consistent |
140 | cache[name].mark_install(from_user=from_user) |
141 | @@ -519,9 +522,10 @@ |
142 | owned = set() |
143 | dpkg_basedir = os.path.dirname(apt_pkg.config.get("Dir::State::status")) |
144 | for f in glob.glob(os.path.join(dpkg_basedir, "info", "*.list")): |
145 | - for line in open(f): |
146 | - if line.startswith("/etc/"): |
147 | - owned.add(line.strip()) |
148 | + with open(f) as fp: |
149 | + for line in fp: |
150 | + if line.startswith("/etc/"): |
151 | + owned.add(line.strip()) |
152 | # now go over etc |
153 | unowned = set() |
154 | for dirpath, dirnames, filenames in os.walk(etcdir): |
155 | @@ -535,37 +539,39 @@ |
156 | dpkg_status = sourcedir+apt_pkg.config.find("Dir::State::status") |
157 | modified = set() |
158 | # iterate dpkg-status file |
159 | - tag = apt_pkg.TagFile(open(dpkg_status)) |
160 | - for entry in tag: |
161 | - if "conffiles" in entry: |
162 | - for line in entry["conffiles"].split("\n"): |
163 | - obsolete = None |
164 | - if len(line.split()) == 3: |
165 | - name, md5sum, obsolete = line.split() |
166 | - else: |
167 | - name, md5sum = line.split() |
168 | - # update |
169 | - path = sourcedir+name |
170 | - md5sum = md5sum.strip() |
171 | - # ignore oboslete conffiles |
172 | - if obsolete == "obsolete": |
173 | - continue |
174 | - # user removed conffile |
175 | - if not os.path.exists(path): |
176 | - logging.debug("conffile %s removed" % path) |
177 | - modified.add(path) |
178 | - continue |
179 | - # check content |
180 | - md5 = hashlib.md5() |
181 | - md5.update(open(path).read()) |
182 | - if md5.hexdigest() != md5sum: |
183 | - logging.debug("conffile %s (%s != %s)" % ( |
184 | - path, md5.hexdigest(), md5sum)) |
185 | - modified.add(path) |
186 | + with open(dpkg_status) as fp: |
187 | + tag = apt_pkg.TagFile(fp) |
188 | + for entry in tag: |
189 | + if "conffiles" in entry: |
190 | + for line in entry["conffiles"].split("\n"): |
191 | + obsolete = None |
192 | + if len(line.split()) == 3: |
193 | + name, md5sum, obsolete = line.split() |
194 | + else: |
195 | + name, md5sum = line.split() |
196 | + # update |
197 | + path = sourcedir+name |
198 | + md5sum = md5sum.strip() |
199 | + # ignore oboslete conffiles |
200 | + if obsolete == "obsolete": |
201 | + continue |
202 | + # user removed conffile |
203 | + if not os.path.exists(path): |
204 | + logging.debug("conffile %s removed" % path) |
205 | + modified.add(path) |
206 | + continue |
207 | + # check content |
208 | + md5 = hashlib.md5() |
209 | + with open(path, 'rb') as fp: |
210 | + md5.update(fp.read()) |
211 | + if md5.hexdigest() != md5sum: |
212 | + logging.debug("conffile %s (%s != %s)" % ( |
213 | + path, md5.hexdigest(), md5sum)) |
214 | + modified.add(path) |
215 | return modified |
216 | |
217 | def _dump_debconf_database(self, sourcedir): |
218 | - print "not implemented yet" |
219 | + print("not implemented yet") |
220 | # debconf-copydb configdb newdb --config=Name:newdb --config=Driver:File --config=Filename:/tmp/lala.db |
221 | # |
222 | # debconf-copydb newdb configdb --config=Name:newdb --config=Driver:File --config=Filename:/tmp/lala.db |
223 | |
224 | === modified file 'debian/changelog' |
225 | --- debian/changelog 2012-01-27 16:54:20 +0000 |
226 | +++ debian/changelog 2012-06-12 09:23:17 +0000 |
227 | @@ -1,11 +1,28 @@ |
228 | -apt-clone (0.2.3) UNRELEASED; urgency=low |
229 | +apt-clone (0.2.2ubuntu1) UNRELEASED; urgency=low |
230 | |
231 | + [ Michael Vogt ] |
232 | * apt_clone.py: |
233 | - fix restoring of the clone with later apt versions (thanks |
234 | to Colin Watson) |
235 | - bind mount /proc, /sys on restore to chroot (thanks to |
236 | Colin Watson) |
237 | |
238 | + [ Colin Watson ] |
239 | + * Port to Python 3: |
240 | + - Use Python 3-style print functions. |
241 | + - Use "raise Exception(value)" syntax rather than the old-style "raise |
242 | + Exception, value". |
243 | + - Use dict.items() rather than dict.iteritems(). |
244 | + |
245 | + [ Evan Dandrea ] |
246 | + * Finish port to Python 3: |
247 | + - Handle unicode changes. |
248 | + - Do not leak file descriptors. |
249 | + - Test against both Python2.7 and Python3. |
250 | + - Do not use the deprecated failUnless. |
251 | + - Use the io module instead of StringIO. |
252 | + - Fix a failing test caused by acpi-support being in main. |
253 | + |
254 | -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 27 Jan 2012 16:29:17 +0100 |
255 | |
256 | apt-clone (0.2.2) unstable; urgency=low |
257 | |
258 | === modified file 'tests/Makefile' |
259 | --- tests/Makefile 2011-04-08 14:36:22 +0000 |
260 | +++ tests/Makefile 2012-06-12 09:23:17 +0000 |
261 | @@ -4,7 +4,9 @@ |
262 | test: |
263 | pyflakes ../apt-clone ../apt_clone.py |
264 | set -e; for f in *.py; do \ |
265 | - PYTHONPATH=.. python $$f; \ |
266 | + for ver in python3 python2; do \ |
267 | + PYTHONPATH=.. $$ver $$f; \ |
268 | + done; \ |
269 | done; \ |
270 | # cruft from the tests |
271 | rm -f ./data/mock-system/var/cache/apt/*.bin |
272 | |
273 | === modified file 'tests/test_clone.py' |
274 | --- tests/test_clone.py 2012-01-02 18:08:31 +0000 |
275 | +++ tests/test_clone.py 2012-06-12 09:23:17 +0000 |
276 | @@ -1,5 +1,7 @@ |
277 | #!/usr/bin/python |
278 | |
279 | +from __future__ import print_function |
280 | + |
281 | import apt |
282 | import apt_pkg |
283 | import mock |
284 | @@ -10,7 +12,7 @@ |
285 | import tempfile |
286 | import unittest |
287 | |
288 | -from StringIO import StringIO |
289 | +from io import StringIO |
290 | |
291 | sys.path.insert(0, "..") |
292 | import apt_clone |
293 | @@ -31,7 +33,8 @@ |
294 | os.makedirs(os.path.join(self.tempdir, "var/lib/dpkg/")) |
295 | # ensure we are the right arch |
296 | os.makedirs(os.path.join(self.tempdir, "etc/apt")) |
297 | - open(os.path.join(self.tempdir, "etc/apt/apt.conf"), "w").write(''' |
298 | + with open(os.path.join(self.tempdir, "etc/apt/apt.conf"), "w") as fp: |
299 | + fp.write(''' |
300 | #clear Dpkg::Post-Invoke; |
301 | #clear Dpkg::Pre-Invoke; |
302 | #clear APT::Update; |
303 | @@ -59,7 +62,7 @@ |
304 | tarname = os.path.join(targetdir, clone.CLONE_FILENAME) |
305 | self.assertTrue(os.path.exists(tarname)) |
306 | tar = tarfile.open(tarname) |
307 | - #print tar.getmembers() |
308 | + #print(tar.getmembers()) |
309 | # verify members in tar |
310 | members = [m.name for m in tar.getmembers()] |
311 | self.assertTrue("./etc/apt/sources.list" in members) |
312 | @@ -108,11 +111,14 @@ |
313 | # create target dir |
314 | targetdir = self.tempdir |
315 | # status file from maverick (to simulate running on a maverick live-cd) |
316 | - s=open("./data/dpkg-status/dpkg-status-ubuntu-maverick").read() |
317 | + with open("./data/dpkg-status/dpkg-status-ubuntu-maverick") as fp: |
318 | + s = fp.read() |
319 | s = s.replace( |
320 | "Architecture: i386", |
321 | "Architecture: %s" % apt_pkg.config.find("Apt::Architecture")) |
322 | - open(os.path.join(targetdir, "var/lib/dpkg", "status"), "w").write(s) |
323 | + path = os.path.join(targetdir, "var/lib/dpkg", "status") |
324 | + with open(path, "w") as fp: |
325 | + fp.write(s) |
326 | # test upgrade clone from lucid system to maverick |
327 | clone = AptClone(cache_cls=MockAptCache) |
328 | clone.restore_state( |
329 | @@ -121,14 +127,16 @@ |
330 | "maverick") |
331 | sources_list = os.path.join(targetdir, "etc","apt","sources.list") |
332 | self.assertTrue(os.path.exists(sources_list)) |
333 | - self.assertTrue("maverick" in open(sources_list).read()) |
334 | - self.assertFalse("lucid" in open(sources_list).read()) |
335 | + with open(sources_list) as fp: |
336 | + self.assertTrue("maverick" in fp.read()) |
337 | + with open(sources_list) as fp: |
338 | + self.assertFalse("lucid" in fp.read()) |
339 | |
340 | def test_restore_state_simulate(self): |
341 | clone = AptClone() |
342 | missing = clone.simulate_restore_state("./data/apt-state.tar.gz") |
343 | # missing, because clone does not have universe enabled |
344 | - self.assertEqual(list(missing), ["accerciser", "acpi-support"]) |
345 | + self.assertEqual(list(missing), ["accerciser"]) |
346 | |
347 | def test_restore_state_simulate_with_new_release(self): |
348 | #apt_pkg.config.set("Debug::PkgProblemResolver", "1") |
349 | @@ -139,7 +147,7 @@ |
350 | missing = clone.simulate_restore_state( |
351 | "./data/apt-state-ubuntu-lucid.tar.gz", "maverick") |
352 | # FIXME: check that the stuff in missing is ok |
353 | - print missing |
354 | + print(missing) |
355 | |
356 | def test_modified_conffiles(self): |
357 | clone = AptClone() |
358 | @@ -162,13 +170,13 @@ |
359 | "Dir::state::status", |
360 | "/var/lib/dpkg/status") |
361 | unowned = clone._find_unowned_in_etc() |
362 | - #print unowned |
363 | + #print(unowned) |
364 | self.assertNotEqual(unowned, set()) |
365 | # negative test, is created by the installer |
366 | self.assertTrue("/etc/apt/sources.list" in unowned) |
367 | # postivie test, belongs to base-files |
368 | self.assertFalse("/etc/issue" in unowned) |
369 | - print "\n".join(sorted(unowned)) |
370 | + print("\n".join(sorted(unowned))) |
371 | |
372 | if __name__ == "__main__": |
373 | unittest.main() |
374 | |
375 | === modified file 'tests/test_clone_upgrade.py' |
376 | --- tests/test_clone_upgrade.py 2011-04-07 13:02:27 +0000 |
377 | +++ tests/test_clone_upgrade.py 2012-06-12 09:23:17 +0000 |
378 | @@ -9,7 +9,7 @@ |
379 | import tempfile |
380 | import unittest |
381 | |
382 | -from StringIO import StringIO |
383 | +from io import StringIO |
384 | |
385 | sys.path.insert(0, "..") |
386 | import apt_clone |
387 | @@ -60,7 +60,8 @@ |
388 | sources_list = os.path.join(tmpdir, "etc", "apt", "sources.list") |
389 | if not os.path.exists(os.path.dirname(sources_list)): |
390 | os.makedirs(os.path.dirname(sources_list)) |
391 | - open(os.path.join(sources_list), "w").write(""" |
392 | + with open(os.path.join(sources_list), "w") as fp: |
393 | + fp.write(""" |
394 | deb http://archive.ubuntu.com/ubuntu %s main restricted universe multiverse |
395 | """ % from_dist) |
396 | cache = apt.Cache(rootdir=tmpdir) |
397 | @@ -74,20 +75,18 @@ |
398 | dpkg_status = os.path.join(tmpdir, "var", "lib", "dpkg", "status") |
399 | if not os.path.exists(os.path.dirname(dpkg_status)): |
400 | os.makedirs(os.path.dirname(dpkg_status)) |
401 | - dpkg = open(dpkg_status, "w") |
402 | - installed = open(installed_pkgs, "w") |
403 | - for pkg in cache: |
404 | - if pkg.marked_install: |
405 | - s = str(pkg.candidate.record) |
406 | - s = s.replace("Package: %s\n" % pkg.name, |
407 | - "Package: %s\n%s\n" % ( |
408 | - pkg.name, "Status: install ok installed")) |
409 | - dpkg.write("%s\n" % s) |
410 | - installed.write("%s %s %s\n" % (pkg.name, |
411 | - pkg.candidate.version, |
412 | - int(pkg.is_auto_installed))) |
413 | - dpkg.close() |
414 | - installed.close() |
415 | + with open(dpkg_status, "w") as dpkg: |
416 | + with open(installed_pkgs, "w") as installed: |
417 | + for pkg in cache: |
418 | + if pkg.marked_install: |
419 | + s = str(pkg.candidate.record) |
420 | + s = s.replace("Package: %s\n" % pkg.name, |
421 | + "Package: %s\n%s\n" % ( |
422 | + pkg.name, "Status: install ok installed")) |
423 | + dpkg.write("%s\n" % s) |
424 | + installed.write("%s %s %s\n" % (pkg.name, |
425 | + pkg.candidate.version, |
426 | + int(pkg.is_auto_installed))) |
427 | return tmpdir |
428 | |
429 | |
430 | |
431 | === modified file 'tests/test_in_chroot.py' |
432 | --- tests/test_in_chroot.py 2012-01-27 17:14:05 +0000 |
433 | +++ tests/test_in_chroot.py 2012-06-12 09:23:17 +0000 |
434 | @@ -1,5 +1,7 @@ |
435 | #!/usr/bin/python |
436 | |
437 | +from __future__ import print_function |
438 | + |
439 | import apt |
440 | import logging |
441 | import os |
442 | @@ -25,7 +27,7 @@ |
443 | |
444 | def test_real(self): |
445 | if os.getuid() != 0: |
446 | - print "Skipping because uid != 0" |
447 | + print("Skipping because uid != 0") |
448 | return |
449 | # do it |
450 | target = "./test-chroot" |
451 | @@ -34,8 +36,8 @@ |
452 | subprocess.call(["debootstrap", "--arch=i386", |
453 | "maverick", target]) |
454 | # force i386 |
455 | - open(os.path.join(target, "etc/apt/apt.conf"), "w").write( |
456 | - 'APT::Architecture "i386";') |
457 | + with open(os.path.join(target, "etc/apt/apt.conf"), "w") as fp: |
458 | + fp.write('APT::Architecture "i386";') |
459 | |
460 | # restore |
461 | clone = AptClone() |
462 | |
463 | === modified file 'tests/test_merge_sources.py' |
464 | --- tests/test_merge_sources.py 2011-04-06 14:06:31 +0000 |
465 | +++ tests/test_merge_sources.py 2012-06-12 09:23:17 +0000 |
466 | @@ -26,8 +26,8 @@ |
467 | if line != '\n' and not line.startswith('#'): |
468 | tally[line] += 1 |
469 | # There should not be any duplicate source lines. |
470 | - for line, count in tally.iteritems(): |
471 | - self.failUnless(count == 1, '"%s" occurred %d times.' |
472 | + for line, count in tally.items(): |
473 | + self.assertTrue(count == 1, '"%s" occurred %d times.' |
474 | % (line, count)) |
475 | |
476 | # Check for extras, others... |
477 | @@ -43,7 +43,7 @@ |
478 | for line in fp: |
479 | if line == match: |
480 | found = True |
481 | - self.failUnless(found, |
482 | + self.assertTrue(found, |
483 | '%s repository not present or disabled.' % pocket) |
484 | |
485 | if __name__ == "__main__": |
> === modified file 'apt_clone.py' TarInfo( "./var/ lib/apt- clone/installed .pkgs") tarinfo, StringIO(s)) tarinfo, BytesIO(s))
> --- apt_clone.py 2012-01-27 17:14:05 +0000
> +++ apt_clone.py 2012-06-11 15:50:47 +0000
> @@ -182,7 +184,8 @@
> tarinfo = tarfile.
> tarinfo.size = len(s)
> tarinfo.mtime = time.time()
> - tar.addfile(
> + s = s.encode('utf-8')
> + tar.addfile(
tarinfo.size will be wrong if s isn't entirely ASCII. I think you need
to encode earlier.
> - #print tar.getnames() getnames( ))
> + print(tar.
Did you mean to uncomment this?
> === modified file 'debian/changelog'
> --- debian/changelog 2012-01-27 16:54:20 +0000
> +++ debian/changelog 2012-06-11 15:50:47 +0000
> @@ -8,6 +8,16 @@
>
> -- Michael Vogt <email address hidden> Fri, 27 Jan 2012 16:29:17 +0100
>
> +apt-clone (0.2.2ubuntu1) UNRELEASED; urgency=low
> +
> + * Port to Python 3:
> + - Use Python 3-style print functions.
> + - Use "raise Exception(value)" syntax rather than the old-style "raise
> + Exception, value".
> + - Use dict.items() rather than dict.iteritems().
> +
> + -- Colin Watson <email address hidden> Mon, 11 Jun 2012 09:12:14 +0100
> +
> apt-clone (0.2.2) unstable; urgency=low
>
> * fix extraction of no-longer downloadable debs, thanks
>
This might need updating :-)
The rest looks good to me.
review needs-fixing