Coverage.py is a Python module that measures code coverage during Python execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. The original version was written by Gareth Rees. I've updated it to determine executable statements more accurately.

Installation

To install coverage, unpack the tar file, and run "setup.py install", or use "easy_install coverage".

Download: coverage-2.80.tar.gz

You will have a coverage module for importing, and a coverage command for command line execution.

There is also a set of tests for coverage: test_coverage.py and coverage_coverage.py. These will only be of interest if you want to modify coverage.py.

Command-line usage

The command line interface hasn't changed from the original version. All of the original instructions still hold. Read that document if you haven't used coverage.py before. Here I'll describe the changes I've introduced.

The identification of executable statements is now more accurate. Docstrings are not flagged as missing statements, nor are "global" statements. Complex multi-line if and elif conditions are handled properly.

Statements can now be excluded from consideration. This is useful if you have lines of code that you know will not be executed, and you don't want the coverage report to be filled with their noise. For example, you may have interactive test clauses at the ends of your modules that your test suite will not execute:

#.. all the real code ..

if __name__ == '__main__':
    # Run the test code from the command-line, for convenience
    blah.run('sample')

This suite of code can be excluded from the coverage report by adding a specially-formed comment:

#.. all the real code ..

if __name__ == '__main__':   #pragma: no cover
    # Run the test code from the command-line, for convenience
    blah.run('sample')

The #pragma line can be placed on any line of code. If the line contains the colon that introduces a suite of statements, the entire suite is excluded. Annotated files (created with "coverage -a") will prefix excluded lines with "-".

Programmatic usage

Again, the original instructions still hold, but I've made a few additions.

The form of the exclusion marker can be changed using the exclude() method. It takes a regular expression, and excludes any line that contains a match:

# Don't count branches that will never execute
coverage.exclude('if super_debug:')

As the example shows, the marker pattern doesn't have to be a comment. Any number of regular expressions can be added by calling exclude a number of times.

The use of a file to hold coverage data can be suppressed by calling use_cache(0) before calling start().

The analysis(module) function still returns a 4-tuple (filename, statement list, missing list, missing string). A new analysis2(module) function extends the return to a 5-tuple (filename, statement list, excluded list, missing list, missing string).

The annotate(modules) function is available to annotate a list of modules, and the annotate_file(filename, statements, excluded, missing) function provides the bulk of annotation in a directly callable form.

Known Problems

Older versions of doctest interfere with coverage's tracing of statements, and you may get reports that none of your code is executing. Use this patch to doctest.py if you are experiencing problems.

History

Changes I've made over time:

Version 2.80, May 25 2008

  • Coverage.py is now installed as an egg, making integration with nose smoother. If you had an older version of coverage, remove the old coverage.py in the command directory (for example, /usr/bin or \Python25\Scripts).
  • Source files are opened in rU mode, preventing problems with errant line endings.

Version 2.78, September 30 2007

  • Better handling of Python source in files that don't end with .py. Thanks, Ben Finney.

Version 2.77, July 29 2007

  • Better packaging, including Cheeseshop goodness.

Version 2.76, July 23 2007

  • Added support for the overlooked "with" statement in Python 2.5.

Version 2.75, July 22 2007

  • The way multi-line statements are handled has been revamped, allowing coverage.py to support Python 2.5.
  • Functions with just a docstring and a pass statement no longer report the pass as uncovered.

Version 2.7, July 21 2007

  • The #pragma:nocover comment syntax is ignored by default, so programmatic invocations of coverage also attend to those declarations.
  • Constants in the middle of functions are properly ignored, since they won't be executed.
  • Code exec'ed from strings no longer clutters reports with exceptions. That code will be ignored by coverage.py, since we can't get the source code to analyze it anyway.
  • Minor fixes: Linux current directory handling (thanks Guillaume Chazarain), globbing for Windows (thanks Noel O'Boyle), and Python 2.2 compatibility (thanks Catherine Proulx).

Version 2.6, August 22 2006

  • Function decorators are now handled properly (thanks Joseph Tate).
  • Fixed a few bugs with the --omit option (thanks Mark van der Wal and Sigve Tjora)
  • Coverage data files can be written from several processes at once with the -p and -c options (thanks Geoff Bache).

Version 2.5, December 4 2005

  • Multi-threaded programs now have all threads properly measured (thanks Martin Fuzzey).
  • The report() method now takes an optional file argument which defaults to stdout.
  • Adapted Greg Rogers' patch to allow omitting files by directory from the report and annotation, sorting files, and reporting files relatively.
  • coverage.py can now recursively measure itself under test!

Version 2.2, December 31 2004

  • Made it possible to use keyword arguments with the module global functions (thanks Allen).

Version 2.1, December 14 2004

  • Fix some backward-compatibility problems with the analysis function.
  • Refactor annotate to provide annotate_file.

Version 2, December 12 2004

  • My first version.

Problems

Coverage.py has been tested successfully on Pythons 2.2.3, 2.3.5, 2.4.3, 2.5.1 and 2.6a3. If you have code that it doesn't handle properly, send it to me! Be sure to mention the version of Python you are using.

See also

  • Gareth Rees's original page about the design of coverage.py
  • Sancho is a unit testing framework that includes code coverage measurement.
  • pycover, another Python implementation of code coverage.
  • The trace module in the Python standard library, which appears to be undocumented.
  • My blog, where topics often include those of interest to both casual and serious Python users.

Comments

[gravatar]
Max Ischenko 4:39 AM on 13 Dec 2004

Nice work, Ned, thanks a lot. That was something I had wanted to do for myself but never managed to.

There is minor incompatibility with original API which IMO should be clearly noted/fixed.
1. analysis() now returns tuples of length 5 (was 4) with the extra parameter being explicitly-excluded lines I guess.
2. There is no more coverage.annotate global (this is probably just an oversight?).

Overall, it works fine. When compared with original version, it correctly identified about 150 lines that were previously marked as executable, giving a nice raise of 4% in total coverage. ;-)

