eric6/DebugClients/Python/coverage/execfile.py

changeset 7427
362cd1b6f81a
parent 6942
2602857055c5
diff -r dc171b1d8261 -r 362cd1b6f81a eric6/DebugClients/Python/coverage/execfile.py
--- a/eric6/DebugClients/Python/coverage/execfile.py	Wed Feb 19 19:38:36 2020 +0100
+++ b/eric6/DebugClients/Python/coverage/execfile.py	Sat Feb 22 14:27:42 2020 +0100
@@ -1,16 +1,19 @@
 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+# 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.files import canonical_filename, python_reported_file
 from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module
 from coverage.phystokens import compile_unicode
 from coverage.python import get_python_source
@@ -31,8 +34,8 @@
     def find_module(modulename):
         """Find the module named `modulename`.
 
-        Returns the file path of the module, and the name of the enclosing
-        package.
+        Returns the file path of the module, the name of the enclosing
+        package, and the spec.
         """
         try:
             spec = importlib_util_find_spec(modulename)
@@ -42,7 +45,7 @@
             raise NoSource("No module named %r" % (modulename,))
         pathname = spec.origin
         packagename = spec.name
-        if pathname.endswith("__init__.py") and not modulename.endswith("__init__"):
+        if spec.submodule_search_locations:
             mod_main = modulename + ".__main__"
             spec = importlib_util_find_spec(mod_main)
             if not spec:
@@ -54,13 +57,13 @@
             pathname = spec.origin
             packagename = spec.name
         packagename = packagename.rpartition(".")[0]
-        return pathname, packagename
+        return pathname, packagename, spec
 else:
     def find_module(modulename):
         """Find the module named `modulename`.
 
-        Returns the file path of the module, and the name of the enclosing
-        package.
+        Returns the file path of the module, the name of the enclosing
+        package, and None (where a spec would have been).
         """
         openfile = None
         glo, loc = globals(), locals()
@@ -96,101 +99,153 @@
             if openfile:
                 openfile.close()
 
