• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 """Routine to "compile" a .py file to a .pyc file.
2 
3 This module has intimate knowledge of the format of .pyc files.
4 """
5 
6 import enum
7 import importlib._bootstrap_external
8 import importlib.machinery
9 import importlib.util
10 import os
11 import os.path
12 import sys
13 import traceback
14 
15 __all__ = ["compile", "main", "PyCompileError", "PycInvalidationMode"]
16 
17 
18 class 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 
66 class PycInvalidationMode(enum.Enum):
67     TIMESTAMP = 1
68     CHECKED_HASH = 2
69     UNCHECKED_HASH = 3
70 
71 
72 def _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 
79 def 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 
176 def 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 
211 if __name__ == "__main__":
212     main()
213