[gravatar]
Max Ischenko 4:52 AM on 13 Dec 2004

Btw, while we are on API issues, can I ask for a small change. Could I suggest to extract body of annotate(morfs) into annotate1(filename, statements, excluded, missing) so I could call the latter directly?

[gravatar]
Ned Batchelder 6:33 AM on 14 Dec 2004

Your wishes are my commands. I've updated the source and page above.

The original coverage.py that I have didn't include a global for annotate, but now it's there.

[gravatar]
Max Ischenko 4:20 AM on 15 Dec 2004

Thanks a lot!

[gravatar]
Martin Fuzzey 3:18 PM on 16 Dec 2004

Hi,
Your new improved coverage looks great.

One problem I had was with multithreaded code - only calls from the main thread are traced.
Looking at the python documentation for sys.settrace() explained why but this may not be obvious to everyone using coverage.

Calling threading.settrace(coverage.t) fixes this but maybe coverage should do this itself?

Another little suggestion, how about passing a file like object to report() rather than using print statements - handy if you want to put the results in a file.

Martin

[gravatar]
Allen 7:04 PM on 30 Dec 2004

There seems to be a bug with keyword handling in the module functions. I think they all need a **kw argument. like:

# Module functions call methods in the singleton object.
def use_cache(*args,**kw): return the_coverage.use_cache(*args,**kw)
def start(*args,**kw): return the_coverage.start(*args,**kw)
def stop(*args,**kw): return the_coverage.stop(*args,**kw)
def erase(*args,**kw): return the_coverage.erase(*args,**kw)
def exclude(*args,**kw): return the_coverage.exclude(*args,**kw)
def analysis(*args,**kw): return the_coverage.analysis(*args,**kw)
def analysis2(*args,**kw): return the_coverage.analysis2(*args,**kw)
def report(*args,**kw): return the_coverage.report(*args,**kw)
def annotate(*args,**kw): return the_coverage.annotate(*args,**kw)
def annotate_file(*args,**kw): return the_coverage.annotate_file(*args,**kw)

[gravatar]
Ned Batchelder 2:42 PM on 31 Dec 2004

Thanks, now they all have them.

[gravatar]
Greg Rogers 10:51 AM on 6 Jan 2005

I've added three features:
1) include the relative or full path of each file in the report output.
2) sort the output by filename
3) -omit dir,dir2,...
* omits from report and annonotation files that have an path beginning with dir,dir2, ...

Here's the svn diff output:
Index: coverage.py
===================================================================
--- coverage.py (revision 4383)
+++ coverage.py (working copy)
@@ -41,6 +41,11 @@
the -d option, make the copies in that directory. Without the -d
option, make each copy in the same directory as the original.

