1 """Test harness for the zipapp module."""
2
3 import io
4 import pathlib
5 import stat
6 import sys
7 import tempfile
8 import unittest
9 import zipapp
10 import zipfile
11 from test.support import requires_zlib
12 from test.support import os_helper
13
14 from unittest.mock import patch
15
16 class ESC[4;38;5;81mZipAppTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
17
18 """Test zipapp module functionality."""
19
20 def setUp(self):
21 tmpdir = tempfile.TemporaryDirectory()
22 self.addCleanup(tmpdir.cleanup)
23 self.tmpdir = pathlib.Path(tmpdir.name)
24
25 def test_create_archive(self):
26 # Test packing a directory.
27 source = self.tmpdir / 'source'
28 source.mkdir()
29 (source / '__main__.py').touch()
30 target = self.tmpdir / 'source.pyz'
31 zipapp.create_archive(str(source), str(target))
32 self.assertTrue(target.is_file())
33
34 def test_create_archive_with_pathlib(self):
35 # Test packing a directory using Path objects for source and target.
36 source = self.tmpdir / 'source'
37 source.mkdir()
38 (source / '__main__.py').touch()
39 target = self.tmpdir / 'source.pyz'
40 zipapp.create_archive(source, target)
41 self.assertTrue(target.is_file())
42
43 def test_create_archive_with_subdirs(self):
44 # Test packing a directory includes entries for subdirectories.
45 source = self.tmpdir / 'source'
46 source.mkdir()
47 (source / '__main__.py').touch()
48 (source / 'foo').mkdir()
49 (source / 'bar').mkdir()
50 (source / 'foo' / '__init__.py').touch()
51 target = io.BytesIO()
52 zipapp.create_archive(str(source), target)
53 target.seek(0)
54 with zipfile.ZipFile(target, 'r') as z:
55 self.assertIn('foo/', z.namelist())
56 self.assertIn('bar/', z.namelist())
57
58 def test_create_archive_with_filter(self):
59 # Test packing a directory and using filter to specify
60 # which files to include.
61 def skip_pyc_files(path):
62 return path.suffix != '.pyc'
63 source = self.tmpdir / 'source'
64 source.mkdir()
65 (source / '__main__.py').touch()
66 (source / 'test.py').touch()
67 (source / 'test.pyc').touch()
68 target = self.tmpdir / 'source.pyz'
69
70 zipapp.create_archive(source, target, filter=skip_pyc_files)
71 with zipfile.ZipFile(target, 'r') as z:
72 self.assertIn('__main__.py', z.namelist())
73 self.assertIn('test.py', z.namelist())
74 self.assertNotIn('test.pyc', z.namelist())
75
76 def test_create_archive_filter_exclude_dir(self):
77 # Test packing a directory and using a filter to exclude a
78 # subdirectory (ensures that the path supplied to include
79 # is relative to the source location, as expected).
80 def skip_dummy_dir(path):
81 return path.parts[0] != 'dummy'
82 source = self.tmpdir / 'source'
83 source.mkdir()
84 (source / '__main__.py').touch()
85 (source / 'test.py').touch()
86 (source / 'dummy').mkdir()
87 (source / 'dummy' / 'test2.py').touch()
88 target = self.tmpdir / 'source.pyz'
89
90 zipapp.create_archive(source, target, filter=skip_dummy_dir)
91 with zipfile.ZipFile(target, 'r') as z:
92 self.assertEqual(len(z.namelist()), 2)
93 self.assertIn('__main__.py', z.namelist())
94 self.assertIn('test.py', z.namelist())
95
96 def test_create_archive_default_target(self):
97 # Test packing a directory to the default name.
98 source = self.tmpdir / 'source'
99 source.mkdir()
100 (source / '__main__.py').touch()
101 zipapp.create_archive(str(source))
102 expected_target = self.tmpdir / 'source.pyz'
103 self.assertTrue(expected_target.is_file())
104
105 @requires_zlib()
106 def test_create_archive_with_compression(self):
107 # Test packing a directory into a compressed archive.
108 source = self.tmpdir / 'source'
109 source.mkdir()
110 (source / '__main__.py').touch()
111 (source / 'test.py').touch()
112 target = self.tmpdir / 'source.pyz'
113
114 zipapp.create_archive(source, target, compressed=True)
115 with zipfile.ZipFile(target, 'r') as z:
116 for name in ('__main__.py', 'test.py'):
117 self.assertEqual(z.getinfo(name).compress_type,
118 zipfile.ZIP_DEFLATED)
119
120 def test_no_main(self):
121 # Test that packing a directory with no __main__.py fails.
122 source = self.tmpdir / 'source'
123 source.mkdir()
124 (source / 'foo.py').touch()
125 target = self.tmpdir / 'source.pyz'
126 with self.assertRaises(zipapp.ZipAppError):
127 zipapp.create_archive(str(source), str(target))
128
129 def test_main_and_main_py(self):
130 # Test that supplying a main argument with __main__.py fails.
131 source = self.tmpdir / 'source'
132 source.mkdir()
133 (source / '__main__.py').touch()
134 target = self.tmpdir / 'source.pyz'
135 with self.assertRaises(zipapp.ZipAppError):
136 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
137
138 def test_main_written(self):
139 # Test that the __main__.py is written correctly.
140 source = self.tmpdir / 'source'
141 source.mkdir()
142 (source / 'foo.py').touch()
143 target = self.tmpdir / 'source.pyz'
144 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
145 with zipfile.ZipFile(str(target), 'r') as z:
146 self.assertIn('__main__.py', z.namelist())
147 self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
148
149 def test_main_only_written_once(self):
150 # Test that we don't write multiple __main__.py files.
151 # The initial implementation had this bug; zip files allow
152 # multiple entries with the same name
153 source = self.tmpdir / 'source'
154 source.mkdir()
155 # Write 2 files, as the original bug wrote __main__.py
156 # once for each file written :-(
157 # See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
158 # (line 67)
159 (source / 'foo.py').touch()
160 (source / 'bar.py').touch()
161 target = self.tmpdir / 'source.pyz'
162 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
163 with zipfile.ZipFile(str(target), 'r') as z:
164 self.assertEqual(1, z.namelist().count('__main__.py'))
165
166 def test_main_validation(self):
167 # Test that invalid values for main are rejected.
168 source = self.tmpdir / 'source'
169 source.mkdir()
170 target = self.tmpdir / 'source.pyz'
171 problems = [
172 '', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
173 '.a:b', 'a:b.', 'a:.b', 'a:silly name'
174 ]
175 for main in problems:
176 with self.subTest(main=main):
177 with self.assertRaises(zipapp.ZipAppError):
178 zipapp.create_archive(str(source), str(target), main=main)
179
180 def test_default_no_shebang(self):
181 # Test that no shebang line is written to the target by default.
182 source = self.tmpdir / 'source'
183 source.mkdir()
184 (source / '__main__.py').touch()
185 target = self.tmpdir / 'source.pyz'
186 zipapp.create_archive(str(source), str(target))
187 with target.open('rb') as f:
188 self.assertNotEqual(f.read(2), b'#!')
189
190 def test_custom_interpreter(self):
191 # Test that a shebang line with a custom interpreter is written
192 # correctly.
193 source = self.tmpdir / 'source'
194 source.mkdir()
195 (source / '__main__.py').touch()
196 target = self.tmpdir / 'source.pyz'
197 zipapp.create_archive(str(source), str(target), interpreter='python')
198 with target.open('rb') as f:
199 self.assertEqual(f.read(2), b'#!')
200 self.assertEqual(b'python\n', f.readline())
201
202 def test_pack_to_fileobj(self):
203 # Test that we can pack to a file object.
204 source = self.tmpdir / 'source'
205 source.mkdir()
206 (source / '__main__.py').touch()
207 target = io.BytesIO()
208 zipapp.create_archive(str(source), target, interpreter='python')
209 self.assertTrue(target.getvalue().startswith(b'#!python\n'))
210
211 def test_read_shebang(self):
212 # Test that we can read the shebang line correctly.
213 source = self.tmpdir / 'source'
214 source.mkdir()
215 (source / '__main__.py').touch()
216 target = self.tmpdir / 'source.pyz'
217 zipapp.create_archive(str(source), str(target), interpreter='python')
218 self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
219
220 def test_read_missing_shebang(self):
221 # Test that reading the shebang line of a file without one returns None.
222 source = self.tmpdir / 'source'
223 source.mkdir()
224 (source / '__main__.py').touch()
225 target = self.tmpdir / 'source.pyz'
226 zipapp.create_archive(str(source), str(target))
227 self.assertEqual(zipapp.get_interpreter(str(target)), None)
228
229 def test_modify_shebang(self):
230 # Test that we can change the shebang of a file.
231 source = self.tmpdir / 'source'
232 source.mkdir()
233 (source / '__main__.py').touch()
234 target = self.tmpdir / 'source.pyz'
235 zipapp.create_archive(str(source), str(target), interpreter='python')
236 new_target = self.tmpdir / 'changed.pyz'
237 zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
238 self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
239
240 def test_write_shebang_to_fileobj(self):
241 # Test that we can change the shebang of a file, writing the result to a
242 # file object.
243 source = self.tmpdir / 'source'
244 source.mkdir()
245 (source / '__main__.py').touch()
246 target = self.tmpdir / 'source.pyz'
247 zipapp.create_archive(str(source), str(target), interpreter='python')
248 new_target = io.BytesIO()
249 zipapp.create_archive(str(target), new_target, interpreter='python2.7')
250 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
251
252 def test_read_from_pathobj(self):
253 # Test that we can copy an archive using a pathlib.Path object
254 # for the source.
255 source = self.tmpdir / 'source'
256 source.mkdir()
257 (source / '__main__.py').touch()
258 target1 = self.tmpdir / 'target1.pyz'
259 target2 = self.tmpdir / 'target2.pyz'
260 zipapp.create_archive(source, target1, interpreter='python')
261 zipapp.create_archive(target1, target2, interpreter='python2.7')
262 self.assertEqual(zipapp.get_interpreter(target2), 'python2.7')
263
264 def test_read_from_fileobj(self):
265 # Test that we can copy an archive using an open file object.
266 source = self.tmpdir / 'source'
267 source.mkdir()
268 (source / '__main__.py').touch()
269 target = self.tmpdir / 'source.pyz'
270 temp_archive = io.BytesIO()
271 zipapp.create_archive(str(source), temp_archive, interpreter='python')
272 new_target = io.BytesIO()
273 temp_archive.seek(0)
274 zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
275 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
276
277 def test_remove_shebang(self):
278 # Test that we can remove the shebang from a file.
279 source = self.tmpdir / 'source'
280 source.mkdir()
281 (source / '__main__.py').touch()
282 target = self.tmpdir / 'source.pyz'
283 zipapp.create_archive(str(source), str(target), interpreter='python')
284 new_target = self.tmpdir / 'changed.pyz'
285 zipapp.create_archive(str(target), str(new_target), interpreter=None)
286 self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
287
288 def test_content_of_copied_archive(self):
289 # Test that copying an archive doesn't corrupt it.
290 source = self.tmpdir / 'source'
291 source.mkdir()
292 (source / '__main__.py').touch()
293 target = io.BytesIO()
294 zipapp.create_archive(str(source), target, interpreter='python')
295 new_target = io.BytesIO()
296 target.seek(0)
297 zipapp.create_archive(target, new_target, interpreter=None)
298 new_target.seek(0)
299 with zipfile.ZipFile(new_target, 'r') as z:
300 self.assertEqual(set(z.namelist()), {'__main__.py'})
301
302 # (Unix only) tests that archives with shebang lines are made executable
303 @unittest.skipIf(sys.platform == 'win32',
304 'Windows does not support an executable bit')
305 @os_helper.skip_unless_working_chmod
306 def test_shebang_is_executable(self):
307 # Test that an archive with a shebang line is made executable.
308 source = self.tmpdir / 'source'
309 source.mkdir()
310 (source / '__main__.py').touch()
311 target = self.tmpdir / 'source.pyz'
312 zipapp.create_archive(str(source), str(target), interpreter='python')
313 self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
314
315 @unittest.skipIf(sys.platform == 'win32',
316 'Windows does not support an executable bit')
317 def test_no_shebang_is_not_executable(self):
318 # Test that an archive with no shebang line is not made executable.
319 source = self.tmpdir / 'source'
320 source.mkdir()
321 (source / '__main__.py').touch()
322 target = self.tmpdir / 'source.pyz'
323 zipapp.create_archive(str(source), str(target), interpreter=None)
324 self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
325
326
327 class ESC[4;38;5;81mZipAppCmdlineTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
328
329 """Test zipapp module command line API."""
330
331 def setUp(self):
332 tmpdir = tempfile.TemporaryDirectory()
333 self.addCleanup(tmpdir.cleanup)
334 self.tmpdir = pathlib.Path(tmpdir.name)
335
336 def make_archive(self):
337 # Test that an archive with no shebang line is not made executable.
338 source = self.tmpdir / 'source'
339 source.mkdir()
340 (source / '__main__.py').touch()
341 target = self.tmpdir / 'source.pyz'
342 zipapp.create_archive(source, target)
343 return target
344
345 def test_cmdline_create(self):
346 # Test the basic command line API.
347 source = self.tmpdir / 'source'
348 source.mkdir()
349 (source / '__main__.py').touch()
350 args = [str(source)]
351 zipapp.main(args)
352 target = source.with_suffix('.pyz')
353 self.assertTrue(target.is_file())
354
355 def test_cmdline_copy(self):
356 # Test copying an archive.
357 original = self.make_archive()
358 target = self.tmpdir / 'target.pyz'
359 args = [str(original), '-o', str(target)]
360 zipapp.main(args)
361 self.assertTrue(target.is_file())
362
363 def test_cmdline_copy_inplace(self):
364 # Test copying an archive in place fails.
365 original = self.make_archive()
366 target = self.tmpdir / 'target.pyz'
367 args = [str(original), '-o', str(original)]
368 with self.assertRaises(SystemExit) as cm:
369 zipapp.main(args)
370 # Program should exit with a non-zero return code.
371 self.assertTrue(cm.exception.code)
372
373 def test_cmdline_copy_change_main(self):
374 # Test copying an archive doesn't allow changing __main__.py.
375 original = self.make_archive()
376 target = self.tmpdir / 'target.pyz'
377 args = [str(original), '-o', str(target), '-m', 'foo:bar']
378 with self.assertRaises(SystemExit) as cm:
379 zipapp.main(args)
380 # Program should exit with a non-zero return code.
381 self.assertTrue(cm.exception.code)
382
383 @patch('sys.stdout', new_callable=io.StringIO)
384 def test_info_command(self, mock_stdout):
385 # Test the output of the info command.
386 target = self.make_archive()
387 args = [str(target), '--info']
388 with self.assertRaises(SystemExit) as cm:
389 zipapp.main(args)
390 # Program should exit with a zero return code.
391 self.assertEqual(cm.exception.code, 0)
392 self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
393
394 def test_info_error(self):
395 # Test the info command fails when the archive does not exist.
396 target = self.tmpdir / 'dummy.pyz'
397 args = [str(target), '--info']
398 with self.assertRaises(SystemExit) as cm:
399 zipapp.main(args)
400 # Program should exit with a non-zero return code.
401 self.assertTrue(cm.exception.code)
402
403
404 if __name__ == "__main__":
405 unittest.main()