1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4"""Execute files of Python code."""
5
6import marshal
7import os
8import sys
9import types
10
11from coverage.backward import BUILTINS
12from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
13from coverage.misc import ExceptionDuringRun, NoCode, NoSource, isolate_module
14from coverage.phystokens import compile_unicode
15from coverage.python import get_python_source
16
17os = isolate_module(os)
18
19
20class DummyLoader(object):
21    """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader.
22
23    Currently only implements the .fullname attribute
24    """
25    def __init__(self, fullname, *_args):
26        self.fullname = fullname
27
28
29if importlib_util_find_spec:
30    def find_module(modulename):
31        """Find the module named `modulename`.
32
33        Returns the file path of the module, and the name of the enclosing
34        package.
35        """
36        try:
37            spec = importlib_util_find_spec(modulename)
38        except ImportError as err:
39            raise NoSource(str(err))
40        if not spec:
41            raise NoSource("No module named %r" % (modulename,))
42        pathname = spec.origin
43        packagename = spec.name
44        if pathname.endswith("__init__.py") and not modulename.endswith("__init__"):
45            mod_main = modulename + ".__main__"
46            spec = importlib_util_find_spec(mod_main)
47            if not spec:
48                raise NoSource(
49                    "No module named %s; "
50                    "%r is a package and cannot be directly executed"
51                    % (mod_main, modulename)
52                )
53            pathname = spec.origin
54            packagename = spec.name
55        packagename = packagename.rpartition(".")[0]
56        return pathname, packagename
57else:
58    def find_module(modulename):
59        """Find the module named `modulename`.
60
61        Returns the file path of the module, and the name of the enclosing
62        package.
63        """
64        openfile = None
65        glo, loc = globals(), locals()
66        try:
67            # Search for the module - inside its parent package, if any - using
68            # standard import mechanics.
69            if '.' in modulename:
70                packagename, name = modulename.rsplit('.', 1)
71                package = __import__(packagename, glo, loc, ['__path__'])
72                searchpath = package.__path__
73            else:
74                packagename, name = None, modulename
75                searchpath = None  # "top-level search" in imp.find_module()
76            openfile, pathname, _ = imp.find_module(name, searchpath)
77
78            # Complain if this is a magic non-file module.
79            if openfile is None and pathname is None:
80                raise NoSource(
81                    "module does not live in a file: %r" % modulename
82                    )
83
84            # If `modulename` is actually a package, not a mere module, then we
85            # pretend to be Python 2.7 and try running its __main__.py script.
86            if openfile is None:
87                packagename = modulename
88                name = '__main__'
89                package = __import__(packagename, glo, loc, ['__path__'])
90                searchpath = package.__path__
91                openfile, pathname, _ = imp.find_module(name, searchpath)
92        except ImportError as err:
93            raise NoSource(str(err))
94        finally:
95            if openfile:
96                openfile.close()
97
98        return pathname, packagename
99
100
101def run_python_module(modulename, args):
102    """Run a Python module, as though with ``python -m name args...``.
103
104    `modulename` is the name of the module, possibly a dot-separated name.
105    `args` is the argument array to present as sys.argv, including the first
106    element naming the module being executed.
107
108    """
109    pathname, packagename = find_module(modulename)
110
111    pathname = os.path.abspath(pathname)
112    args[0] = pathname
113    run_python_file(pathname, args, package=packagename, modulename=modulename, path0="")
114
115
116def run_python_file(filename, args, package=None, modulename=None, path0=None):
117    """Run a Python file as if it were the main program on the command line.
118
119    `filename` is the path to the file to execute, it need not be a .py file.
120    `args` is the argument array to present as sys.argv, including the first
121    element naming the file being executed.  `package` is the name of the
122    enclosing package, if any.
123
124    `modulename` is the name of the module the file was run as.
125
126    `path0` is the value to put into sys.path[0].  If it's None, then this
127    function will decide on a value.
128
129    """
130    if modulename is None and sys.version_info >= (3, 3):
131        modulename = '__main__'
132
133    # Create a module to serve as __main__
134    old_main_mod = sys.modules['__main__']
135    main_mod = types.ModuleType('__main__')
136    sys.modules['__main__'] = main_mod
137    main_mod.__file__ = filename
138    if package:
139        main_mod.__package__ = package
140    if modulename:
141        main_mod.__loader__ = DummyLoader(modulename)
142
143    main_mod.__builtins__ = BUILTINS
144
145    # Set sys.argv properly.
146    old_argv = sys.argv
147    sys.argv = args
148
149    if os.path.isdir(filename):
150        # Running a directory means running the __main__.py file in that
151        # directory.
152        my_path0 = filename
153
154        for ext in [".py", ".pyc", ".pyo"]:
155            try_filename = os.path.join(filename, "__main__" + ext)
156            if os.path.exists(try_filename):
157                filename = try_filename
158                break
159        else:
160            raise NoSource("Can't find '__main__' module in '%s'" % filename)
161    else:
162        my_path0 = os.path.abspath(os.path.dirname(filename))
163
164    # Set sys.path correctly.
165    old_path0 = sys.path[0]
166    sys.path[0] = path0 if path0 is not None else my_path0
167
168    try:
169        # Make a code object somehow.
170        if filename.endswith((".pyc", ".pyo")):
171            code = make_code_from_pyc(filename)
172        else:
173            code = make_code_from_py(filename)
174
175        # Execute the code object.
176        try:
177            exec(code, main_mod.__dict__)
178        except SystemExit:
179            # The user called sys.exit().  Just pass it along to the upper
180            # layers, where it will be handled.
181            raise
182        except:
183            # Something went wrong while executing the user code.
184            # Get the exc_info, and pack them into an exception that we can
185            # throw up to the outer loop.  We peel one layer off the traceback
186            # so that the coverage.py code doesn't appear in the final printed
187            # traceback.
188            typ, err, tb = sys.exc_info()
189
190            # PyPy3 weirdness.  If I don't access __context__, then somehow it
191            # is non-None when the exception is reported at the upper layer,
192            # and a nested exception is shown to the user.  This getattr fixes
193            # it somehow? https://bitbucket.org/pypy/pypy/issue/1903
194            getattr(err, '__context__', None)
195
196            raise ExceptionDuringRun(typ, err, tb.tb_next)
197    finally:
198        # Restore the old __main__, argv, and path.
199        sys.modules['__main__'] = old_main_mod
200        sys.argv = old_argv
201        sys.path[0] = old_path0
202
203
204def make_code_from_py(filename):
205    """Get source from `filename` and make a code object of it."""
206    # Open the source file.
207    try:
208        source = get_python_source(filename)
209    except (IOError, NoSource):
210        raise NoSource("No file to run: '%s'" % filename)
211
212    code = compile_unicode(source, filename, "exec")
213    return code
214
215
216def make_code_from_pyc(filename):
217    """Get a code object from a .pyc file."""
218    try:
219        fpyc = open(filename, "rb")
220    except IOError:
221        raise NoCode("No file to run: '%s'" % filename)
222
223    with fpyc:
224        # First four bytes are a version-specific magic number.  It has to
225        # match or we won't run the file.
226        magic = fpyc.read(4)
227        if magic != PYC_MAGIC_NUMBER:
228            raise NoCode("Bad magic number in .pyc file")
229
230        # Skip the junk in the header that we don't need.
231        fpyc.read(4)            # Skip the moddate.
232        if sys.version_info >= (3, 3):
233            # 3.3 added another long to the header (size), skip it.
234            fpyc.read(4)
235
236        # The rest of the file is the code object we want.
237        code = marshal.load(fpyc)
238
239    return code
240