+-o dir,dir2,...
+ Omit reporting or annotating files when their filename path starts with
+ a directory listed in the omit list.
+ e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits,lib\enthought\logging,lib\enthought\util
+
Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else."""

@@ -289,6 +294,7 @@
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
+ '-o:': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()
@@ -339,10 +345,20 @@
ignore_errors = settings.get('ignore-errors')
show_missing = settings.get('show-missing')
directory = settings.get('directory=')
+
+ omit = settings.get('omit=')
+ if omit is not None :
+ omit = omit.split(',')
+ else:
+ omit = []
+
+ self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+"\\")
+ self.relative_dir = self.relative_dir.replace("\\\\", "\\")
+
if settings.get('report'):
- self.report(args, show_missing, ignore_errors)
+ self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
if settings.get('annotate'):
- self.annotate(args, directory, ignore_errors)
+ self.annotate(args, directory, ignore_errors, omit_prefixes=omit)

def use_cache(self, usecache):
self.usecache = usecache
@@ -588,15 +604,43 @@
return (filename, statements, excluded, missing,
self.format_lines(statements, missing))

+ def relative_filename( self, filename ) :
+ """ convert filename to relative filename from self.relative_dir """
+ relfilename = filename.replace( self.relative_dir, "")
+ return relfilename
+
def morf_name(self, morf):
+ """ name of morf as used in report """
if isinstance(morf, types.ModuleType):
return morf.__name__
else:
- return os.path.splitext(os.path.basename(morf))[0]
+ return self.relative_filename( os.path.normcase(morf) )

- def report(self, morfs, show_missing=1, ignore_errors=0):
+ def filter_by_prefix( self, morfs, omit_prefixes ) :
+ """ return list of morfs where the morf name does not begin
+ with any one of the omit_prefixes
+ """
+ filtered_morfs = []
+ for morf in morfs :
+ for prefix in omit_prefixes :
+ if self.morf_name(morf).startswith(prefix) :
+ break
+ else :
+ filtered_morfs.append(morf)
+
+ return filtered_morfs
+
+ def morf_name_compare( self, x, y ) :
+ return cmp( self.morf_name(x), self.morf_name(y) )
+
+ def report(self, morfs, show_missing=1, ignore_errors=0, omit_prefixes=[]):
if not isinstance(morfs, types.ListType):
morfs = [morfs]
+
+ morfs = self.filter_by_prefix( morfs, omit_prefixes)
+
+ morfs.sort( self.morf_name_compare )
+
max_name = max([5,] + map(len, map(self.morf_name, morfs)))
fmt_name = "%%- %ds " % max_name
fmt_err = fmt_name + "%s: %s"
@@ -612,7 +656,7 @@
for morf in morfs:
name = self.morf_name(morf)
try:
- _, statements, _, missing, readable = self.analysis2(morf)
+ filename, statements, _, missing, readable = self.analysis2(morf)
n = len(statements)
m = n - len(missing)
if n > 0:
@@ -647,7 +691,8 @@
blank_re = re.compile("\\s*(#|$)")
else_re = re.compile("\\s*else\\s*:\\s*(#|$)")

- def annotate(self, morfs, directory=None, ignore_errors=0):
+ def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]):
+ morfs = self.filter_by_prefix( morfs, omit_prefixes)
for morf in morfs:
try:
filename, statements, excluded, missing, _ = self.analysis2(morf)

[gravatar]
Jay P 1:37 PM on 10 Jan 2005

Has anyone had any luck running this with doctest? I use some reasonably full featured doctest tests with my current project (with both doctest.testfile and doctest.testmod), but coverage.py doesn't seem to "see" any of the tests running.

It always reports that none of the code in my various modules was actually run.

[gravatar]
Ned Batchelder 3:03 PM on 10 Jan 2005

I don't use doctest, but I'd like to make coverage.py as useful as possible. Can you send me a runnable sample of what you are seeing?

[gravatar]
Jay P 3:53 PM on 10 Jan 2005

Alright, I emailed you a quick example of what I'm seeing. I didn't post it here because I never trust blogs and whitespace.

[gravatar]
Ned Batchelder 8:54 AM on 14 Jan 2005

Turns out the Zope svn repository already has a patch to doctest.py for this problem:

http://svn.zope.org/Zope3/trunk/src/zope/testing/doctest.py?view=diff&r1=28703&r2=28705

[gravatar]
Robert Brewer 4:29 PM on 6 Jul 2005

Thanks for the improvements, Ned!

I found I wanted full file names in my reports, so I modified morf_name a bit:

def morf_name(self, morf):
if isinstance(morf, types.ModuleType):
return morf.__file__
else:
return morf

Maybe that could be controlled via another flag (command-line arg)?

[gravatar]
hugo 8:12 AM on 1 Nov 2005

The original site seems gone - do you have copies of the original usage notes available?

[gravatar]
Ned Batchelder 9:45 PM on 2 Nov 2005

Hugo: thanks for noticing the broken links. I snarfed the pages from archive.org and have made local copies. The links above now point to those.

[gravatar]
Noah Spurrier 7:00 PM on 9 Nov 2005

I'm using "coverage" to start a test
suite script which in turn runs a bunch
of pyunit tests designed to exercise "pexpect.py". When I use the "-r" option I get a report like this:
Name Stmts Exec Cover
-------------------------------------
...
pexpect 511 367 71%
...

This doesn't look too bad. The problem
is when I try to use the "-a" option
I get a file "pexpect.py,cover" where
almost every line starts with "!". Only
the global doc string and imports show ">". Any idea what is going on?

This could be a great tool to use
with pyUnit testing!

Yours,
Noah

[gravatar]
Mark van der Wal 2:53 PM on 13 Feb 2006

I fixed a small bug in the 'omit' commandline option. It needed a ':' behind the 'o' for the getopt parameters.

The original patch had it, so it probably went missing somewhere.

regards,
Mark

*** coverage.py.original 2006-02-13 20:48:30.000000000 +0100
--- coverage.py 2006-02-13 20:45:29.000000000 +0100
***************
*** 299,305 ****
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
! '-o': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()
--- 299,305 ----
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
! '-o:': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()

[gravatar]
Drew Smathers 6:03 PM on 15 Apr 2006

Thanks Ned. Python really needs a set of tools to help convince the fuddy-wuds that test-driven development is more than possible in Python. IMO, coverage is an invaluable metric in verifying the efficacy of unit tests.

Just thought I'd pass this along, but I've created a simple module to play with annotations generated by coverage (basically generate XML and thus HTML throught XSLT). This is in the xix.utils.cover module of xix-utils:

$ svn co http://svn.xix.python-hosting.com/trunk XixUtils

The API is pretty simple:

>>> from xix.utils.cover import AnnotationParser, annotationToXML
>>> ann = AnnotationParser().parse('blah.py,cover')
>>> html = annotationToHTML(ann)

[gravatar]
Geoff Bache 2:29 PM on 9 May 2006

Hi Ned,

I had a go at generating coverage for my PyGTK GUI using this, but no statements were marked covered if they were covered from the GUI (i.e. via the user doing something). This made the coverage rather unusable.

I guess this is because GTK is C code underneath so control passes out of Python when the GUI main loop is entered.

Do you/anyone else have any bright ideas as to what to do about this?

[gravatar]
Geoff Bache 3:46 PM on 9 May 2006

Sorry, ignore the previous comment. I was confused. The problem was actually caused by running tests simultaneously.

[gravatar]
Sigve Tjora 9:16 AM on 19 May 2006

Hi, Ned,

It seems there are two errors with handling of --omit / -o options. One of the fixes is described above, the other is new, I think.

C:\Python24\Lib\site-packages>"c:\Program Files\GnuWin32\bin\diff.exe" coverage.
py.org coverage.py
302c302
< '-o': 'omit=',
---
> '-o:': 'omit=',
315c315
< settings[o[2:]] = a
---
> settings[o[2:] + "="] = a
352a353
>

[gravatar]
Steven Bethard 4:34 PM on 30 Jun 2006

This is great work -- thanks so much!

I did find a decorator case it's still having trouble with:

> class Decorator(object):
> def __init__(self, func):
> self.func = func
> def __call__(self, *args, **kwargs):
> return self.func(*args, **kwargs)

> class C(object):
> @Decorator
! def f(self):
> return 42

> if __name__ == '__main__':
> C.f(C())

[gravatar]
Geoff Bache 4:44 AM on 11 Jul 2006

I've added the feature of being able to handle collecting information from multiple processes at once. (My system has multiple components in Python and besides this I like to run my tests in parallel on a grid. Everything writing to the same file was rather a limitation - it meant I could only collect one component at once and had to run everything in series).

To achieve this, there are two new options:

1) "-p" in conjunction with -x will append the machine name and process ID to the file to write to, so that every process is guaranteed a unique file.

2) "-c" will read from all such files and merge their information into the parent coverage file.

Patch (from diff -c) follows:

*** coverage.py 2006-05-16 16:20:36.013313000 +0200
--- /usr/local/bin/coverage.py 2006-07-10 18:46:12.622698000 +0200
***************
*** 24,36 ****

"""Usage:

