1"""Routine to "compile" a .py file to a .pyc file.
2
3This module has intimate knowledge of the format of .pyc files.
4"""
5
6import enum
7import importlib._bootstrap_external
8import importlib.machinery
9import importlib.util
10import os
11import os.path
12import sys
13import traceback
14
15__all__ = ["compile", "main", "PyCompileError", "PycInvalidationMode"]
16
17
18class PyCompileError(Exception):
19    """Exception raised when an error occurs while attempting to
20    compile the file.
21
22    To raise this exception, use
23
24        raise PyCompileError(exc_type,exc_value,file[,msg])
25
26    where
27
28        exc_type:   exception type to be used in error message
29                    type name can be accesses as class variable
30                    'exc_type_name'
31
32        exc_value:  exception value to be used in error message
33                    can be accesses as class variable 'exc_value'
34
35        file:       name of file being compiled to be used in error message
36                    can be accesses as class variable 'file'
37
38        msg:        string message to be written as error message
39                    If no value is given, a default exception message will be
40                    given, consistent with 'standard' py_compile output.
41                    message (or default) can be accesses as class variable
42                    'msg'
43
44    """
45
46    def __init__(self, exc_type, exc_value, file, msg=''):
47        exc_type_name = exc_type.__name__
48        if exc_type is SyntaxError:
49            tbtext = ''.join(traceback.format_exception_only(
50                exc_type, exc_value))
51            errmsg = tbtext.replace('File "<string>"', 'File "%s"' % file)
52        else:
53            errmsg = "Sorry: %s: %s" % (exc_type_name,exc_value)
54
55        Exception.__init__(self,msg or errmsg,exc_type_name,exc_value,file)
56
57        self.exc_type_name = exc_type_name
58        self.exc_value = exc_value
59        self.file = file
60        self.msg = msg or errmsg
61
62    def __str__(self):
63        return self.msg
64
65
66class PycInvalidationMode(enum.Enum):
67    TIMESTAMP = 1
68    CHECKED_HASH = 2
69    UNCHECKED_HASH = 3
70
71
72def _get_default_invalidation_mode():
73    if os.environ.get('SOURCE_DATE_EPOCH'):
74        return PycInvalidationMode.CHECKED_HASH
75    else:
76        return PycInvalidationMode.TIMESTAMP
77
78
79def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1,
80            invalidation_mode=None, quiet=0):
81    """Byte-compile one Python source file to Python bytecode.
82
83    :param file: The source file name.
84    :param cfile: The target byte compiled file name.  When not given, this
85        defaults to the PEP 3147/PEP 488 location.
86    :param dfile: Purported file name, i.e. the file name that shows up in
87        error messages.  Defaults to the source file name.
88    :param doraise: Flag indicating whether or not an exception should be
89        raised when a compile error is found.  If an exception occurs and this
90        flag is set to False, a string indicating the nature of the exception
91        will be printed, and the function will return to the caller. If an
92        exception occurs and this flag is set to True, a PyCompileError
93        exception will be raised.
94    :param optimize: The optimization level for the compiler.  Valid values
95        are -1, 0, 1 and 2.  A value of -1 means to use the optimization
96        level of the current interpreter, as given by -O command line options.
97    :param invalidation_mode:
98    :param quiet: Return full output with False or 0, errors only with 1,
99        and no output with 2.
100
101    :return: Path to the resulting byte compiled file.
102
103    Note that it isn't necessary to byte-compile Python modules for
104    execution efficiency -- Python itself byte-compiles a module when
105    it is loaded, and if it can, writes out the bytecode to the
106    corresponding .pyc file.
107
108    However, if a Python installation is shared between users, it is a
109    good idea to byte-compile all modules upon installation, since
110    other users may not be able to write in the source directories,
111    and thus they won't be able to write the .pyc file, and then
112    they would be byte-compiling every module each time it is loaded.
113    This can slow down program start-up considerably.
114
115    See compileall.py for a script/module that uses this module to
116    byte-compile all installed files (or all files in selected
117    directories).
118
119    Do note that FileExistsError is raised if cfile ends up pointing at a
120    non-regular file or symlink. Because the compilation uses a file renaming,
121    the resulting file would be regular and thus not the same type of file as
122    it was previously.
123    """
124    if invalidation_mode is None:
125        invalidation_mode = _get_default_invalidation_mode()
126    if cfile is None:
127        if optimize >= 0:
128            optimization = optimize if optimize >= 1 else ''
129            cfile = importlib.util.cache_from_source(file,
130                                                     optimization=optimization)
131        else:
132            cfile = importlib.util.cache_from_source(file)
133    if os.path.islink(cfile):
134        msg = ('{} is a symlink and will be changed into a regular file if '
135               'import writes a byte-compiled file to it')
136        raise FileExistsError(msg.format(cfile))
137    elif os.path.exists(cfile) and not os.path.isfile(cfile):
138        msg = ('{} is a non-regular file and will be changed into a regular '
139               'one if import writes a byte-compiled file to it')
140        raise FileExistsError(msg.format(cfile))
141    loader = importlib.machinery.SourceFileLoader('<py_compile>', file)
142    source_bytes = loader.get_data(file)
143    try:
144        code = loader.source_to_code(source_bytes, dfile or file,
145                                     _optimize=optimize)
146    except Exception as err:
147        py_exc = PyCompileError(err.__class__, err, dfile or file)
148        if quiet < 2:
149            if doraise:
150                raise py_exc
151            else:
152                sys.stderr.write(py_exc.msg + '\n')
153        return
154    try:
155        dirname = os.path.dirname(cfile)
156        if dirname:
157            os.makedirs(dirname)
158    except FileExistsError:
159        pass
160    if invalidation_mode == PycInvalidationMode.TIMESTAMP:
161        source_stats = loader.path_stats(file)
162        bytecode = importlib._bootstrap_external._code_to_timestamp_pyc(
163            code, source_stats['mtime'], source_stats['size'])
164    else:
165        source_hash = importlib.util.source_hash(source_bytes)
166        bytecode = importlib._bootstrap_external._code_to_hash_pyc(
167            code,
168            source_hash,
169            (invalidation_mode == PycInvalidationMode.CHECKED_HASH),
170        )
171    mode = importlib._bootstrap_external._calc_mode(file)
172    importlib._bootstrap_external._write_atomic(cfile, bytecode, mode)
173    return cfile
174
175
176def main():
177    import argparse
178
179    description = 'A simple command-line interface for py_compile module.'
180    parser = argparse.ArgumentParser(description=description)
181    parser.add_argument(
182        '-q', '--quiet',
183        action='store_true',
184        help='Suppress error output',
185    )
186    parser.add_argument(
187        'filenames',
188        nargs='+',
189        help='Files to compile',
190    )
191    args = parser.parse_args()
192    if args.filenames == ['-']:
193        filenames = [filename.rstrip('\n') for filename in sys.stdin.readlines()]
194    else:
195        filenames = args.filenames
196    for filename in filenames:
197        try:
198            compile(filename, doraise=True)
199        except PyCompileError as error:
200            if args.quiet:
201                parser.exit(1)
202            else:
203                parser.exit(1, error.msg)
204        except OSError as error:
205            if args.quiet:
206                parser.exit(1)
207            else:
208                parser.exit(1, str(error))
209
210
211if __name__ == "__main__":
212    main()
213