summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt3
-rw-r--r--coverage/execfile.py72
-rw-r--r--coverage/misc.py4
-rw-r--r--tests/test_execfile.py59
4 files changed, 117 insertions, 21 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index bff92145..829ad02f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -13,6 +13,8 @@ Change history for Coverage.py
- Running code with ``coverage run -m`` now behaves more like Python does,
setting sys.path properly, which fixes `issue 207`_ and `issue 242`_.
+- Coverage can now run .pyc files directly, closing `issue 264`_.
+
- Omitting files within a tree specified with the ``source`` option would
cause them to be incorrectly marked as unexecuted, as described in
`issue 218`_. This is now fixed.
@@ -41,6 +43,7 @@ Change history for Coverage.py
.. _issue 242: https://bitbucket.org/ned/coveragepy/issue/242/running-a-two-level-package-doesnt-work
.. _issue 218: https://bitbucket.org/ned/coveragepy/issue/218/run-command-does-not-respect-the-omit-flag
.. _issue 255: https://bitbucket.org/ned/coveragepy/issue/255/directory-level-__main__py-not-included-in
+.. _issue 264: https://bitbucket.org/ned/coveragepy/issue/264/coverage-wont-run-pyc-files
.. _issue 267: https://bitbucket.org/ned/coveragepy/issue/267/relative-path-aliases-dont-work
diff --git a/coverage/execfile.py b/coverage/execfile.py
index fbb49b2a..2e892903 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -1,9 +1,9 @@
"""Execute files of Python code."""
-import imp, os, sys
+import imp, marshal, os, sys
from coverage.backward import exec_code_object, open_source
-from coverage.misc import NoSource, ExceptionDuringRun
+from coverage.misc import ExceptionDuringRun, NoCode, NoSource
try:
@@ -93,24 +93,13 @@ def run_python_file(filename, args, package=None):
sys.argv = args
try:
- # Open the source file.
- try:
- source_file = open_source(filename)
- except IOError:
- raise NoSource("No file to run: %r" % filename)
-
- try:
- source = source_file.read()
- finally:
- source_file.close()
-
- # We have the source. `compile` still needs the last line to be clean,
- # so make sure it is, then compile a code object from it.
- if not source or source[-1] != '\n':
- source += '\n'
- code = compile(source, filename, "exec")
+ # Make a code object somehow.
+ if filename.endswith(".pyc") or filename.endswith(".pyo"):
+ code = make_code_from_pyc(filename)
+ else:
+ code = make_code_from_py(filename)
- # Execute the source file.
+ # Execute the code object.
try:
exec_code_object(code, main_mod.__dict__)
except SystemExit:
@@ -131,3 +120,48 @@ def run_python_file(filename, args, package=None):
# Restore the old argv and path
sys.argv = old_argv
+
+def make_code_from_py(filename):
+ """Get source from `filename` and make a code object of it."""
+ # Open the source file.
+ try:
+ source_file = open_source(filename)
+ except IOError:
+ raise NoSource("No file to run: %r" % filename)
+
+ try:
+ source = source_file.read()
+ finally:
+ source_file.close()
+
+ # We have the source. `compile` still needs the last line to be clean,
+ # so make sure it is, then compile a code object from it.
+ if not source or source[-1] != '\n':
+ source += '\n'
+ code = compile(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: %r" % filename)
+
+ # 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 != imp.get_magic():
+ raise NoCode("Bad magic number in .pyc file")
+
+ # Skip the junk in the header that we don't need.
+ fpyc.read(4) # Skip the moddate.
+ if sys.version_info >= (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
diff --git a/coverage/misc.py b/coverage/misc.py
index 473d7d43..2d2662da 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -146,6 +146,10 @@ class NoSource(CoverageException):
"""We couldn't find the source for a module."""
pass
+class NoCode(NoSource):
+ """We couldn't find any code at all."""
+ pass
+
class NotPython(CoverageException):
"""A source file turned out not to be parsable Python."""
pass
diff --git a/tests/test_execfile.py b/tests/test_execfile.py
index 7da2854d..24c521bd 100644
--- a/tests/test_execfile.py
+++ b/tests/test_execfile.py
@@ -1,9 +1,10 @@
"""Tests for coverage.execfile"""
-import os, sys
+import compileall, os, re, sys
+from coverage.backward import binary_bytes
from coverage.execfile import run_python_file, run_python_module
-from coverage.misc import NoSource
+from coverage.misc import NoCode, NoSource
from tests.coveragetest import CoverageTest
@@ -77,6 +78,60 @@ class RunFileTest(CoverageTest):
self.assertRaises(NoSource, run_python_file, "xyzzy.py", [])
+class RunPycFileTest(CoverageTest):
+ """Test cases for `run_python_file`."""
+
+ def make_pyc(self):
+ """Create a .pyc file, and return the relative path to it."""
+ self.make_file("compiled.py", """\
+ def doit():
+ print("I am here!")
+
+ doit()
+ """)
+ compileall.compile_dir(".", quiet=True)
+ os.remove("compiled.py")
+
+ # Find the .pyc file!
+ for there, _, files in os.walk("."):
+ for f in files:
+ if f.endswith(".pyc"):
+ return os.path.join(there, f)
+
+ def test_running_pyc(self):
+ pycfile = self.make_pyc()
+ run_python_file(pycfile, [pycfile])
+ self.assertEqual(self.stdout(), "I am here!\n")
+
+ def test_running_pyo(self):
+ pycfile = self.make_pyc()
+ pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile)
+ self.assertNotEqual(pycfile, pyofile)
+ os.rename(pycfile, pyofile)
+ run_python_file(pyofile, [pyofile])
+ self.assertEqual(self.stdout(), "I am here!\n")
+
+ def test_running_pyc_from_wrong_python(self):
+ pycfile = self.make_pyc()
+
+ # Jam Python 2.1 magic number into the .pyc file.
+ fpyc = open(pycfile, "r+b")
+ fpyc.seek(0)
+ fpyc.write(binary_bytes([0x2a, 0xeb, 0x0d, 0x0a]))
+ fpyc.close()
+
+ self.assertRaisesRegexp(
+ NoCode, "Bad magic number in .pyc file",
+ run_python_file, pycfile, [pycfile]
+ )
+
+ def test_no_such_pyc_file(self):
+ self.assertRaisesRegexp(
+ NoCode, "No file to run: 'xyzzy.pyc'",
+ run_python_file, "xyzzy.pyc", []
+ )
+
+
class RunModuleTest(CoverageTest):
"""Test run_python_module."""