diff --git a/Lib/test/test_zipapp.py b/Lib/test/test_zipapp.py index 8fb0a68deba535..b40784c185735d 100644 --- a/Lib/test/test_zipapp.py +++ b/Lib/test/test_zipapp.py @@ -185,8 +185,13 @@ def test_main_and_main_py(self): source.mkdir() (source / '__main__.py').touch() target = self.tmpdir / 'source.pyz' - with self.assertRaises(zipapp.ZipAppError): - zipapp.create_archive(str(source), str(target), main='pkg.mod:fn') + for main in ('pkg.mod:fn', ''): + with self.subTest(main=main): + target.write_bytes(b'original content') + with self.assertRaisesRegex(zipapp.ZipAppError, + 'Cannot specify entry point'): + zipapp.create_archive(str(source), str(target), main=main) + self.assertEqual(target.read_bytes(), b'original content') def test_main_written(self): # Test that the __main__.py is written correctly. @@ -227,8 +232,10 @@ def test_main_validation(self): ] for main in problems: with self.subTest(main=main): - with self.assertRaises(zipapp.ZipAppError): + with self.assertRaisesRegex(zipapp.ZipAppError, + 'Invalid entry point:'): zipapp.create_archive(str(source), str(target), main=main) + self.assertFalse(target.exists()) def test_default_no_shebang(self): # Test that no shebang line is written to the target by default. @@ -328,6 +335,19 @@ def test_read_from_fileobj(self): zipapp.create_archive(temp_archive, new_target, interpreter='python2.7') self.assertStartsWith(new_target.getvalue(), b'#!python2.7\n') + def test_main_with_existing_archive(self): + archive = self.tmpdir / 'source.pyz' + with zipfile.ZipFile(archive, 'w') as z: + z.writestr('__main__.py', '') + target = self.tmpdir / 'target.pyz' + for main in ('pkg.mod:fn', ''): + for source in (archive, io.BytesIO(archive.read_bytes())): + with self.subTest(main=main, source=source): + target.write_bytes(b'original content') + with self.assertRaises(zipapp.ZipAppError): + zipapp.create_archive(source, target, main=main) + self.assertEqual(target.read_bytes(), b'original content') + def test_remove_shebang(self): # Test that we can remove the shebang from a file. source = self.tmpdir / 'source' @@ -428,11 +448,14 @@ def test_cmdline_copy_change_main(self): # Test copying an archive doesn't allow changing __main__.py. original = self.make_archive() target = self.tmpdir / 'target.pyz' - args = [str(original), '-o', str(target), '-m', 'foo:bar'] - with self.assertRaises(SystemExit) as cm: - zipapp.main(args) - # Program should exit with a non-zero return code. - self.assertTrue(cm.exception.code) + for main in ('foo:bar', ''): + with self.subTest(main=main): + args = [str(original), '-o', str(target), '-m', main] + with self.assertRaises(SystemExit) as cm: + zipapp.main(args) + # Program should exit with a non-zero return code. + self.assertTrue(cm.exception.code) + self.assertFalse(target.exists()) @patch('sys.stdout', new_callable=io.StringIO) def test_info_command(self, mock_stdout): diff --git a/Lib/zipapp.py b/Lib/zipapp.py index a1cef18ada9d05..cb73c0beb7e4e1 100644 --- a/Lib/zipapp.py +++ b/Lib/zipapp.py @@ -103,6 +103,8 @@ def create_archive(source, target=None, interpreter=None, main=None, source_is_file = True if source_is_file: + if main is not None: + raise ZipAppError("Cannot change the main function when copying") _copy_archive(source, target, interpreter) return @@ -110,14 +112,14 @@ def create_archive(source, target=None, interpreter=None, main=None, if not source.exists(): raise ZipAppError("Source does not exist") has_main = (source / '__main__.py').is_file() - if main and has_main: + if main is not None and has_main: raise ZipAppError( "Cannot specify entry point if the source has __main__.py") - if not (main or has_main): + if main is None and not has_main: raise ZipAppError("Archive has no entry point") main_py = None - if main: + if main is not None: # Check that main has the right format. mod, sep, fn = main.partition(':') mod_ok = all(part.isidentifier() for part in mod.split('.')) @@ -219,7 +221,7 @@ def main(args=None): if args.output is None or (os.path.exists(args.output) and os.path.samefile(args.source, args.output)): raise SystemExit("In-place editing of archives is not supported") - if args.main: + if args.main is not None: raise SystemExit("Cannot change the main function when copying") create_archive(args.source, args.output, diff --git a/Misc/NEWS.d/next/Library/2026-10-13-21-20-01.gh-issue-159354.zIpMai.rst b/Misc/NEWS.d/next/Library/2026-10-13-21-20-01.gh-issue-159354.zIpMai.rst new file mode 100644 index 00000000000000..8cdc40b1e72c03 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-13-21-20-01.gh-issue-159354.zIpMai.rst @@ -0,0 +1,3 @@ +Fix :func:`zipapp.create_archive` to reject a ``main`` argument when copying +an existing archive, instead of silently ignoring it. An empty ``main`` is +also rejected when creating an archive from a directory.