From a81014a12196fc182fd212521d92b5140672daa6 Mon Sep 17 00:00:00 2001 From: Danil Sidoruk Date: Sat, 29 Aug 2026 14:22:07 +0300 Subject: [PATCH 1/3] Make zipapp archives executable with PathLike targets --- Lib/test/test_zipapp.py | 14 ++++++++++++++ Lib/zipapp.py | 2 +- .../2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst diff --git a/Lib/test/test_zipapp.py b/Lib/test/test_zipapp.py index 8fb0a68deba535c..466cdc2f785c75a 100644 --- a/Lib/test/test_zipapp.py +++ b/Lib/test/test_zipapp.py @@ -366,6 +366,20 @@ def test_shebang_is_executable(self): zipapp.create_archive(str(source), str(target), interpreter='python') self.assertTrue(target.stat().st_mode & stat.S_IEXEC) + @unittest.skipIf(sys.platform == 'win32', + 'Windows does not support an executable bit') + @os_helper.skip_unless_working_chmod + def test_copied_archive_with_pathlike_target_is_executable(self): + # Test that copying an archive to a PathLike target makes it executable. + source = self.tmpdir / 'source' + source.mkdir() + (source / '__main__.py').touch() + target = self.tmpdir / 'source.pyz' + zipapp.create_archive(source, target, interpreter='python') + new_target = self.tmpdir / 'changed.pyz' + zipapp.create_archive(target, new_target, interpreter='python') + self.assertTrue(new_target.stat().st_mode & stat.S_IEXEC) + @unittest.skipIf(sys.platform == 'win32', 'Windows does not support an executable bit') def test_no_shebang_is_not_executable(self): diff --git a/Lib/zipapp.py b/Lib/zipapp.py index a1cef18ada9d05d..39e765d1ffdc6d4 100644 --- a/Lib/zipapp.py +++ b/Lib/zipapp.py @@ -69,7 +69,7 @@ def _copy_archive(archive, new_archive, interpreter=None): dst.write(first_2) shutil.copyfileobj(src, dst) - if interpreter and isinstance(new_archive, str): + if interpreter and isinstance(new_archive, (str, os.PathLike)): os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC) diff --git a/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst b/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst new file mode 100644 index 000000000000000..18ea6ecba9a0d2b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst @@ -0,0 +1,2 @@ +:func:`zipapp.create_archive` now correctly sets the executable bit on the +target archive when the target is a path-like object. From 212ae2f1058a653576277cb61dfa34a197d5ee4e Mon Sep 17 00:00:00 2001 From: Danil Sidoruk Date: Sat, 29 Aug 2026 21:32:48 +0300 Subject: [PATCH 2/3] Update Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst Co-authored-by: Paul Moore --- .../next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst b/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst index 18ea6ecba9a0d2b..3efd5a0481874c3 100644 --- a/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst +++ b/Misc/NEWS.d/next/Library/2026-08-29-14-28-46.gh-issue-156568.mkxLY8.rst @@ -1,2 +1,2 @@ :func:`zipapp.create_archive` now correctly sets the executable bit on the -target archive when the target is a path-like object. +target archive when copying a zipapp where the target is a path-like object. From 9168203d3a85a7acc71002f3c017980bd7e0dae8 Mon Sep 17 00:00:00 2001 From: Danil Sidoruk Date: Sat, 29 Aug 2026 21:55:21 +0300 Subject: [PATCH 3/3] Clarify intention of `zipapp.create_archive` test Co-authored-by: Paul Moore --- Lib/test/test_zipapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_zipapp.py b/Lib/test/test_zipapp.py index 466cdc2f785c75a..88cc55716f59a80 100644 --- a/Lib/test/test_zipapp.py +++ b/Lib/test/test_zipapp.py @@ -376,6 +376,10 @@ def test_copied_archive_with_pathlike_target_is_executable(self): (source / '__main__.py').touch() target = self.tmpdir / 'source.pyz' zipapp.create_archive(source, target, interpreter='python') + # Copying an archive uses a different code path than creating + # one from scratch. Ensure that the executable bit is set + # even if the target is a path-like object. + # See https://github.com/python/cpython/issues/156568 new_target = self.tmpdir / 'changed.pyz' zipapp.create_archive(target, new_target, interpreter='python') self.assertTrue(new_target.stat().st_mode & stat.S_IEXEC)