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