Howdy Lars, Here is an updated patch bundle against your current 'python-coverage/sid' branch. It includes all changes from my earlier message and adjusts the Build-Depends for the required 'python-central' version.
-- \ "I wish a robot would get elected president. That way, when he | `\ came to town, we could all take a shot at him and not feel too | _o__) bad." -- Jack Handey | Ben Finney <[EMAIL PROTECTED]>
# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: [EMAIL PROTECTED] # target_branch: http://liw.iki.fi/bzr/python-coverage/sid/ # testament_sha1: 5afc119e4bd58578ab4aa4ee48a8b9ff5a342956 # timestamp: 2008-05-11 17:01:26 +1000 # base_revision_id: [EMAIL PROTECTED] # # Begin patch === modified file 'coverage.py' --- coverage.py 2007-07-30 17:31:03 +0000 +++ coverage.py 2008-05-10 00:36:53 +0000 @@ -52,18 +52,27 @@ Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else.""" -__version__ = "2.6.20060823" # see detailed history at the end of this file. +__version__ = "2.78.20070930" # see detailed history at the end of this file. import compiler import compiler.visitor +import glob import os import re import string +import symbol import sys import threading +import token import types from socket import gethostname +# Python version compatibility +try: + strclass = basestring # new to 2.3 +except: + strclass = str + # 2. IMPLEMENTATION # # This uses the "singleton" pattern. @@ -85,6 +94,9 @@ # names to increase speed. class StatementFindingAstVisitor(compiler.visitor.ASTVisitor): + """ A visitor for a parsed Abstract Syntax Tree which finds executable + statements. + """ def __init__(self, statements, excluded, suite_spots): compiler.visitor.ASTVisitor.__init__(self) self.statements = statements @@ -93,7 +105,6 @@ self.excluding_suite = 0 def doRecursive(self, node): - self.recordNodeLine(node) for n in node.getChildNodes(): self.dispatch(n) @@ -129,12 +140,35 @@ def doStatement(self, node): self.recordLine(self.getFirstLine(node)) - visitAssert = visitAssign = visitAssTuple = visitDiscard = visitPrint = \ + visitAssert = visitAssign = visitAssTuple = visitPrint = \ visitPrintnl = visitRaise = visitSubscript = visitDecorators = \ doStatement + def visitPass(self, node): + # Pass statements have weird interactions with docstrings. If this + # pass statement is part of one of those pairs, claim that the statement + # is on the later of the two lines. + l = node.lineno + if l: + lines = self.suite_spots.get(l, [l,l]) + self.statements[lines[1]] = 1 + + def visitDiscard(self, node): + # Discard nodes are statements that execute an expression, but then + # discard the results. This includes function calls, so we can't + # ignore them all. But if the expression is a constant, the statement + # won't be "executed", so don't count it now. + if node.expr.__class__.__name__ != 'Const': + self.doStatement(node) + def recordNodeLine(self, node): - return self.recordLine(node.lineno) + # Stmt nodes often have None, but shouldn't claim the first line of + # their children (because the first child might be an ignorable line + # like "global a"). + if node.__class__.__name__ != 'Stmt': + return self.recordLine(self.getFirstLine(node)) + else: + return 0 def recordLine(self, lineno): # Returns a bool, whether the line is included or excluded. @@ -143,7 +177,7 @@ # keyword. if lineno in self.suite_spots: lineno = self.suite_spots[lineno][0] - # If we're inside an exluded suite, record that this line was + # If we're inside an excluded suite, record that this line was # excluded. if self.excluding_suite: self.excluded[lineno] = 1 @@ -195,6 +229,8 @@ self.doSuite(node, node.body) self.doElse(node.body, node) + visitWhile = visitFor + def visitIf(self, node): # The first test has to be handled separately from the rest. # The first test is credited to the line with the "if", but the others @@ -204,10 +240,6 @@ self.doSuite(t, n) self.doElse(node.tests[-1][1], node) - def visitWhile(self, node): - self.doSuite(node, node.body) - self.doElse(node.body, node) - def visitTryExcept(self, node): self.doSuite(node, node.body) for i in range(len(node.handlers)): @@ -227,6 +259,9 @@ self.doSuite(node, node.body) self.doPlainWordSuite(node.body, node.final) + def visitWith(self, node): + self.doSuite(node, node.body) + def visitGlobal(self, node): # "global" statements don't execute like others (they don't call the # trace function), so don't record their line numbers. @@ -263,14 +298,16 @@ def __init__(self): global the_coverage if the_coverage: - raise CoverageException, "Only one coverage object allowed." + raise CoverageException("Only one coverage object allowed.") self.usecache = 1 self.cache = None + self.parallel_mode = False self.exclude_re = '' self.nesting = 0 self.cstack = [] self.xstack = [] - self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.path.sep) + self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.sep) + self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') # t(f, x, y). This method is passed to sys.settrace as a trace function. # See [van Rossum 2001-07-20b, 9.2] for an explanation of sys.settrace and @@ -278,23 +315,24 @@ # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code # objects. - def t(self, f, w, a): #pragma: no cover + def t(self, f, w, unused): #pragma: no cover if w == 'line': + #print "Executing %s @ %d" % (f.f_code.co_filename, f.f_lineno) self.c[(f.f_code.co_filename, f.f_lineno)] = 1 for c in self.cstack: c[(f.f_code.co_filename, f.f_lineno)] = 1 return self.t - def help(self, error=None): + def help(self, error=None): #pragma: no cover if error: print error print print __doc__ sys.exit(1) - def command_line(self, argv, help=None): + def command_line(self, argv, help_fn=None): import getopt - help = help or self.help + help_fn = help_fn or self.help settings = {} optmap = { '-a': 'annotate', @@ -325,12 +363,12 @@ pass # Can't get here, because getopt won't return anything unknown. if settings.get('help'): - help() + help_fn() for i in ['erase', 'execute']: for j in ['annotate', 'report', 'collect']: if settings.get(i) and settings.get(j): - help("You can't specify the '%s' and '%s' " + help_fn("You can't specify the '%s' and '%s' " "options at the same time." % (i, j)) args_needed = (settings.get('execute') @@ -340,18 +378,18 @@ or settings.get('collect') or args_needed) if not action: - help("You must specify at least one of -e, -x, -c, -r, or -a.") + help_fn("You must specify at least one of -e, -x, -c, -r, or -a.") if not args_needed and args: - help("Unexpected arguments: %s" % " ".join(args)) + help_fn("Unexpected arguments: %s" % " ".join(args)) - self.get_ready(settings.get('parallel-mode')) - self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]') + self.parallel_mode = settings.get('parallel-mode') + self.get_ready() if settings.get('erase'): self.erase() if settings.get('execute'): if not args: - help("Nothing to do.") + help_fn("Nothing to do.") sys.argv = args self.start() import __main__ @@ -385,13 +423,13 @@ 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: + if self.parallel_mode: self.cache += "." + gethostname() + "." + str(os.getpid()) self.restore() self.analysis_cache = {} def start(self, parallel_mode=False): - self.get_ready(parallel_mode) + self.get_ready() if self.nesting == 0: #pragma: no cover sys.settrace(self.t) if hasattr(threading, 'settrace'): @@ -406,12 +444,12 @@ threading.settrace(None) def erase(self): + self.get_ready() self.c = {} self.analysis_cache = {} self.cexecuted = {} if self.cache and os.path.exists(self.cache): os.remove(self.cache) - self.exclude_re = "" def exclude(self, re): if self.exclude_re: @@ -462,11 +500,11 @@ def collect(self): cache_dir, local = os.path.split(self.cache) - for file in os.listdir(cache_dir): - if not file.startswith(local): + for f in os.listdir(cache_dir or '.'): + if not f.startswith(local): continue - full_path = os.path.join(cache_dir, file) + full_path = os.path.join(cache_dir, f) cexecuted = self.restore_file(full_path) self.merge_data(cexecuted) @@ -506,6 +544,9 @@ def canonicalize_filenames(self): for filename, lineno in self.c.keys(): + if filename == '<string>': + # Can't do anything useful with exec'd strings, so skip them. + continue f = self.canonical_filename(filename) if not self.cexecuted.has_key(f): self.cexecuted[f] = {} @@ -517,41 +558,85 @@ def morf_filename(self, morf): if isinstance(morf, types.ModuleType): if not hasattr(morf, '__file__'): - raise CoverageException, "Module has no __file__ attribute." - file = morf.__file__ + raise CoverageException("Module has no __file__ attribute.") + f = morf.__file__ else: - file = morf - return self.canonical_filename(file) + f = morf + return self.canonical_filename(f) # analyze_morf(morf). Analyze the module or filename passed as # the argument. If the source code can't be found, raise an error. # Otherwise, return a tuple of (1) the canonical filename of the # source code for the module, (2) a list of lines of statements - # in the source code, and (3) a list of lines of excluded statements. - + # in the source code, (3) a list of lines of excluded statements, + # and (4), a map of line numbers to multi-line line number ranges, for + # statements that cross lines. + def analyze_morf(self, morf): if self.analysis_cache.has_key(morf): return self.analysis_cache[morf] filename = self.morf_filename(morf) ext = os.path.splitext(filename)[1] if ext == '.pyc': - if not os.path.exists(filename[0:-1]): - raise CoverageException, ("No source for compiled code '%s'." - % filename) - filename = filename[0:-1] - elif ext != '.py': - raise CoverageException, "File '%s' not Python source." % filename + if not os.path.exists(filename[:-1]): + raise CoverageException( + "No source for compiled code '%s'." % filename + ) + filename = filename[:-1] source = open(filename, 'r') - lines, excluded_lines = self.find_executable_statements( - source.read(), exclude=self.exclude_re - ) + try: + lines, excluded_lines, line_map = self.find_executable_statements( + source.read(), exclude=self.exclude_re + ) + except SyntaxError, synerr: + raise CoverageException( + "Couldn't parse '%s' as Python source: '%s' at line %d" % + (filename, synerr.msg, synerr.lineno) + ) source.close() - result = filename, lines, excluded_lines + result = filename, lines, excluded_lines, line_map self.analysis_cache[morf] = result return result + def first_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[1] + + def last_line_of_tree(self, tree): + while True: + if len(tree) == 3 and type(tree[2]) == type(1): + return tree[2] + tree = tree[-1] + + def find_docstring_pass_pair(self, tree, spots): + for i in range(1, len(tree)): + if self.is_string_constant(tree[i]) and self.is_pass_stmt(tree[i+1]): + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i+1]) + self.record_multiline(spots, first_line, last_line) + + def is_string_constant(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.expr_stmt + except: + return False + + def is_pass_stmt(self, tree): + try: + return tree[0] == symbol.stmt and tree[1][1][1][0] == symbol.pass_stmt + except: + return False + + def record_multiline(self, spots, i, j): + for l in range(i, j+1): + spots[l] = (i, j) + def get_suite_spots(self, tree, spots): - import symbol, token + """ Analyze a parse tree to find suite introducers which span a number + of lines. + """ for i in range(1, len(tree)): if type(tree[i]) == type(()): if tree[i][0] == symbol.suite: @@ -559,7 +644,9 @@ lineno_colon = lineno_word = None for j in range(i-1, 0, -1): if tree[j][0] == token.COLON: - lineno_colon = tree[j][2] + # Colons are never executed themselves: we want the + # line number of the last token before the colon. + lineno_colon = self.last_line_of_tree(tree[j-1]) elif tree[j][0] == token.NAME: if tree[j][1] == 'elif': # Find the line number of the first non-terminal @@ -581,8 +668,18 @@ if lineno_colon and lineno_word: # Found colon and keyword, mark all the lines # between the two with the two line numbers. - for l in range(lineno_word, lineno_colon+1): - spots[l] = (lineno_word, lineno_colon) + self.record_multiline(spots, lineno_word, lineno_colon) + + # "pass" statements are tricky: different versions of Python + # treat them differently, especially in the common case of a + # function with a doc string and a single pass statement. + self.find_docstring_pass_pair(tree[i], spots) + + elif tree[i][0] == symbol.simple_stmt: + first_line = self.first_line_of_tree(tree[i]) + last_line = self.last_line_of_tree(tree[i]) + if first_line != last_line: + self.record_multiline(spots, first_line, last_line) self.get_suite_spots(tree[i], spots) def find_executable_statements(self, text, exclude=None): @@ -596,10 +693,13 @@ if reExclude.search(lines[i]): excluded[i+1] = 1 + # Parse the code and analyze the parse tree to find out which statements + # are multiline, and where suites begin and end. import parser tree = parser.suite(text+'\n\n').totuple(1) self.get_suite_spots(tree, suite_spots) - + #print "Suite spots:", suite_spots + # Use the compiler module to parse the text and find the executable # statements. We add newlines to be impervious to final partial lines. statements = {} @@ -611,7 +711,7 @@ lines.sort() excluded_lines = excluded.keys() excluded_lines.sort() - return lines, excluded_lines + return lines, excluded_lines, suite_spots # format_lines(statements, lines). Format a list of line numbers # for printing by coalescing groups of lines as long as the lines @@ -644,7 +744,8 @@ return "%d" % start else: return "%d-%d" % (start, end) - return string.join(map(stringify, pairs), ", ") + ret = string.join(map(stringify, pairs), ", ") + return ret # Backward compatibility with version 1. def analysis(self, morf): @@ -652,13 +753,17 @@ return f, s, m, mf def analysis2(self, morf): - filename, statements, excluded = self.analyze_morf(morf) + filename, statements, excluded, line_map = self.analyze_morf(morf) self.canonicalize_filenames() if not self.cexecuted.has_key(filename): self.cexecuted[filename] = {} missing = [] for line in statements: - if not self.cexecuted[filename].has_key(line): + lines = line_map.get(line, [line, line]) + for l in range(lines[0], lines[1]+1): + if self.cexecuted[filename].has_key(l): + break + else: missing.append(line) return (filename, statements, excluded, missing, self.format_lines(statements, missing)) @@ -696,6 +801,15 @@ def report(self, morfs, show_missing=1, ignore_errors=0, file=None, omit_prefixes=[]): if not isinstance(morfs, types.ListType): morfs = [morfs] + # On windows, the shell doesn't expand wildcards. Do it here. + globbed = [] + for morf in morfs: + if isinstance(morf, strclass): + globbed.extend(glob.glob(morf)) + else: + globbed.append(morf) + morfs = globbed + morfs = self.filter_by_prefix(morfs, omit_prefixes) morfs.sort(self.morf_name_compare) @@ -733,8 +847,8 @@ raise except: if not ignore_errors: - type, msg = sys.exc_info()[0:2] - print >>file, fmt_err % (name, type, msg) + typ, msg = sys.exc_info()[:2] + print >>file, fmt_err % (name, typ, msg) if len(morfs) > 1: print >>file, "-" * len(header) if total_statements > 0: @@ -814,18 +928,41 @@ the_coverage = coverage() # 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 begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw) -def end_recursive(*args, **kw): return the_coverage.end_recursive(*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) +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 begin_recursive(*args, **kw): + return the_coverage.begin_recursive(*args, **kw) + +def end_recursive(*args, **kw): + return the_coverage.end_recursive(*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) # Save coverage data when Python exits. (The atexit module wasn't # introduced until Python 2.0, so use sys.exitfunc when it's not @@ -916,11 +1053,41 @@ # # 2006-08-23 NMB Refactorings to improve testability. Fixes to command-line # logic for parallel mode and collect. +# +# 2006-08-25 NMB "#pragma: nocover" is excluded by default. +# +# 2006-09-10 NMB Properly ignore docstrings and other constant expressions that +# appear in the middle of a function, a problem reported by Tim Leslie. +# Minor changes to avoid lint warnings. +# +# 2006-09-17 NMB coverage.erase() shouldn't clobber the exclude regex. +# Change how parallel mode is invoked, and fix erase() so that it erases the +# cache when called programmatically. +# +# 2007-07-21 NMB In reports, ignore code executed from strings, since we can't +# do anything useful with it anyway. +# Better file handling on Linux, thanks Guillaume Chazarain. +# Better shell support on Windows, thanks Noel O'Boyle. +# Python 2.2 support maintained, thanks Catherine Proulx. +# +# 2007-07-22 NMB Python 2.5 now fully supported. The method of dealing with +# multi-line statements is now less sensitive to the exact line that Python +# reports during execution. Pass statements are handled specially so that their +# disappearance during execution won't throw off the measurement. +# +# 2007-07-23 NMB Now Python 2.5 is *really* fully supported: the body of the +# new with statement is counted as executable. +# +# 2007-07-29 NMB Better packaging. +# +# 2007-09-30 NMB Don't try to predict whether a file is Python source based on +# the extension. Extensionless files are often Pythons scripts. Instead, simply +# parse the file and catch the syntax errors. Hat tip to Ben Finney. # C. COPYRIGHT AND LICENCE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2006 Ned Batchelder. All rights reserved. +# Copyright 2004-2007 Ned Batchelder. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are @@ -947,4 +1114,4 @@ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. # -# $Id: coverage.py 47 2006-08-24 01:08:48Z Ned $ +# $Id: coverage.py 79 2007-10-01 01:01:52Z nedbat $ === modified file 'debian/README.source' --- debian/README.source 2007-07-30 17:31:34 +0000 +++ debian/README.source 2008-05-10 00:36:53 +0000 @@ -3,7 +3,7 @@ * http://liw.iki.fi/bzr/python-coverage/ is the bzr repository; all the branches mentioned in this document are in that repository, so to access them append their name to the url. - + * There are three branches: upstream, sid, and tarballs. You need to check out all of them ("bzr branch $BRANCHURL"). @@ -11,7 +11,11 @@ a single Python file (coverage.py), and that's what upstream publishes. For each new release, I add the new coverage.py to the upstream branch, and commit. - + + (As of 2007-07-29, the upstream package uses distutils packaging and + is no longer published as "a single Python file". TODO: Work with + the distutils packaging being provided by upstream.) + * I then generate an .orig.tar.gz tarball, from within the upstream branch: V=2.6 === modified file 'debian/changelog' --- debian/changelog 2007-08-19 19:54:32 +0000 +++ debian/changelog 2008-05-11 07:00:53 +0000 @@ -1,3 +1,16 @@ +python-coverage (2.78-0.1) UNRELEASED; urgency=low + + * Non-maintainer upload. + * New upstream version (closes: #454982). + * debian/python-coverage.install: + + Install Python modules to /usr/share/pyshared as per new behaviour of + python-central. + * debian/control: + + Update to Standards-Version: 3.7.3 (no changes required). + + Build-Depends: python-central (>= 0.6). + + -- Ben Finney <[EMAIL PROTECTED]> Sat, 10 May 2008 10:53:09 +1000 + python-coverage (2.6-1) unstable; urgency=low * Initial version. Closes: #405230. === modified file 'debian/control' --- debian/control 2007-12-16 10:23:00 +0000 +++ debian/control 2008-05-11 07:00:53 +0000 @@ -2,8 +2,8 @@ Maintainer: Lars Wirzenius <[EMAIL PROTECTED]> Section: devel Priority: optional -Standards-Version: 3.7.2 -Build-Depends-Indep: python-central (>= 0.5.6), python (>= 2.3) +Standards-Version: 3.7.3 +Build-Depends-Indep: python-central (>= 0.6), python (>= 2.3) Build-Depends: cdbs (>= 0.4.43), debhelper (>= 5.0.38) XS-Python-Version: all @@ -15,5 +15,4 @@ This tool measures which parts (statements) of a Python program are executed while it is run. This is useful for testing: those parts of a program that are not executed by the tests have not been tested. - . Homepage: http://www.nedbatchelder.com/code/modules/coverage.html === modified file 'debian/python-coverage.install' --- debian/python-coverage.install 2007-07-30 17:18:05 +0000 +++ debian/python-coverage.install 2008-05-10 01:00:25 +0000 @@ -1,2 +1,2 @@ debian/python-coverage.1 usr/share/man/man1 -coverage.py usr/share/pycentral/python-coverage/site-packages +coverage.py usr/share/pyshared/python-coverage/site-packages # Begin bundle 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 5IpwoSHn+VsU
signature.asc
Description: Digital signature