|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Execute files of Python code.""" |
|
5 |
|
6 import inspect |
|
7 import marshal |
|
8 import os |
|
9 import struct |
|
10 import sys |
|
11 import types |
|
12 |
|
13 from coverage import env |
|
14 from coverage.backward import BUILTINS |
|
15 from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec |
|
16 from coverage.files import canonical_filename, python_reported_file |
|
17 from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module |
|
18 from coverage.phystokens import compile_unicode |
|
19 from coverage.python import get_python_source |
|
20 |
|
21 os = isolate_module(os) |
|
22 |
|
23 |
|
24 class DummyLoader(object): |
|
25 """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. |
|
26 |
|
27 Currently only implements the .fullname attribute |
|
28 """ |
|
29 def __init__(self, fullname, *_args): |
|
30 self.fullname = fullname |
|
31 |
|
32 |
|
33 if importlib_util_find_spec: |
|
34 def find_module(modulename): |
|
35 """Find the module named `modulename`. |
|
36 |
|
37 Returns the file path of the module, the name of the enclosing |
|
38 package, and the spec. |
|
39 """ |
|
40 try: |
|
41 spec = importlib_util_find_spec(modulename) |
|
42 except ImportError as err: |
|
43 raise NoSource(str(err)) |
|
44 if not spec: |
|
45 raise NoSource("No module named %r" % (modulename,)) |
|
46 pathname = spec.origin |
|
47 packagename = spec.name |
|
48 if spec.submodule_search_locations: |
|
49 mod_main = modulename + ".__main__" |
|
50 spec = importlib_util_find_spec(mod_main) |
|
51 if not spec: |
|
52 raise NoSource( |
|
53 "No module named %s; " |
|
54 "%r is a package and cannot be directly executed" |
|
55 % (mod_main, modulename) |
|
56 ) |
|
57 pathname = spec.origin |
|
58 packagename = spec.name |
|
59 packagename = packagename.rpartition(".")[0] |
|
60 return pathname, packagename, spec |
|
61 else: |
|
62 def find_module(modulename): |
|
63 """Find the module named `modulename`. |
|
64 |
|
65 Returns the file path of the module, the name of the enclosing |
|
66 package, and None (where a spec would have been). |
|
67 """ |
|
68 openfile = None |
|
69 glo, loc = globals(), locals() |
|
70 try: |
|
71 # Search for the module - inside its parent package, if any - using |
|
72 # standard import mechanics. |
|
73 if '.' in modulename: |
|
74 packagename, name = modulename.rsplit('.', 1) |
|
75 package = __import__(packagename, glo, loc, ['__path__']) |
|
76 searchpath = package.__path__ |
|
77 else: |
|
78 packagename, name = None, modulename |
|
79 searchpath = None # "top-level search" in imp.find_module() |
|
80 openfile, pathname, _ = imp.find_module(name, searchpath) |
|
81 |
|
82 # Complain if this is a magic non-file module. |
|
83 if openfile is None and pathname is None: |
|
84 raise NoSource( |
|
85 "module does not live in a file: %r" % modulename |
|
86 ) |
|
87 |
|
88 # If `modulename` is actually a package, not a mere module, then we |
|
89 # pretend to be Python 2.7 and try running its __main__.py script. |
|
90 if openfile is None: |
|
91 packagename = modulename |
|
92 name = '__main__' |
|
93 package = __import__(packagename, glo, loc, ['__path__']) |
|
94 searchpath = package.__path__ |
|
95 openfile, pathname, _ = imp.find_module(name, searchpath) |
|
96 except ImportError as err: |
|
97 raise NoSource(str(err)) |
|
98 finally: |
|
99 if openfile: |
|
100 openfile.close() |
|
101 |
|
102 return pathname, packagename, None |
|
103 |
|
104 |
|
105 class PyRunner(object): |
|
106 """Multi-stage execution of Python code. |
|
107 |
|
108 This is meant to emulate real Python execution as closely as possible. |
|
109 |
|
110 """ |
|
111 def __init__(self, args, as_module=False): |
|
112 self.args = args |
|
113 self.as_module = as_module |
|
114 |
|
115 self.arg0 = args[0] |
|
116 self.package = self.modulename = self.pathname = self.loader = self.spec = None |
|
117 |
|
118 def prepare(self): |
|
119 """Set sys.path properly. |
|
120 |
|
121 This needs to happen before any importing, and without importing anything. |
|
122 """ |
|
123 if self.as_module: |
|
124 if env.PYBEHAVIOR.actual_syspath0_dash_m: |
|
125 path0 = os.getcwd() |
|
126 else: |
|
127 path0 = "" |
|
128 elif os.path.isdir(self.arg0): |
|
129 # Running a directory means running the __main__.py file in that |
|
130 # directory. |
|
131 path0 = self.arg0 |
|
132 else: |
|
133 path0 = os.path.abspath(os.path.dirname(self.arg0)) |
|
134 |
|
135 if os.path.isdir(sys.path[0]): |
|
136 # sys.path fakery. If we are being run as a command, then sys.path[0] |
|
137 # is the directory of the "coverage" script. If this is so, replace |
|
138 # sys.path[0] with the directory of the file we're running, or the |
|
139 # current directory when running modules. If it isn't so, then we |
|
140 # don't know what's going on, and just leave it alone. |
|
141 top_file = inspect.stack()[-1][0].f_code.co_filename |
|
142 sys_path_0_abs = os.path.abspath(sys.path[0]) |
|
143 top_file_dir_abs = os.path.abspath(os.path.dirname(top_file)) |
|
144 sys_path_0_abs = canonical_filename(sys_path_0_abs) |
|
145 top_file_dir_abs = canonical_filename(top_file_dir_abs) |
|
146 if sys_path_0_abs != top_file_dir_abs: |
|
147 path0 = None |
|
148 |
|
149 else: |
|
150 # sys.path[0] is a file. Is the next entry the directory containing |
|
151 # that file? |
|
152 if sys.path[1] == os.path.dirname(sys.path[0]): |
|
153 # Can it be right to always remove that? |
|
154 del sys.path[1] |
|
155 |
|
156 if path0 is not None: |
|
157 sys.path[0] = python_reported_file(path0) |
|
158 |
|
159 def _prepare2(self): |
|
160 """Do more preparation to run Python code. |
|
161 |
|
162 Includes finding the module to run and adjusting sys.argv[0]. |
|
163 This method is allowed to import code. |
|
164 |
|
165 """ |
|
166 if self.as_module: |
|
167 self.modulename = self.arg0 |
|
168 pathname, self.package, self.spec = find_module(self.modulename) |
|
169 if self.spec is not None: |
|
170 self.modulename = self.spec.name |
|
171 self.loader = DummyLoader(self.modulename) |
|
172 self.pathname = os.path.abspath(pathname) |
|
173 self.args[0] = self.arg0 = self.pathname |
|
174 elif os.path.isdir(self.arg0): |
|
175 # Running a directory means running the __main__.py file in that |
|
176 # directory. |
|
177 for ext in [".py", ".pyc", ".pyo"]: |
|
178 try_filename = os.path.join(self.arg0, "__main__" + ext) |
|
179 if os.path.exists(try_filename): |
|
180 self.arg0 = try_filename |
|
181 break |
|
182 else: |
|
183 raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) |
|
184 |
|
185 if env.PY2: |
|
186 self.arg0 = os.path.abspath(self.arg0) |
|
187 |
|
188 # Make a spec. I don't know if this is the right way to do it. |
|
189 try: |
|
190 import importlib.machinery |
|
191 except ImportError: |
|
192 pass |
|
193 else: |
|
194 try_filename = python_reported_file(try_filename) |
|
195 self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) |
|
196 self.spec.has_location = True |
|
197 self.package = "" |
|
198 self.loader = DummyLoader("__main__") |
|
199 else: |
|
200 if env.PY3: |
|
201 self.loader = DummyLoader("__main__") |
|
202 |
|
203 self.arg0 = python_reported_file(self.arg0) |
|
204 |
|
205 def run(self): |
|
206 """Run the Python code!""" |
|
207 |
|
208 self._prepare2() |
|
209 |
|
210 # Create a module to serve as __main__ |
|
211 main_mod = types.ModuleType('__main__') |
|
212 |
|
213 from_pyc = self.arg0.endswith((".pyc", ".pyo")) |
|
214 main_mod.__file__ = self.arg0 |
|
215 if from_pyc: |
|
216 main_mod.__file__ = main_mod.__file__[:-1] |
|
217 if self.package is not None: |
|
218 main_mod.__package__ = self.package |
|
219 main_mod.__loader__ = self.loader |
|
220 if self.spec is not None: |
|
221 main_mod.__spec__ = self.spec |
|
222 |
|
223 main_mod.__builtins__ = BUILTINS |
|
224 |
|
225 sys.modules['__main__'] = main_mod |
|
226 |
|
227 # Set sys.argv properly. |
|
228 sys.argv = self.args |
|
229 |
|
230 try: |
|
231 # Make a code object somehow. |
|
232 if from_pyc: |
|
233 code = make_code_from_pyc(self.arg0) |
|
234 else: |
|
235 code = make_code_from_py(self.arg0) |
|
236 except CoverageException: |
|
237 raise |
|
238 except Exception as exc: |
|
239 msg = "Couldn't run '{filename}' as Python code: {exc.__class__.__name__}: {exc}" |
|
240 raise CoverageException(msg.format(filename=self.arg0, exc=exc)) |
|
241 |
|
242 # Execute the code object. |
|
243 # Return to the original directory in case the test code exits in |
|
244 # a non-existent directory. |
|
245 cwd = os.getcwd() |
|
246 try: |
|
247 exec(code, main_mod.__dict__) |
|
248 except SystemExit: # pylint: disable=try-except-raise |
|
249 # The user called sys.exit(). Just pass it along to the upper |
|
250 # layers, where it will be handled. |
|
251 raise |
|
252 except Exception: |
|
253 # Something went wrong while executing the user code. |
|
254 # Get the exc_info, and pack them into an exception that we can |
|
255 # throw up to the outer loop. We peel one layer off the traceback |
|
256 # so that the coverage.py code doesn't appear in the final printed |
|
257 # traceback. |
|
258 typ, err, tb = sys.exc_info() |
|
259 |
|
260 # PyPy3 weirdness. If I don't access __context__, then somehow it |
|
261 # is non-None when the exception is reported at the upper layer, |
|
262 # and a nested exception is shown to the user. This getattr fixes |
|
263 # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 |
|
264 getattr(err, '__context__', None) |
|
265 |
|
266 # Call the excepthook. |
|
267 try: |
|
268 if hasattr(err, "__traceback__"): |
|
269 err.__traceback__ = err.__traceback__.tb_next |
|
270 sys.excepthook(typ, err, tb.tb_next) |
|
271 except SystemExit: # pylint: disable=try-except-raise |
|
272 raise |
|
273 except Exception: |
|
274 # Getting the output right in the case of excepthook |
|
275 # shenanigans is kind of involved. |
|
276 sys.stderr.write("Error in sys.excepthook:\n") |
|
277 typ2, err2, tb2 = sys.exc_info() |
|
278 err2.__suppress_context__ = True |
|
279 if hasattr(err2, "__traceback__"): |
|
280 err2.__traceback__ = err2.__traceback__.tb_next |
|
281 sys.__excepthook__(typ2, err2, tb2.tb_next) |
|
282 sys.stderr.write("\nOriginal exception was:\n") |
|
283 raise ExceptionDuringRun(typ, err, tb.tb_next) |
|
284 else: |
|
285 sys.exit(1) |
|
286 finally: |
|
287 os.chdir(cwd) |
|
288 |
|
289 |
|
290 def run_python_module(args): |
|
291 """Run a Python module, as though with ``python -m name args...``. |
|
292 |
|
293 `args` is the argument array to present as sys.argv, including the first |
|
294 element naming the module being executed. |
|
295 |
|
296 This is a helper for tests, to encapsulate how to use PyRunner. |
|
297 |
|
298 """ |
|
299 runner = PyRunner(args, as_module=True) |
|
300 runner.prepare() |
|
301 runner.run() |
|
302 |
|
303 |
|
304 def run_python_file(args): |
|
305 """Run a Python file as if it were the main program on the command line. |
|
306 |
|
307 `args` is the argument array to present as sys.argv, including the first |
|
308 element naming the file being executed. `package` is the name of the |
|
309 enclosing package, if any. |
|
310 |
|
311 This is a helper for tests, to encapsulate how to use PyRunner. |
|
312 |
|
313 """ |
|
314 runner = PyRunner(args, as_module=False) |
|
315 runner.prepare() |
|
316 runner.run() |
|
317 |
|
318 |
|
319 def make_code_from_py(filename): |
|
320 """Get source from `filename` and make a code object of it.""" |
|
321 # Open the source file. |
|
322 try: |
|
323 source = get_python_source(filename) |
|
324 except (IOError, NoSource): |
|
325 raise NoSource("No file to run: '%s'" % filename) |
|
326 |
|
327 code = compile_unicode(source, filename, "exec") |
|
328 return code |
|
329 |
|
330 |
|
331 def make_code_from_pyc(filename): |
|
332 """Get a code object from a .pyc file.""" |
|
333 try: |
|
334 fpyc = open(filename, "rb") |
|
335 except IOError: |
|
336 raise NoCode("No file to run: '%s'" % filename) |
|
337 |
|
338 with fpyc: |
|
339 # First four bytes are a version-specific magic number. It has to |
|
340 # match or we won't run the file. |
|
341 magic = fpyc.read(4) |
|
342 if magic != PYC_MAGIC_NUMBER: |
|
343 raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER)) |
|
344 |
|
345 date_based = True |
|
346 if env.PYBEHAVIOR.hashed_pyc_pep552: |
|
347 flags = struct.unpack('<L', fpyc.read(4))[0] |
|
348 hash_based = flags & 0x01 |
|
349 if hash_based: |
|
350 fpyc.read(8) # Skip the hash. |
|
351 date_based = False |
|
352 if date_based: |
|
353 # Skip the junk in the header that we don't need. |
|
354 fpyc.read(4) # Skip the moddate. |
|
355 if env.PYBEHAVIOR.size_in_pyc: |
|
356 # 3.3 added another long to the header (size), skip it. |
|
357 fpyc.read(4) |
|
358 |
|
359 # The rest of the file is the code object we want. |
|
360 code = marshal.load(fpyc) |
|
361 |
|
362 return code |