! coverage.py -x MODULE.py [ARG1 ARG2 ...]
Execute module, passing the given command-line arguments, collecting
! coverage data.

coverage.py -e
Erase collected coverage data.

coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
Report on the statement coverage for the given files. With the -m
option, show line numbers of the statements that weren't executed.
--- 24,41 ----

"""Usage:

! coverage.py -x MODULE.py [-p] [ARG1 ARG2 ...]
Execute module, passing the given command-line arguments, collecting
! coverage data. With the -p option, write to a temporary file containing
! the machine name and process ID.

coverage.py -e
Erase collected coverage data.

+ coverage.py -c
+ Collect data from multiple coverage files (as created by -p option above)
+ and store it into a single file representing the union of the coverage.
+
coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
Report on the statement coverage for the given files. With the -m
option, show line numbers of the statements that weren't executed.
***************
*** 59,64 ****
--- 64,70 ----
import sys
import threading
import types
+ from socket import gethostname

# 2. IMPLEMENTATION
#
***************
*** 292,302 ****
--- 298,310 ----
settings = {}
optmap = {
'-a': 'annotate',
+ '-c': 'collect',
'-d:': 'directory=',
'-e': 'erase',
'-h': 'help',
'-i': 'ignore-errors',
'-m': 'show-missing',
+ '-p': 'parallel-mode',
'-r': 'report',
'-x': 'execute',
'-o': 'omit=',
***************
*** 318,337 ****
if settings.get('help'):
self.help()
for i in ['erase', 'execute']:
! for j in ['annotate', 'report']:
if settings.get(i) and settings.get(j):
self.help("You can't specify the '%s' and '%s' "
"options at the same time." % (i, j))
args_needed = (settings.get('execute')
or settings.get('annotate')
! or settings.get('report'))
action = settings.get('erase') or args_needed
if not action:
! self.help("You must specify at least one of -e, -x, -r, or -a.")
if not args_needed and args:
self.help("Unexpected arguments %s." % args)

! self.get_ready()
self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')

if settings.get('erase'):
--- 326,346 ----
if settings.get('help'):
self.help()
for i in ['erase', 'execute']:
! for j in ['annotate', 'report', 'collect']:
if settings.get(i) and settings.get(j):
self.help("You can't specify the '%s' and '%s' "
"options at the same time." % (i, j))
args_needed = (settings.get('execute')
or settings.get('annotate')
! or settings.get('report')
! or settings.get('collect'))
action = settings.get('erase') or args_needed
if not action:
! self.help("You must specify at least one of -c, -e, -x, -r, or -a.")
if not args_needed and args:
self.help("Unexpected arguments %s." % args)

! self.get_ready(settings.get('parallel-mode'))
self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')

if settings.get('erase'):
***************
*** 344,349 ****
--- 353,360 ----
import __main__
sys.path[0] = os.path.dirname(sys.argv[0])
execfile(sys.argv[0], __main__.__dict__)
+ if settings.get('collect'):
+ self.collect()
if not args:
args = self.cexecuted.keys()
ignore_errors = settings.get('ignore-errors')
***************
*** 363,371 ****
def use_cache(self, usecache):
self.usecache = usecache

! def get_ready(self):
if self.usecache and not self.cache:
self.cache = os.environ.get(self.cache_env, self.cache_default)
self.restore()
self.analysis_cache = {}

--- 374,384 ----
def use_cache(self, usecache):
self.usecache = usecache

! def get_ready(self, parallel_mode=False):
if self.usecache and not self.cache:
self.cache = os.environ.get(self.cache_env, self.cache_default)
+ if parallel_mode:
+ self.cache += "." + gethostname() + "." + str(os.getpid())
self.restore()
self.analysis_cache = {}

***************
*** 421,437 ****
self.c = {}
self.cexecuted = {}
assert self.usecache
! if not os.path.exists(self.cache):
! return
try:
! cache = open(self.cache, 'rb')
import marshal
cexecuted = marshal.load(cache)
cache.close()
if isinstance(cexecuted, types.DictType):
! self.cexecuted = cexecuted
except:
! pass

# canonical_filename(filename). Return a canonical filename for the
# file (that is, an absolute path with no redundant components and
--- 434,478 ----
self.c = {}
self.cexecuted = {}
assert self.usecache
! if os.path.exists(self.cache):
! self.cexecuted = self.restore_file(self.cache)
!
! def restore_file(self, file_name):
try:
! cache = open(file_name, 'rb')
import marshal
cexecuted = marshal.load(cache)
cache.close()
if isinstance(cexecuted, types.DictType):
! return cexecuted
! else:
! return {}
except:
! return {}
!
! # collect(). Collect data in multiple files produced by parallel mode
!
! def collect(self):
! cache_dir, local = os.path.split(self.cache)
! for file in os.listdir(cache_dir):
! if not file.startswith(local):
! continue
!
! full_path = os.path.join(cache_dir, file)
! cexecuted = self.restore_file(full_path)
! self.merge_data(cexecuted)
!
! def merge_data(self, new_data):
! for file_name, file_data in new_data.items():
! if self.cexecuted.has_key(file_name):
! self.merge_file_data(self.cexecuted[file_name], file_data)
! else:
! self.cexecuted[file_name] = file_data
!

[gravatar]
Tim Leslie 7:31 AM on 5 Sep 2006

Hi There,

If I have code along the lines of:

x = 37
""" this is a docstring for the value x """

the docstring line will not be executed, so it does not get marked as covered, but when the parser goes to find "executable lines" for reporting, this comes up in the parser and gets treated as a normal, executable line of code.

I fixed this by changing visitDiscard to do nothing. I don't know if such a change would be considered a bug fix, but it certainly makes things play nice for me.

Cheers,

Tim

[gravatar]
Ned Batchelder 8:21 AM on 5 Sep 2006

Tim, that's a very unusual usage. Why not simply use a comment for the value x? Docstrings have a special place in Python because they are available as __doc__. Your "docstring" is inaccessible except to read in the source code. You may as well use a comment and save some space in your object files.

But I'll look into the change you suggest. It may help for a different case I've been looking into...

[gravatar]
Gardner Pomper 7:23 PM on 5 Sep 2006

Please help a very stupid person. I downloaded coverage.py, made it exectuable, added it to my site-packages and my path and tried the following on a piece of my code:

coverage.py -x dbauth.py

there seems to be no output anywhere. I get my programs prints to the console, but nothing from coverage and there are no files created in the current dir or in tmp.

What am I doing wrong?

[gravatar]
Gardner Pomper 7:26 PM on 5 Sep 2006

Ok, never mind.. I reread and found that it is stored in .coverage in the current dir by default, so it didn't show up in my ls -l. Sorry for the dumb question

[gravatar]
Ullysses Eoff 2:05 PM on 8 Sep 2006

I have a testsuite module that I run coverage.py on. The testsuite recursively searches a source directory and loads all the modules dynamically through the __import__ function. These modules are then added to a unitttest.testsuite. However, when a located module A imports B, and A is dynamically loaded first, B does not get proper coverage reported when it comes time to load and exercise it.

However, when I run the testsuite module on module B only with coverage, then coverage is reported properly.

What seems to be going wrong??

[gravatar]
Ullysses Eoff 9:43 PM on 8 Sep 2006

Found It!!!

psyco is the culprit!!

Not sure exactly what psyco is doing to break coverage.py. But, as soon as I removed it from my python path, coverage.py started to report proper results.

Can anyone explain why psyco causes this? or know how to fix this?

[gravatar]
Catherine Proulx 11:21 AM on 17 Oct 2006

Just a little note to let you know that your script does not work on 2.2.3 anymore due to the use of os.path.sep which was introduced sometime later.

I simply changed it to os.sep, and it works just fine now.

[gravatar]
Julie Wu 1:11 AM on 8 Dec 2006

Hi,

Your release notes for v.2.6 says 'Functional Decorator' are now handled correctly. But I still see the problem reported by Steven Bethard in his post.

Is that issue fixed?

Thanks,

Julie

[gravatar]
Todd O'Bryan 8:27 PM on 24 Dec 2006

Now that doctest is part of Python, how would one go about patching it? Also, is the version of doctest in 2.4.4 one of the older versions you mention as being incompatible?

Terrific tool!!!!
(And it's nice to see a name I recognize from the Django lists)
Todd

[gravatar]
Jon-Eric Simmons 1:30 PM on 24 Jan 2007

I like the '#pragma: no cover' feature but it appears to only work when coverage.py is invoked from the command line.

Might it be possible to have the pragma feature work consistently across both the command line and programmatic invocations?

[gravatar]
Noel O'Boyle 3:22 PM on 7 Feb 2007

The windows shell doesn't expand wildcards so scripts have to do it themselves with glob. Here's a patch against 2.6:
--- coverage_orig.py 2007-02-07 20:17:25.781250000 +0000
+++ coverage.py 2007-02-07 19:04:34.781250000 +0000
@@ -63,6 +63,7 @@
import string
import sys
import threading
+import glob
import types
from socket import gethostname

@@ -715,6 +716,10 @@
print >>file, "-" * len(header)
total_statements = 0
total_executed = 0
+ newmorfs = []
+ for morf in morfs:
+ newmorfs.extend(glob.glob(morf))
+ morfs = newmorfs
for morf in morfs:
name = self.morf_name(morf)
try:

[gravatar]
Guillaume Chazarain 4:48 PM on 16 Feb 2007

Hi Ned,

Thank you very much for this neat tool. And, btw here is a fix for the -c option. By default the cache path is '.coverage', with no '/' so os.path.split() puts an empty dirname and listdir chokes on it. At least on Linux with python 2.4.4.

--- coverage.py
+++ coverage.py
@@ -464,6 +464,8 @@

def collect(self):
cache_dir, local = os.path.split(self.cache)
+ if not cache_dir:
+ cache_dir = "."
for file in os.listdir(cache_dir):
if not file.startswith(local):
continue


Cheers.

[gravatar]
Pierre Gradit 4:15 AM on 19 Feb 2007

Very nice tool,
I was about to handle the problem, googleing just if someone... and one hour after it works on my computer in my real situation.

Not so frequent at this level of need/result fit.

Thanks.

[gravatar]
will 3:33 PM on 6 Dec 2007

I use nose and coverage and bumped into the issue described here:

http://nose.python-hosting.com/ticket/119

I didn't see anyone else mentioning the problem here, so I didn't know if you knew about it or not.

At the bottom of that bug link is a couple of attachments that might fix the issue. I haven't looked at them, though, so that's just a cursory guess.

If you want me to look into it further, let me know.

Thanks and rock on!

[gravatar]
Dominik Szopa 6:41 PM on 7 Dec 2007

Hi,
I have found bug in find_executable_statements method. Problem is when text contains a Windows endofline chars. Try following in Python interpreter:

import parser
tree = parser.suite('from os import path\r\n\n').totuple(1)

It will raise a SyntaxError exception.
If you will remove a '\r' end of line char everything will be ok.
Solution for this is remove all '\r' from text variable in the beginning of method find_executable_statements:

text = string.replace(text, '\r', '')

[gravatar]
Patrick Mezard 6:11 PM on 3 Jan 2008

Hello,

I have a setup where the modules being traced and coverage.py startup directory are in /tmp, which happens to be symlinked to /private/tmp under OSX. Two things happen:

1- relative_dir is computed from os.curdir and resolves to the real path. But modules canonalized paths still start with /tmp. I think they should be normalized to realpath()

2- If [1] is done, obviously the supplied omitted paths should be normalized as well with realpath()

Does it make any sense ?

diff a/tests/coverage.py b/tests/coverage.py
--- a/tests/coverage.py
+++ b/tests/coverage.py
@@ -412,6 +412,9 @@ class coverage:
else:
omit = []

+ omit = [os.path.normcase(os.path.abspath(os.path.realpath(p)))
+ for p in omit]
+
if settings.get('report'):
self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
if settings.get('annotate'):
@@ -537,7 +540,7 @@ class coverage:
if os.path.exists(g):
f = g
break
- cf = os.path.normcase(os.path.abspath(f))
+ cf = os.path.normcase(os.path.abspath(os.path.realpath(f)))
self.canonical_filename_cache[filename] = cf
return self.canonical_filename_cache[filename]

[gravatar]
Matt Boersma 5:42 PM on 13 Jan 2008

The python-nose test runner points out a possible bug in coverage.py: http://code.google.com/p/python-nose/issues/detail?id=90

The problem is that at run-time coverage receives an email.LazyImporter object that acts as a Module but doesn't subclass it, so it gets treated as a string, then throws an error since it doesn't have an "rfind" method.

This patch fixes it:
C:\projects\coverage-2.78>diff -w coverage.py C:\Python25\lib\site-packages\coverage-2.78-py2.5.egg\coverage.py
61d60
< import email
562c561
< if isinstance(morf, (types.ModuleType, email.LazyImporter)):
---
> if isinstance(morf, types.ModuleType):
782c781
< if isinstance(morf, (types.ModuleType, email.LazyImporter)):
---
> if isinstance(morf, types.ModuleType):

[gravatar]
Carl Meyer 1:27 PM on 30 Jan 2008

The doctest.py from Python 2.5 still has the problem (most code is reported as not executed), and the Zope patch linked does not apply cleanly to it. I applied the patch changes manually and it fixed the problem. Here's the unified diff:

--- doctest.py.orig 2008-01-30 13:21:17.000000000 -0500
+++ doctest.py 2008-01-30 13:26:16.000000000 -0500
@@ -317,8 +317,19 @@
"""
def __init__(self, out):
self.__out = out
+ self.__debugger_used = False
pdb.Pdb.__init__(self, stdout=out)

