Howdy Lars, This patch adds revisions to your 'python-coverage/sid/' branch for upstream's release 2.7.8, and changes some of the Debian files as required for newer behaviour. I followed the 'README.source' as closely as I could, so I hope this is okay.
The 'debian/changelog' currently has the release as an NMU, and release is "UNRELEASED"; you'll want to change those of course if you decide to do the release yourself from these patches. -- \ “He that would make his own liberty secure must guard even | `\ his enemy from oppression.” —Thomas Paine | _o__) | 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: eccff9fb942c30fdbc496f249191fdcdb032b589 # timestamp: 2008-05-10 11:14:35 +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-10 01:03:06 +0000 @@ -1,3 +1,15 @@ +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 >= 0.6.0. + * debian/control: + + Update to Standards-Version: 3.7.3 (no changes required). + + -- 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-10 01:03:06 +0000 @@ -2,7 +2,7 @@ Maintainer: Lars Wirzenius <[EMAIL PROTECTED]> Section: devel Priority: optional -Standards-Version: 3.7.2 +Standards-Version: 3.7.3 Build-Depends-Indep: python-central (>= 0.5.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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 k+IOevWS0mUBm6EUhWRwfuIX/xdyRThQkGdSC3M=
signature.asc
Description: Digital signature