home *** CD-ROM | disk | FTP | other *** search
/ Freelog 33 / Freelog033.iso / Progr / Python-2.2.1.exe / TRACE.PY < prev    next >
Encoding:
Python Source  |  2001-11-28  |  31.3 KB  |  749 lines

  1. #!/usr/bin/env python
  2.  
  3. # portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
  4. # err...  reserved and offered to the public under the terms of the
  5. # Python 2.2 license.
  6. # Author: Zooko O'Whielacronx
  7. # http://zooko.com/
  8. # mailto:zooko@zooko.com
  9. #
  10. # Copyright 2000, Mojam Media, Inc., all rights reserved.
  11. # Author: Skip Montanaro
  12. #
  13. # Copyright 1999, Bioreason, Inc., all rights reserved.
  14. # Author: Andrew Dalke
  15. #
  16. # Copyright 1995-1997, Automatrix, Inc., all rights reserved.
  17. # Author: Skip Montanaro
  18. #
  19. # Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
  20. #
  21. #
  22. # Permission to use, copy, modify, and distribute this Python software and
  23. # its associated documentation for any purpose without fee is hereby
  24. # granted, provided that the above copyright notice appears in all copies,
  25. # and that both that copyright notice and this permission notice appear in
  26. # supporting documentation, and that the name of neither Automatrix,
  27. # Bioreason or Mojam Media be used in advertising or publicity pertaining to
  28. # distribution of the software without specific, written prior permission.
  29. #
  30. #
  31. # Cleaned up the usage message --GvR 11/28/01
  32. #
  33. # Summary of even more recent changes, --Zooko 2001-10-14
  34. #   Used new `inspect' module for better (?) determination of file<->module
  35. #      mappings, line numbers, and source code.
  36. #   Used new local trace function for faster (and better?) operation.
  37. #   Removed "speed hack", which, as far as I can tell, meant that it would
  38. #      ignore all files ??? (When I tried it, it would ignore only *most* of my
  39. #      files.  In any case with the speed hack removed in favor of actually
  40. #      calling `Ignore.names()', it ignores only those files that I told it to
  41. #      ignore, so I am happy.)
  42. #   Rolled the `Coverage' class into `Trace', which now does either tracing or
  43. #      counting or both according to constructor flags.
  44. #   Moved the construction of the `Ignore' object inside the constructor of
  45. #      `Trace', simplifying usage.
  46. #   Changed function `create_results_log()' into method
  47. #      `CoverageResults.write_results()'.
  48. #   Add new mode "countfuncs" which is faster and which just reports which
  49. #      functions were invoked.
  50.  
  51. #   Made `write_results' create `coverdir' if it doesn't already exist.
  52. #   Moved the `run' funcs into `Trace' for simpler usage.
  53. #   Use pickle instead of marshal for persistence.
  54. #
  55. # Summary of recent changes:
  56. #   Support for files with the same basename (submodules in packages)
  57. #   Expanded the idea of how to ignore files or modules
  58. #   Split tracing and counting into different classes
  59. #   Extracted count information and reporting from the count class
  60. #   Added some ability to detect which missing lines could be executed
  61. #   Added pseudo-pragma to prohibit complaining about unexecuted lines
  62. #   Rewrote the main program
  63.  
  64. # Summary of older changes:
  65. #   Added run-time display of statements being executed
  66. #   Incorporated portability and performance fixes from Greg Stein
  67. #   Incorporated main program from Michael Scharf
  68.  
  69. """
  70. program/module to trace Python program or function execution
  71.  
  72. Sample use, command line:
  73.   trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
  74.   trace.py -t --ignore-dir '$prefix' spam.py eggs
  75.  
  76. Sample use, programmatically
  77.    # create a Trace object, telling it what to ignore, and whether to do tracing
  78.    # or line-counting or both.
  79.    trace = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0, count=1)
  80.    # run the new command using the given trace
  81.    trace.run(coverage.globaltrace, 'main()')
  82.    # make a report, telling it where you want output
  83.    trace.print_results(show_missing=1)
  84. """
  85.  
  86. import sys, os, string, tempfile, types, copy, operator, inspect, exceptions, marshal
  87. try:
  88.     import cPickle
  89.     pickle = cPickle
  90. except ImportError:
  91.     import pickle
  92.  
  93. true = 1
  94. false = None
  95.  
  96. # DEBUG_MODE=1  # make this true to get printouts which help you understand what's going on
  97.  
  98. def usage(outfile):
  99.     outfile.write("""Usage: %s [OPTIONS] <file> [ARGS]
  100.  
  101. Meta-options:
  102. --help                Display this help then exit.
  103. --version             Output version information then exit.
  104.  
  105. Otherwise, exactly one of the following three options must be given:
  106. -t, --trace           Print each line to sys.stdout before it is executed.
  107. -c, --count           Count the number of times each line is executed
  108.                       and write the counts to <module>.cover for each
  109.                       module executed, in the module's directory.
  110.                       See also `--coverdir', `--file', `--no-report' below.
  111. -r, --report          Generate a report from a counts file; do not execute
  112.                       any code.  `--file' must specify the results file to
  113.                       read, which must have been created in a previous run
  114.                       with `--count --file=FILE'.
  115.  
  116. Modifiers:
  117. -f, --file=<file>     File to accumulate counts over several runs.
  118. -R, --no-report       Do not generate the coverage report files.
  119.                       Useful if you want to accumulate over several runs.
  120. -C, --coverdir=<dir>  Directory where the report files.  The coverage
  121.                       report for <package>.<module> is written to file
  122.                       <dir>/<package>/<module>.cover.
  123. -m, --missing         Annotate executable lines that were not executed
  124.                       with '>>>>>> '.
  125. -s, --summary         Write a brief summary on stdout for each file.
  126.                       (Can only be used with --count or --report.)
  127.  
  128. Filters, may be repeated multiple times:
  129. --ignore-module=<mod> Ignore the given module and its submodules
  130.                       (if it is a package).
  131. --ignore-dir=<dir>    Ignore files in the given directory (multiple
  132.                       directories can be joined by os.pathsep).
  133. """ % sys.argv[0])
  134.  
  135. class Ignore:
  136.     def __init__(self, modules = None, dirs = None):
  137.         self._mods = modules or []
  138.         self._dirs = dirs or []
  139.  
  140.         self._dirs = map(os.path.normpath, self._dirs)
  141.         self._ignore = { '<string>': 1 }
  142.  
  143.     def names(self, filename, modulename):
  144.         if self._ignore.has_key(modulename):
  145.             return self._ignore[modulename]
  146.  
  147.         # haven't seen this one before, so see if the module name is
  148.         # on the ignore list.  Need to take some care since ignoring
  149.         # "cmp" musn't mean ignoring "cmpcache" but ignoring
  150.         # "Spam" must also mean ignoring "Spam.Eggs".
  151.         for mod in self._mods:
  152.             if mod == modulename:  # Identical names, so ignore
  153.                 self._ignore[modulename] = 1
  154.                 return 1
  155.             # check if the module is a proper submodule of something on
  156.             # the ignore list
  157.             n = len(mod)
  158.             # (will not overflow since if the first n characters are the
  159.             # same and the name has not already occured, then the size
  160.             # of "name" is greater than that of "mod")
  161.             if mod == modulename[:n] and modulename[n] == '.':
  162.                 self._ignore[modulename] = 1
  163.                 return 1
  164.  
  165.         # Now check that __file__ isn't in one of the directories
  166.         if filename is None:
  167.             # must be a built-in, so we must ignore
  168.             self._ignore[modulename] = 1
  169.             return 1
  170.  
  171.         # Ignore a file when it contains one of the ignorable paths
  172.         for d in self._dirs:
  173.             # The '+ os.sep' is to ensure that d is a parent directory,
  174.             # as compared to cases like:
  175.             #  d = "/usr/local"
  176.             #  filename = "/usr/local.py"
  177.             # or
  178.             #  d = "/usr/local.py"
  179.             #  filename = "/usr/local.py"
  180.             if string.find(filename, d + os.sep) == 0:
  181.                 self._ignore[modulename] = 1
  182.                 return 1
  183.  
  184.         # Tried the different ways, so we don't ignore this module
  185.         self._ignore[modulename] = 0
  186.         return 0
  187.  
  188. class CoverageResults:
  189.     def __init__(self, counts=None, calledfuncs=None, infile=None, outfile=None):
  190.         self.counts = counts
  191.         if self.counts is None:
  192.             self.counts = {}
  193.         self.counter = self.counts.copy() # map (filename, lineno) to count
  194.         self.calledfuncs = calledfuncs
  195.         if self.calledfuncs is None:
  196.             self.calledfuncs = {}
  197.         self.calledfuncs = self.calledfuncs.copy()
  198.         self.infile = infile
  199.         self.outfile = outfile
  200.         if self.infile:
  201.             # try and merge existing counts file
  202.             try:
  203.                 thingie = pickle.load(open(self.infile, 'r'))
  204.                 if type(thingie) is types.DictType:
  205.                     # backwards compatibility for old trace.py after Zooko touched it but before calledfuncs  --Zooko 2001-10-24
  206.                     self.update(self.__class__(thingie))
  207.                 elif type(thingie) is types.TupleType and len(thingie) == 2:
  208.                     (counts, calledfuncs,) = thingie
  209.                     self.update(self.__class__(counts, calledfuncs))
  210.             except (IOError, EOFError,):
  211.                 pass
  212.             except pickle.UnpicklingError:
  213.                 # backwards compatibility for old trace.py before Zooko touched it  --Zooko 2001-10-24
  214.                 self.update(self.__class__(marshal.load(open(self.infile))))
  215.  
  216.     def update(self, other):
  217.         """Merge in the data from another CoverageResults"""
  218.         counts = self.counts
  219.         calledfuncs = self.calledfuncs
  220.         other_counts = other.counts
  221.         other_calledfuncs = other.calledfuncs
  222.  
  223.         for key in other_counts.keys():
  224.             if key != 'calledfuncs': # backwards compatibility for abortive attempt to stuff calledfuncs into self.counts, by Zooko  --Zooko 2001-10-24
  225.                 counts[key] = counts.get(key, 0) + other_counts[key]
  226.  
  227.         for key in other_calledfuncs.keys():
  228.             calledfuncs[key] = 1
  229.  
  230.     def write_results(self, show_missing = 1, summary = 0, coverdir = None):
  231.         """
  232.         @param coverdir
  233.         """
  234.         for (filename, modulename, funcname,) in self.calledfuncs.keys():
  235.             print "filename: %s, modulename: %s, funcname: %s" % (filename, modulename, funcname,)
  236.  
  237.         import re
  238.         # turn the counts data ("(filename, lineno) = count") into something
  239.         # accessible on a per-file basis
  240.         per_file = {}
  241.         for thingie in self.counts.keys():
  242.             if thingie != "calledfuncs": # backwards compatibility for abortive attempt to stuff calledfuncs into self.counts, by Zooko  --Zooko 2001-10-24
  243.                 (filename, lineno,) = thingie
  244.                 lines_hit = per_file[filename] = per_file.get(filename, {})
  245.                 lines_hit[lineno] = self.counts[(filename, lineno)]
  246.  
  247.         # there are many places where this is insufficient, like a blank
  248.         # line embedded in a multiline string.
  249.         blank = re.compile(r'^\s*(#.*)?$')
  250.  
  251.         # accumulate summary info, if needed
  252.         sums = {}
  253.  
  254.         # generate file paths for the coverage files we are going to write...
  255.         fnlist = []
  256.         tfdir = tempfile.gettempdir()
  257.         for key in per_file.keys():
  258.             filename = key
  259.  
  260.             # skip some "files" we don't care about...
  261.             if filename == "<string>":
  262.                 continue
  263.             # are these caused by code compiled using exec or something?
  264.             if filename.startswith(tfdir):
  265.                 continue
  266.  
  267.             modulename = inspect.getmodulename(filename)
  268.  
  269.             if filename.endswith(".pyc") or filename.endswith(".pyo"):
  270.                 filename = filename[:-1]
  271.  
  272.             if coverdir:
  273.                 thiscoverdir = coverdir
  274.             else:
  275.                 thiscoverdir = os.path.dirname(os.path.abspath(filename))
  276.  
  277.             # the code from here to "<<<" is the contents of the `fileutil.make_dirs()' function in the Mojo Nation project.  --Zooko 2001-10-14
  278.             # http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/mojonation/evil/common/fileutil.py?rev=HEAD&content-type=text/vnd.viewcvs-markup
  279.             tx = None
  280.             try:
  281.                 os.makedirs(thiscoverdir)
  282.             except OSError, x:
  283.                 tx = x
  284.  
  285.             if not os.path.isdir(thiscoverdir):
  286.                 if tx:
  287.                     raise tx
  288.                 raise exceptions.IOError, "unknown error prevented creation of directory: %s" % thiscoverdir # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
  289.             # <<<
  290.  
  291.             # build list file name by appending a ".cover" to the module name
  292.             # and sticking it into the specified directory
  293.             if "." in modulename:
  294.                 # A module in a package
  295.                 finalname = modulename.split(".")[-1]
  296.                 listfilename = os.path.join(thiscoverdir, finalname + ".cover")
  297.             else:
  298.                 listfilename = os.path.join(thiscoverdir, modulename + ".cover")
  299.  
  300.             # Get the original lines from the .py file
  301.             try:
  302.                 lines = open(filename, 'r').readlines()
  303.             except IOError, err:
  304.                 sys.stderr.write("trace: Could not open %s for reading because: %s - skipping\n" % (`filename`, err))
  305.                 continue
  306.  
  307.             try:
  308.                 outfile = open(listfilename, 'w')
  309.             except IOError, err:
  310.                 sys.stderr.write(
  311.                     '%s: Could not open %s for writing because: %s" \
  312.                     "- skipping\n' % ("trace", `listfilename`, err))
  313.                 continue
  314.  
  315.             # If desired, get a list of the line numbers which represent
  316.             # executable content (returned as a dict for better lookup speed)
  317.             if show_missing:
  318.                 executable_linenos = find_executable_linenos(filename)
  319.             else:
  320.                 executable_linenos = {}
  321.  
  322.             n_lines = 0
  323.             n_hits = 0
  324.             lines_hit = per_file[key]
  325.             for i in range(len(lines)):
  326.                 line = lines[i]
  327.  
  328.                 # do the blank/comment match to try to mark more lines
  329.                 # (help the reader find stuff that hasn't been covered)
  330.                 if lines_hit.has_key(i+1):
  331.                     # count precedes the lines that we captured
  332.                     outfile.write('%5d: ' % lines_hit[i+1])
  333.                     n_hits = n_hits + 1
  334.                     n_lines = n_lines + 1
  335.                 elif blank.match(line):
  336.                     # blank lines and comments are preceded by dots
  337.                     outfile.write('    . ')
  338.                 else:
  339.                     # lines preceded by no marks weren't hit
  340.                     # Highlight them if so indicated, unless the line contains
  341.                     # '#pragma: NO COVER' (it is possible to embed this into
  342.                     # the text as a non-comment; no easy fix)
  343.                     if executable_linenos.has_key(i+1) and \
  344.                        string.find(lines[i],
  345.                                    string.join(['#pragma', 'NO COVER'])) == -1:
  346.                         outfile.write('>>>>>> ')
  347.                     else:
  348.                         outfile.write(' '*7)
  349.                     n_lines = n_lines + 1
  350.                 outfile.write(string.expandtabs(lines[i], 8))
  351.  
  352.             outfile.close()
  353.  
  354.             if summary and n_lines:
  355.                 percent = int(100 * n_hits / n_lines)
  356.                 sums[modulename] = n_lines, percent, modulename, filename
  357.  
  358.         if summary and sums:
  359.             mods = sums.keys()
  360.             mods.sort()
  361.             print "lines   cov%   module   (path)"
  362.             for m in mods:
  363.                 n_lines, percent, modulename, filename = sums[m]
  364.                 print "%5d   %3d%%   %s   (%s)" % sums[m]
  365.  
  366.         if self.outfile:
  367.             # try and store counts and module info into self.outfile
  368.             try:
  369.                 pickle.dump((self.counts, self.calledfuncs,), open(self.outfile, 'w'), 1)
  370.             except IOError, err:
  371.                 sys.stderr.write("cannot save counts files because %s" % err)
  372.  
  373. # Given a code string, return the SET_LINENO information
  374. def _find_LINENO_from_string(co_code):
  375.     """return all of the SET_LINENO information from a code string"""
  376.     import dis
  377.     linenos = {}
  378.  
  379.     # This code was filched from the `dis' module then modified
  380.     n = len(co_code)
  381.     i = 0
  382.     prev_op = None
  383.     prev_lineno = 0
  384.     while i < n:
  385.         c = co_code[i]
  386.         op = ord(c)
  387.         if op == dis.SET_LINENO:
  388.             if prev_op == op:
  389.                 # two SET_LINENO in a row, so the previous didn't
  390.                 # indicate anything.  This occurs with triple
  391.                 # quoted strings (?).  Remove the old one.
  392.                 del linenos[prev_lineno]
  393.             prev_lineno = ord(co_code[i+1]) + ord(co_code[i+2])*256
  394.             linenos[prev_lineno] = 1
  395.         if op >= dis.HAVE_ARGUMENT:
  396.             i = i + 3
  397.         else:
  398.             i = i + 1
  399.         prev_op = op
  400.     return linenos
  401.  
  402. def _find_LINENO(code):
  403.     """return all of the SET_LINENO information from a code object"""
  404.     import types
  405.  
  406.     # get all of the lineno information from the code of this scope level
  407.     linenos = _find_LINENO_from_string(code.co_code)
  408.  
  409.     # and check the constants for references to other code objects
  410.     for c in code.co_consts:
  411.         if type(c) == types.CodeType:
  412.             # find another code object, so recurse into it
  413.             linenos.update(_find_LINENO(c))
  414.     return linenos
  415.  
  416. def find_executable_linenos(filename):
  417.     """return a dict of the line numbers from executable statements in a file
  418.  
  419.     Works by finding all of the code-like objects in the module then searching
  420.     the byte code for 'SET_LINENO' terms (so this won't work one -O files).
  421.  
  422.     """
  423.     import parser
  424.  
  425.     assert filename.endswith('.py')
  426.  
  427.     prog = open(filename).read()
  428.     ast = parser.suite(prog)
  429.     code = parser.compileast(ast, filename)
  430.  
  431.     # The only way I know to find line numbers is to look for the
  432.     # SET_LINENO instructions.  Isn't there some way to get it from
  433.     # the AST?
  434.  
  435.     return _find_LINENO(code)
  436.  
  437. ### XXX because os.path.commonprefix seems broken by my way of thinking...
  438. def commonprefix(dirs):
  439.     "Given a list of pathnames, returns the longest common leading component"
  440.     if not dirs: return ''
  441.     n = copy.copy(dirs)
  442.     for i in range(len(n)):
  443.         n[i] = n[i].split(os.sep)
  444.     prefix = n[0]
  445.     for item in n:
  446.         for i in range(len(prefix)):
  447.             if prefix[:i+1] <> item[:i+1]:
  448.                 prefix = prefix[:i]
  449.                 if i == 0: return ''
  450.                 break
  451.     return os.sep.join(prefix)
  452.  
  453. class Trace:
  454.     def __init__(self, count=1, trace=1, countfuncs=0, ignoremods=(), ignoredirs=(), infile=None, outfile=None):
  455.         """
  456.         @param count true iff it should count number of times each line is executed
  457.         @param trace true iff it should print out each line that is being counted
  458.         @param countfuncs true iff it should just output a list of (filename, modulename, funcname,) for functions that were called at least once;  This overrides `count' and `trace'
  459.         @param ignoremods a list of the names of modules to ignore
  460.         @param ignoredirs a list of the names of directories to ignore all of the (recursive) contents of
  461.         @param infile file from which to read stored counts to be added into the results
  462.         @param outfile file in which to write the results
  463.         """
  464.         self.infile = infile
  465.         self.outfile = outfile
  466.         self.ignore = Ignore(ignoremods, ignoredirs)
  467.         self.counts = {}   # keys are (filename, linenumber)
  468.         self.blabbed = {} # for debugging
  469.         self.pathtobasename = {} # for memoizing os.path.basename
  470.         self.donothing = 0
  471.         self.trace = trace
  472.         self._calledfuncs = {}
  473.         if countfuncs:
  474.             self.globaltrace = self.globaltrace_countfuncs
  475.         elif trace and count:
  476.             self.globaltrace = self.globaltrace_lt
  477.             self.localtrace = self.localtrace_trace_and_count
  478.         elif trace:
  479.             self.globaltrace = self.globaltrace_lt
  480.             self.localtrace = self.localtrace_trace
  481.         elif count:
  482.             self.globaltrace = self.globaltrace_lt
  483.             self.localtrace = self.localtrace_count
  484.         else:
  485.             # Ahem -- do nothing?  Okay.
  486.             self.donothing = 1
  487.  
  488.     def run(self, cmd):
  489.         import __main__
  490.         dict = __main__.__dict__
  491.         if not self.donothing:
  492.             sys.settrace(self.globaltrace)
  493.         try:
  494.             exec cmd in dict, dict
  495.         finally:
  496.             if not self.donothing:
  497.                 sys.settrace(None)
  498.  
  499.     def runctx(self, cmd, globals=None, locals=None):
  500.         if globals is None: globals = {}
  501.         if locals is None: locals = {}
  502.         if not self.donothing:
  503.             sys.settrace(gself.lobaltrace)
  504.         try:
  505.             exec cmd in dict, dict
  506.         finally:
  507.             if not self.donothing:
  508.                 sys.settrace(None)
  509.  
  510.     def runfunc(self, func, *args, **kw):
  511.         result = None
  512.         if not self.donothing:
  513.             sys.settrace(self.globaltrace)
  514.         try:
  515.             result = apply(func, args, kw)
  516.         finally:
  517.             if not self.donothing:
  518.                 sys.settrace(None)
  519.         return result
  520.  
  521.     def globaltrace_countfuncs(self, frame, why, arg):
  522.         """
  523.         Handles `call' events (why == 'call') and adds the (filename, modulename, funcname,) to the self._calledfuncs dict.
  524.         """
  525.         if why == 'call':
  526.             (filename, lineno, funcname, context, lineindex,) = inspect.getframeinfo(frame, 0)
  527.             if filename:
  528.                 modulename = inspect.getmodulename(filename)
  529.             else:
  530.                 modulename = None
  531.             self._calledfuncs[(filename, modulename, funcname,)] = 1
  532.  
  533.     def globaltrace_lt(self, frame, why, arg):
  534.         """
  535.         Handles `call' events (why == 'call') and if the code block being entered is to be ignored then it returns `None', else it returns `self.localtrace'.
  536.         """
  537.         if why == 'call':
  538.             (filename, lineno, funcname, context, lineindex,) = inspect.getframeinfo(frame, 0)
  539.             # if DEBUG_MODE and not filename:
  540.             #     print "%s.globaltrace(frame: %s, why: %s, arg: %s): filename: %s, lineno: %s, funcname: %s, context: %s, lineindex: %s\n" % (self, frame, why, arg, filename, lineno, funcname, context, lineindex,)
  541.             if filename:
  542.                 modulename = inspect.getmodulename(filename)
  543.                 ignore_it = self.ignore.names(filename, modulename)
  544.                 # if DEBUG_MODE and not self.blabbed.has_key((filename, modulename,)):
  545.                 #     self.blabbed[(filename, modulename,)] = None
  546.                 #     print "%s.globaltrace(frame: %s, why: %s, arg: %s, filename: %s, modulename: %s, ignore_it: %s\n" % (self, frame, why, arg, filename, modulename, ignore_it,)
  547.                 if not ignore_it:
  548.                     if self.trace:
  549.                         print " --- modulename: %s, funcname: %s" % (modulename, funcname,)
  550.                     # if DEBUG_MODE:
  551.                     #     print "%s.globaltrace(frame: %s, why: %s, arg: %s, filename: %s, modulename: %s, ignore_it: %s -- about to localtrace\n" % (self, frame, why, arg, filename, modulename, ignore_it,)
  552.                     return self.localtrace
  553.             else:
  554.                 # XXX why no filename?
  555.                 return None
  556.  
  557.     def localtrace_trace_and_count(self, frame, why, arg):
  558.         if why == 'line':
  559.             # record the file name and line number of every trace
  560.             # XXX I wish inspect offered me an optimized `getfilename(frame)' to use in place of the presumably heavier `getframeinfo()'.  --Zooko 2001-10-14
  561.             (filename, lineno, funcname, context, lineindex,) = inspect.getframeinfo(frame, 1)
  562.             key = (filename, lineno,)
  563.             self.counts[key] = self.counts.get(key, 0) + 1
  564.             # XXX not convinced that this memoizing is a performance win -- I don't know enough about Python guts to tell.  --Zooko 2001-10-14
  565.             bname = self.pathtobasename.get(filename)
  566.             if bname is None:
  567.                 # Using setdefault faster than two separate lines?  --Zooko 2001-10-14
  568.                 bname = self.pathtobasename.setdefault(filename, os.path.basename(filename))
  569.             try:
  570.                 print "%s(%d): %s" % (bname, lineno, context[lineindex],),
  571.             except IndexError:
  572.                 # Uh.. sometimes getframeinfo gives me a context of length 1 and a lineindex of -2.  Oh well.
  573.                 pass
  574.         return self.localtrace
  575.  
  576.     def localtrace_trace(self, frame, why, arg):
  577.         if why == 'line':
  578.             # XXX shouldn't do the count increment when arg is exception?  But be careful to return self.localtrace when arg is exception! ?  --Zooko 2001-10-14
  579.             # record the file name and line number of every trace
  580.             # XXX I wish inspect offered me an optimized `getfilename(frame)' to use in place of the presumably heavier `getframeinfo()'.  --Zooko 2001-10-14
  581.             (filename, lineno, funcname, context, lineindex,) = inspect.getframeinfo(frame)
  582.             # if DEBUG_MODE:
  583.             #     print "%s.localtrace_trace(frame: %s, why: %s, arg: %s); filename: %s, lineno: %s, funcname: %s, context: %s, lineindex: %s\n" % (self, frame, why, arg, filename, lineno, funcname, context, lineindex,)
  584.             # XXX not convinced that this memoizing is a performance win -- I don't know enough about Python guts to tell.  --Zooko 2001-10-14
  585.             bname = self.pathtobasename.get(filename)
  586.             if bname is None:
  587.                 # Using setdefault faster than two separate lines?  --Zooko 2001-10-14
  588.                 bname = self.pathtobasename.setdefault(filename, os.path.basename(filename))
  589.             try:
  590.                 print "%s(%d): %s" % (bname, lineno, context[lineindex],),
  591.             except IndexError:
  592.                 # Uh.. sometimes getframeinfo gives me a context of length 1 and a lineindex of -2.  Oh well.
  593.                 pass
  594.         return self.localtrace
  595.  
  596.     def localtrace_count(self, frame, why, arg):
  597.         if why == 'line':
  598.             # XXX shouldn't do the count increment when arg is exception?  But be careful to return self.localtrace when arg is exception! ?  --Zooko 2001-10-14
  599.             # record the file name and line number of every trace
  600.             # XXX I wish inspect offered me an optimized `getfilename(frame)' to use in place of the presumably heavier `getframeinfo()'.  --Zooko 2001-10-14
  601.             (filename, lineno, funcname, context, lineindex,) = inspect.getframeinfo(frame)
  602.             key = (filename, lineno,)
  603.             self.counts[key] = self.counts.get(key, 0) + 1
  604.         return self.localtrace
  605.  
  606.     def results(self):
  607.         return CoverageResults(self.counts, infile=self.infile, outfile=self.outfile, calledfuncs=self._calledfuncs)
  608.  
  609. def _err_exit(msg):
  610.     sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
  611.     sys.exit(1)
  612.  
  613. def main(argv=None):
  614.     import getopt
  615.  
  616.     if argv is None:
  617.         argv = sys.argv
  618.     try:
  619.         opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:l",
  620.                                         ["help", "version", "trace", "count",
  621.                                          "report", "no-report",
  622.                                          "file=", "missing",
  623.                                          "ignore-module=", "ignore-dir=",
  624.                                          "coverdir=", "listfuncs",])
  625.  
  626.     except getopt.error, msg:
  627.         sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
  628.         sys.stderr.write("Try `%s --help' for more information\n" % sys.argv[0])
  629.         sys.exit(1)
  630.  
  631.     trace = 0
  632.     count = 0
  633.     report = 0
  634.     no_report = 0
  635.     counts_file = None
  636.     missing = 0
  637.     ignore_modules = []
  638.     ignore_dirs = []
  639.     coverdir = None
  640.     summary = 0
  641.     listfuncs = false
  642.  
  643.     for opt, val in opts:
  644.         if opt == "--help":
  645.             usage(sys.stdout)
  646.             sys.exit(0)
  647.  
  648.         if opt == "--version":
  649.             sys.stdout.write("trace 2.0\n")
  650.             sys.exit(0)
  651.  
  652.         if opt == "-l" or opt == "--listfuncs":
  653.             listfuncs = true
  654.             continue
  655.  
  656.         if opt == "-t" or opt == "--trace":
  657.             trace = 1
  658.             continue
  659.  
  660.         if opt == "-c" or opt == "--count":
  661.             count = 1
  662.             continue
  663.  
  664.         if opt == "-r" or opt == "--report":
  665.             report = 1
  666.             continue
  667.  
  668.         if opt == "-R" or opt == "--no-report":
  669.             no_report = 1
  670.             continue
  671.  
  672.         if opt == "-f" or opt == "--file":
  673.             counts_file = val
  674.             continue
  675.  
  676.         if opt == "-m" or opt == "--missing":
  677.             missing = 1
  678.             continue
  679.  
  680.         if opt == "-C" or opt == "--coverdir":
  681.             coverdir = val
  682.             continue
  683.  
  684.         if opt == "-s" or opt == "--summary":
  685.             summary = 1
  686.             continue
  687.  
  688.         if opt == "--ignore-module":
  689.             ignore_modules.append(val)
  690.             continue
  691.  
  692.         if opt == "--ignore-dir":
  693.             for s in string.split(val, os.pathsep):
  694.                 s = os.path.expandvars(s)
  695.                 # should I also call expanduser? (after all, could use $HOME)
  696.  
  697.                 s = string.replace(s, "$prefix",
  698.                                    os.path.join(sys.prefix, "lib",
  699.                                                 "python" + sys.version[:3]))
  700.                 s = string.replace(s, "$exec_prefix",
  701.                                    os.path.join(sys.exec_prefix, "lib",
  702.                                                 "python" + sys.version[:3]))
  703.                 s = os.path.normpath(s)
  704.                 ignore_dirs.append(s)
  705.             continue
  706.  
  707.         assert 0, "Should never get here"
  708.  
  709.     if listfuncs and (count or trace):
  710.         _err_exit("cannot specify both --listfuncs and (--trace or --count)")
  711.  
  712.     if not count and not trace and not report and not listfuncs:
  713.         _err_exit("must specify one of --trace, --count, --report or --listfuncs")
  714.  
  715.     if report and no_report:
  716.         _err_exit("cannot specify both --report and --no-report")
  717.  
  718.     if report and not counts_file:
  719.         _err_exit("--report requires a --file")
  720.  
  721.     if no_report and len(prog_argv) == 0:
  722.         _err_exit("missing name of file to run")
  723.  
  724.     # everything is ready
  725.     if report:
  726.         results = CoverageResults(infile=counts_file, outfile=counts_file)
  727.         results.write_results(missing, summary=summary, coverdir=coverdir)
  728.     else:
  729.         sys.argv = prog_argv
  730.         progname = prog_argv[0]
  731.         if eval(sys.version[:3])>1.3:
  732.             sys.path[0] = os.path.split(progname)[0] # ???
  733.  
  734.         t = Trace(count, trace, countfuncs=listfuncs, ignoremods=ignore_modules, ignoredirs=ignore_dirs, infile=counts_file, outfile=counts_file)
  735.         try:
  736.             t.run('execfile(' + `progname` + ')')
  737.         except IOError, err:
  738.             _err_exit("Cannot run file %s because: %s" % (`sys.argv[0]`, err))
  739.         except SystemExit:
  740.             pass
  741.  
  742.         results = t.results()
  743.  
  744.         if not no_report:
  745.             results.write_results(missing, summary=summary, coverdir=coverdir)
  746.  
  747. if __name__=='__main__':
  748.     main()
  749.