src/eric7/DebugClients/Python/coverage/execfile.py

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

eric ide

mercurial