# portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
# err... reserved and offered to the public under the terms of the
# Author: Zooko O'Whielacronx
# Copyright 2000, Mojam Media, Inc., all rights reserved.
# Copyright 1999, Bioreason, Inc., all rights reserved.
# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
# Permission to use, copy, modify, and distribute this Python software and
# its associated documentation for any purpose without fee is hereby
# granted, provided that the above copyright notice appears in all copies,
# and that both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of neither Automatrix,
# Bioreason or Mojam Media be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior permission.
"""program/module to trace Python program or function execution
Sample use, command line:
trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
trace.py -t --ignore-dir '$prefix' spam.py eggs
trace.py --trackcalls spam.py eggs
Sample use, programmatically
# create a Trace object, telling it what to ignore, and whether to
# do tracing or line-counting or both.
tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,],
# run the new command using the given tracer
# make a report, placing output in /tmp
r.write_results(show_missing=True, coverdir="/tmp")
__all__ = ['Trace', 'CoverageResults']
from time import monotonic as _time
PRAGMA_NOCOVER = "#pragma NO COVER"
def __init__(self, modules=None, dirs=None):
self._mods = set() if not modules else set(modules)
self._dirs = [] if not dirs else [os.path.normpath(d)
self._ignore = { '<string>': 1 }
def names(self, filename, modulename):
if modulename in self._ignore:
return self._ignore[modulename]
# haven't seen this one before, so see if the module name is
if modulename in self._mods: # Identical names, so ignore
self._ignore[modulename] = 1
# check if the module is a proper submodule of something on
# Need to take some care since ignoring
# "cmp" mustn't mean ignoring "cmpcache" but ignoring
# "Spam" must also mean ignoring "Spam.Eggs".
if modulename.startswith(mod + '.'):
self._ignore[modulename] = 1
# Now check that filename isn't in one of the directories
# must be a built-in, so we must ignore
self._ignore[modulename] = 1
# Ignore a file when it contains one of the ignorable paths
# The '+ os.sep' is to ensure that d is a parent directory,
# as compared to cases like:
# filename = "/usr/local.py"
# filename = "/usr/local.py"
if filename.startswith(d + os.sep):
self._ignore[modulename] = 1
# Tried the different ways, so we don't ignore this module
self._ignore[modulename] = 0
"""Return a plausible module name for the patch."""
base = os.path.basename(path)
filename, ext = os.path.splitext(base)
"""Return a plausible module name for the path."""
# If the file 'path' is part of a package, then the filename isn't
# enough to uniquely identify it. Try to do the right thing by
# looking in sys.path for the longest matching prefix. We'll
# assume that the rest is the package name.
comparepath = os.path.normcase(path)
dir = os.path.normcase(dir)
if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
if len(dir) > len(longest):
base = path[len(longest) + 1:]
# the drive letter is never part of the module name
drive, base = os.path.splitdrive(base)
base = base.replace(os.sep, ".")
base = base.replace(os.altsep, ".")
filename, ext = os.path.splitext(base)
return filename.lstrip(".")
def __init__(self, counts=None, calledfuncs=None, infile=None,
callers=None, outfile=None):
self.counter = self.counts.copy() # map (filename, lineno) to count
self.calledfuncs = calledfuncs
if self.calledfuncs is None:
self.calledfuncs = self.calledfuncs.copy()
self.callers = self.callers.copy()
# Try to merge existing counts file.
with open(self.infile, 'rb') as f:
counts, calledfuncs, callers = pickle.load(f)
self.update(self.__class__(counts, calledfuncs, callers))
except (OSError, EOFError, ValueError) as err:
print(("Skipping counts file %r: %s"
% (self.infile, err)), file=sys.stderr)
def is_ignored_filename(self, filename):
"""Return True if the filename does not refer to a file
we want to have reported.
return filename.startswith('<') and filename.endswith('>')
"""Merge in the data from another CoverageResults"""
calledfuncs = self.calledfuncs
other_counts = other.counts
other_calledfuncs = other.calledfuncs
other_callers = other.callers
counts[key] = counts.get(key, 0) + other_counts[key]
for key in other_calledfuncs:
for key in other_callers:
def write_results(self, show_missing=True, summary=False, coverdir=None):
Write the coverage results.
:param show_missing: Show lines that had no hits.
:param summary: Include coverage summary per module.
:param coverdir: If None, the results of each module are placed in its
directory, otherwise it is included in the directory
print("functions called:")
for filename, modulename, funcname in sorted(calls):
print(("filename: %s, modulename: %s, funcname: %s"
% (filename, modulename, funcname)))
print("calling relationships:")
lastfile = lastcfile = ""
for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \
print("***", pfile, "***")
if cfile != pfile and lastcfile != cfile:
print(" %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
# turn the counts data ("(filename, lineno) = count") into something
# accessible on a per-file basis
for filename, lineno in self.counts:
lines_hit = per_file[filename] = per_file.get(filename, {})
lines_hit[lineno] = self.counts[(filename, lineno)]
# accumulate summary info, if needed
for filename, count in per_file.items():
if self.is_ignored_filename(filename):
if filename.endswith(".pyc"):
dir = os.path.dirname(os.path.abspath(filename))
modulename = _modname(filename)
if not os.path.exists(dir):
modulename = _fullmodname(filename)
# If desired, get a list of the line numbers which represent
# executable content (returned as a dict for better lookup speed)
lnotab = _find_executable_linenos(filename)
source = linecache.getlines(filename)
coverpath = os.path.join(dir, modulename + ".cover")
with open(filename, 'rb') as fp:
encoding, _ = tokenize.detect_encoding(fp.readline)
n_hits, n_lines = self.write_results_file(coverpath, source,
percent = int(100 * n_hits / n_lines)
sums[modulename] = n_lines, percent, modulename, filename
print("lines cov% module (path)")
n_lines, percent, modulename, filename = sums[m]
print("%5d %3d%% %s (%s)" % sums[m])
# try and store counts and module info into self.outfile
with open(self.outfile, 'wb') as f:
pickle.dump((self.counts, self.calledfuncs, self.callers),
print("Can't save counts files because %s" % err, file=sys.stderr)
def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None):
"""Return a coverage results file in path."""
# ``lnotab`` is a dict of executable lines, or a line number "table"
outfile = open(path, "w", encoding=encoding)
print(("trace: Could not open %r for writing: %s "
"- skipping" % (path, err)), file=sys.stderr)
for lineno, line in enumerate(lines, 1):
# do the blank/comment match to try to mark more lines
# (help the reader find stuff that hasn't been covered)
outfile.write("%5d: " % lines_hit[lineno])
elif lineno in lnotab and not PRAGMA_NOCOVER in line:
# Highlight never-executed lines, unless the line contains
outfile.write(line.expandtabs(8))
def _find_lines_from_code(code, strs):
"""Return dict where keys are lines in the line number table."""
for _, lineno in dis.findlinestarts(code):
def _find_lines(code, strs):
"""Return lineno dict for all code objects reachable from code."""
# get all of the lineno information from the code of this scope level
linenos = _find_lines_from_code(code, strs)
# and check the constants for references to other code objects
# find another code object, so recurse into it
linenos.update(_find_lines(c, strs))
def _find_strings(filename, encoding=None):
"""Return a dict of possible docstring positions.
The dict maps line numbers to strings. There is an entry for
line that contains only a string or a part of a triple-quoted
# If the first token is a string, then it's the module docstring.
# Add this special case so that the test in the loop passes.
prev_ttype = token.INDENT
with open(filename, encoding=encoding) as f:
tok = tokenize.generate_tokens(f.readline)
for ttype, tstr, start, end, line in tok:
if ttype == token.STRING:
if prev_ttype == token.INDENT:
for i in range(sline, eline + 1):
def _find_executable_linenos(filename):
"""Return dict where keys are line numbers in the line number table."""
with tokenize.open(filename) as f:
print(("Not printing coverage data for %r: %s"
% (filename, err)), file=sys.stderr)
code = compile(prog, filename, "exec")
strs = _find_strings(filename, encoding)
return _find_lines(code, strs)
def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
ignoremods=(), ignoredirs=(), infile=None, outfile=None,
@param count true iff it should count number of times each
@param trace true iff it should print out each line that is
@param countfuncs true iff it should just output a list of
(filename, modulename, funcname,) for functions
that were called at least once; This overrides
@param ignoremods a list of the names of modules to ignore
@param ignoredirs a list of the names of directories to ignore
all of the (recursive) contents of
@param infile file from which to read stored counts to be
@param outfile file in which to write the results
@param timing true iff timing information be displayed
self.ignore = _Ignore(ignoremods, ignoredirs)
self.counts = {} # keys are (filename, linenumber)
self.pathtobasename = {} # for memoizing os.path.basename
self.start_time = _time()
self.globaltrace = self.globaltrace_trackcallers
self.globaltrace = self.globaltrace_countfuncs
self.globaltrace = self.globaltrace_lt
self.localtrace = self.localtrace_trace_and_count
self.globaltrace = self.globaltrace_lt
self.localtrace = self.localtrace_trace
self.globaltrace = self.globaltrace_lt
self.localtrace = self.localtrace_count
# Ahem -- do nothing? Okay.
self.runctx(cmd, dict, dict)
def runctx(self, cmd, globals=None, locals=None):
if globals is None: globals = {}
if locals is None: locals = {}
threading.settrace(self.globaltrace)
sys.settrace(self.globaltrace)
exec(cmd, globals, locals)
def runfunc(*args, **kw):
raise TypeError("descriptor 'runfunc' of 'Trace' object "
warnings.warn("Passing 'func' as keyword argument is deprecated",
DeprecationWarning, stacklevel=2)
raise TypeError('runfunc expected at least 1 positional argument, '
'got %d' % (len(args)-1))
sys.settrace(self.globaltrace)
result = func(*args, **kw)
runfunc.__text_signature__ = '($self, func, /, *args, **kw)'
def file_module_function_of(self, frame):
filename = code.co_filename
modulename = _modname(filename)
if code in self._caller_cache:
if self._caller_cache[code] is not None:
clsname = self._caller_cache[code]
self._caller_cache[code] = None
## use of gc.get_referrers() was suggested by Michael Hudson
# all functions which refer to this code object