Merge lp:~james-w/linaro-image-tools/tarfile-improvements into lp:linaro-image-tools/11.11

Proposed by James Westby
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 68
Merged at revision: 54
Proposed branch: lp:~james-w/linaro-image-tools/tarfile-improvements
Merge into: lp:linaro-image-tools/11.11
Prerequisite: lp:~james-w/linaro-image-tools/config-docs
Diff against target: 294 lines (+274/-0)
3 files modified
hwpack/better_tarfile.py (+97/-0)
hwpack/tests/__init__.py (+1/-0)
hwpack/tests/test_better_tarfile.py (+176/-0)
To merge this branch: bzr merge lp:~james-w/linaro-image-tools/tarfile-improvements
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle (community) Approve
Review via email: mp+34144@code.launchpad.net

Description of the change

Hi,

This adds a subclass of the stdlib tarfile.TarFile that aids
in creating tarfiles without touching the filesystem.

I want this for two reasons, the first being for testing my next
branch, which adds some matchers for tarfile contents. I want to
test those without hitting the filesystem.

The second reason is that if possible I want to create the hwpack
tarball without putting all the files on the filesystem first.

Thanks,

James

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

Merged config-docs into tarfile-improvements.

67. By James Westby

Factor out some common code in the tests.

68. By James Westby

Use dict.pop instead of the custom implementation. Thanks Michael.

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

hwpack/better_tarfile.py could use a module docstring.

It's not clear to me why standard_tarfile or better_tarfile have default arguments: they're both only ever called with the non-defaulted arguments supplied (relatedly, I wonder what the combination of mode and fileobj to TarFile.__init__ means.....)

The standard_tarfile context manager is only used in tests, so maybe it should be defined in the test module.

I don't like the way that the tests "know" that create_simple_tarball creates a file called foo with content bar. I think it would be better to use a pattern like bzrlib's build_tree where you say create_simple_tarball([('foo', 'bar')]) or something.

All else looks ok.

review: Approve
Revision history for this message
James Westby (james-w) wrote :

On Tue, 31 Aug 2010 01:13:40 -0000, Michael Hudson <email address hidden> wrote:
> Review: Approve
> hwpack/better_tarfile.py could use a module docstring.

Done, thanks.

> It's not clear to me why standard_tarfile or better_tarfile have
> default arguments: they're both only ever called with the
> non-defaulted arguments supplied (relatedly, I wonder what the
> combination of mode and fileobj to TarFile.__init__ means.....)

Later branches supply different mode arguments, so I probably should
have made that change there.

mode can also be used to specify the compression that will be used, so
while a mode of "w" on a read-only file object makes no sense, it is
needed to have both.

> The standard_tarfile context manager is only used in tests, so maybe
> it should be defined in the test module.

It's used in two test modules later, which is why I moved it here, but
I'll move it to a testing module (if it stays given your comments on the
next proposal.)

> I don't like the way that the tests "know" that create_simple_tarball
> creates a file called foo with content bar. I think it would be
> better to use a pattern like bzrlib's build_tree where you say
> create_simple_tarball([('foo', 'bar')]) or something.

I think that's a good idea. I had them all explicitly creating the
paths, but given that they all created identical paths I just factored
it out.

The other option would be fixture, where the path would be an attribute,
and so wouldn't be hardcoded like that.

> All else looks ok.

Thanks, tweaks are on their way.

James

69. By James Westby

Add a module docstring to better_tarfile. Thanks Michael.

70. By James Westby

Less magic in the tests. Thanks Michael.

Revision history for this message
James Westby (james-w) wrote :

On Tue, 31 Aug 2010 10:48:06 -0400, James Westby <email address hidden> wrote:
> On Tue, 31 Aug 2010 01:13:40 -0000, Michael Hudson <email address hidden> wrote:
> > The standard_tarfile context manager is only used in tests, so maybe
> > it should be defined in the test module.
>
> It's used in two test modules later, which is why I moved it here, but
> I'll move it to a testing module (if it stays given your comments on the
> next proposal.)

Moved.

> > I don't like the way that the tests "know" that create_simple_tarball
> > creates a file called foo with content bar. I think it would be
> > better to use a pattern like bzrlib's build_tree where you say
> > create_simple_tarball([('foo', 'bar')]) or something.
>
> I think that's a good idea. I had them all explicitly creating the
> paths, but given that they all created identical paths I just factored
> it out.

