diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2013-09-28 11:07:41 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2013-09-28 11:07:41 -0400 |
commit | aafd82cc752bb16fe217656a2cae4e531cfc611f (patch) | |
tree | b7f665d5b7dea9449490bd4ab4aa26419c73e1ae | |
parent | 5e5cf2d5b9d7decfce16142a7cf7cc140fcbf354 (diff) | |
download | python-coveragepy-git-aafd82cc752bb16fe217656a2cae4e531cfc611f.tar.gz |
Now we can run .pyc files directly. Closes #264.
-rw-r--r-- | CHANGES.txt | 3 | ||||
-rw-r--r-- | coverage/execfile.py | 72 | ||||
-rw-r--r-- | coverage/misc.py | 4 | ||||
-rw-r--r-- | tests/test_execfile.py | 59 |
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.""" |