| # 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.""" |
| |
| import inspect |
| import marshal |
| import os |
| import struct |
| import sys |
| import types |
| |
| from coverage import env |
| from coverage.backward import BUILTINS |
| from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec |
| from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module |
| from coverage.phystokens import compile_unicode |
| from coverage.python import get_python_source |
| |
| os = isolate_module(os) |
| |
| |
| class DummyLoader(object): |
| """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader. |
| |
| Currently only implements the .fullname attribute |
| """ |
| def __init__(self, fullname, *_args): |
| self.fullname = fullname |
| |
| |
| if importlib_util_find_spec: |
| def find_module(modulename): |
| """Find the module named `modulename`. |
| |
| Returns the file path of the module, and the name of the enclosing |
| package. |
| """ |
| try: |
| spec = importlib_util_find_spec(modulename) |
| except ImportError as err: |
| raise NoSource(str(err)) |
| if not spec: |
| raise NoSource("No module named %r" % (modulename,)) |
| pathname = spec.origin |
| packagename = spec.name |
| if pathname.endswith("__init__.py") and not modulename.endswith("__init__"): |
| mod_main = modulename + ".__main__" |
| spec = importlib_util_find_spec(mod_main) |
| if not spec: |
| raise NoSource( |
| "No module named %s; " |
| "%r is a package and cannot be directly executed" |
| % (mod_main, modulename) |
| ) |
| pathname = spec.origin |
| packagename = spec.name |
| packagename = packagename.rpartition(".")[0] |
| return pathname, packagename |
| else: |
| def find_module(modulename): |
| """Find the module named `modulename`. |
| |
| Returns the file path of the module, and the name of the enclosing |
| package. |
| """ |
| openfile = None |
| glo, loc = globals(), locals() |
| try: |
| # Search for the module - inside its parent package, if any - using |
| # standard import mechanics. |
| if '.' in modulename: |
| packagename, name = modulename.rsplit('.', 1) |
| package = __import__(packagename, glo, loc, ['__path__']) |
| searchpath = package.__path__ |
| else: |
| packagename, name = None, modulename |
| searchpath = None # "top-level search" in imp.find_module() |
| openfile, pathname, _ = imp.find_module(name, searchpath) |
| |
| # Complain if this is a magic non-file module. |
| if openfile is None and pathname is None: |
| raise NoSource( |
| "module does not live in a file: %r" % modulename |
| ) |
| |
| # If `modulename` is actually a package, not a mere module, then we |
| # pretend to be Python 2.7 and try running its __main__.py script. |
| if openfile is None: |
| packagename = modulename |
| name = '__main__' |
| package = __import__(packagename, glo, loc, ['__path__']) |
| searchpath = package.__path__ |
| openfile, pathname, _ = imp.find_module(name, searchpath) |
| except ImportError as err: |
| raise NoSource(str(err)) |
| finally: |
| if openfile: |
| openfile.close() |
| |
| return pathname, packagename |
| |
| |
| class PyRunner(object): |
| """Multi-stage execution of Python code. |
| |
| This is meant to emulate real Python execution as closely as possible. |
| |
| """ |
| def __init__(self, args, as_module=False): |
| self.args = args |
| self.as_module = as_module |
| |
| self.arg0 = args[0] |
| self.package = self.modulename = self.pathname = None |
| |
| def prepare(self): |
| """Do initial preparation to run Python code. |
| |
| Includes finding the module to run, adjusting sys.argv[0], and changing |
| sys.path to match what Python does. |
| |
| """ |
| should_update_sys_path = True |
| |
| if self.as_module: |
| # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It |
| # used to be an empty string (meaning the current directory). It changed |
| # to be the actual path to the current directory, so that os.chdir wouldn't |
| # affect the outcome. |
| if env.PYVERSION >= (3, 7, 0, 'beta', 3): |
| path0 = os.getcwd() |
| else: |
| path0 = "" |
| sys.path[0] = path0 |
| should_update_sys_path = False |
| self.modulename = self.arg0 |
| pathname, self.package = find_module(self.modulename) |
| 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. |
| path0 = self.arg0 |
| for ext in [".py", ".pyc", ".pyo"]: |
| try_filename = os.path.join(self.arg0, "__main__" + ext) |
| if os.path.exists(try_filename): |
| self.arg0 = try_filename |
| break |
| else: |
| raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) |
| else: |
| path0 = os.path.abspath(os.path.dirname(self.arg0)) |
| |
| if self.modulename is None and env.PYVERSION >= (3, 3): |
| self.modulename = '__main__' |
| |
| if should_update_sys_path: |
| # 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 |
| if os.path.abspath(sys.path[0]) == os.path.abspath(os.path.dirname(top_file)): |
| # Set sys.path correctly. |
| sys.path[0] = path0 |
| |
| def run(self): |
| """Run the Python code!""" |
| |
| # Create a module to serve as __main__ |
| main_mod = types.ModuleType('__main__') |
| sys.modules['__main__'] = main_mod |
| main_mod.__file__ = self.arg0 |
| if self.package: |
| main_mod.__package__ = self.package |
| if self.modulename: |
| main_mod.__loader__ = DummyLoader(self.modulename) |
| |
| main_mod.__builtins__ = BUILTINS |
| |
| # Set sys.argv properly. |
| sys.argv = self.args |
| |
| try: |
| # Make a code object somehow. |
| if self.arg0.endswith((".pyc", ".pyo")): |
| code = make_code_from_pyc(self.arg0) |
| else: |
| code = make_code_from_py(self.arg0) |
| except CoverageException: |
| raise |
| except Exception as exc: |
| msg = "Couldn't run {filename!r} as Python code: {exc.__class__.__name__}: {exc}" |
| raise CoverageException(msg.format(filename=self.arg0, exc=exc)) |
| |
| # Execute the code object. |
| try: |
| exec(code, main_mod.__dict__) |
| except SystemExit: |
| # 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() |
| |
| # 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: |
| if hasattr(err, "__traceback__"): |
| err.__traceback__ = err.__traceback__.tb_next |
| sys.excepthook(typ, err, tb.tb_next) |
| except SystemExit: |
| raise |
| except Exception: |
| # 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() |
| err2.__suppress_context__ = True |
| if hasattr(err2, "__traceback__"): |
| 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) |
| else: |
| sys.exit(1) |
| |
| |
| def run_python_module(args): |
| """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. |
| |
| """ |
| runner = PyRunner(args, as_module=True) |
| runner.prepare() |
| runner.run() |
| |
| |
| def run_python_file(args): |
| """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. |
| |
| """ |
| runner = PyRunner(args, as_module=False) |
| runner.prepare() |
| runner.run() |
| |
| |
| def make_code_from_py(filename): |
| """Get source from `filename` and make a code object of it.""" |
| # Open the source file. |
| try: |
| source = get_python_source(filename) |
| except (IOError, NoSource): |
| raise NoSource("No file to run: '%s'" % filename) |
| |
| code = compile_unicode(source, filename, "exec") |
| return code |
| |
| |
| def make_code_from_pyc(filename): |
| """Get a code object from a .pyc file.""" |
| try: |
| fpyc = open(filename, "rb") |
| except IOError: |
| raise NoCode("No file to run: '%s'" % filename) |
| |
| 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("Bad magic number in .pyc file") |
| |
| date_based = True |
| if env.PYVERSION >= (3, 7, 0, 'alpha', 4): |
| flags = struct.unpack('<L', fpyc.read(4))[0] |
| hash_based = flags & 0x01 |
| if hash_based: |
| fpyc.read(8) # Skip the hash. |
| date_based = False |
| if date_based: |
| # Skip the junk in the header that we don't need. |
| fpyc.read(4) # Skip the moddate. |
| if env.PYVERSION >= (3, 3): |
| # 3.3 added another long to the header (size), skip it. |
| fpyc.read(4) |
| |
| # The rest of the file is the code object we want. |
| code = marshal.load(fpyc) |
| |
| return code |