Skip to content
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
12 changes: 8 additions & 4 deletions Doc/library/zipapp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ The module defines two convenience functions:


.. function:: create_archive(source, target=None, interpreter=None, main=None,
include_file=None)
filter=None)

Create an application archive from *source*. The source can be any
of the following:
Expand Down Expand Up @@ -144,9 +144,10 @@ The module defines two convenience functions:
contain a ``__main__.py`` file, as otherwise the resulting archive
would not be executable.

The *include_file* argument specifies a callback function that is passed the
relative path to the file in order to determine which files to store when
being called against a directory.
The optional *filter* argument specifies a callback function that
is passed a Path object representing the path to the file being added
(relative to the source directory). It should return ``True`` if the
file is to be added.

If a file object is specified for *source* or *target*, it is the
caller's responsibility to close it after calling create_archive.
Expand All @@ -157,6 +158,9 @@ The module defines two convenience functions:
passed to the ``zipfile.ZipFile`` class, and must supply the methods
needed by that class.

.. versionadded:: 3.7
Added the *filter* argument.

.. function:: get_interpreter(archive)

Return the interpreter specified in the ``#!`` line at the start of the
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ Function :func:`~uu.encode` now accepts an optional *backtick*
keyword argument. When it's true, zeros are represented by ``'`'``
instead of spaces. (Contributed by Xiang Zhang in :issue:`30103`.)

zipapp
------

Function :func:`zipapp.create_archive` now accepts an optional *filter*
argument, to allow the user to select which files should be included in the
archive.


Optimizations
=============
Expand Down
31 changes: 26 additions & 5 deletions Lib/test/test_zipapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,44 @@ def test_create_archive_with_subdirs(self):
self.assertIn('foo/', z.namelist())
self.assertIn('bar/', z.namelist())

def test_create_archive_with_include_file(self):
# Test packing a directory and using include_file to specify which files to include.
def skip_pyc_files(file):
return '.pyc' not in str(file)
def test_create_archive_with_filter(self):
# Test packing a directory and using filter to specify
# which files to include.
def skip_pyc_files(path):
return path.suffix != '.pyc'
source = self.tmpdir / 'source'
source.mkdir()
(source / '__main__.py').touch()
(source / 'test.py').touch()
(source / 'test.pyc').touch()
target = self.tmpdir / 'source.pyz'

zipapp.create_archive(source, target, include_file=skip_pyc_files)
zipapp.create_archive(source, target, filter=skip_pyc_files)
with zipfile.ZipFile(target, 'r') as z:
self.assertIn('__main__.py', z.namelist())
self.assertIn('test.py', z.namelist())
self.assertNotIn('test.pyc', z.namelist())

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
# is relative to the source location, as expected).
def skip_dummy_dir(path):
return path.parts[0] != 'dummy'
source = self.tmpdir / 'source'
source.mkdir()
(source / '__main__.py').touch()
(source / 'test.py').touch()
(source / 'dummy').mkdir()
(source / 'dummy' / 'test2.py').touch()
target = self.tmpdir / 'source.pyz'

zipapp.create_archive(source, target, filter=skip_dummy_dir)
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_create_archive_default_target(self):
# Test packing a directory to the default name.
source = self.tmpdir / 'source'
Expand Down
8 changes: 4 additions & 4 deletions Lib/zipapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def _copy_archive(archive, new_archive, interpreter=None):


def create_archive(source, target=None, interpreter=None, main=None,
include_file=None):
filter=None):
"""Create an application archive from SOURCE.

The SOURCE can be the name of a directory, or a filename or a file-like
Expand Down Expand Up @@ -135,9 +135,9 @@ def create_archive(source, target=None, interpreter=None, main=None,
_write_file_prefix(fd, interpreter)
with zipfile.ZipFile(fd, 'w') as z:
for child in source.rglob('*'):
arcname = child.relative_to(source).as_posix()
if include_file is None or include_file(pathlib.Path(arcname)):
z.write(child, arcname)
arcname = child.relative_to(source)
if filter is None or filter(arcname):
z.write(child, arcname.as_posix())
if main_py:
z.writestr('__main__.py', main_py.encode('utf-8'))

Expand Down