+ def set_trace(self):
+ self.__debugger_used = True
+ pdb.Pdb.set_trace(self)
+
+ def set_continue(self):
+ # Calling set_continue unconditionally would break unit test coverage
+ # reporting, as Bdb.set_continue calls sys.settrace(None).
+ if self.__debugger_used:
+ pdb.Pdb.set_continue(self)
+
def trace_dispatch(self, *args):
# Redirect stdout to the given stream.
save_stdout = sys.stdout

[gravatar]
Preeti Kohok 4:40 PM on 12 Feb 2008

I have installed Pydev plugin with Eclipse. I can get the coverage details within eclipse.
1. Is there a way to view the coverage results outside of eclipse.
2. Is is possible to hook the coverage start/stop with Ant.

[gravatar]
Edward Loper 7:07 PM on 15 Feb 2008

I'd recommend changing the line that reads the source code to use universal newlines; I encountered at least one file where coverage.py failed because the newline conventions were not the same as the local os:

- source = open(filename, 'rU')
+ source = open(filename, 'r')

[gravatar]
Edward Loper 7:13 PM on 15 Feb 2008

I couldn't find anything on this webpage or in the downloaded tarball that says what license (if any) this package is released under. Could you please let me know? Thanks.

[gravatar]
Edward Loper 1:22 PM on 17 Feb 2008