-        return pathname, packagename
+        return pathname, packagename, None
 
 
-def run_python_module(modulename, args):
-    """Run a Python module, as though with ``python -m name args...``.
-
-    `modulename` is the name of the module, possibly a dot-separated name.
-    `args` is the argument array to present as sys.argv, including the first
-    element naming the module being executed.
-
-    """
-    pathname, packagename = find_module(modulename)
+class PyRunner(object):
+    """Multi-stage execution of Python code.
 
-    pathname = os.path.abspath(pathname)
-    args[0] = pathname
-    # 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 sys.version_info >= (3, 7, 0, 'beta', 3):
-        path0 = os.getcwd()
-    else:
-        path0 = ""
-    run_python_file(pathname, args, package=packagename, modulename=modulename, path0=path0)
-
-
-def run_python_file(filename, args, package=None, modulename=None, path0=None):
-    """Run a Python file as if it were the main program on the command line.
-
-    `filename` is the path to the file to execute, it need not be a .py file.
-    `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.
-
-    `modulename` is the name of the module the file was run as.
-
-    `path0` is the value to put into sys.path[0].  If it's None, then this
-    function will decide on a value.
+    This is meant to emulate real Python execution as closely as possible.
 
     """
-    if modulename is None and sys.version_info >= (3, 3):
-        modulename = '__main__'
+    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 = self.loader = self.spec = None
+
+    def prepare(self):
+        """Set sys.path properly.
+
+        This needs to happen before any importing, and without importing anything.
+        """
+        if self.as_module:
+            if env.PYBEHAVIOR.actual_syspath0_dash_m:
+                path0 = os.getcwd()
+            else:
+                path0 = ""
+        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))
 
-    # Create a module to serve as __main__
-    old_main_mod = sys.modules['__main__']
-    main_mod = types.ModuleType('__main__')
-    sys.modules['__main__'] = main_mod
-    main_mod.__file__ = filename
-    if package:
-        main_mod.__package__ = package
-    if modulename:
-        main_mod.__loader__ = DummyLoader(modulename)
+        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
 
-    main_mod.__builtins__ = BUILTINS
+        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]
 
-    # Set sys.argv properly.
-    old_argv = sys.argv
-    sys.argv = args
+        if path0 is not None:
+            sys.path[0] = python_reported_file(path0)
+
+    def _prepare2(self):
+        """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 os.path.isdir(filename):
-        # Running a directory means running the __main__.py file in that
-        # directory.
-        my_path0 = filename
+        """
+        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)
+            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)
+                if os.path.exists(try_filename):
+                    self.arg0 = try_filename
+                    break
+            else:
+                raise NoSource("Can't find '__main__' module in '%s'" % self.arg0)
+
+            if env.PY2:
+                self.arg0 = os.path.abspath(self.arg0)
 
-        for ext in [".py", ".pyc", ".pyo"]:
-            try_filename = os.path.join(filename, "__main__" + ext)
-            if os.path.exists(try_filename):
-                filename = try_filename
-                break
+            # Make a spec. I don't know if this is the right way to do it.
+            try:
+                import importlib.machinery
+            except ImportError:
+                pass
+            else:
+                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:
-            raise NoSource("Can't find '__main__' module in '%s'" % filename)
-    else:
-        my_path0 = os.path.abspath(os.path.dirname(filename))
+            if env.PY3:
+                self.loader = DummyLoader("__main__")
+
+        self.arg0 = python_reported_file(self.arg0)
+
+    def run(self):
+        """Run the Python code!"""
+
+        self._prepare2()
+
+        # Create a module to serve as __main__
+        main_mod = types.ModuleType('__main__')
 
-    # Set sys.path correctly.
-    old_path0 = sys.path[0]
-    sys.path[0] = path0 if path0 is not None else my_path0
+        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
+        if self.spec is not None:
+            main_mod.__spec__ = self.spec
 
-    try:
+        main_mod.__builtins__ = BUILTINS
+
+        sys.modules['__main__'] = main_mod
+
+        # Set sys.argv properly.
+        sys.argv = self.args
+
         try:
             # Make a code object somehow.
-            if filename.endswith((".pyc", ".pyo")):
-                code = make_code_from_pyc(filename)
+            if from_pyc:
+                code = make_code_from_pyc(self.arg0)
             else:
-                code = make_code_from_py(filename)
+                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=filename, exc=exc))
+            msg = "Couldn't run '{filename}' as Python code: {exc.__class__.__name__}: {exc}"
+            raise CoverageException(msg.format(filename=self.arg0, exc=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:
+        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
@@ -213,7 +268,7 @@
                 if hasattr(err, "__traceback__"):
                     err.__traceback__ = err.__traceback__.tb_next
                 sys.excepthook(typ, err, tb.tb_next)
-            except SystemExit:
+            except SystemExit:                      # pylint: disable=try-except-raise
                 raise
             except Exception:
                 # Getting the output right in the case of excepthook
@@ -228,12 +283,37 @@
                 raise ExceptionDuringRun(typ, err, tb.tb_next)
             else:
                 sys.exit(1)
+        finally:
+            os.chdir(cwd)
 
-    finally:
-        # Restore the old __main__, argv, and path.
-        sys.modules['__main__'] = old_main_mod
-        sys.argv = old_argv
-        sys.path[0] = old_path0
+
+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.
+
+    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):
+    """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):
@@ -260,10 +340,10 @@
         # 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")
+            raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER))
 
         date_based = True
-        if sys.version_info >= (3, 7, 0, 'alpha', 4):
+        if env.PYBEHAVIOR.hashed_pyc_pep552:
             flags = struct.unpack('<L', fpyc.read(4))[0]
             hash_based = flags & 0x01
             if hash_based:
@@ -272,7 +352,7 @@
         if date_based:
             # Skip the junk in the header that we don't need.
             fpyc.read(4)            # Skip the moddate.
-            if sys.version_info >= (3, 3):
+            if env.PYBEHAVIOR.size_in_pyc:
                 # 3.3 added another long to the header (size), skip it.
                 fpyc.read(4)
 

eric ide

mercurial