Skip to content

Update zipapp.py from 3.13.5 #6075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions Lib/test/test_zipapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import zipapp
import zipfile
from test.support import requires_zlib
from test.support import os_helper

from unittest.mock import patch

Expand Down Expand Up @@ -54,6 +55,22 @@ def test_create_archive_with_subdirs(self):
self.assertIn('foo/', z.namelist())
self.assertIn('bar/', z.namelist())

def test_create_sorted_archive(self):
# Test that zipapps order their files by name
source = self.tmpdir / 'source'
source.mkdir()
(source / 'zed.py').touch()
(source / 'bin').mkdir()
(source / 'bin' / 'qux').touch()
(source / 'bin' / 'baz').touch()
(source / '__main__.py').touch()
target = io.BytesIO()
zipapp.create_archive(str(source), target)
target.seek(0)
with zipfile.ZipFile(target, 'r') as zf:
self.assertEqual(zf.namelist(),
["__main__.py", "bin/", "bin/baz", "bin/qux", "zed.py"])

def test_create_archive_with_filter(self):
# Test packing a directory and using filter to specify
# which files to include.
Expand All @@ -72,6 +89,30 @@ def skip_pyc_files(path):
self.assertIn('test.py', z.namelist())
self.assertNotIn('test.pyc', z.namelist())

def test_create_archive_self_insertion(self):
# When creating an archive, we shouldn't
# include the archive in the list of files to add.
source = self.tmpdir
(source / '__main__.py').touch()
(source / 'test.py').touch()
target = self.tmpdir / 'target.pyz'

zipapp.create_archive(source, target)
with zipfile.ZipFile(target, 'r') as z:
self.assertEqual(len(z.namelist()), 2)
self.assertIn('__main__.py', z.namelist())
self.assertIn('test.py', z.namelist())

def test_target_overwrites_source_file(self):
# The target cannot be one of the files to add.
source = self.tmpdir
(source / '__main__.py').touch()
target = source / 'target.pyz'
target.touch()

with self.assertRaises(zipapp.ZipAppError):
zipapp.create_archive(source, target)

def test_create_archive_filter_exclude_dir(self):
# Test packing a directory and using a filter to exclude a
# subdirectory (ensures that the path supplied to include
Expand Down Expand Up @@ -248,14 +289,15 @@ def test_write_shebang_to_fileobj(self):
zipapp.create_archive(str(target), new_target, interpreter='python2.7')
self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))

def test_read_from_pathobj(self):
# Test that we can copy an archive using a pathlib.Path object
def test_read_from_pathlike_obj(self):
# Test that we can copy an archive using a path-like object
# for the source.
source = self.tmpdir / 'source'
source.mkdir()
(source / '__main__.py').touch()
target1 = self.tmpdir / 'target1.pyz'
target2 = self.tmpdir / 'target2.pyz'
source = os_helper.FakePath(str(source))
target1 = os_helper.FakePath(str(self.tmpdir / 'target1.pyz'))
target2 = os_helper.FakePath(str(self.tmpdir / 'target2.pyz'))
zipapp.create_archive(source, target1, interpreter='python')
zipapp.create_archive(target1, target2, interpreter='python2.7')
self.assertEqual(zipapp.get_interpreter(target2), 'python2.7')
Expand Down Expand Up @@ -301,6 +343,7 @@ def test_content_of_copied_archive(self):
# (Unix only) tests that archives with shebang lines are made executable
@unittest.skipIf(sys.platform == 'win32',
'Windows does not support an executable bit')
@os_helper.skip_unless_working_chmod
def test_shebang_is_executable(self):
# Test that an archive with a shebang line is made executable.
source = self.tmpdir / 'source'
Expand Down
25 changes: 24 additions & 1 deletion Lib/zipapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,35 @@ def create_archive(source, target=None, interpreter=None, main=None,
elif not hasattr(target, 'write'):
target = pathlib.Path(target)

# Create the list of files to add to the archive now, in case
# the target is being created in the source directory - we
# don't want the target being added to itself
files_to_add = sorted(source.rglob('*'))

# The target cannot be in the list of files to add. If it were, we'd
# end up overwriting the source file and writing the archive into
# itself, which is an error. We therefore check for that case and
# provide a helpful message for the user.

# Note that we only do a simple path equality check. This won't
# catch every case, but it will catch the common case where the
# source is the CWD and the target is a file in the CWD. More
# thorough checks don't provide enough value to justify the extra
# cost.

# If target is a file-like object, it will simply fail to compare
# equal to any of the entries in files_to_add, so there's no need
# to add a special check for that.
if target in files_to_add:
raise ZipAppError(
f"The target archive {target} overwrites one of the source files.")

with _maybe_open(target, 'wb') as fd:
_write_file_prefix(fd, interpreter)
compression = (zipfile.ZIP_DEFLATED if compressed else
zipfile.ZIP_STORED)
with zipfile.ZipFile(fd, 'w', compression=compression) as z:
for child in source.rglob('*'):
for child in files_to_add:
arcname = child.relative_to(source)
if filter is None or filter(arcname):
z.write(child, arcname.as_posix())
Expand Down
Loading