tests/coverage.py
changeset 7047 277c91fe8384
parent 6349 6aaf5b1d8f15
child 10282 08a0f04b56bd
--- 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 $