changeset 7047:277c91fe8384

Update coverage.py There is no technical reason to update it except it contains all the patches already done in mercurial plus other stuff. It will be easier to update and maintain in the future.
author Patrick Mezard <pmezard@gmail.com>
date Wed, 17 Sep 2008 22:15:36 +0200
parents f29b674cc221
children 25961e53a07f
files tests/coverage.py
diffstat 1 files changed, 152 insertions(+), 102 deletions(-) [+]
line wrap: on
line diff
--- 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 $