Made the change as suggested. I don't like the duplication with the
helper I add in the next branch, but I'm not sure I like indirecting
through a helper in order to test the functionality.

Thanks,

James

71. By James Westby

Merged config-docs into tarfile-improvements.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'hwpack/better_tarfile.py'
--- hwpack/better_tarfile.py 1970-01-01 00:00:00 +0000
+++ hwpack/better_tarfile.py 2010-09-01 20:48:41 +0000
@@ -0,0 +1,97 @@
1from contextlib import contextmanager
2from StringIO import StringIO
3from tarfile import DIRTYPE, TarFile as StandardTarFile, TarInfo
4
5"""Improvements to the standard library's tarfile module.
6
7In particular this module provides a tarfile.TarFile subclass that aids
8in adding paths to the tarfile that aren't present on the filesystem,
9with the ability to specify file content as strings, and provide
10default values for the mtime, uid, etc. of the created paths.
11"""
12
13@contextmanager
14def writeable_tarfile(backing_file, mode="w", **kwargs):
15 """A context manager to get a writeable better tarfile.
16
17 :param backing_file: a file object to write the tarfile contents
18 to.
19 :param mode: the mode to open the tarfile with. Default is
20 "w".
21 :param kwargs: other keyword arguments to pass to the TarFile
22 constructor.
23 """
24 tf = TarFile.open(mode=mode, fileobj=backing_file, **kwargs)
25 try:
26 yield tf
27 finally:
28 tf.close()
29
30
31class TarFile(StandardTarFile):
32 """An improvement to tarfile that can add paths not on the filesystem.
33
34 With the standard tarfile implementation adding paths that are not
35 present on the filesystem is convoluted. This subclass adds methods
36 to create paths in the tarfile that are not present on the filesystem.
37
38 In addition, it can take constructor parameters to set the defaults
39 of various attributes of the paths that it adds.
40 """
41
42 def __init__(self, *args, **kwargs):
43 """Create a TarFile.
44
45 :param default_mtime: the default mtime to create paths with,
46 an int or None to use the stdlib default.
47 :param default_uid: the default user id to set as the owner of
48 created paths, an int or None to use the stdlib default.
49 :param default_gid: the default group id to set as the owner of
50 created paths, an int or None to use the stdlib default.
51 :param default_uname: the default user name to set as the owner
52 of created paths, a string, or None to use the stdlib default.
53 :param default_gname: the default group name ot set as the owner
54 of created paths, a string, or None to use the stdlib default.
55 """
56 self.default_mtime = kwargs.pop("default_mtime", None)
57 self.default_uid = kwargs.pop("default_uid", None)
58 self.default_gid = kwargs.pop("default_gid", None)
59 self.default_uname = kwargs.pop("default_uname", None)
60 self.default_gname = kwargs.pop("default_gname", None)
61 super(TarFile, self).__init__(*args, **kwargs)
62
63 def _set_defaults(self, tarinfo):
64 if self.default_mtime is not None:
65 tarinfo.mtime = self.default_mtime
66 if self.default_uid is not None:
67 tarinfo.uid = self.default_uid
68 if self.default_gid is not None:
69 tarinfo.gid = self.default_gid
70 if self.default_uname is not None:
71 tarinfo.uname = self.default_uname
72 if self.default_gname is not None:
73 tarinfo.gname = self.default_gname
74
75 def create_file_from_string(self, filename, content):
76 """Create a file with the contents passed as a string.
77
78 :param filename: the path to put the file at inside the
79 tarfile.
80 :param content: the content to put in the created file.
81 """
82 tarinfo = TarInfo(name=filename)
83 tarinfo.size = len(content)
84 self._set_defaults(tarinfo)
85 fileobj = StringIO(content)
86 self.addfile(tarinfo, fileobj=fileobj)
87
88 def create_dir(self, path):
89 """Create a directory within the tarfile.
90
91 :param path: the path to put the directory at.
92 """
93 tarinfo = TarInfo(name=path)
94 tarinfo.type = DIRTYPE
95 tarinfo.mode = 0755
96 self._set_defaults(tarinfo)
97 self.addfile(tarinfo)
098
=== modified file 'hwpack/tests/__init__.py'
--- hwpack/tests/__init__.py 2010-08-27 19:35:40 +0000
+++ hwpack/tests/__init__.py 2010-09-01 20:48:41 +0000
@@ -2,6 +2,7 @@
22
3def test_suite():3def test_suite():
4 module_names = ['hwpack.tests.test_config',4 module_names = ['hwpack.tests.test_config',
5 'hwpack.tests.test_better_tarfile',
5 ]6 ]
6 loader = unittest.TestLoader()7 loader = unittest.TestLoader()
7 suite = loader.loadTestsFromNames(module_names)8 suite = loader.loadTestsFromNames(module_names)
89
=== added file 'hwpack/tests/test_better_tarfile.py'
--- hwpack/tests/test_better_tarfile.py 1970-01-01 00:00:00 +0000
+++ hwpack/tests/test_better_tarfile.py 2010-09-01 20:48:41 +0000
@@ -0,0 +1,176 @@
1from contextlib import contextmanager
2from StringIO import StringIO
3import tarfile
4
5from testtools import TestCase
6
7from hwpack.better_tarfile import writeable_tarfile
8
9
10@contextmanager
11def standard_tarfile(backing_file, mode="r", seek=True):
12 """A context manager to open a stdlib tarfile.
13
14 :param backing_file: the file object to take the tarfile
15 contents from.
16 :param mode: the mode to open the tarfile with.
17 :param seek: whether to seek the backing file to 0 before
18 opening.
19 """
20 if seek:
21 backing_file.seek(0)
22 tf = tarfile.TarFile.open(mode=mode, fileobj=backing_file)
23 try:
24 yield tf
25 finally:
26 tf.close()
27
28
29class TarFileTests(TestCase):
30
31 def test_creates_empty_tarfile(self):
32 backing_file = StringIO()
33 with writeable_tarfile(backing_file):
34 pass
35 with standard_tarfile(backing_file, seek=False) as tf:
36 self.assertEqual([], tf.getnames())
37
38 def create_simple_tarball(self, contents, **kwargs):
39 backing_file = StringIO()
40 with writeable_tarfile(backing_file, **kwargs) as tf:
41 for path, content in contents:
42 if path[-1] == '/':
43 tf.create_dir(path)
44 else:
45 tf.create_file_from_string(path, content)
46 return backing_file
47
48 def test_create_file_from_string_adds_path(self):
49 backing_file = self.create_simple_tarball([("foo", "bar")])
50 with standard_tarfile(backing_file) as tf:
51 self.assertEqual(["foo"], tf.getnames())
52
53 def test_create_file_from_string_uses_content(self):
54 backing_file = self.create_simple_tarball([("foo", "bar")])
55 with standard_tarfile(backing_file) as tf:
56 self.assertEqual("bar", tf.extractfile("foo").read())
57
58 def test_create_file_from_string_sets_size(self):
59 backing_file = self.create_simple_tarball([("foo", "bar")])
60 with standard_tarfile(backing_file) as tf:
61 self.assertEqual(3, tf.getmember("foo").size)
62
63 def test_create_file_from_string_sets_mode(self):
64 backing_file = self.create_simple_tarball([("foo", "bar")])
65 with standard_tarfile(backing_file) as tf:
66 self.assertEqual(0644, tf.getmember("foo").mode)
67
68 def test_create_file_from_string_sets_type(self):
69 backing_file = self.create_simple_tarball([("foo", "bar")])
70 with standard_tarfile(backing_file) as tf:
71 self.assertEqual(tarfile.REGTYPE, tf.getmember("foo").type)
72
73 def test_create_file_from_string_sets_linkname(self):
74 backing_file = self.create_simple_tarball([("foo", "bar")])
75 with standard_tarfile(backing_file) as tf:
76 self.assertEqual('', tf.getmember("foo").linkname)
77
78 def test_create_file_uses_default_mtime(self):
79 now = 126793
80 backing_file = self.create_simple_tarball(
81 [("foo", "bar")], default_mtime=now)
82 with standard_tarfile(backing_file) as tf:
83 self.assertEqual(now, tf.getmember("foo").mtime)
84
85 def test_create_file_uses_default_uid(self):
86 uid = 1259
87 backing_file = self.create_simple_tarball(
88 [("foo", "bar")], default_uid=uid)
89 with standard_tarfile(backing_file) as tf:
90 self.assertEqual(uid, tf.getmember("foo").uid)
91
92 def test_create_file_uses_default_gid(self):
93 gid = 2259
94 backing_file = self.create_simple_tarball(
95 [("foo", "bar")], default_gid=gid)
96 with standard_tarfile(backing_file) as tf:
97 self.assertEqual(gid, tf.getmember("foo").gid)
98
99 def test_create_file_uses_default_uname(self):
100 uname = "someperson"
101 backing_file = self.create_simple_tarball(
102 [("foo", "bar")], default_uname=uname)
103 with standard_tarfile(backing_file) as tf:
104 self.assertEqual(uname, tf.getmember("foo").uname)
105
106 def test_create_file_uses_default_gname(self):
107 gname = "somegroup"
108 backing_file = self.create_simple_tarball(
109 [("foo", "bar")], default_gname=gname)
110 with standard_tarfile(backing_file) as tf:
111 self.assertEqual(gname, tf.getmember("foo").gname)
112
113 def test_create_dir_adds_path(self):
114 backing_file = self.create_simple_tarball([("foo/", "")])
115 with standard_tarfile(backing_file) as tf:
116 self.assertEqual(["foo"], tf.getnames())
117
118 def test_create_dir_sets_name(self):
119 backing_file = self.create_simple_tarball([("foo/", "")])
120 with standard_tarfile(backing_file) as tf:
121 self.assertEqual("foo", tf.getmember("foo").name)
122
123 def test_create_dir_sets_type(self):
124 backing_file = self.create_simple_tarball([("foo/", "")])
125 with standard_tarfile(backing_file) as tf:
126 self.assertEqual(tarfile.DIRTYPE, tf.getmember("foo").type)
127
128 def test_create_dir_sets_size(self):
129 backing_file = self.create_simple_tarball([("foo/", "")])
130 with standard_tarfile(backing_file) as tf:
131 self.assertEqual(0, tf.getmember("foo").size)
132
133 def test_create_dir_sets_mode(self):
134 backing_file = self.create_simple_tarball([("foo/", "")])
135 with standard_tarfile(backing_file) as tf:
136 self.assertEqual(0755, tf.getmember("foo").mode)
137
138 def test_create_dir_sets_linkname(self):
139 backing_file = self.create_simple_tarball([("foo/", "")])
140 with standard_tarfile(backing_file) as tf:
141 self.assertEqual('', tf.getmember("foo").linkname)
142
143 def test_create_dir_uses_default_mtime(self):
144 now = 126793
145 backing_file = self.create_simple_tarball(
146 [("foo/", "")], default_mtime=now)
147 with standard_tarfile(backing_file) as tf:
148 self.assertEqual(now, tf.getmember("foo").mtime)
149
150 def test_create_dir_uses_default_uid(self):
151 uid = 1259
152 backing_file = self.create_simple_tarball(
153 [("foo/", "")], default_uid=uid)
154 with standard_tarfile(backing_file) as tf:
155 self.assertEqual(uid, tf.getmember("foo").uid)
156
157 def test_create_dir_uses_default_gid(self):
158 gid = 2259
159 backing_file = self.create_simple_tarball(
160 [("foo/", "")], default_gid=gid)
161 with standard_tarfile(backing_file) as tf:
162 self.assertEqual(gid, tf.getmember("foo").gid)
163
164 def test_create_dir_uses_default_uname(self):
165 uname = "someperson"
166 backing_file = self.create_simple_tarball(
167 [("foo/", "")], default_uname=uname)
168 with standard_tarfile(backing_file) as tf:
169 self.assertEqual(uname, tf.getmember("foo").uname)
170
171 def test_create_dir_uses_default_gname(self):
172 gname = "somegroup"
173 backing_file = self.create_simple_tarball(
174 [("foo/", "")], default_gname=gname)
175 with standard_tarfile(backing_file) as tf:
176 self.assertEqual(gname, tf.getmember("foo").gname)

Subscribers

People subscribed via source and target branches