1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
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 |
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
3 |
3 |
4 """Execute files of Python code.""" |
4 """Execute files of Python code.""" |
5 |
5 |
|
6 import importlib.machinery |
|
7 import importlib.util |
6 import inspect |
8 import inspect |
7 import marshal |
9 import marshal |
8 import os |
10 import os |
9 import struct |
11 import struct |
10 import sys |
12 import sys |
11 import types |
13 import types |
12 |
14 |
13 from coverage import env |
15 from coverage import env |
14 from coverage.backward import BUILTINS |
16 from coverage.exceptions import CoverageException, ExceptionDuringRun, NoCode, NoSource |
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.files import canonical_filename, python_reported_file |
17 from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module |
18 from coverage.misc import isolate_module |
18 from coverage.phystokens import compile_unicode |
19 from coverage.phystokens import compile_unicode |
19 from coverage.python import get_python_source |
20 from coverage.python import get_python_source |
20 |
21 |
21 os = isolate_module(os) |
22 os = isolate_module(os) |
22 |
23 |
23 |
24 |
24 class DummyLoader(object): |
25 PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER |
|
26 |
|
27 class DummyLoader: |
25 """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. |
28 """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. |
26 |
29 |
27 Currently only implements the .fullname attribute |
30 Currently only implements the .fullname attribute |
28 """ |
31 """ |
29 def __init__(self, fullname, *_args): |
32 def __init__(self, fullname, *_args): |
30 self.fullname = fullname |
33 self.fullname = fullname |
31 |
34 |
32 |
35 |
33 if importlib_util_find_spec: |
36 def find_module(modulename): |
34 def find_module(modulename): |
37 """Find the module named `modulename`. |
35 """Find the module named `modulename`. |
38 |
36 |
39 Returns the file path of the module, the name of the enclosing |
37 Returns the file path of the module, the name of the enclosing |
40 package, and the spec. |
38 package, and the spec. |
41 """ |
39 """ |
42 try: |
40 try: |
43 spec = importlib.util.find_spec(modulename) |
41 spec = importlib_util_find_spec(modulename) |
44 except ImportError as err: |
42 except ImportError as err: |
45 raise NoSource(str(err)) from err |
43 raise NoSource(str(err)) |
46 if not spec: |
|
47 raise NoSource(f"No module named {modulename!r}") |
|
48 pathname = spec.origin |
|
49 packagename = spec.name |
|
50 if spec.submodule_search_locations: |
|
51 mod_main = modulename + ".__main__" |
|
52 spec = importlib.util.find_spec(mod_main) |
44 if not spec: |
53 if not spec: |
45 raise NoSource("No module named %r" % (modulename,)) |
54 raise NoSource( |
|
55 f"No module named {mod_main}; " + |
|
56 f"{modulename!r} is a package and cannot be directly executed" |
|
57 ) |
46 pathname = spec.origin |
58 pathname = spec.origin |
47 packagename = spec.name |
59 packagename = spec.name |
48 if spec.submodule_search_locations: |
60 packagename = packagename.rpartition(".")[0] |
49 mod_main = modulename + ".__main__" |
61 return pathname, packagename, spec |
50 spec = importlib_util_find_spec(mod_main) |
62 |
51 if not spec: |
63 |
52 raise NoSource( |
64 class PyRunner: |
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. |
65 """Multi-stage execution of Python code. |
107 |
66 |
108 This is meant to emulate real Python execution as closely as possible. |
67 This is meant to emulate real Python execution as closely as possible. |
109 |
68 |
110 """ |
69 """ |
174 elif os.path.isdir(self.arg0): |
133 elif os.path.isdir(self.arg0): |
175 # Running a directory means running the __main__.py file in that |
134 # Running a directory means running the __main__.py file in that |
176 # directory. |
135 # directory. |
177 for ext in [".py", ".pyc", ".pyo"]: |
136 for ext in [".py", ".pyc", ".pyo"]: |
178 try_filename = os.path.join(self.arg0, "__main__" + ext) |
137 try_filename = os.path.join(self.arg0, "__main__" + ext) |
|
138 # 3.8.10 changed how files are reported when running a |
|
139 # directory. But I'm not sure how far this change is going to |
|
140 # spread, so I'll just hard-code it here for now. |
|
141 if env.PYVERSION >= (3, 8, 10): |
|
142 try_filename = os.path.abspath(try_filename) |
179 if os.path.exists(try_filename): |
143 if os.path.exists(try_filename): |
180 self.arg0 = try_filename |
144 self.arg0 = try_filename |
181 break |
145 break |
182 else: |
146 else: |
183 raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) |
147 raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) |
184 |
148 |
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. |
149 # Make a spec. I don't know if this is the right way to do it. |
189 try: |
150 try_filename = python_reported_file(try_filename) |
190 import importlib.machinery |
151 self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) |
191 except ImportError: |
152 self.spec.has_location = True |
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 = "" |
153 self.package = "" |
198 self.loader = DummyLoader("__main__") |
154 self.loader = DummyLoader("__main__") |
199 else: |
155 else: |
200 if env.PY3: |
156 self.loader = DummyLoader("__main__") |
201 self.loader = DummyLoader("__main__") |
|
202 |
157 |
203 self.arg0 = python_reported_file(self.arg0) |
158 self.arg0 = python_reported_file(self.arg0) |
204 |
159 |
205 def run(self): |
160 def run(self): |
206 """Run the Python code!""" |
161 """Run the Python code!""" |
263 # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 |
218 # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 |
264 getattr(err, '__context__', None) |
219 getattr(err, '__context__', None) |
265 |
220 |
266 # Call the excepthook. |
221 # Call the excepthook. |
267 try: |
222 try: |
268 if hasattr(err, "__traceback__"): |
223 err.__traceback__ = err.__traceback__.tb_next |
269 err.__traceback__ = err.__traceback__.tb_next |
|
270 sys.excepthook(typ, err, tb.tb_next) |
224 sys.excepthook(typ, err, tb.tb_next) |
271 except SystemExit: # pylint: disable=try-except-raise |
225 except SystemExit: # pylint: disable=try-except-raise |
272 raise |
226 raise |
273 except Exception: |
227 except Exception as exc: |
274 # Getting the output right in the case of excepthook |
228 # Getting the output right in the case of excepthook |
275 # shenanigans is kind of involved. |
229 # shenanigans is kind of involved. |
276 sys.stderr.write("Error in sys.excepthook:\n") |
230 sys.stderr.write("Error in sys.excepthook:\n") |
277 typ2, err2, tb2 = sys.exc_info() |
231 typ2, err2, tb2 = sys.exc_info() |
278 err2.__suppress_context__ = True |
232 err2.__suppress_context__ = True |
279 if hasattr(err2, "__traceback__"): |
233 err2.__traceback__ = err2.__traceback__.tb_next |
280 err2.__traceback__ = err2.__traceback__.tb_next |
|
281 sys.__excepthook__(typ2, err2, tb2.tb_next) |
234 sys.__excepthook__(typ2, err2, tb2.tb_next) |
282 sys.stderr.write("\nOriginal exception was:\n") |
235 sys.stderr.write("\nOriginal exception was:\n") |
283 raise ExceptionDuringRun(typ, err, tb.tb_next) |
236 raise ExceptionDuringRun(typ, err, tb.tb_next) from exc |
284 else: |
237 else: |
285 sys.exit(1) |
238 sys.exit(1) |
286 finally: |
239 finally: |
287 os.chdir(cwd) |
240 os.chdir(cwd) |
288 |
241 |
319 def make_code_from_py(filename): |
272 def make_code_from_py(filename): |
320 """Get source from `filename` and make a code object of it.""" |
273 """Get source from `filename` and make a code object of it.""" |
321 # Open the source file. |
274 # Open the source file. |
322 try: |
275 try: |
323 source = get_python_source(filename) |
276 source = get_python_source(filename) |
324 except (IOError, NoSource): |
277 except (OSError, NoSource) as exc: |
325 raise NoSource("No file to run: '%s'" % filename) |
278 raise NoSource(f"No file to run: '{filename}'") from exc |
326 |
279 |
327 code = compile_unicode(source, filename, "exec") |
280 code = compile_unicode(source, filename, "exec") |
328 return code |
281 return code |
329 |
282 |
330 |
283 |
331 def make_code_from_pyc(filename): |
284 def make_code_from_pyc(filename): |
332 """Get a code object from a .pyc file.""" |
285 """Get a code object from a .pyc file.""" |
333 try: |
286 try: |
334 fpyc = open(filename, "rb") |
287 fpyc = open(filename, "rb") |
335 except IOError: |
288 except OSError as exc: |
336 raise NoCode("No file to run: '%s'" % filename) |
289 raise NoCode(f"No file to run: '{filename}'") from exc |
337 |
290 |
338 with fpyc: |
291 with fpyc: |
339 # First four bytes are a version-specific magic number. It has to |
292 # First four bytes are a version-specific magic number. It has to |
340 # match or we won't run the file. |
293 # match or we won't run the file. |
341 magic = fpyc.read(4) |
294 magic = fpyc.read(4) |
342 if magic != PYC_MAGIC_NUMBER: |
295 if magic != PYC_MAGIC_NUMBER: |
343 raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER)) |
296 raise NoCode(f"Bad magic number in .pyc file: {magic} != {PYC_MAGIC_NUMBER}") |
344 |
297 |
345 date_based = True |
298 date_based = True |
346 if env.PYBEHAVIOR.hashed_pyc_pep552: |
299 if env.PYBEHAVIOR.hashed_pyc_pep552: |
347 flags = struct.unpack('<L', fpyc.read(4))[0] |
300 flags = struct.unpack('<L', fpyc.read(4))[0] |
348 hash_based = flags & 0x01 |
301 hash_based = flags & 0x01 |