ยปCore Development>Code coverage>Lib/test/test_zipapp.py

Python code coverage for Lib/test/test_zipapp.py

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