cpython: d1e9f337fea1 (original) (raw)
Mercurial > cpython
changeset 94979:d1e9f337fea1
Issue #23491: Implement PEP 441: Improving Python Zip Application Support Thanks to Paul Moore for the PEP and implementation. [#23491]
Brett Cannon brett@python.org | |
---|---|
date | Fri, 13 Mar 2015 10:40:49 -0400 |
parents | 211e29335e72 |
children | 65a2b468fb3c |
files | Doc/library/distribution.rst Doc/library/zipapp.rst Doc/whatsnew/3.5.rst Lib/test/test_zipapp.py Lib/zipapp.py Tools/msi/launcher/launcher_en-US.wxl Tools/msi/launcher/launcher_reg.wxs |
diffstat | 7 files changed, 720 insertions(+), 4 deletions(-)[+] [-] Doc/library/distribution.rst 1 Doc/library/zipapp.rst 257 Doc/whatsnew/3.5.rst 21 Lib/test/test_zipapp.py 250 Lib/zipapp.py 179 Tools/msi/launcher/launcher_en-US.wxl 2 Tools/msi/launcher/launcher_reg.wxs 14 |
line wrap: on
line diff
--- a/Doc/library/distribution.rst +++ b/Doc/library/distribution.rst @@ -12,3 +12,4 @@ with a local index server, or without an distutils.rst ensurepip.rst venv.rst
new file mode 100644
--- /dev/null
+++ b/Doc/library/zipapp.rst
@@ -0,0 +1,257 @@
+:mod:zipapp
--- Manage executable python zip archives
+=======================================================
+
+.. module:: zipapp
+.. versionadded:: 3.5
+
+Source code: :source:Lib/zipapp.py
+
+--------------
+
+This module provides tools to manage the creation of zip files containing
+Python code, which can be :ref:executed directly by the Python interpreter[](#l2.22) +<using-on-interface-options>
. The module provides both a
+:ref:zipapp-command-line-interface
and a :ref:zipapp-python-api
.
+
+
+Basic Example
+-------------
+
+The following example shows how the :ref:command-line-interface
+can be used to create an executable archive from a directory containing
+Python code. When run, the archive will execute the main
function from
+the module myapp
in the archive.
+
+.. code-block:: sh
+
+ +.. _zipapp-command-line-interface: + +Command-Line Interface +---------------------- + +When called as a program from the command line, the following form is used: + +.. code-block:: sh +
+If source is a directory, this will create an archive from the contents of +source. If source is a file, it should be an archive, and it will be +copied to the target archive (or the contents of its shebang line will be +displayed if the --info option is specified). + +The following options are understood: + +.. program:: zipapp + +.. cmdoption:: -o , --output= +
- Write the output to a file named output. If this option is not specified,
- the output filename will be the same as the input source, with the
- extension
.pyz
added. If an explicit filename is given, it is used as - is (so a
.pyz
extension should be included if required). + - An output filename must be specified if the source is an archive (and in
- that case, output must not be the same as source). +
+.. cmdoption:: -p , --python= +
- Add a
#!
line to the archive specifying interpreter as the command - to run. Also, on POSIX, make the archive executable. The default is to
- write no
#!
line, and not make the file executable. +
+.. cmdoption:: -m , --main= +
- Write a
__main__.py
file to the archive that executes mainfn. The - mainfn argument should have the form "pkg.mod:fn", where "pkg.mod" is a
- package/module in the archive, and "fn" is a callable in the given module.
- The
__main__.py
file will execute that callable. + - :option:
--main
cannot be specified when copying an archive. +
- Display the interpreter embedded in the archive, for diagnostic purposes. In
- this case, any other options are ignored and SOURCE must be an archive, not a
- directory. +
+ +.. _zipapp-python-api: + +Python API +---------- + +The module defines two convenience functions: + + +.. function:: create_archive(source, target=None, interpreter=None, main=None) +
- Create an application archive from source. The source can be any
- of the following:
will be created from the content of that directory.[](#l2.112)
copied to the target (modifying it to reflect the value given for the[](#l2.114)
*interpreter* argument). The file name should include the ``.pyz``[](#l2.115)
extension, if required.[](#l2.116)
file should be an application archive, and the file object is[](#l2.118)
assumed to be positioned at the start of the archive.[](#l2.119)
- The target argument determines where the resulting archive will be
- written:
file.[](#l2.125)
file object, which must be open for writing in bytes mode.[](#l2.127)
and the target will be a file with the same name as the source, with[](#l2.129)
a ``.pyz`` extension added.[](#l2.130)
- The interpreter argument specifies the name of the Python
- interpreter with which the archive will be executed. It is written as
- a "shebang" line at the start of the archive. On POSIX, this will be
- interpreted by the OS, and on Windows it will be handled by the Python
- launcher. Omitting the interpreter results in no shebang line being
- written. If an interpreter is specified, and the target is a
- filename, the executable bit of the target file will be set.
- The main argument specifies the name of a callable which will be
- used as the main program for the archive. It can only be specified if
- the source is a directory, and the source does not already contain a
__main__.py
file. The main argument should take the form- "pkg.module:callable" and the archive will be run by importing
- "pkg.module" and executing the given callable with no arguments. It
- is an error to omit main if the source is a directory and does not
- contain a
__main__.py
file, as otherwise the resulting archive - would not be executable.
- If a file object is specified for source or target, it is the
- caller's responsibility to close it after calling create_archive.
- When copying an existing archive, file objects supplied only need
read
andreadline
, orwrite
methods. When creating an- archive from a directory, if the target is a file object it will be
- passed to the
zipfile.ZipFile
class, and must supply the methods - needed by that class. +
+.. function:: get_interpreter(archive) +
- Return the interpreter specified in the
#!
line at the start of the - archive. If there is no
#!
line, return :const:None
. - The archive argument can be a filename or a file-like object open
- for reading in bytes mode. It is assumed to be at the start of the archive. +
+ +.. _zipapp-examples: + +Examples +-------- + +Pack up a directory into an archive, and run it. + +.. code-block:: sh +
+The same can be done using the :func:create_archive
functon::
+
+To make the application directly executable on POSIX, specify an interpreter +to use. + +.. code-block:: sh +
+To replace the shebang line on an existing archive, create a modified archive
+using the :func:create_archive
function::
+
+To update the file in place, do the replacement in memory using a :class:BytesIO
+object, and then overwrite the source afterwards. Note that there is a risk
+when overwriting a file in place that an error will result in the loss of
+the original file. This code does not protect against such errors, but
+production code should do so. Also, this method will only work if the archive
+fits in memory::
+
+Note that if you specify an interpreter and then distribute your application
+archive, you need to ensure that the interpreter used is portable. The Python
+launcher for Windows supports most common forms of POSIX #!
line, but there
+are other issues to consider:
+
+* If you use "/usr/bin/env python" (or other forms of the "python" command,
- such as "/usr/bin/python"), you need to consider that your users may have
- either Python 2 or Python 3 as their default, and write your code to work
- under both versions. +* If you use an explicit version, for example "/usr/bin/env python3" your
- application will not work for users who do not have that version. (This
- may be what you want if you have not made your code Python 2 compatible). +* There is no way to say "python X.Y or later", so be careful of using an
- exact version like "/usr/bin/env python3.4" as you will need to change your
- shebang line for users of Python 3.5, for example.
+
+The Python Zip Application Archive Format
+-----------------------------------------
+
+Python has been able to execute zip files which contain a
__main__.py
file +since version 2.6. In order to be executed by Python, an application archive +simply has to be a standard zip file containing a__main__.py
file which +will be run as the entry point for the application. As usual for any Python +script, the parent of the script (in this case the zip file) will be placed on +:data:sys.path
and thus further modules can be imported from the zip file. + +The zip file format allows arbitrary data to be prepended to a zip file. The +zip application format uses this ability to prepend a standard POSIX "shebang" +line to the file (#!/path/to/interpreter
). + +Formally, the Python zip application format is therefore: + +1. An optional shebang line, containing the charactersb'#!'
followed by an - interpreter name, and then a newline (
b'\n'
) character. The interpreter - name can be anything acceptable to the OS "shebang" processing, or the Python
- launcher on Windows. The interpreter should be encoded in UTF-8 on Windows,
- and in :func:
sys.getfilesystemencoding()
on POSIX. +2. Standard zipfile data, as generated by the :mod:zipfile
module. The - zipfile content must include a file called
__main__.py
(which must be - in the "root" of the zipfile - i.e., it cannot be in a subdirectory). The
- zipfile data can be compressed or uncompressed. +
+If an application archive has a shebang line, it may have the executable bit set +on POSIX systems, to allow it to be executed directly. + +There is no requirement that the tools in this module are used to create +application archives - the module is a convenience, but archives in the above +format created by any means are acceptable to Python.
--- a/Doc/whatsnew/3.5.rst
+++ b/Doc/whatsnew/3.5.rst
@@ -71,7 +71,8 @@ New syntax features:
New library modules:
-* None yet.
+* :mod:zipapp
: :ref:`Improving Python ZIP Application Support
(:pep:
441).[](#l3.9) [](#l3.10) New built-in features:[](#l3.11) [](#l3.12) @@ -166,10 +167,22 @@ Some smaller changes made to the core Py[](#l3.13) New Modules[](#l3.14) ===========[](#l3.15) [](#l3.16) -.. module name[](#l3.17) -.. -----------[](#l3.18) +.. _whatsnew-zipapp:[](#l3.19) +[](#l3.20) +zipapp[](#l3.21) +------[](#l3.22) [](#l3.23) -* None yet.[](#l3.24) +The new :mod:
zipappmodule (specified in :pep:
441) provides an API and[](#l3.25) +command line tool for creating executable Python Zip Applications, which[](#l3.26) +were introduced in Python 2.6 in :issue:
1739468` but which were not well +publicised, either at the time or since. + +With the new module, bundling your application is as simple as putting all +the files, including a__main__.py
file, into a directorymyapp
+and running:: +- $ python -m zipapp myapp
- $ python myapp.pyz
new file mode 100644 --- /dev/null +++ b/Lib/test/test_zipapp.py @@ -0,0 +1,250 @@ +"""Test harness for the zipapp module.""" + +import io +import pathlib +import stat +import sys +import tempfile +import unittest +import zipapp +import zipfile + + +class ZipAppTest(unittest.TestCase): +
- def setUp(self):
tmpdir = tempfile.TemporaryDirectory()[](#l4.22)
self.addCleanup(tmpdir.cleanup)[](#l4.23)
self.tmpdir = pathlib.Path(tmpdir.name)[](#l4.24)
- def test_create_archive(self):
# Test packing a directory.[](#l4.27)
source = self.tmpdir / 'source'[](#l4.28)
source.mkdir()[](#l4.29)
(source / '__main__.py').touch()[](#l4.30)
target = self.tmpdir / 'source.pyz'[](#l4.31)
zipapp.create_archive(str(source), str(target))[](#l4.32)
self.assertTrue(target.is_file())[](#l4.33)
- def test_create_archive_with_subdirs(self):
# Test packing a directory includes entries for subdirectories.[](#l4.36)
source = self.tmpdir / 'source'[](#l4.37)
source.mkdir()[](#l4.38)
(source / '__main__.py').touch()[](#l4.39)
(source / 'foo').mkdir()[](#l4.40)
(source / 'bar').mkdir()[](#l4.41)
(source / 'foo' / '__init__.py').touch()[](#l4.42)
target = io.BytesIO()[](#l4.43)
zipapp.create_archive(str(source), target)[](#l4.44)
target.seek(0)[](#l4.45)
with zipfile.ZipFile(target, 'r') as z:[](#l4.46)
self.assertIn('foo/', z.namelist())[](#l4.47)
self.assertIn('bar/', z.namelist())[](#l4.48)
- def test_create_archive_default_target(self):
# Test packing a directory to the default name.[](#l4.51)
source = self.tmpdir / 'source'[](#l4.52)
source.mkdir()[](#l4.53)
(source / '__main__.py').touch()[](#l4.54)
zipapp.create_archive(str(source))[](#l4.55)
expected_target = self.tmpdir / 'source.pyz'[](#l4.56)
self.assertTrue(expected_target.is_file())[](#l4.57)
- def test_no_main(self):
# Test that packing a directory with no __main__.py fails.[](#l4.60)
source = self.tmpdir / 'source'[](#l4.61)
source.mkdir()[](#l4.62)
(source / 'foo.py').touch()[](#l4.63)
target = self.tmpdir / 'source.pyz'[](#l4.64)
with self.assertRaises(zipapp.ZipAppError):[](#l4.65)
zipapp.create_archive(str(source), str(target))[](#l4.66)
- def test_main_and_main_py(self):
# Test that supplying a main argument with __main__.py fails.[](#l4.69)
source = self.tmpdir / 'source'[](#l4.70)
source.mkdir()[](#l4.71)
(source / '__main__.py').touch()[](#l4.72)
target = self.tmpdir / 'source.pyz'[](#l4.73)
with self.assertRaises(zipapp.ZipAppError):[](#l4.74)
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')[](#l4.75)
- def test_main_written(self):
# Test that the __main__.py is written correctly.[](#l4.78)
source = self.tmpdir / 'source'[](#l4.79)
source.mkdir()[](#l4.80)
(source / 'foo.py').touch()[](#l4.81)
target = self.tmpdir / 'source.pyz'[](#l4.82)
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')[](#l4.83)
with zipfile.ZipFile(str(target), 'r') as z:[](#l4.84)
self.assertIn('__main__.py', z.namelist())[](#l4.85)
self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))[](#l4.86)
- def test_main_only_written_once(self):
# Test that we don't write multiple __main__.py files.[](#l4.89)
# The initial implementation had this bug; zip files allow[](#l4.90)
# multiple entries with the same name[](#l4.91)
source = self.tmpdir / 'source'[](#l4.92)
source.mkdir()[](#l4.93)
# Write 2 files, as the original bug wrote __main__.py[](#l4.94)
# once for each file written :-([](#l4.95)
# See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67[](#l4.96)
# (line 67)[](#l4.97)
(source / 'foo.py').touch()[](#l4.98)
(source / 'bar.py').touch()[](#l4.99)
target = self.tmpdir / 'source.pyz'[](#l4.100)
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')[](#l4.101)
with zipfile.ZipFile(str(target), 'r') as z:[](#l4.102)
self.assertEqual(1, z.namelist().count('__main__.py'))[](#l4.103)
- def test_main_validation(self):
# Test that invalid values for main are rejected.[](#l4.106)
source = self.tmpdir / 'source'[](#l4.107)
source.mkdir()[](#l4.108)
target = self.tmpdir / 'source.pyz'[](#l4.109)
problems = [[](#l4.110)
'', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',[](#l4.111)
'.a:b', 'a:b.', 'a:.b', 'a:silly name'[](#l4.112)
][](#l4.113)
for main in problems:[](#l4.114)
with self.subTest(main=main):[](#l4.115)
with self.assertRaises(zipapp.ZipAppError):[](#l4.116)
zipapp.create_archive(str(source), str(target), main=main)[](#l4.117)
- def test_default_no_shebang(self):
# Test that no shebang line is written to the target by default.[](#l4.120)
source = self.tmpdir / 'source'[](#l4.121)
source.mkdir()[](#l4.122)
(source / '__main__.py').touch()[](#l4.123)
target = self.tmpdir / 'source.pyz'[](#l4.124)
zipapp.create_archive(str(source), str(target))[](#l4.125)
with target.open('rb') as f:[](#l4.126)
self.assertNotEqual(f.read(2), b'#!')[](#l4.127)
- def test_custom_interpreter(self):
# Test that a shebang line with a custom interpreter is written[](#l4.130)
# correctly.[](#l4.131)
source = self.tmpdir / 'source'[](#l4.132)
source.mkdir()[](#l4.133)
(source / '__main__.py').touch()[](#l4.134)
target = self.tmpdir / 'source.pyz'[](#l4.135)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.136)
with target.open('rb') as f:[](#l4.137)
self.assertEqual(f.read(2), b'#!')[](#l4.138)
self.assertEqual(b'python\n', f.readline())[](#l4.139)
- def test_pack_to_fileobj(self):
# Test that we can pack to a file object.[](#l4.142)
source = self.tmpdir / 'source'[](#l4.143)
source.mkdir()[](#l4.144)
(source / '__main__.py').touch()[](#l4.145)
target = io.BytesIO()[](#l4.146)
zipapp.create_archive(str(source), target, interpreter='python')[](#l4.147)
self.assertTrue(target.getvalue().startswith(b'#!python\n'))[](#l4.148)
- def test_read_shebang(self):
# Test that we can read the shebang line correctly.[](#l4.151)
source = self.tmpdir / 'source'[](#l4.152)
source.mkdir()[](#l4.153)
(source / '__main__.py').touch()[](#l4.154)
target = self.tmpdir / 'source.pyz'[](#l4.155)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.156)
self.assertEqual(zipapp.get_interpreter(str(target)), 'python')[](#l4.157)
- def test_read_missing_shebang(self):
# Test that reading the shebang line of a file without one returns None.[](#l4.160)
source = self.tmpdir / 'source'[](#l4.161)
source.mkdir()[](#l4.162)
(source / '__main__.py').touch()[](#l4.163)
target = self.tmpdir / 'source.pyz'[](#l4.164)
zipapp.create_archive(str(source), str(target))[](#l4.165)
self.assertEqual(zipapp.get_interpreter(str(target)), None)[](#l4.166)
- def test_modify_shebang(self):
# Test that we can change the shebang of a file.[](#l4.169)
source = self.tmpdir / 'source'[](#l4.170)
source.mkdir()[](#l4.171)
(source / '__main__.py').touch()[](#l4.172)
target = self.tmpdir / 'source.pyz'[](#l4.173)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.174)
new_target = self.tmpdir / 'changed.pyz'[](#l4.175)
zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')[](#l4.176)
self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')[](#l4.177)
- def test_write_shebang_to_fileobj(self):
# Test that we can change the shebang of a file, writing the result to a[](#l4.180)
# file object.[](#l4.181)
source = self.tmpdir / 'source'[](#l4.182)
source.mkdir()[](#l4.183)
(source / '__main__.py').touch()[](#l4.184)
target = self.tmpdir / 'source.pyz'[](#l4.185)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.186)
new_target = io.BytesIO()[](#l4.187)
zipapp.create_archive(str(target), new_target, interpreter='python2.7')[](#l4.188)
self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))[](#l4.189)
- def test_read_from_fileobj(self):
# Test that we can copy an archive using an open file object.[](#l4.192)
source = self.tmpdir / 'source'[](#l4.193)
source.mkdir()[](#l4.194)
(source / '__main__.py').touch()[](#l4.195)
target = self.tmpdir / 'source.pyz'[](#l4.196)
temp_archive = io.BytesIO()[](#l4.197)
zipapp.create_archive(str(source), temp_archive, interpreter='python')[](#l4.198)
new_target = io.BytesIO()[](#l4.199)
temp_archive.seek(0)[](#l4.200)
zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')[](#l4.201)
self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))[](#l4.202)
- def test_remove_shebang(self):
# Test that we can remove the shebang from a file.[](#l4.205)
source = self.tmpdir / 'source'[](#l4.206)
source.mkdir()[](#l4.207)
(source / '__main__.py').touch()[](#l4.208)
target = self.tmpdir / 'source.pyz'[](#l4.209)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.210)
new_target = self.tmpdir / 'changed.pyz'[](#l4.211)
zipapp.create_archive(str(target), str(new_target), interpreter=None)[](#l4.212)
self.assertEqual(zipapp.get_interpreter(str(new_target)), None)[](#l4.213)
- def test_content_of_copied_archive(self):
# Test that copying an archive doesn't corrupt it.[](#l4.216)
source = self.tmpdir / 'source'[](#l4.217)
source.mkdir()[](#l4.218)
(source / '__main__.py').touch()[](#l4.219)
target = io.BytesIO()[](#l4.220)
zipapp.create_archive(str(source), target, interpreter='python')[](#l4.221)
new_target = io.BytesIO()[](#l4.222)
target.seek(0)[](#l4.223)
zipapp.create_archive(target, new_target, interpreter=None)[](#l4.224)
new_target.seek(0)[](#l4.225)
with zipfile.ZipFile(new_target, 'r') as z:[](#l4.226)
self.assertEqual(set(z.namelist()), {'__main__.py'})[](#l4.227)
(Unix only) tests that archives with shebang lines are made executable
- @unittest.skipIf(sys.platform == 'win32',
'Windows does not support an executable bit')[](#l4.231)
- def test_shebang_is_executable(self):
# Test that an archive with a shebang line is made executable.[](#l4.233)
source = self.tmpdir / 'source'[](#l4.234)
source.mkdir()[](#l4.235)
(source / '__main__.py').touch()[](#l4.236)
target = self.tmpdir / 'source.pyz'[](#l4.237)
zipapp.create_archive(str(source), str(target), interpreter='python')[](#l4.238)
self.assertTrue(target.stat().st_mode & stat.S_IEXEC)[](#l4.239)
- @unittest.skipIf(sys.platform == 'win32',
'Windows does not support an executable bit')[](#l4.242)
- def test_no_shebang_is_not_executable(self):
# Test that an archive with no shebang line is not made executable.[](#l4.244)
source = self.tmpdir / 'source'[](#l4.245)
source.mkdir()[](#l4.246)
(source / '__main__.py').touch()[](#l4.247)
target = self.tmpdir / 'source.pyz'[](#l4.248)
zipapp.create_archive(str(source), str(target), interpreter=None)[](#l4.249)
self.assertFalse(target.stat().st_mode & stat.S_IEXEC)[](#l4.250)
new file mode 100644 --- /dev/null +++ b/Lib/zipapp.py @@ -0,0 +1,179 @@ +import contextlib +import os +import pathlib +import shutil +import stat +import sys +import zipfile + +all = ['ZipAppError', 'create_archive', 'get_interpreter'] + + +# The main.py used if the users specifies "-m module:fn". +# Note that this will always be written as UTF-8 (module and +# function names can be non-ASCII in Python 3). +# We add a coding cookie even though UTF-8 is the default in Python 3 +# because the resulting archive may be intended to be run under Python 2. +MAIN_TEMPLATE = """[](#l5.21) +# -- coding: utf-8 -- +import {module} +{module}.{fn}() +""" + + +# The Windows launcher defaults to UTF-8 when parsing shebang lines if the +# file has no BOM. So use UTF-8 on Windows. +# On Unix, use the filesystem encoding. +if sys.platform.startswith('win'):
+ + +class ZipAppError(ValueError):
+ + +@contextlib.contextmanager +def _maybe_open(archive, mode):
- if isinstance(archive, str):
with open(archive, mode) as f:[](#l5.44)
yield f[](#l5.45)
- else:
yield archive[](#l5.47)
+ + +def _write_file_prefix(f, interpreter):
- """Write a shebang line."""
- if interpreter:
shebang = b'#!%b\n' % (interpreter.encode(shebang_encoding),)[](#l5.53)
f.write(shebang)[](#l5.54)
+ + +def _copy_archive(archive, new_archive, interpreter=None):
- """Copy an application archive, modifying the shebang line."""
- with _maybe_open(archive, 'rb') as src:
# Skip the shebang line from the source.[](#l5.60)
# Read 2 bytes of the source and check if they are #!.[](#l5.61)
first_2 = src.read(2)[](#l5.62)
if first_2 == b'#!':[](#l5.63)
# Discard the initial 2 bytes and the rest of the shebang line.[](#l5.64)
first_2 = b''[](#l5.65)
src.readline()[](#l5.66)
with _maybe_open(new_archive, 'wb') as dst:[](#l5.68)
_write_file_prefix(dst, interpreter)[](#l5.69)
# If there was no shebang, "first_2" contains the first 2 bytes[](#l5.70)
# of the source file, so write them before copying the rest[](#l5.71)
# of the file.[](#l5.72)
dst.write(first_2)[](#l5.73)
shutil.copyfileobj(src, dst)[](#l5.74)
- if interpreter and isinstance(new_archive, str):
os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)[](#l5.77)
+ + +def create_archive(source, target=None, interpreter=None, main=None):
- The SOURCE can be the name of a directory, or a filename or a file-like
- object referring to an existing archive.
- The content of SOURCE is packed into an application archive in TARGET,
- which can be a filename or a file-like object. If SOURCE is a directory,
- TARGET can be omitted and will default to the name of SOURCE with .pyz
- appended.
- The created application archive will have a shebang line specifying
- that it should run with INTERPRETER (there will be no shebang line if
- INTERPRETER is None), and a main.py which runs MAIN (if MAIN is
- not specified, an existing main.py will be used). It is an to specify
- MAIN for anything other than a directory source with no main.py, and it
- is an error to omit MAIN if the directory has no main.py.
- """
Are we copying an existing archive?
- if not (isinstance(source, str) and os.path.isdir(source)):
_copy_archive(source, target, interpreter)[](#l5.100)
return[](#l5.101)
We are creating a new archive from a directory
- has_main = os.path.exists(os.path.join(source, 'main.py'))
- if main and has_main:
raise ZipAppError([](#l5.106)
"Cannot specify entry point if the source has __main__.py")[](#l5.107)
- if not (main or has_main):
raise ZipAppError("Archive has no entry point")[](#l5.109)
- main_py = None
- if main:
# Check that main has the right format[](#l5.113)
mod, sep, fn = main.partition(':')[](#l5.114)
mod_ok = all(part.isidentifier() for part in mod.split('.'))[](#l5.115)
fn_ok = all(part.isidentifier() for part in fn.split('.'))[](#l5.116)
if not (sep == ':' and mod_ok and fn_ok):[](#l5.117)
raise ZipAppError("Invalid entry point: " + main)[](#l5.118)
main_py = MAIN_TEMPLATE.format(module=mod, fn=fn)[](#l5.119)
- with _maybe_open(target, 'wb') as fd:
_write_file_prefix(fd, interpreter)[](#l5.125)
with zipfile.ZipFile(fd, 'w') as z:[](#l5.126)
root = pathlib.Path(source)[](#l5.127)
for child in root.rglob('*'):[](#l5.128)
arcname = str(child.relative_to(root))[](#l5.129)
z.write(str(child), arcname)[](#l5.130)
if main_py:[](#l5.131)
z.writestr('__main__.py', main_py.encode('utf-8'))[](#l5.132)
- if interpreter and isinstance(target, str):
os.chmod(target, os.stat(target).st_mode | stat.S_IEXEC)[](#l5.135)
+ + +def get_interpreter(archive):
- with _maybe_open(archive, 'rb') as f:
if f.read(2) == b'#!':[](#l5.140)
return f.readline().strip().decode(shebang_encoding)[](#l5.141)
- parser = argparse.ArgumentParser()
- parser.add_argument('--output', '-o', default=None,
help="The name of the output archive. "[](#l5.149)
"Required if SOURCE is an archive.")[](#l5.150)
- parser.add_argument('--python', '-p', default=None,
help="The name of the Python interpreter to use "[](#l5.152)
"(default: no shebang line).")[](#l5.153)
- parser.add_argument('--main', '-m', default=None,
help="The main function of the application "[](#l5.155)
"(default: use an existing __main__.py).")[](#l5.156)
- parser.add_argument('--info', default=False, action='store_true',
help="Display the interpreter from the archive.")[](#l5.158)
- parser.add_argument('source',
help="Source directory (or existing archive).")[](#l5.160)
Handlepython -m zipapp archive.pyz --info
.- if args.info:
if not os.path.isfile(args.source):[](#l5.166)
raise SystemExit("Can only get info for an archive file")[](#l5.167)
interpreter = get_interpreter(args.source)[](#l5.168)
print("Interpreter: {}".format(interpreter or "<none>"))[](#l5.169)
sys.exit(0)[](#l5.170)
- if os.path.isfile(args.source):
if args.output is None or os.path.samefile(args.source, args.output):[](#l5.173)
raise SystemExit("In-place editing of archives is not supported")[](#l5.174)
if args.main:[](#l5.175)
raise SystemExit("Cannot change the main function when copying")[](#l5.176)
--- a/Tools/msi/launcher/launcher_en-US.wxl +++ b/Tools/msi/launcher/launcher_en-US.wxl @@ -5,4 +5,6 @@ Python File Python File (no console) Compiled Python File
--- a/Tools/msi/launcher/launcher_reg.wxs +++ b/Tools/msi/launcher/launcher_reg.wxs @@ -26,6 +26,20 @@ +
<ProgId Id="$(var.TestPrefix)Python.ArchiveFile" Description="!(loc.PythonArchiveFileDescription)" Advertise="no" Icon="py.exe" IconIndex="1">[](#l7.8)
<Extension Id="$(var.ArchiveFileExtension)" ContentType="application/x-zip-compressed">[](#l7.9)
<Verb Id="open" TargetFile="py.exe" Argument=""%L" %*" />[](#l7.10)
</Extension>[](#l7.11)
</ProgId>[](#l7.12)
<RegistryValue Root="HKCR" Key="$(var.TestPrefix)Python.ArchiveFile\shellex\DropHandler" Value="{60254CA5-953B-11CF-8C96-00AA00B8708C}" Type="string" />[](#l7.13)
[](#l7.14)
<ProgId Id="$(var.TestPrefix)Python.NoConArchiveFile" Description="!(loc.PythonNoConArchiveFileDescription)" Advertise="no" Icon="py.exe" IconIndex="1">[](#l7.15)
<Extension Id="$(var.ArchiveFileExtension)w" ContentType="application/x-zip-compressed">[](#l7.16)
<Verb Id="open" TargetFile="pyw.exe" Argument=""%L" %*" />[](#l7.17)
</Extension>[](#l7.18)
</ProgId>[](#l7.19)
<RegistryValue Root="HKCR" Key="$(var.TestPrefix)Python.NoConArchiveFile\shellex\DropHandler" Value="{60254CA5-953B-11CF-8C96-00AA00B8708C}" Type="string" />[](#l7.20) </Component>[](#l7.21) </ComponentGroup>[](#l7.22)