mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
zipapp crlf->lf
This commit is contained in:
810
Lib/test/test_zipapp.py
vendored
810
Lib/test/test_zipapp.py
vendored
@@ -1,405 +1,405 @@
|
||||
"""Test harness for the zipapp module."""
|
||||
|
||||
import io
|
||||
import pathlib
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipapp
|
||||
import zipfile
|
||||
from test.support import requires_zlib
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
class ZipAppTest(unittest.TestCase):
|
||||
|
||||
"""Test zipapp module functionality."""
|
||||
|
||||
def setUp(self):
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmpdir.cleanup)
|
||||
self.tmpdir = pathlib.Path(tmpdir.name)
|
||||
|
||||
def test_create_archive(self):
|
||||
# Test packing a directory.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_create_archive_with_pathlib(self):
|
||||
# Test packing a directory using Path objects for source and target.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(source, target)
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_create_archive_with_subdirs(self):
|
||||
# Test packing a directory includes entries for subdirectories.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
(source / 'foo').mkdir()
|
||||
(source / 'bar').mkdir()
|
||||
(source / 'foo' / '__init__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target)
|
||||
target.seek(0)
|
||||
with zipfile.ZipFile(target, 'r') as z:
|
||||
self.assertIn('foo/', z.namelist())
|
||||
self.assertIn('bar/', z.namelist())
|
||||
|
||||
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, 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'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
zipapp.create_archive(str(source))
|
||||
expected_target = self.tmpdir / 'source.pyz'
|
||||
self.assertTrue(expected_target.is_file())
|
||||
|
||||
@requires_zlib
|
||||
def test_create_archive_with_compression(self):
|
||||
# Test packing a directory into a compressed archive.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
(source / 'test.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
|
||||
zipapp.create_archive(source, target, compressed=True)
|
||||
with zipfile.ZipFile(target, 'r') as z:
|
||||
for name in ('__main__.py', 'test.py'):
|
||||
self.assertEqual(z.getinfo(name).compress_type,
|
||||
zipfile.ZIP_DEFLATED)
|
||||
|
||||
def test_no_main(self):
|
||||
# Test that packing a directory with no __main__.py fails.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / 'foo.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
with self.assertRaises(zipapp.ZipAppError):
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
|
||||
def test_main_and_main_py(self):
|
||||
# Test that supplying a main argument with __main__.py fails.
|
||||
source = self.tmpdir / 'source'
|
||||
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')
|
||||
|
||||
def test_main_written(self):
|
||||
# Test that the __main__.py is written correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / 'foo.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
|
||||
with zipfile.ZipFile(str(target), 'r') as z:
|
||||
self.assertIn('__main__.py', z.namelist())
|
||||
self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
|
||||
|
||||
def test_main_only_written_once(self):
|
||||
# Test that we don't write multiple __main__.py files.
|
||||
# The initial implementation had this bug; zip files allow
|
||||
# multiple entries with the same name
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
# Write 2 files, as the original bug wrote __main__.py
|
||||
# once for each file written :-(
|
||||
# See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
|
||||
# (line 67)
|
||||
(source / 'foo.py').touch()
|
||||
(source / 'bar.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
|
||||
with zipfile.ZipFile(str(target), 'r') as z:
|
||||
self.assertEqual(1, z.namelist().count('__main__.py'))
|
||||
|
||||
def test_main_validation(self):
|
||||
# Test that invalid values for main are rejected.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
problems = [
|
||||
'', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
|
||||
'.a:b', 'a:b.', 'a:.b', 'a:silly name'
|
||||
]
|
||||
for main in problems:
|
||||
with self.subTest(main=main):
|
||||
with self.assertRaises(zipapp.ZipAppError):
|
||||
zipapp.create_archive(str(source), str(target), main=main)
|
||||
|
||||
def test_default_no_shebang(self):
|
||||
# Test that no shebang line is written to the target by default.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
with target.open('rb') as f:
|
||||
self.assertNotEqual(f.read(2), b'#!')
|
||||
|
||||
def test_custom_interpreter(self):
|
||||
# Test that a shebang line with a custom interpreter is written
|
||||
# correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
with target.open('rb') as f:
|
||||
self.assertEqual(f.read(2), b'#!')
|
||||
self.assertEqual(b'python\n', f.readline())
|
||||
|
||||
def test_pack_to_fileobj(self):
|
||||
# Test that we can pack to a file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target, interpreter='python')
|
||||
self.assertTrue(target.getvalue().startswith(b'#!python\n'))
|
||||
|
||||
def test_read_shebang(self):
|
||||
# Test that we can read the shebang line correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
|
||||
|
||||
def test_read_missing_shebang(self):
|
||||
# Test that reading the shebang line of a file without one returns None.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
self.assertEqual(zipapp.get_interpreter(str(target)), None)
|
||||
|
||||
def test_modify_shebang(self):
|
||||
# Test that we can change the shebang of a file.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = self.tmpdir / 'changed.pyz'
|
||||
zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
|
||||
self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
|
||||
|
||||
def test_write_shebang_to_fileobj(self):
|
||||
# Test that we can change the shebang of a file, writing the result to a
|
||||
# file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
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
|
||||
# for the source.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target1 = self.tmpdir / 'target1.pyz'
|
||||
target2 = 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')
|
||||
|
||||
def test_read_from_fileobj(self):
|
||||
# Test that we can copy an archive using an open file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
temp_archive = io.BytesIO()
|
||||
zipapp.create_archive(str(source), temp_archive, interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
temp_archive.seek(0)
|
||||
zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
|
||||
self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
|
||||
|
||||
def test_remove_shebang(self):
|
||||
# Test that we can remove the shebang from a file.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = self.tmpdir / 'changed.pyz'
|
||||
zipapp.create_archive(str(target), str(new_target), interpreter=None)
|
||||
self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
|
||||
|
||||
def test_content_of_copied_archive(self):
|
||||
# Test that copying an archive doesn't corrupt it.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target, interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
target.seek(0)
|
||||
zipapp.create_archive(target, new_target, interpreter=None)
|
||||
new_target.seek(0)
|
||||
with zipfile.ZipFile(new_target, 'r') as z:
|
||||
self.assertEqual(set(z.namelist()), {'__main__.py'})
|
||||
|
||||
# (Unix only) tests that archives with shebang lines are made executable
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'Windows does not support an executable bit')
|
||||
def test_shebang_is_executable(self):
|
||||
# Test that an archive with a shebang line is made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'Windows does not support an executable bit')
|
||||
def test_no_shebang_is_not_executable(self):
|
||||
# Test that an archive with no shebang line is not made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter=None)
|
||||
self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
|
||||
|
||||
|
||||
class ZipAppCmdlineTest(unittest.TestCase):
|
||||
|
||||
"""Test zipapp module command line API."""
|
||||
|
||||
def setUp(self):
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmpdir.cleanup)
|
||||
self.tmpdir = pathlib.Path(tmpdir.name)
|
||||
|
||||
def make_archive(self):
|
||||
# Test that an archive with no shebang line is not made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(source, target)
|
||||
return target
|
||||
|
||||
def test_cmdline_create(self):
|
||||
# Test the basic command line API.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
args = [str(source)]
|
||||
zipapp.main(args)
|
||||
target = source.with_suffix('.pyz')
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_cmdline_copy(self):
|
||||
# Test copying an archive.
|
||||
original = self.make_archive()
|
||||
target = self.tmpdir / 'target.pyz'
|
||||
args = [str(original), '-o', str(target)]
|
||||
zipapp.main(args)
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_cmdline_copy_inplace(self):
|
||||
# Test copying an archive in place fails.
|
||||
original = self.make_archive()
|
||||
target = self.tmpdir / 'target.pyz'
|
||||
args = [str(original), '-o', str(original)]
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a non-zero return code.
|
||||
self.assertTrue(cm.exception.code)
|
||||
|
||||
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)
|
||||
|
||||
@patch('sys.stdout', new_callable=io.StringIO)
|
||||
def test_info_command(self, mock_stdout):
|
||||
# Test the output of the info command.
|
||||
target = self.make_archive()
|
||||
args = [str(target), '--info']
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a zero return code.
|
||||
self.assertEqual(cm.exception.code, 0)
|
||||
self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
|
||||
|
||||
def test_info_error(self):
|
||||
# Test the info command fails when the archive does not exist.
|
||||
target = self.tmpdir / 'dummy.pyz'
|
||||
args = [str(target), '--info']
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a non-zero return code.
|
||||
self.assertTrue(cm.exception.code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
"""Test harness for the zipapp module."""
|
||||
|
||||
import io
|
||||
import pathlib
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipapp
|
||||
import zipfile
|
||||
from test.support import requires_zlib
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
class ZipAppTest(unittest.TestCase):
|
||||
|
||||
"""Test zipapp module functionality."""
|
||||
|
||||
def setUp(self):
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmpdir.cleanup)
|
||||
self.tmpdir = pathlib.Path(tmpdir.name)
|
||||
|
||||
def test_create_archive(self):
|
||||
# Test packing a directory.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_create_archive_with_pathlib(self):
|
||||
# Test packing a directory using Path objects for source and target.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(source, target)
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_create_archive_with_subdirs(self):
|
||||
# Test packing a directory includes entries for subdirectories.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
(source / 'foo').mkdir()
|
||||
(source / 'bar').mkdir()
|
||||
(source / 'foo' / '__init__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target)
|
||||
target.seek(0)
|
||||
with zipfile.ZipFile(target, 'r') as z:
|
||||
self.assertIn('foo/', z.namelist())
|
||||
self.assertIn('bar/', z.namelist())
|
||||
|
||||
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, 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'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
zipapp.create_archive(str(source))
|
||||
expected_target = self.tmpdir / 'source.pyz'
|
||||
self.assertTrue(expected_target.is_file())
|
||||
|
||||
@requires_zlib
|
||||
def test_create_archive_with_compression(self):
|
||||
# Test packing a directory into a compressed archive.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
(source / 'test.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
|
||||
zipapp.create_archive(source, target, compressed=True)
|
||||
with zipfile.ZipFile(target, 'r') as z:
|
||||
for name in ('__main__.py', 'test.py'):
|
||||
self.assertEqual(z.getinfo(name).compress_type,
|
||||
zipfile.ZIP_DEFLATED)
|
||||
|
||||
def test_no_main(self):
|
||||
# Test that packing a directory with no __main__.py fails.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / 'foo.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
with self.assertRaises(zipapp.ZipAppError):
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
|
||||
def test_main_and_main_py(self):
|
||||
# Test that supplying a main argument with __main__.py fails.
|
||||
source = self.tmpdir / 'source'
|
||||
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')
|
||||
|
||||
def test_main_written(self):
|
||||
# Test that the __main__.py is written correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / 'foo.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
|
||||
with zipfile.ZipFile(str(target), 'r') as z:
|
||||
self.assertIn('__main__.py', z.namelist())
|
||||
self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
|
||||
|
||||
def test_main_only_written_once(self):
|
||||
# Test that we don't write multiple __main__.py files.
|
||||
# The initial implementation had this bug; zip files allow
|
||||
# multiple entries with the same name
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
# Write 2 files, as the original bug wrote __main__.py
|
||||
# once for each file written :-(
|
||||
# See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
|
||||
# (line 67)
|
||||
(source / 'foo.py').touch()
|
||||
(source / 'bar.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
|
||||
with zipfile.ZipFile(str(target), 'r') as z:
|
||||
self.assertEqual(1, z.namelist().count('__main__.py'))
|
||||
|
||||
def test_main_validation(self):
|
||||
# Test that invalid values for main are rejected.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
problems = [
|
||||
'', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
|
||||
'.a:b', 'a:b.', 'a:.b', 'a:silly name'
|
||||
]
|
||||
for main in problems:
|
||||
with self.subTest(main=main):
|
||||
with self.assertRaises(zipapp.ZipAppError):
|
||||
zipapp.create_archive(str(source), str(target), main=main)
|
||||
|
||||
def test_default_no_shebang(self):
|
||||
# Test that no shebang line is written to the target by default.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
with target.open('rb') as f:
|
||||
self.assertNotEqual(f.read(2), b'#!')
|
||||
|
||||
def test_custom_interpreter(self):
|
||||
# Test that a shebang line with a custom interpreter is written
|
||||
# correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
with target.open('rb') as f:
|
||||
self.assertEqual(f.read(2), b'#!')
|
||||
self.assertEqual(b'python\n', f.readline())
|
||||
|
||||
def test_pack_to_fileobj(self):
|
||||
# Test that we can pack to a file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target, interpreter='python')
|
||||
self.assertTrue(target.getvalue().startswith(b'#!python\n'))
|
||||
|
||||
def test_read_shebang(self):
|
||||
# Test that we can read the shebang line correctly.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
|
||||
|
||||
def test_read_missing_shebang(self):
|
||||
# Test that reading the shebang line of a file without one returns None.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target))
|
||||
self.assertEqual(zipapp.get_interpreter(str(target)), None)
|
||||
|
||||
def test_modify_shebang(self):
|
||||
# Test that we can change the shebang of a file.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = self.tmpdir / 'changed.pyz'
|
||||
zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
|
||||
self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
|
||||
|
||||
def test_write_shebang_to_fileobj(self):
|
||||
# Test that we can change the shebang of a file, writing the result to a
|
||||
# file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
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
|
||||
# for the source.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target1 = self.tmpdir / 'target1.pyz'
|
||||
target2 = 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')
|
||||
|
||||
def test_read_from_fileobj(self):
|
||||
# Test that we can copy an archive using an open file object.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
temp_archive = io.BytesIO()
|
||||
zipapp.create_archive(str(source), temp_archive, interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
temp_archive.seek(0)
|
||||
zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
|
||||
self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
|
||||
|
||||
def test_remove_shebang(self):
|
||||
# Test that we can remove the shebang from a file.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
new_target = self.tmpdir / 'changed.pyz'
|
||||
zipapp.create_archive(str(target), str(new_target), interpreter=None)
|
||||
self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
|
||||
|
||||
def test_content_of_copied_archive(self):
|
||||
# Test that copying an archive doesn't corrupt it.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = io.BytesIO()
|
||||
zipapp.create_archive(str(source), target, interpreter='python')
|
||||
new_target = io.BytesIO()
|
||||
target.seek(0)
|
||||
zipapp.create_archive(target, new_target, interpreter=None)
|
||||
new_target.seek(0)
|
||||
with zipfile.ZipFile(new_target, 'r') as z:
|
||||
self.assertEqual(set(z.namelist()), {'__main__.py'})
|
||||
|
||||
# (Unix only) tests that archives with shebang lines are made executable
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'Windows does not support an executable bit')
|
||||
def test_shebang_is_executable(self):
|
||||
# Test that an archive with a shebang line is made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter='python')
|
||||
self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'Windows does not support an executable bit')
|
||||
def test_no_shebang_is_not_executable(self):
|
||||
# Test that an archive with no shebang line is not made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(str(source), str(target), interpreter=None)
|
||||
self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
|
||||
|
||||
|
||||
class ZipAppCmdlineTest(unittest.TestCase):
|
||||
|
||||
"""Test zipapp module command line API."""
|
||||
|
||||
def setUp(self):
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmpdir.cleanup)
|
||||
self.tmpdir = pathlib.Path(tmpdir.name)
|
||||
|
||||
def make_archive(self):
|
||||
# Test that an archive with no shebang line is not made executable.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
target = self.tmpdir / 'source.pyz'
|
||||
zipapp.create_archive(source, target)
|
||||
return target
|
||||
|
||||
def test_cmdline_create(self):
|
||||
# Test the basic command line API.
|
||||
source = self.tmpdir / 'source'
|
||||
source.mkdir()
|
||||
(source / '__main__.py').touch()
|
||||
args = [str(source)]
|
||||
zipapp.main(args)
|
||||
target = source.with_suffix('.pyz')
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_cmdline_copy(self):
|
||||
# Test copying an archive.
|
||||
original = self.make_archive()
|
||||
target = self.tmpdir / 'target.pyz'
|
||||
args = [str(original), '-o', str(target)]
|
||||
zipapp.main(args)
|
||||
self.assertTrue(target.is_file())
|
||||
|
||||
def test_cmdline_copy_inplace(self):
|
||||
# Test copying an archive in place fails.
|
||||
original = self.make_archive()
|
||||
target = self.tmpdir / 'target.pyz'
|
||||
args = [str(original), '-o', str(original)]
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a non-zero return code.
|
||||
self.assertTrue(cm.exception.code)
|
||||
|
||||
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)
|
||||
|
||||
@patch('sys.stdout', new_callable=io.StringIO)
|
||||
def test_info_command(self, mock_stdout):
|
||||
# Test the output of the info command.
|
||||
target = self.make_archive()
|
||||
args = [str(target), '--info']
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a zero return code.
|
||||
self.assertEqual(cm.exception.code, 0)
|
||||
self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
|
||||
|
||||
def test_info_error(self):
|
||||
# Test the info command fails when the archive does not exist.
|
||||
target = self.tmpdir / 'dummy.pyz'
|
||||
args = [str(target), '--info']
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
zipapp.main(args)
|
||||
# Program should exit with a non-zero return code.
|
||||
self.assertTrue(cm.exception.code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
412
Lib/zipapp.py
vendored
412
Lib/zipapp.py
vendored
@@ -1,206 +1,206 @@
|
||||
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 = """\
|
||||
# -*- 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'):
|
||||
shebang_encoding = 'utf-8'
|
||||
else:
|
||||
shebang_encoding = sys.getfilesystemencoding()
|
||||
|
||||
|
||||
class ZipAppError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _maybe_open(archive, mode):
|
||||
if isinstance(archive, (str, os.PathLike)):
|
||||
with open(archive, mode) as f:
|
||||
yield f
|
||||
else:
|
||||
yield archive
|
||||
|
||||
|
||||
def _write_file_prefix(f, interpreter):
|
||||
"""Write a shebang line."""
|
||||
if interpreter:
|
||||
shebang = b'#!' + interpreter.encode(shebang_encoding) + b'\n'
|
||||
f.write(shebang)
|
||||
|
||||
|
||||
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.
|
||||
# Read 2 bytes of the source and check if they are #!.
|
||||
first_2 = src.read(2)
|
||||
if first_2 == b'#!':
|
||||
# Discard the initial 2 bytes and the rest of the shebang line.
|
||||
first_2 = b''
|
||||
src.readline()
|
||||
|
||||
with _maybe_open(new_archive, 'wb') as dst:
|
||||
_write_file_prefix(dst, interpreter)
|
||||
# If there was no shebang, "first_2" contains the first 2 bytes
|
||||
# of the source file, so write them before copying the rest
|
||||
# of the file.
|
||||
dst.write(first_2)
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
if interpreter and isinstance(new_archive, str):
|
||||
os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def create_archive(source, target=None, interpreter=None, main=None,
|
||||
filter=None, compressed=False):
|
||||
"""Create an application archive from SOURCE.
|
||||
|
||||
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 error
|
||||
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?
|
||||
source_is_file = False
|
||||
if hasattr(source, 'read') and hasattr(source, 'readline'):
|
||||
source_is_file = True
|
||||
else:
|
||||
source = pathlib.Path(source)
|
||||
if source.is_file():
|
||||
source_is_file = True
|
||||
|
||||
if source_is_file:
|
||||
_copy_archive(source, target, interpreter)
|
||||
return
|
||||
|
||||
# We are creating a new archive from a directory.
|
||||
if not source.exists():
|
||||
raise ZipAppError("Source does not exist")
|
||||
has_main = (source / '__main__.py').is_file()
|
||||
if main and has_main:
|
||||
raise ZipAppError(
|
||||
"Cannot specify entry point if the source has __main__.py")
|
||||
if not (main or has_main):
|
||||
raise ZipAppError("Archive has no entry point")
|
||||
|
||||
main_py = None
|
||||
if main:
|
||||
# Check that main has the right format.
|
||||
mod, sep, fn = main.partition(':')
|
||||
mod_ok = all(part.isidentifier() for part in mod.split('.'))
|
||||
fn_ok = all(part.isidentifier() for part in fn.split('.'))
|
||||
if not (sep == ':' and mod_ok and fn_ok):
|
||||
raise ZipAppError("Invalid entry point: " + main)
|
||||
main_py = MAIN_TEMPLATE.format(module=mod, fn=fn)
|
||||
|
||||
if target is None:
|
||||
target = source.with_suffix('.pyz')
|
||||
elif not hasattr(target, 'write'):
|
||||
target = pathlib.Path(target)
|
||||
|
||||
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('*'):
|
||||
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'))
|
||||
|
||||
if interpreter and not hasattr(target, 'write'):
|
||||
target.chmod(target.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def get_interpreter(archive):
|
||||
with _maybe_open(archive, 'rb') as f:
|
||||
if f.read(2) == b'#!':
|
||||
return f.readline().strip().decode(shebang_encoding)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Run the zipapp command line interface.
|
||||
|
||||
The ARGS parameter lets you specify the argument list directly.
|
||||
Omitting ARGS (or setting it to None) works as for argparse, using
|
||||
sys.argv[1:] as the argument list.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--output', '-o', default=None,
|
||||
help="The name of the output archive. "
|
||||
"Required if SOURCE is an archive.")
|
||||
parser.add_argument('--python', '-p', default=None,
|
||||
help="The name of the Python interpreter to use "
|
||||
"(default: no shebang line).")
|
||||
parser.add_argument('--main', '-m', default=None,
|
||||
help="The main function of the application "
|
||||
"(default: use an existing __main__.py).")
|
||||
parser.add_argument('--compress', '-c', action='store_true',
|
||||
help="Compress files with the deflate method. "
|
||||
"Files are stored uncompressed by default.")
|
||||
parser.add_argument('--info', default=False, action='store_true',
|
||||
help="Display the interpreter from the archive.")
|
||||
parser.add_argument('source',
|
||||
help="Source directory (or existing archive).")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# Handle `python -m zipapp archive.pyz --info`.
|
||||
if args.info:
|
||||
if not os.path.isfile(args.source):
|
||||
raise SystemExit("Can only get info for an archive file")
|
||||
interpreter = get_interpreter(args.source)
|
||||
print("Interpreter: {}".format(interpreter or "<none>"))
|
||||
sys.exit(0)
|
||||
|
||||
if os.path.isfile(args.source):
|
||||
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:
|
||||
raise SystemExit("Cannot change the main function when copying")
|
||||
|
||||
create_archive(args.source, args.output,
|
||||
interpreter=args.python, main=args.main,
|
||||
compressed=args.compress)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
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 = """\
|
||||
# -*- 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'):
|
||||
shebang_encoding = 'utf-8'
|
||||
else:
|
||||
shebang_encoding = sys.getfilesystemencoding()
|
||||
|
||||
|
||||
class ZipAppError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _maybe_open(archive, mode):
|
||||
if isinstance(archive, (str, os.PathLike)):
|
||||
with open(archive, mode) as f:
|
||||
yield f
|
||||
else:
|
||||
yield archive
|
||||
|
||||
|
||||
def _write_file_prefix(f, interpreter):
|
||||
"""Write a shebang line."""
|
||||
if interpreter:
|
||||
shebang = b'#!' + interpreter.encode(shebang_encoding) + b'\n'
|
||||
f.write(shebang)
|
||||
|
||||
|
||||
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.
|
||||
# Read 2 bytes of the source and check if they are #!.
|
||||
first_2 = src.read(2)
|
||||
if first_2 == b'#!':
|
||||
# Discard the initial 2 bytes and the rest of the shebang line.
|
||||
first_2 = b''
|
||||
src.readline()
|
||||
|
||||
with _maybe_open(new_archive, 'wb') as dst:
|
||||
_write_file_prefix(dst, interpreter)
|
||||
# If there was no shebang, "first_2" contains the first 2 bytes
|
||||
# of the source file, so write them before copying the rest
|
||||
# of the file.
|
||||
dst.write(first_2)
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
if interpreter and isinstance(new_archive, str):
|
||||
os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def create_archive(source, target=None, interpreter=None, main=None,
|
||||
filter=None, compressed=False):
|
||||
"""Create an application archive from SOURCE.
|
||||
|
||||
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 error
|
||||
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?
|
||||
source_is_file = False
|
||||
if hasattr(source, 'read') and hasattr(source, 'readline'):
|
||||
source_is_file = True
|
||||
else:
|
||||
source = pathlib.Path(source)
|
||||
if source.is_file():
|
||||
source_is_file = True
|
||||
|
||||
if source_is_file:
|
||||
_copy_archive(source, target, interpreter)
|
||||
return
|
||||
|
||||
# We are creating a new archive from a directory.
|
||||
if not source.exists():
|
||||
raise ZipAppError("Source does not exist")
|
||||
has_main = (source / '__main__.py').is_file()
|
||||
if main and has_main:
|
||||
raise ZipAppError(
|
||||
"Cannot specify entry point if the source has __main__.py")
|
||||
if not (main or has_main):
|
||||
raise ZipAppError("Archive has no entry point")
|
||||
|
||||
main_py = None
|
||||
if main:
|
||||
# Check that main has the right format.
|
||||
mod, sep, fn = main.partition(':')
|
||||
mod_ok = all(part.isidentifier() for part in mod.split('.'))
|
||||
fn_ok = all(part.isidentifier() for part in fn.split('.'))
|
||||
if not (sep == ':' and mod_ok and fn_ok):
|
||||
raise ZipAppError("Invalid entry point: " + main)
|
||||
main_py = MAIN_TEMPLATE.format(module=mod, fn=fn)
|
||||
|
||||
if target is None:
|
||||
target = source.with_suffix('.pyz')
|
||||
elif not hasattr(target, 'write'):
|
||||
target = pathlib.Path(target)
|
||||
|
||||
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('*'):
|
||||
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'))
|
||||
|
||||
if interpreter and not hasattr(target, 'write'):
|
||||
target.chmod(target.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def get_interpreter(archive):
|
||||
with _maybe_open(archive, 'rb') as f:
|
||||
if f.read(2) == b'#!':
|
||||
return f.readline().strip().decode(shebang_encoding)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Run the zipapp command line interface.
|
||||
|
||||
The ARGS parameter lets you specify the argument list directly.
|
||||
Omitting ARGS (or setting it to None) works as for argparse, using
|
||||
sys.argv[1:] as the argument list.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--output', '-o', default=None,
|
||||
help="The name of the output archive. "
|
||||
"Required if SOURCE is an archive.")
|
||||
parser.add_argument('--python', '-p', default=None,
|
||||
help="The name of the Python interpreter to use "
|
||||
"(default: no shebang line).")
|
||||
parser.add_argument('--main', '-m', default=None,
|
||||
help="The main function of the application "
|
||||
"(default: use an existing __main__.py).")
|
||||
parser.add_argument('--compress', '-c', action='store_true',
|
||||
help="Compress files with the deflate method. "
|
||||
"Files are stored uncompressed by default.")
|
||||
parser.add_argument('--info', default=False, action='store_true',
|
||||
help="Display the interpreter from the archive.")
|
||||
parser.add_argument('source',
|
||||
help="Source directory (or existing archive).")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# Handle `python -m zipapp archive.pyz --info`.
|
||||
if args.info:
|
||||
if not os.path.isfile(args.source):
|
||||
raise SystemExit("Can only get info for an archive file")
|
||||
interpreter = get_interpreter(args.source)
|
||||
print("Interpreter: {}".format(interpreter or "<none>"))
|
||||
sys.exit(0)
|
||||
|
||||
if os.path.isfile(args.source):
|
||||
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:
|
||||
raise SystemExit("Cannot change the main function when copying")
|
||||
|
||||
create_archive(args.source, args.output,
|
||||
interpreter=args.python, main=args.main,
|
||||
compressed=args.compress)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user