I found the license -- it's at the bottom of the 'coverage.py' source file. Thanks.

[gravatar]
Philip Jenvey 6:42 PM on 20 Feb 2008

The nosetests --with-coverage bug will reported earlier ( http://nose.python-hosting.com/ticket/119 ) is due to the fact that coverage's command line script runner is named coverage.py, which clashes with the coverage module itself.

This problem only creeps up when using nose because nose is ran with its 'nosetests' command line script, which resides in the same directory of the coverage.py command line script. Python adds any script it runs' working directory to sys.path -- so when running nosetests its 'import coverage' actually incorrectly imports the coverage script runner.

The only solution is for Ned to rename coverage's command line script to something else -- like 'coverage' as opposed to 'coverage.py' would work (like 'easy_install', 'paster' and 'nosetests')

[gravatar]
Jack Atkinson 3:05 AM on 15 Mar 2008

Here's a quick hack to get coverage 2.78 working with nosetests.

Edit the coverage.py script inside your python2x/scripts directory. Basically, you point it at the "coverage.py" two directories above the one it currently points to (EGGINFO/scripts/coverage.py). This worked on Windows for me.

#!c:\python25\python.exe
# EASY-INSTALL-SCRIPT: 'coverage==2.78','coverage.py'
__requires__ = 'coverage==2.78'
import pkg_resources
pkg_resources.run_script('coverage==2.78', '..\..\coverage.py')

[gravatar]
Ben Hudson 11:12 AM on 10 Apr 2008

I might be missing something pretty basic here, but when you have the .coverage file how do you analyse the results. When I open it up in notepad I get unreadable information,.

[gravatar]
Ned Batchelder 6:47 AM on 18 Apr 2008

@Ben: you need to run coverage -r to interpret and report on the .coverage file.

[gravatar]
Ben Hudson 9:00 AM on 24 Apr 2008

Thanks for your help.

One more thing. Is there anyway I can produce the annotated files (displaying the ! against lines of code that have not been exectuted). I know how to do this through a batch file but can not do it through python directly. Can this be done?

[gravatar]
Ben Hudson 9:15 AM on 24 Apr 2008

Dont worry. I have figured it out!

[gravatar]
Benjamin Kampmann 5:29 AM on 15 May 2008

Note that there is a package for this module for ubuntu since gutsy:
http://packages.ubuntu.com/search?suite=default§ion=all&arch=any&searchon=names&keywords=python-coverage

[gravatar]
Michael McNeil Forbes 8:41 PM on 29 Jun 2008

There seems to be a problem with #pragma: no cover and empty suites (containing only pass)

$ more bug.py
def f():
if False: # pragma: no cover
pass # This line still reported as missing
if False: # pragma: no cover
x = 1 # Now it is skipped.

def test():
f()

test()

$ coverage -e
$ coverage -x bug.py
$ coverage -rm bug.py
Name Stmts Exec Cover Missing
-------------------------------------
bug 5 4 80% 3
$ python
Python 2.5.2 (r252:60911, Mar 21 2008, 23:32:43)
[GCC 4.0.1 (Apple Computer, Inc. build 5363)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import coverage
>>> coverage.__version__
'2.80.20080525'
>>>

[gravatar]
Geoff Bache 3:30 AM on 27 Aug 2008

Hi,

Here is an additional enhancement for the -c option. The basic issue is what to do when trying to get unified coverage from tests run on UNIX and tests run on Windows. You need to be able to recognise that /home/geoff/some/path/file.py and h:\some\path\file.py are the same file and unify coverage information for them.

So the changes will now try to canonicalize the filenames on reading in when running with -c (instead of just when writing as with the other options), and I also added some hacks so that Windows paths can be recognised when running coverage.py on UNIX.

Regards,
Geoff Bache

*** /users/geoff/work/downloads/coverage-2.80/coverage.py 2008-05-25 23:32:12.000000000 +0200
--- /users/geoff/bin/coverage.py 2008-08-26 17:25:33.898334000 +0200
***************
*** 507,513 ****

full_path = os.path.join(cache_dir, f)
cexecuted = self.restore_file(full_path)
! self.merge_data(cexecuted)

def merge_data(self, new_data):
for file_name, file_data in new_data.items():
--- 507,524 ----

full_path = os.path.join(cache_dir, f)
cexecuted = self.restore_file(full_path)
! # When collecting, we expect many different sources
! # which may be different platforms and refer to the same path names
! # in different ways. We standardise around our current platform
! canonical_cexecuted = self.canonicalize_input(cexecuted)
! self.merge_data(canonical_cexecuted)
!
! def canonicalize_input(self, cexecuted):
! result = {}
! for key, value in cexecuted.items():
! canonical_key = self.canonical_filename(key)
! result[canonical_key] = value
! return result

def merge_data(self, new_data):
for file_name, file_data in new_data.items():
***************
*** 528,535 ****
def canonical_filename(self, filename):
if not self.canonical_filename_cache.has_key(filename):
f = filename
! if os.path.isabs(f) and not os.path.exists(f):
! f = os.path.basename(f)
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
g = os.path.join(path, f)
--- 539,552 ----
def canonical_filename(self, filename):
if not self.canonical_filename_cache.has_key(filename):
f = filename
! if not os.path.exists(f):
! if os.path.isabs(f):
! f = os.path.basename(f)
! # If we're on UNIX, we can't spot an absolute Windows path with os.path.isabs.
! # (The converse isn't true)
! # So check if it contains the Windows file separator
! elif os.name == "posix" and f.find("\\") != -1:
! f = os.path.basename(f.replace("\\", "/"))
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
g = os.path.join(path, f)

[gravatar]
Skip Montanaro 12:31 PM on 9 Sep 2008

Ned,

I just installed coverage 2.80 from PyPI to use the
nosetests --with-coverage option. At the end it complains
about a file in an egg:

magnitude coverage.CoverageException: No source for compiled code '/home/tuba/skipm/src/tl-filters/python/modules/snake/magnitude.pyc'.

The magnitude module is installed as an egg:

% python
iPython 2.4.5 (#4, Apr 12 2008, 09:09:16)
[GCC 3.4.1] on sunos5
Type "help", "copyright", "credits" or "license" for more information.
>>> import magnitude
>>> magnitude.__file__
% unzip -t /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg
Archive: /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg
testing: magnitude.py OK
testing: magnitude.pyc OK
testing: EGG-INFO/PKG-INFO OK
testing: EGG-INFO/SOURCES.txt OK
testing: EGG-INFO/dependency_links.txt OK
testing: EGG-INFO/top_level.txt OK
testing: EGG-INFO/zip-safe OK
testing: EGG-INFO/scripts/mgload.py OK
No errors detected in compressed data of /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg.

Is this an issue with coverage or nose? Is there a workaround?

Thanks,

Skip Montanaro

[gravatar]
Yann 1:57 PM on 14 Sep 2008

Hi,

In my *,cover file every 'class' or 'def' line is marked with a '!'; is that a normal thing ?

[gravatar]
Ned Batchelder 2:08 PM on 14 Sep 2008

Yann, that probably means that you imported your module before starting the coverage module. The class and def lines are executed when the module is imported, not when they are called, so you have to make sure to begin the coverage analysis before importing your module.

[gravatar]
Yann 2:41 PM on 14 Sep 2008

Thanks Ned,
Maybe I should go to bed ^^.

Thanks for this module, it helps a lot.

[gravatar]
Doug Latornell 7:42 PM on 16 Sep 2008

Thanks for a great tool, Ned.

I've come across a code case that seems to cause trouble for coverage though: http://paste.turbogears.org/paste/7081 This is with Python 2.5.2.

It's distilled, with some help from the SQLAlchemy list (http://groups.google.com/group/sqlalchemy/browse_thread/thread/66eea947fdb79b81), from an issue that was raised re: coverage applied to TurboGears 2 projects: http://groups.google.com/group/turbogears/browse_thread/thread/7fd3639a5a4d4b8c

Do you have any suggestions?

Add a comment:

name
email
Ignore this:
not displayed and no spam.
Leave this empty:
www
not searched.
 
Name and either email or www are required.
Don't put anything here:
Leave this empty:
URLs auto-link and some tags are allowed: <a><b><i><p><br><pre>.