| # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
| # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
| |
| """Execute files of Python code.""" |
| |
| from __future__ import annotations |
| |
| import importlib.machinery |
| import importlib.util |
| import inspect |
| import marshal |
| import os |
| import struct |
| import sys |
| |
| from importlib.machinery import ModuleSpec |
| from types import CodeType, ModuleType |
| from typing import Any |
| |
| from coverage import env |
| from coverage.exceptions import CoverageException, _ExceptionDuringRun, NoCode, NoSource |
| from coverage.files import canonical_filename, python_reported_file |
| from coverage.misc import isolate_module |
| from coverage.python import get_python_source |
| |
| os = isolate_module(os) |
| |
| |
| PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER |
| |
| class DummyLoader: |
| """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. |
| |
| Currently only implements the .fullname attribute |
| """ |
| def __init__(self, fullname: str, *_args: Any) -> None: |
| self.fullname = fullname |
| |
| |
| def find_module( |
| modulename: str, |
| ) -> tuple[str | None, str, ModuleSpec]: |
| """Find the module named `modulename`. |
| |
| Returns the file path of the module, the name of the enclosing |
| package, and the spec. |
| """ |
| try: |
| spec = importlib.util.find_spec(modulename) |
| except ImportError as err: |
| raise NoSource(str(err)) from err |
| if not spec: |
| raise NoSource(f"No module named {modulename!r}") |
| pathname = spec.origin |
| packagename = spec.name |
| if spec.submodule_search_locations: |
| mod_main = modulename + ".__main__" |
| spec = importlib.util.find_spec(mod_main) |
| if not spec: |
| raise NoSource( |
| f"No module named {mod_main}; " + |
| f"{modulename!r} is a package and cannot be directly executed", |
| ) |
| pathname = spec.origin |
| packagename = spec.name |
| packagename = packagename.rpartition(".")[0] |
| return pathname, packagename, spec |
| |
| |
| class PyRunner: |
| """Multi-stage execution of Python code. |
| |
| This is meant to emulate real Python execution as closely as possible. |
| |
| """ |
| def __init__(self, args: list[str], as_module: bool = False) -> None: |
| self.args = args |
| self.as_module = as_module |
| |
| self.arg0 = args[0] |
| self.package: str | None = None |
| self.modulename: str | None = None |
| self.pathname: str | None = None |
| self.loader: DummyLoader | None = None |
| self.spec: ModuleSpec | None = None |
| |
| def prepare(self) -> None: |
| """Set sys.path properly. |
| |
| This needs to happen before any importing, and without importing anything. |
| """ |
| path0: str | None |
| if self.as_module: |
| path0 = os.getcwd() |
| elif os.path.isdir(self.arg0): |
| # Running a directory means running the __main__.py file in that |
| # directory. |
| path0 = self.arg0 |
| else: |
| path0 = os.path.abspath(os.path.dirname(self.arg0)) |
| |
| if os.path.isdir(sys.path[0]): |
| # sys.path fakery. If we are being run as a command, then sys.path[0] |
| # is the directory of the "coverage" script. If this is so, replace |
| # sys.path[0] with the directory of the file we're running, or the |
| # current directory when running modules. If it isn't so, then we |
| # don't know what's going on, and just leave it alone. |
| top_file = inspect.stack()[-1][0].f_code.co_filename |
| sys_path_0_abs = os.path.abspath(sys.path[0]) |
| top_file_dir_abs = os.path.abspath(os.path.dirname(top_file)) |
| sys_path_0_abs = canonical_filename(sys_path_0_abs) |
| top_file_dir_abs = canonical_filename(top_file_dir_abs) |
| if sys_path_0_abs != top_file_dir_abs: |
| path0 = None |
| |
| else: |
| # sys.path[0] is a file. Is the next entry the directory containing |
| # that file? |
| if sys.path[1] == os.path.dirname(sys.path[0]): |
| # Can it be right to always remove that? |
| del sys.path[1] |
| |
| if path0 is not None: |
| sys.path[0] = python_reported_file(path0) |
| |
| def _prepare2(self) -> None: |
| """Do more preparation to run Python code. |
| |
| Includes finding the module to run and adjusting sys.argv[0]. |
| This method is allowed to import code. |
| |
| """ |
| if self.as_module: |
| self.modulename = self.arg0 |
| pathname, self.package, self.spec = find_module(self.modulename) |
| if self.spec is not None: |
| self.modulename = self.spec.name |
| self.loader = DummyLoader(self.modulename) |
| assert pathname is not None |
| self.pathname = os.path.abspath(pathname) |
| self.args[0] = self.arg0 = self.pathname |
| elif os.path.isdir(self.arg0): |
| # Running a directory means running the __main__.py file in that |
| # directory. |
| for ext in [".py", ".pyc", ".pyo"]: |
| try_filename = os.path.join(self.arg0, "__main__" + ext) |
| # 3.8.10 changed how files are reported when running a |
| # directory. But I'm not sure how far this change is going to |
| # spread, so I'll just hard-code it here for now. |
| if env.PYVERSION >= (3, 8, 10): |
| try_filename = os.path.abspath(try_filename) |
| if os.path.exists(try_filename): |
| self.arg0 = try_filename |
| break |
| else: |
| raise NoSource(f"Can't find '__main__' module in '{self.arg0}'") |
| |
| # Make a spec. I don't know if this is the right way to do it. |
| try_filename = python_reported_file(try_filename) |
| self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) |
| self.spec.has_location = True |
| self.package = "" |
| self.loader = DummyLoader("__main__") |
| else: |
| self.loader = DummyLoader("__main__") |
| |
| self.arg0 = python_reported_file(self.arg0) |
| |
| def run(self) -> None: |
| """Run the Python code!""" |
| |
| self._prepare2() |
| |
| # Create a module to serve as __main__ |
| main_mod = ModuleType("__main__") |
| |
| from_pyc = self.arg0.endswith((".pyc", ".pyo")) |
| main_mod.__file__ = self.arg0 |
| if from_pyc: |
| main_mod.__file__ = main_mod.__file__[:-1] |
| if self.package is not None: |
| main_mod.__package__ = self.package |
| main_mod.__loader__ = self.loader # type: ignore[assignment] |
| if self.spec is not None: |
| main_mod.__spec__ = self.spec |
| |
| main_mod.__builtins__ = sys.modules["builtins"] # type: ignore[attr-defined] |
| |
| sys.modules["__main__"] = main_mod |
| |
| # Set sys.argv properly. |
| sys.argv = self.args |
| |
| try: |
| # Make a code object somehow. |
| if from_pyc: |
| code = make_code_from_pyc(self.arg0) |
| else: |
| code = make_code_from_py(self.arg0) |
| except CoverageException: |
| raise |
| except Exception as exc: |
| msg = f"Couldn't run '{self.arg0}' as Python code: {exc.__class__.__name__}: {exc}" |
| raise CoverageException(msg) from exc |
| |
| # Execute the code object. |
| # Return to the original directory in case the test code exits in |
| # a non-existent directory. |
| cwd = os.getcwd() |
| try: |
| exec(code, main_mod.__dict__) |
| except SystemExit: # pylint: disable=try-except-raise |
| # The user called sys.exit(). Just pass it along to the upper |
| # layers, where it will be handled. |
| raise |
| except Exception: |
| # Something went wrong while executing the user code. |
| # Get the exc_info, and pack them into an exception that we can |
| # throw up to the outer loop. We peel one layer off the traceback |
| # so that the coverage.py code doesn't appear in the final printed |
| # traceback. |
| typ, err, tb = sys.exc_info() |
| assert typ is not None |
| assert err is not None |
| assert tb is not None |
| |
| # PyPy3 weirdness. If I don't access __context__, then somehow it |
| # is non-None when the exception is reported at the upper layer, |
| # and a nested exception is shown to the user. This getattr fixes |
| # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 |
| getattr(err, "__context__", None) |
| |
| # Call the excepthook. |
| try: |
| assert err.__traceback__ is not None |
| err.__traceback__ = err.__traceback__.tb_next |
| sys.excepthook(typ, err, tb.tb_next) |
| except SystemExit: # pylint: disable=try-except-raise |
| raise |
| except Exception as exc: |
| # Getting the output right in the case of excepthook |
| # shenanigans is kind of involved. |
| sys.stderr.write("Error in sys.excepthook:\n") |
| typ2, err2, tb2 = sys.exc_info() |
| assert typ2 is not None |
| assert err2 is not None |
| assert tb2 is not None |
| err2.__suppress_context__ = True |
| assert err2.__traceback__ is not None |
| err2.__traceback__ = err2.__traceback__.tb_next |
| sys.__excepthook__(typ2, err2, tb2.tb_next) |
| sys.stderr.write("\nOriginal exception was:\n") |
| raise _ExceptionDuringRun(typ, err, tb.tb_next) from exc |
| else: |
| sys.exit(1) |
| finally: |
| os.chdir(cwd) |
| |
| |
| def run_python_module(args: list[str]) -> None: |
| """Run a Python module, as though with ``python -m name args...``. |
| |
| `args` is the argument array to present as sys.argv, including the first |
| element naming the module being executed. |
| |
| This is a helper for tests, to encapsulate how to use PyRunner. |
| |
| """ |
| runner = PyRunner(args, as_module=True) |
| runner.prepare() |
| runner.run() |
| |
| |
| def run_python_file(args: list[str]) -> None: |
| """Run a Python file as if it were the main program on the command line. |
| |
| `args` is the argument array to present as sys.argv, including the first |
| element naming the file being executed. `package` is the name of the |
| enclosing package, if any. |
| |
| This is a helper for tests, to encapsulate how to use PyRunner. |
| |
| """ |
| runner = PyRunner(args, as_module=False) |
| runner.prepare() |
| runner.run() |
| |
| |
| def make_code_from_py(filename: str) -> CodeType: |
| """Get source from `filename` and make a code object of it.""" |
| # Open the source file. |
| try: |
| source = get_python_source(filename) |
| except (OSError, NoSource) as exc: |
| raise NoSource(f"No file to run: '{filename}'") from exc |
| |
| return compile(source, filename, "exec", dont_inherit=True) |
| |
| |
| def make_code_from_pyc(filename: str) -> CodeType: |
| """Get a code object from a .pyc file.""" |
| try: |
| fpyc = open(filename, "rb") |
| except OSError as exc: |
| raise NoCode(f"No file to run: '{filename}'") from exc |
| |
| with fpyc: |
| # First four bytes are a version-specific magic number. It has to |
| # match or we won't run the file. |
| magic = fpyc.read(4) |
| if magic != PYC_MAGIC_NUMBER: |
| raise NoCode(f"Bad magic number in .pyc file: {magic!r} != {PYC_MAGIC_NUMBER!r}") |
| |
| flags = struct.unpack("<L", fpyc.read(4))[0] |
| hash_based = flags & 0x01 |
| if hash_based: |
| fpyc.read(8) # Skip the hash. |
| else: |
| # Skip the junk in the header that we don't need. |
| fpyc.read(4) # Skip the moddate. |
| fpyc.read(4) # Skip the size. |
| |
| # The rest of the file is the code object we want. |
| code = marshal.load(fpyc) |
| assert isinstance(code, CodeType) |
| |
| return code |