diff options
-rw-r--r-- | CHANGES.rst | 7 | ||||
-rw-r--r-- | coverage/execfile.py | 55 | ||||
-rw-r--r-- | tests/modules/process_test/try_execfile.py | 15 | ||||
-rw-r--r-- | tests/test_process.py | 33 |
4 files changed, 86 insertions, 24 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 154ca78d..af1e5788 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,10 +24,17 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. Unreleased ---------- +- Python files run with ``-m`` now have ``__spec__`` defined properly. This + fixes `issue 745`_ (about not being able to run unittest tests that spawn + subprocesses), and `issue 838`_, which described the problem directly. + - The :func:`.coverage.numbits.register_sqlite_functions` function now also registers `numbits_to_nums` for use in SQLite queries. Thanks, Simon Willison. +.. _issue 745: https://github.com/nedbat/coveragepy/issues/745 +.. _issue 838: https://github.com/nedbat/coveragepy/issues/838 + .. _changes_50b1: diff --git a/coverage/execfile.py b/coverage/execfile.py index 828df68e..3096138f 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -33,8 +33,8 @@ 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. + Returns the file path of the module, the name of the enclosing + package, and the spec. """ try: spec = importlib_util_find_spec(modulename) @@ -44,7 +44,7 @@ if importlib_util_find_spec: 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: @@ -56,13 +56,13 @@ if importlib_util_find_spec: 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() @@ -98,7 +98,7 @@ else: if openfile: openfile.close() - return pathname, packagename + return pathname, packagename, None class PyRunner(object): @@ -112,7 +112,7 @@ class PyRunner(object): self.as_module = as_module self.arg0 = args[0] - self.package = self.modulename = self.pathname = None + self.package = self.modulename = self.pathname = self.loader = self.spec = None def prepare(self): """Do initial preparation to run Python code. @@ -131,7 +131,10 @@ class PyRunner(object): sys.path[0] = path0 should_update_sys_path = False self.modulename = self.arg0 - pathname, self.package = find_module(self.modulename) + 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): @@ -145,10 +148,26 @@ class PyRunner(object): break else: raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) + + if env.PY2: + self.arg0 = os.path.abspath(self.arg0) + + # Make a spec. I don't know if this is the right way to do it. + try: + import importlib.machinery + except ImportError: + pass + else: + self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) + self.spec.has_location = True + self.package = "" + self.loader = DummyLoader("__main__") else: path0 = os.path.abspath(os.path.dirname(self.arg0)) + if env.PY3: + self.loader = DummyLoader("__main__") - if self.modulename is None and env.PYVERSION >= (3, 3): + if self.modulename is None: self.modulename = '__main__' if should_update_sys_path: @@ -167,21 +186,27 @@ class PyRunner(object): # Create a module to serve as __main__ main_mod = types.ModuleType('__main__') - sys.modules['__main__'] = main_mod + + from_pyc = self.arg0.endswith((".pyc", ".pyo")) main_mod.__file__ = self.arg0 - if self.package: + if from_pyc: + main_mod.__file__ = main_mod.__file__[:-1] + if self.package is not None: main_mod.__package__ = self.package - if self.modulename: - main_mod.__loader__ = DummyLoader(self.modulename) + main_mod.__loader__ = self.loader + if self.spec is not None: + main_mod.__spec__ = self.spec main_mod.__builtins__ = BUILTINS + sys.modules['__main__'] = main_mod + # Set sys.argv properly. sys.argv = self.args try: # Make a code object somehow. - if self.arg0.endswith((".pyc", ".pyo")): + if from_pyc: code = make_code_from_pyc(self.arg0) else: code = make_code_from_py(self.arg0) diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py index 706fe39f..48f9d098 100644 --- a/tests/modules/process_test/try_execfile.py +++ b/tests/modules/process_test/try_execfile.py @@ -66,7 +66,7 @@ def my_function(a): FN_VAL = my_function("fooey") loader = globals().get('__loader__') -fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None) +spec = globals().get('__spec__') # A more compact ad-hoc grouped-by-first-letter list of builtins. CLUMPS = "ABC,DEF,GHI,JKLMN,OPQR,ST,U,VWXYZ_,ab,cd,efg,hij,lmno,pqr,stuvwxyz".split(",") @@ -88,8 +88,8 @@ globals_to_check = { '__builtins__.has_open': hasattr(__builtins__, 'open'), '__builtins__.dir': builtin_dir, '__loader__ exists': loader is not None, - '__loader__.fullname': fullname, '__package__': __package__, + '__spec__ exists': spec is not None, 'DATA': DATA, 'FN_VAL': FN_VAL, '__main__.DATA': getattr(__main__, "DATA", "nothing"), @@ -98,4 +98,15 @@ globals_to_check = { 'path': cleaned_sys_path, } +if loader is not None: + globals_to_check.update({ + '__loader__.fullname': getattr(loader, 'fullname', None) or getattr(loader, 'name', None) + }) + +if spec is not None: + globals_to_check.update({ + '__spec__.' + aname: getattr(spec, aname) + for aname in ['name', 'origin', 'submodule_search_locations', 'parent', 'has_location'] + }) + print(json.dumps(globals_to_check, indent=4, sort_keys=True)) diff --git a/tests/test_process.py b/tests/test_process.py index 1579ec5e..06e429dd 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -866,17 +866,36 @@ class EnvironmentTest(CoverageTest): expected = self.run_command("python with_main") actual = self.run_command("coverage run with_main") - # The coverage.py results are not identical to the Python results, and - # I don't know why. For now, ignore those failures. If someone finds - # a real problem with the discrepancies, we can work on it some more. - ignored = r"__file__|__loader__|__package__" # PyPy includes the current directory in the path when running a # directory, while CPython and coverage.py do not. Exclude that from # the comparison also... if env.PYPY: - ignored += "|"+re.escape(os.getcwd()) - expected = re_lines(expected, ignored, match=False) - actual = re_lines(actual, ignored, match=False) + ignored = re.escape(os.getcwd()) + expected = re_lines(expected, ignored, match=False) + actual = re_lines(actual, ignored, match=False) + self.assert_tryexecfile_output(expected, actual) + + def test_coverage_run_dashm_dir_no_init_is_like_python(self): + with open(TRY_EXECFILE) as f: + self.make_file("with_main/__main__.py", f.read()) + + expected = self.run_command("python -m with_main") + actual = self.run_command("coverage run -m with_main") + if env.PY2: + assert expected.endswith("No module named with_main\n") + assert actual.endswith("No module named with_main\n") + else: + self.assert_tryexecfile_output(expected, actual) + + def test_coverage_run_dashm_dir_with_init_is_like_python(self): + if env.PY2: + self.skipTest("Python 2 runs __main__ twice, I can't be bothered to make it work.") + with open(TRY_EXECFILE) as f: + self.make_file("with_main/__main__.py", f.read()) + self.make_file("with_main/__init__.py", "") + + expected = self.run_command("python -m with_main") + actual = self.run_command("coverage run -m with_main") self.assert_tryexecfile_output(expected, actual) def test_coverage_run_dashm_equal_to_doubledashsource(self): |