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