diff -r f29b674cc221 -r 277c91fe8384 tests/coverage.py --- a/tests/coverage.py Wed Sep 17 11:34:37 2008 +0200 +++ b/tests/coverage.py Wed Sep 17 22:15:36 2008 +0200 @@ -54,7 +54,7 @@ Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else.""" -__version__ = "2.77.20070729" # see detailed history at the end of this file. +__version__ = "2.85.20080914" # see detailed history at the end of this file. import compiler import compiler.visitor @@ -67,6 +67,7 @@ import threading import token import types +import zipimport from socket import gethostname # Python version compatibility @@ -105,20 +106,20 @@ self.excluded = excluded self.suite_spots = suite_spots self.excluding_suite = 0 - + def doRecursive(self, node): for n in node.getChildNodes(): self.dispatch(n) visitStmt = visitModule = doRecursive - + def doCode(self, node): if hasattr(node, 'decorators') and node.decorators: self.dispatch(node.decorators) self.recordAndDispatch(node.code) else: self.doSuite(node, node.code) - + visitFunction = visitClass = doCode def getFirstLine(self, node): @@ -138,14 +139,14 @@ for n in node.getChildNodes(): lineno = max(lineno, self.getLastLine(n)) return lineno - + def doStatement(self, node): self.recordLine(self.getFirstLine(node)) 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 @@ -154,10 +155,10 @@ 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 + # 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': @@ -171,7 +172,7 @@ return self.recordLine(self.getFirstLine(node)) else: return 0 - + def recordLine(self, lineno): # Returns a bool, whether the line is included or excluded. if lineno: @@ -186,18 +187,18 @@ return 0 # If this line is excluded, or suite_spots maps this line to # another line that is exlcuded, then we're excluded. - elif lineno in self.excluded or \ - lineno in self.suite_spots and \ - self.suite_spots[lineno][1] in self.excluded: + elif self.excluded.has_key(lineno) or \ + self.suite_spots.has_key(lineno) and \ + self.excluded.has_key(self.suite_spots[lineno][1]): return 0 # Otherwise, this is an executable line. else: self.statements[lineno] = 1 return 1 return 0 - + default = recordNodeLine - + def recordAndDispatch(self, node): self.recordNodeLine(node) self.dispatch(node) @@ -208,7 +209,7 @@ self.excluding_suite = 1 self.recordAndDispatch(body) self.excluding_suite = exsuite - + def doPlainWordSuite(self, prevsuite, suite): # Finding the exclude lines for else's is tricky, because they aren't # present in the compiler parse tree. Look at the previous suite, @@ -217,16 +218,16 @@ lastprev = self.getLastLine(prevsuite) firstelse = self.getFirstLine(suite) for l in range(lastprev+1, firstelse): - if l in self.suite_spots: - self.doSuite(None, suite, exclude=l in self.excluded) + if self.suite_spots.has_key(l): + self.doSuite(None, suite, exclude=self.excluded.has_key(l)) break else: self.doSuite(None, suite) - + def doElse(self, prevsuite, node): if node.else_: self.doPlainWordSuite(prevsuite, node.else_) - + def visitFor(self, node): self.doSuite(node, node.body) self.doElse(node.body, node) @@ -256,14 +257,14 @@ else: self.doSuite(a, h) self.doElse(node.handlers[-1][2], node) - + def visitTryFinally(self, node): 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. @@ -271,7 +272,8 @@ the_coverage = None -class CoverageException(Exception): pass +class CoverageException(Exception): + pass class coverage: # Name of the cache file (unless environment variable is set). @@ -283,7 +285,7 @@ # A dictionary with an entry for (Python source file name, line number # in that file) if that line has been executed. c = {} - + # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been # executed. @@ -300,7 +302,7 @@ 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 @@ -308,23 +310,22 @@ self.nesting = 0 self.cstack = [] self.xstack = [] - self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+os.sep) + self.relative_dir = self.abs_file(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 + # 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 # the arguments and return value of the trace function. # See [van Rossum 2001-07-20a, 3.2] for a description of frame and code # objects. - - def t(self, f, w, unused): #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 + #-for c in self.cstack: + #- c[(f.f_code.co_filename, f.f_lineno)] = 1 return self.t - + def help(self, error=None): #pragma: no cover if error: print error @@ -353,9 +354,9 @@ long_opts = optmap.values() options, args = getopt.getopt(argv, short_opts, long_opts) for o, a in options: - if o in optmap: + if optmap.has_key(o): settings[optmap[o]] = 1 - elif o + ':' in optmap: + elif optmap.has_key(o + ':'): settings[optmap[o + ':']] = a elif o[2:] in long_opts: settings[o[2:]] = 1 @@ -376,14 +377,14 @@ args_needed = (settings.get('execute') or settings.get('annotate') or settings.get('report')) - action = (settings.get('erase') + action = (settings.get('erase') or settings.get('collect') or args_needed) if not action: help_fn("You must specify at least one of -e, -x, -c, -r, or -a.") if not args_needed and args: help_fn("Unexpected arguments: %s" % " ".join(args)) - + self.parallel_mode = settings.get('parallel-mode') self.get_ready() @@ -401,20 +402,17 @@ self.collect() if not args: args = self.cexecuted.keys() - + 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(',') + omit = [self.abs_file(p) for p in omit.split(',')] 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'): @@ -424,7 +422,7 @@ self.usecache = usecache if cache_file and not self.cache: self.cache_default = cache_file - + 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) @@ -432,7 +430,7 @@ self.cache += "." + gethostname() + "." + str(os.getpid()) self.restore() self.analysis_cache = {} - + def start(self, parallel_mode=False): self.get_ready() if self.nesting == 0: #pragma: no cover @@ -440,7 +438,7 @@ if hasattr(threading, 'settrace'): threading.settrace(self.t) self.nesting += 1 - + def stop(self): self.nesting -= 1 if self.nesting == 0: #pragma: no cover @@ -464,7 +462,7 @@ def begin_recursive(self): self.cstack.append(self.c) self.xstack.append(self.exclude_re) - + def end_recursive(self): self.c = self.cstack.pop() self.exclude_re = self.xstack.pop() @@ -515,36 +513,62 @@ def merge_data(self, new_data): for file_name, file_data in new_data.items(): - if file_name in self.cexecuted: + if self.cexecuted.has_key(file_name): self.merge_file_data(self.cexecuted[file_name], file_data) else: self.cexecuted[file_name] = file_data def merge_file_data(self, cache_data, new_data): for line_number in new_data.keys(): - if not line_number in cache_data: + if not cache_data.has_key(line_number): cache_data[line_number] = new_data[line_number] + def abs_file(self, filename): + """ Helper function to turn a filename into an absolute normalized + filename. + """ + return os.path.normcase(os.path.abspath(os.path.realpath(filename))) + + def get_zip_data(self, filename): + """ Get data from `filename` if it is a zip file path, or return None + if it is not. + """ + markers = ['.zip'+os.sep, '.egg'+os.sep] + for marker in markers: + if marker in filename: + parts = filename.split(marker) + try: + zi = zipimport.zipimporter(parts[0]+marker[:-1]) + except zipimport.ZipImportError: + continue + try: + data = zi.get_data(parts[1]) + except IOError: + continue + return data + return None + # canonical_filename(filename). Return a canonical filename for the # file (that is, an absolute path with no redundant components and # normalized case). See [GDR 2001-12-04b, 3.3]. def canonical_filename(self, filename): - if not filename in self.canonical_filename_cache: + 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 self.get_zip_data(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) if os.path.exists(g): f = g break - cf = os.path.normcase(os.path.abspath(os.path.realpath(f))) + cf = self.abs_file(f) self.canonical_filename_cache[filename] = cf return self.canonical_filename_cache[filename] - # canonicalize_filenames(). Copy results from "c" to "cexecuted", + # canonicalize_filenames(). Copy results from "c" to "cexecuted", # canonicalizing filenames on the way. Clear the "c" map. def canonicalize_filenames(self): @@ -553,7 +577,7 @@ # Can't do anything useful with exec'd strings, so skip them. continue f = self.canonical_filename(filename) - if not f in self.cexecuted: + if not self.cexecuted.has_key(f): self.cexecuted[f] = {} self.cexecuted[f][lineno] = 1 self.c = {} @@ -561,9 +585,7 @@ # morf_filename(morf). Return the filename for a module or file. def morf_filename(self, morf): - if isinstance(morf, types.ModuleType): - if not hasattr(morf, '__file__'): - raise CoverageException, "Module has no __file__ attribute." + if hasattr(morf, '__file__'): f = morf.__file__ else: f = morf @@ -576,24 +598,35 @@ # 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 morf in self.analysis_cache: + if self.analysis_cache.has_key(morf): return self.analysis_cache[morf] filename = self.morf_filename(morf) ext = os.path.splitext(filename)[1] + source, sourcef = None, None 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 - source = open(filename, 'r') - lines, excluded_lines, line_map = self.find_executable_statements( - source.read(), exclude=self.exclude_re - ) - source.close() + if not os.path.exists(filename[:-1]): + source = self.get_zip_data(filename[:-1]) + if not source: + raise CoverageException( + "No source for compiled code '%s'." % filename + ) + filename = filename[:-1] + if not source: + sourcef = open(filename, 'rU') + source = sourcef.read() + try: + lines, excluded_lines, line_map = self.find_executable_statements( + source, 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) + ) + if sourcef: + sourcef.close() result = filename, lines, excluded_lines, line_map self.analysis_cache[morf] = result return result @@ -603,26 +636,26 @@ 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 @@ -632,7 +665,7 @@ 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): """ Analyze a parse tree to find suite introducers which span a number of lines. @@ -674,7 +707,7 @@ # 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]) @@ -699,7 +732,7 @@ 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 = {} @@ -755,13 +788,13 @@ def analysis2(self, morf): filename, statements, excluded, line_map = self.analyze_morf(morf) self.canonicalize_filenames() - if not filename in self.cexecuted: + if not self.cexecuted.has_key(filename): self.cexecuted[filename] = {} missing = [] for line in statements: lines = line_map.get(line, [line, line]) for l in range(lines[0], lines[1]+1): - if l in self.cexecuted[filename]: + if self.cexecuted[filename].has_key(l): break else: missing.append(line) @@ -776,7 +809,7 @@ def morf_name(self, morf): """ Return the name of morf as used in report. """ - if isinstance(morf, types.ModuleType): + if hasattr(morf, '__name__'): return morf.__name__ else: return self.relative_filename(os.path.splitext(morf)[0]) @@ -809,7 +842,7 @@ else: globbed.append(morf) morfs = globbed - + morfs = self.filter_by_prefix(morfs, omit_prefixes) morfs.sort(self.morf_name_compare) @@ -847,7 +880,7 @@ raise except: if not ignore_errors: - typ, msg = sys.exc_info()[0:2] + typ, msg = sys.exc_info()[:2] print >>file, fmt_err % (name, typ, msg) if len(morfs) > 1: print >>file, "-" * len(header) @@ -876,7 +909,7 @@ except: if not ignore_errors: raise - + def annotate_file(self, filename, statements, excluded, missing, directory=None): source = open(filename, 'r') if directory: @@ -904,7 +937,7 @@ if self.blank_re.match(line): dest.write(' ') elif self.else_re.match(line): - # Special logic for lines containing only 'else:'. + # Special logic for lines containing only 'else:'. # See [GDR 2001-12-04b, 3.2]. if i >= len(statements) and j >= len(missing): dest.write('! ') @@ -928,40 +961,40 @@ the_coverage = coverage() # Module functions call methods in the singleton object. -def use_cache(*args, **kw): +def use_cache(*args, **kw): return the_coverage.use_cache(*args, **kw) -def start(*args, **kw): +def start(*args, **kw): return the_coverage.start(*args, **kw) -def stop(*args, **kw): +def stop(*args, **kw): return the_coverage.stop(*args, **kw) -def erase(*args, **kw): +def erase(*args, **kw): return the_coverage.erase(*args, **kw) -def begin_recursive(*args, **kw): +def begin_recursive(*args, **kw): return the_coverage.begin_recursive(*args, **kw) -def end_recursive(*args, **kw): +def end_recursive(*args, **kw): return the_coverage.end_recursive(*args, **kw) -def exclude(*args, **kw): +def exclude(*args, **kw): return the_coverage.exclude(*args, **kw) -def analysis(*args, **kw): +def analysis(*args, **kw): return the_coverage.analysis(*args, **kw) -def analysis2(*args, **kw): +def analysis2(*args, **kw): return the_coverage.analysis2(*args, **kw) -def report(*args, **kw): +def report(*args, **kw): return the_coverage.report(*args, **kw) -def annotate(*args, **kw): +def annotate(*args, **kw): return the_coverage.annotate(*args, **kw) -def annotate_file(*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 @@ -973,9 +1006,12 @@ except ImportError: sys.exitfunc = the_coverage.save +def main(): + the_coverage.command_line(sys.argv[1:]) + # Command-line interface. if __name__ == '__main__': - the_coverage.command_line(sys.argv[1:]) + main() # A. REFERENCES @@ -1036,7 +1072,7 @@ # Thanks, Allen. # # 2005-12-02 NMB Call threading.settrace so that all threads are measured. -# Thanks Martin Fuzzey. Add a file argument to report so that reports can be +# Thanks Martin Fuzzey. Add a file argument to report so that reports can be # captured to a different destination. # # 2005-12-03 NMB coverage.py can now measure itself. @@ -1079,11 +1115,25 @@ # 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. +# +# 2008-05-25 NMB Open files in rU mode to avoid line ending craziness. +# Thanks, Edward Loper. +# +# 2008-09-14 NMB Add support for finding source files in eggs. +# Don't check for morf's being instances of ModuleType, instead use duck typing +# so that pseudo-modules can participate. Thanks, Imri Goldberg. +# Use os.realpath as part of the fixing of filenames so that symlinks won't +# confuse things. Thanks, Patrick Mezard. +# +# # C. COPYRIGHT AND LICENCE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2007 Ned Batchelder. All rights reserved. +# Copyright 2004-2008 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 @@ -1110,4 +1160,4 @@ # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. # -# $Id: coverage.py 74 2007-07-29 22:28:35Z nedbat $ +# $Id: coverage.py 96 2008-09-14 18:34:13Z nedbat $