ยปCore Development>Code coverage>Lib/zipapp.py

Python code coverage for Lib/zipapp.py

#countcontent
1n/aimport contextlib
2n/aimport os
3n/aimport pathlib
4n/aimport shutil
5n/aimport stat
6n/aimport sys
7n/aimport zipfile
8n/a
9n/a__all__ = ['ZipAppError', 'create_archive', 'get_interpreter']
10n/a
11n/a
12n/a# The __main__.py used if the users specifies "-m module:fn".
13n/a# Note that this will always be written as UTF-8 (module and
14n/a# function names can be non-ASCII in Python 3).
15n/a# We add a coding cookie even though UTF-8 is the default in Python 3
16n/a# because the resulting archive may be intended to be run under Python 2.
17n/aMAIN_TEMPLATE = """\
18n/a# -*- coding: utf-8 -*-
19n/aimport {module}
20n/a{module}.{fn}()
21n/a"""
22n/a
23n/a
24n/a# The Windows launcher defaults to UTF-8 when parsing shebang lines if the
25n/a# file has no BOM. So use UTF-8 on Windows.
26n/a# On Unix, use the filesystem encoding.
27n/aif sys.platform.startswith('win'):
28n/a shebang_encoding = 'utf-8'
29n/aelse:
30n/a shebang_encoding = sys.getfilesystemencoding()
31n/a
32n/a
33n/aclass ZipAppError(ValueError):
34n/a pass
35n/a
36n/a
37n/a@contextlib.contextmanager
38n/adef _maybe_open(archive, mode):
39n/a if isinstance(archive, pathlib.Path):
40n/a archive = str(archive)
41n/a if isinstance(archive, str):
42n/a with open(archive, mode) as f:
43n/a yield f
44n/a else:
45n/a yield archive
46n/a
47n/a
48n/adef _write_file_prefix(f, interpreter):
49n/a """Write a shebang line."""
50n/a if interpreter:
51n/a shebang = b'#!' + interpreter.encode(shebang_encoding) + b'\n'
52n/a f.write(shebang)
53n/a
54n/a
55n/adef _copy_archive(archive, new_archive, interpreter=None):
56n/a """Copy an application archive, modifying the shebang line."""
57n/a with _maybe_open(archive, 'rb') as src:
58n/a # Skip the shebang line from the source.
59n/a # Read 2 bytes of the source and check if they are #!.
60n/a first_2 = src.read(2)
61n/a if first_2 == b'#!':
62n/a # Discard the initial 2 bytes and the rest of the shebang line.
63n/a first_2 = b''
64n/a src.readline()
65n/a
66n/a with _maybe_open(new_archive, 'wb') as dst:
67n/a _write_file_prefix(dst, interpreter)
68n/a # If there was no shebang, "first_2" contains the first 2 bytes
69n/a # of the source file, so write them before copying the rest
70n/a # of the file.
71n/a dst.write(first_2)
72n/a shutil.copyfileobj(src, dst)
73n/a
74n/a if interpreter and isinstance(new_archive, str):
75n/a os.chmod(new_archive, os.stat(new_archive).st_mode | stat.S_IEXEC)
76n/a
77n/a
78n/adef create_archive(source, target=None, interpreter=None, main=None):
79n/a """Create an application archive from SOURCE.
80n/a
81n/a The SOURCE can be the name of a directory, or a filename or a file-like
82n/a object referring to an existing archive.
83n/a
84n/a The content of SOURCE is packed into an application archive in TARGET,
85n/a which can be a filename or a file-like object. If SOURCE is a directory,
86n/a TARGET can be omitted and will default to the name of SOURCE with .pyz
87n/a appended.
88n/a
89n/a The created application archive will have a shebang line specifying
90n/a that it should run with INTERPRETER (there will be no shebang line if
91n/a INTERPRETER is None), and a __main__.py which runs MAIN (if MAIN is
92n/a not specified, an existing __main__.py will be used). It is an error
93n/a to specify MAIN for anything other than a directory source with no
94n/a __main__.py, and it is an error to omit MAIN if the directory has no
95n/a __main__.py.
96n/a """
97n/a # Are we copying an existing archive?
98n/a source_is_file = False
99n/a if hasattr(source, 'read') and hasattr(source, 'readline'):
100n/a source_is_file = True
101n/a else:
102n/a source = pathlib.Path(source)
103n/a if source.is_file():
104n/a source_is_file = True
105n/a
106n/a if source_is_file:
107n/a _copy_archive(source, target, interpreter)
108n/a return
109n/a
110n/a # We are creating a new archive from a directory.
111n/a if not source.exists():
112n/a raise ZipAppError("Source does not exist")
113n/a has_main = (source / '__main__.py').is_file()
114n/a if main and has_main:
115n/a raise ZipAppError(
116n/a "Cannot specify entry point if the source has __main__.py")
117n/a if not (main or has_main):
118n/a raise ZipAppError("Archive has no entry point")
119n/a
120n/a main_py = None
121n/a if main:
122n/a # Check that main has the right format.
123n/a mod, sep, fn = main.partition(':')
124n/a mod_ok = all(part.isidentifier() for part in mod.split('.'))
125n/a fn_ok = all(part.isidentifier() for part in fn.split('.'))
126n/a if not (sep == ':' and mod_ok and fn_ok):
127n/a raise ZipAppError("Invalid entry point: " + main)
128n/a main_py = MAIN_TEMPLATE.format(module=mod, fn=fn)
129n/a
130n/a if target is None:
131n/a target = source.with_suffix('.pyz')
132n/a elif not hasattr(target, 'write'):
133n/a target = pathlib.Path(target)
134n/a
135n/a with _maybe_open(target, 'wb') as fd:
136n/a _write_file_prefix(fd, interpreter)
137n/a with zipfile.ZipFile(fd, 'w') as z:
138n/a root = pathlib.Path(source)
139n/a for child in root.rglob('*'):
140n/a arcname = str(child.relative_to(root))
141n/a z.write(str(child), arcname)
142n/a if main_py:
143n/a z.writestr('__main__.py', main_py.encode('utf-8'))
144n/a
145n/a if interpreter and not hasattr(target, 'write'):
146n/a target.chmod(target.stat().st_mode | stat.S_IEXEC)
147n/a
148n/a
149n/adef get_interpreter(archive):
150n/a with _maybe_open(archive, 'rb') as f:
151n/a if f.read(2) == b'#!':
152n/a return f.readline().strip().decode(shebang_encoding)
153n/a
154n/a
155n/adef main(args=None):
156n/a """Run the zipapp command line interface.
157n/a
158n/a The ARGS parameter lets you specify the argument list directly.
159n/a Omitting ARGS (or setting it to None) works as for argparse, using
160n/a sys.argv[1:] as the argument list.
161n/a """
162n/a import argparse
163n/a
164n/a parser = argparse.ArgumentParser()
165n/a parser.add_argument('--output', '-o', default=None,
166n/a help="The name of the output archive. "
167n/a "Required if SOURCE is an archive.")
168n/a parser.add_argument('--python', '-p', default=None,
169n/a help="The name of the Python interpreter to use "
170n/a "(default: no shebang line).")
171n/a parser.add_argument('--main', '-m', default=None,
172n/a help="The main function of the application "
173n/a "(default: use an existing __main__.py).")
174n/a parser.add_argument('--info', default=False, action='store_true',
175n/a help="Display the interpreter from the archive.")
176n/a parser.add_argument('source',
177n/a help="Source directory (or existing archive).")
178n/a
179n/a args = parser.parse_args(args)
180n/a
181n/a # Handle `python -m zipapp archive.pyz --info`.
182n/a if args.info:
183n/a if not os.path.isfile(args.source):
184n/a raise SystemExit("Can only get info for an archive file")
185n/a interpreter = get_interpreter(args.source)
186n/a print("Interpreter: {}".format(interpreter or "<none>"))
187n/a sys.exit(0)
188n/a
189n/a if os.path.isfile(args.source):
190n/a if args.output is None or (os.path.exists(args.output) and
191n/a os.path.samefile(args.source, args.output)):
192n/a raise SystemExit("In-place editing of archives is not supported")
193n/a if args.main:
194n/a raise SystemExit("Cannot change the main function when copying")
195n/a
196n/a create_archive(args.source, args.output,
197n/a interpreter=args.python, main=args.main)
198n/a
199n/a
200n/aif __name__ == '__main__':
201n/a main()