trace.py 28.5 KB
Newer Older
1
#!/usr/bin/env python3
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

# portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
# err...  reserved and offered to the public under the terms of the
# Python 2.2 license.
# Author: Zooko O'Whielacronx
# http://zooko.com/
# mailto:zooko@zooko.com
#
# Copyright 2000, Mojam Media, Inc., all rights reserved.
# Author: Skip Montanaro
#
# Copyright 1999, Bioreason, Inc., all rights reserved.
# Author: Andrew Dalke
#
# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
# Author: Skip Montanaro
#
# 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
35
  trace.py --trackcalls spam.py eggs
36 37

Sample use, programmatically
38 39 40 41
  import sys

  # create a Trace object, telling it what to ignore, and whether to
  # do tracing or line-counting or both.
42 43
  tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,],
                       trace=0, count=1)
44 45 46 47 48
  # run the new command using the given tracer
  tracer.run('main()')
  # make a report, placing output in /tmp
  r = tracer.results()
  r.write_results(show_missing=True, coverdir="/tmp")
49
"""
50
__all__ = ['Trace', 'CoverageResults']
51

Jeremy Hylton's avatar
Jeremy Hylton committed
52 53 54 55 56
import linecache
import os
import sys
import token
import tokenize
57
import inspect
58
import gc
59
import dis
60
import pickle
61
from time import monotonic as _time
62

63
import threading
64

Jeremy Hylton's avatar
Jeremy Hylton committed
65 66
PRAGMA_NOCOVER = "#pragma NO COVER"

67
class _Ignore:
68 69 70 71
    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)
                                          for d in dirs]
72 73 74
        self._ignore = { '<string>': 1 }

    def names(self, filename, modulename):
75
        if modulename in self._ignore:
76 77 78
            return self._ignore[modulename]

        # haven't seen this one before, so see if the module name is
79 80 81 82 83 84 85
        # on the ignore list.
        if modulename in self._mods:  # Identical names, so ignore
            self._ignore[modulename] = 1
            return 1

        # check if the module is a proper submodule of something on
        # the ignore list
86
        for mod in self._mods:
87 88 89 90
            # 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 + '.'):
91 92 93
                self._ignore[modulename] = 1
                return 1

94
        # Now check that filename isn't in one of the directories
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
        if filename is None:
            # must be a built-in, so we must ignore
            self._ignore[modulename] = 1
            return 1

        # Ignore a file when it contains one of the ignorable paths
        for d in self._dirs:
            # The '+ os.sep' is to ensure that d is a parent directory,
            # as compared to cases like:
            #  d = "/usr/local"
            #  filename = "/usr/local.py"
            # or
            #  d = "/usr/local.py"
            #  filename = "/usr/local.py"
            if filename.startswith(d + os.sep):
                self._ignore[modulename] = 1
                return 1

        # Tried the different ways, so we don't ignore this module
        self._ignore[modulename] = 0
        return 0

117
def _modname(path):
Jeremy Hylton's avatar
Jeremy Hylton committed
118
    """Return a plausible module name for the patch."""
119

Jeremy Hylton's avatar
Jeremy Hylton committed
120 121 122 123
    base = os.path.basename(path)
    filename, ext = os.path.splitext(base)
    return filename

124
def _fullmodname(path):
125
    """Return a plausible module name for the path."""
126 127 128 129 130 131

    # 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.

132
    comparepath = os.path.normcase(path)
133 134
    longest = ""
    for dir in sys.path:
135 136
        dir = os.path.normcase(dir)
        if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
137 138 139
            if len(dir) > len(longest):
                longest = dir

140 141 142 143
    if longest:
        base = path[len(longest) + 1:]
    else:
        base = path
144 145
    # the drive letter is never part of the module name
    drive, base = os.path.splitdrive(base)
146 147 148
    base = base.replace(os.sep, ".")
    if os.altsep:
        base = base.replace(os.altsep, ".")
149
    filename, ext = os.path.splitext(base)
150
    return filename.lstrip(".")
151

152 153
class CoverageResults:
    def __init__(self, counts=None, calledfuncs=None, infile=None,
154
                 callers=None, outfile=None):
155 156 157 158 159 160 161 162
        self.counts = counts
        if self.counts is None:
            self.counts = {}
        self.counter = self.counts.copy() # map (filename, lineno) to count
        self.calledfuncs = calledfuncs
        if self.calledfuncs is None:
            self.calledfuncs = {}
        self.calledfuncs = self.calledfuncs.copy()
163 164 165 166
        self.callers = callers
        if self.callers is None:
            self.callers = {}
        self.callers = self.callers.copy()
167 168 169
        self.infile = infile
        self.outfile = outfile
        if self.infile:
170
            # Try to merge existing counts file.
171
            try:
172 173
                with open(self.infile, 'rb') as f:
                    counts, calledfuncs, callers = pickle.load(f)
174
                self.update(self.__class__(counts, calledfuncs, callers))
175
            except (OSError, EOFError, ValueError) as err:
176 177
                print(("Skipping counts file %r: %s"
                                      % (self.infile, err)), file=sys.stderr)
178

179 180 181 182
    def is_ignored_filename(self, filename):
        """Return True if the filename does not refer to a file
        we want to have reported.
        """
183
        return filename.startswith('<') and filename.endswith('>')
184

185 186 187 188
    def update(self, other):
        """Merge in the data from another CoverageResults"""
        counts = self.counts
        calledfuncs = self.calledfuncs
189
        callers = self.callers
190 191
        other_counts = other.counts
        other_calledfuncs = other.calledfuncs
192
        other_callers = other.callers
193

194
        for key in other_counts:
Jeremy Hylton's avatar
Jeremy Hylton committed
195
            counts[key] = counts.get(key, 0) + other_counts[key]
196

197
        for key in other_calledfuncs:
198 199
            calledfuncs[key] = 1

200
        for key in other_callers:
201 202
            callers[key] = 1

Jeremy Hylton's avatar
Jeremy Hylton committed
203
    def write_results(self, show_missing=True, summary=False, coverdir=None):
204
        """
205 206 207 208
        Write the coverage results.

        :param show_missing: Show lines that had no hits.
        :param summary: Include coverage summary per module.
209
        :param coverdir: If None, the results of each module are placed in its
210 211
                         directory, otherwise it is included in the directory
                         specified.
212
        """
213
        if self.calledfuncs:
214 215
            print()
            print("functions called:")
216
            calls = self.calledfuncs
217
            for filename, modulename, funcname in sorted(calls):
218 219
                print(("filename: %s, modulename: %s, funcname: %s"
                       % (filename, modulename, funcname)))
220 221

        if self.callers:
222 223
            print()
            print("calling relationships:")
224
            lastfile = lastcfile = ""
225
            for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \
226
                    in sorted(self.callers):
227
                if pfile != lastfile:
228 229
                    print()
                    print("***", pfile, "***")
230 231 232
                    lastfile = pfile
                    lastcfile = ""
                if cfile != pfile and lastcfile != cfile:
233
                    print("  -->", cfile)
234
                    lastcfile = cfile
235
                print("    %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
236 237 238 239

        # turn the counts data ("(filename, lineno) = count") into something
        # accessible on a per-file basis
        per_file = {}
240
        for filename, lineno in self.counts:
Jeremy Hylton's avatar
Jeremy Hylton committed
241 242
            lines_hit = per_file[filename] = per_file.get(filename, {})
            lines_hit[lineno] = self.counts[(filename, lineno)]
243 244 245 246

        # accumulate summary info, if needed
        sums = {}

247
        for filename, count in per_file.items():
248
            if self.is_ignored_filename(filename):
249
                continue
250

251
            if filename.endswith(".pyc"):
252 253
                filename = filename[:-1]

Jeremy Hylton's avatar
Jeremy Hylton committed
254 255
            if coverdir is None:
                dir = os.path.dirname(os.path.abspath(filename))
256
                modulename = _modname(filename)
257
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
258 259 260
                dir = coverdir
                if not os.path.exists(dir):
                    os.makedirs(dir)
261
                modulename = _fullmodname(filename)
262 263 264 265

            # If desired, get a list of the line numbers which represent
            # executable content (returned as a dict for better lookup speed)
            if show_missing:
266
                lnotab = _find_executable_linenos(filename)
267
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
268
                lnotab = {}
269 270 271 272 273 274 275 276 277
            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,
                                                      lnotab, count, encoding)
            if summary and n_lines:
                percent = int(100 * n_hits / n_lines)
                sums[modulename] = n_lines, percent, modulename, filename
278 279 280


        if summary and sums:
281
            print("lines   cov%   module   (path)")
282
            for m in sorted(sums):
283
                n_lines, percent, modulename, filename = sums[m]
284
                print("%5d   %3d%%   %s   (%s)" % sums[m])
285 286 287 288

        if self.outfile:
            # try and store counts and module info into self.outfile
            try:
289
                pickle.dump((self.counts, self.calledfuncs, self.callers),
290
                            open(self.outfile, 'wb'), 1)
291
            except OSError as err:
292
                print("Can't save counts files because %s" % err, file=sys.stderr)
Jeremy Hylton's avatar
Jeremy Hylton committed
293

294
    def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
295
        """Return a coverage results file in path."""
296
        # ``lnotab`` is a dict of executable lines, or a line number "table"
297

Jeremy Hylton's avatar
Jeremy Hylton committed
298
        try:
299
            outfile = open(path, "w", encoding=encoding)
300
        except OSError as err:
301
            print(("trace: Could not open %r for writing: %s "
302
                                  "- skipping" % (path, err)), file=sys.stderr)
303
            return 0, 0
Jeremy Hylton's avatar
Jeremy Hylton committed
304 305 306

        n_lines = 0
        n_hits = 0
307 308 309 310 311 312 313
        with outfile:
            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)
                if lineno in lines_hit:
                    outfile.write("%5d: " % lines_hit[lineno])
                    n_hits += 1
314
                    n_lines += 1
315 316
                elif lineno in lnotab and not PRAGMA_NOCOVER in line:
                    # Highlight never-executed lines, unless the line contains
317
                    # #pragma: NO COVER
318 319 320 321
                    outfile.write(">>>>>> ")
                    n_lines += 1
                else:
                    outfile.write("       ")
322
                outfile.write(line.expandtabs(8))
Jeremy Hylton's avatar
Jeremy Hylton committed
323 324 325

        return n_hits, n_lines

326
def _find_lines_from_code(code, strs):
Jeremy Hylton's avatar
Jeremy Hylton committed
327
    """Return dict where keys are lines in the line number table."""
328 329
    linenos = {}

330
    for _, lineno in dis.findlinestarts(code):
Jeremy Hylton's avatar
Jeremy Hylton committed
331 332
        if lineno not in strs:
            linenos[lineno] = 1
333 334 335

    return linenos

336
def _find_lines(code, strs):
Jeremy Hylton's avatar
Jeremy Hylton committed
337
    """Return lineno dict for all code objects reachable from code."""
338
    # get all of the lineno information from the code of this scope level
339
    linenos = _find_lines_from_code(code, strs)
340 341 342

    # and check the constants for references to other code objects
    for c in code.co_consts:
343
        if inspect.iscode(c):
344
            # find another code object, so recurse into it
345
            linenos.update(_find_lines(c, strs))
346 347
    return linenos

348
def _find_strings(filename, encoding=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
349
    """Return a dict of possible docstring positions.
350

Jeremy Hylton's avatar
Jeremy Hylton committed
351 352 353
    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
    string.
354
    """
Jeremy Hylton's avatar
Jeremy Hylton committed
355 356 357 358
    d = {}
    # 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
359 360 361 362 363 364 365 366 367 368
    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:
                    sline, scol = start
                    eline, ecol = end
                    for i in range(sline, eline + 1):
                        d[i] = 1
            prev_ttype = ttype
Jeremy Hylton's avatar
Jeremy Hylton committed
369
    return d
370

371
def _find_executable_linenos(filename):
Jeremy Hylton's avatar
Jeremy Hylton committed
372 373
    """Return dict where keys are line numbers in the line number table."""
    try:
374
        with tokenize.open(filename) as f:
375
            prog = f.read()
376
            encoding = f.encoding
377
    except OSError as err:
378 379
        print(("Not printing coverage data for %r: %s"
                              % (filename, err)), file=sys.stderr)
Jeremy Hylton's avatar
Jeremy Hylton committed
380 381
        return {}
    code = compile(prog, filename, "exec")
382 383
    strs = _find_strings(filename, encoding)
    return _find_lines(code, strs)
384 385

class Trace:
386
    def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
Christian Heimes's avatar
Christian Heimes committed
387 388
                 ignoremods=(), ignoredirs=(), infile=None, outfile=None,
                 timing=False):
389 390
        """
        @param count true iff it should count number of times each
Tim Peters's avatar
Tim Peters committed
391
                     line is executed
392
        @param trace true iff it should print out each line that is
Tim Peters's avatar
Tim Peters committed
393
                     being counted
394 395 396
        @param countfuncs true iff it should just output a list of
                     (filename, modulename, funcname,) for functions
                     that were called at least once;  This overrides
Tim Peters's avatar
Tim Peters committed
397
                     `count' and `trace'
398 399
        @param ignoremods a list of the names of modules to ignore
        @param ignoredirs a list of the names of directories to ignore
Tim Peters's avatar
Tim Peters committed
400
                     all of the (recursive) contents of
401
        @param infile file from which to read stored counts to be
Tim Peters's avatar
Tim Peters committed
402
                     added into the results
403
        @param outfile file in which to write the results
Christian Heimes's avatar
Christian Heimes committed
404
        @param timing true iff timing information be displayed
405 406 407
        """
        self.infile = infile
        self.outfile = outfile
408
        self.ignore = _Ignore(ignoremods, ignoredirs)
409 410 411 412 413
        self.counts = {}   # keys are (filename, linenumber)
        self.pathtobasename = {} # for memoizing os.path.basename
        self.donothing = 0
        self.trace = trace
        self._calledfuncs = {}
414
        self._callers = {}
415
        self._caller_cache = {}
Christian Heimes's avatar
Christian Heimes committed
416 417
        self.start_time = None
        if timing:
418
            self.start_time = _time()
419 420 421
        if countcallers:
            self.globaltrace = self.globaltrace_trackcallers
        elif countfuncs:
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
            self.globaltrace = self.globaltrace_countfuncs
        elif trace and count:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_trace_and_count
        elif trace:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_trace
        elif count:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_count
        else:
            # Ahem -- do nothing?  Okay.
            self.donothing = 1

    def run(self, cmd):
        import __main__
        dict = __main__.__dict__
439
        self.runctx(cmd, dict, dict)
440 441 442 443 444

    def runctx(self, cmd, globals=None, locals=None):
        if globals is None: globals = {}
        if locals is None: locals = {}
        if not self.donothing:
445 446
            threading.settrace(self.globaltrace)
            sys.settrace(self.globaltrace)
447
        try:
448
            exec(cmd, globals, locals)
449 450
        finally:
            if not self.donothing:
451 452
                sys.settrace(None)
                threading.settrace(None)
453

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    def runfunc(*args, **kw):
        if len(args) >= 2:
            self, func, *args = args
        elif not args:
            raise TypeError("descriptor 'runfunc' of 'Trace' object "
                            "needs an argument")
        elif 'func' in kw:
            func = kw.pop('func')
            self, *args = args
            import warnings
            warnings.warn("Passing 'func' as keyword argument is deprecated",
                          DeprecationWarning, stacklevel=2)
        else:
            raise TypeError('runfunc expected at least 1 positional argument, '
                            'got %d' % (len(args)-1))

470 471 472 473
        result = None
        if not self.donothing:
            sys.settrace(self.globaltrace)
        try:
474
            result = func(*args, **kw)
475 476 477 478 479
        finally:
            if not self.donothing:
                sys.settrace(None)
        return result

480 481 482 483
    def file_module_function_of(self, frame):
        code = frame.f_code
        filename = code.co_filename
        if filename:
484
            modulename = _modname(filename)
485 486 487 488 489 490 491 492 493 494 495 496 497
        else:
            modulename = None

        funcname = code.co_name
        clsname = None
        if code in self._caller_cache:
            if self._caller_cache[code] is not None:
                clsname = self._caller_cache[code]
        else:
            self._caller_cache[code] = None
            ## use of gc.get_referrers() was suggested by Michael Hudson
            # all functions which refer to this code object
            funcs = [f for f in gc.get_referrers(code)
498
                         if inspect.isfunction(f)]
499 500 501 502 503 504 505 506 507 508 509
            # require len(func) == 1 to avoid ambiguity caused by calls to
            # new.function(): "In the face of ambiguity, refuse the
            # temptation to guess."
            if len(funcs) == 1:
                dicts = [d for d in gc.get_referrers(funcs[0])
                             if isinstance(d, dict)]
                if len(dicts) == 1:
                    classes = [c for c in gc.get_referrers(dicts[0])
                                   if hasattr(c, "__bases__")]
                    if len(classes) == 1:
                        # ditto for new.classobj()
510
                        clsname = classes[0].__name__
511 512 513 514 515 516 517 518 519 520
                        # cache the result - assumption is that new.* is
                        # not called later to disturb this relationship
                        # _caller_cache could be flushed if functions in
                        # the new module get called.
                        self._caller_cache[code] = clsname
        if clsname is not None:
            funcname = "%s.%s" % (clsname, funcname)

        return filename, modulename, funcname

521 522 523 524 525 526 527
    def globaltrace_trackcallers(self, frame, why, arg):
        """Handler for call events.

        Adds information about who called who to the self._callers dict.
        """
        if why == 'call':
            # XXX Should do a better job of identifying methods
528 529
            this_func = self.file_module_function_of(frame)
            parent_func = self.file_module_function_of(frame.f_back)
530 531
            self._callers[(parent_func, this_func)] = 1

532
    def globaltrace_countfuncs(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
533
        """Handler for call events.
Tim Peters's avatar
Tim Peters committed
534

Jeremy Hylton's avatar
Jeremy Hylton committed
535
        Adds (filename, modulename, funcname) to the self._calledfuncs dict.
536 537
        """
        if why == 'call':
538 539
            this_func = self.file_module_function_of(frame)
            self._calledfuncs[this_func] = 1
540 541

    def globaltrace_lt(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
542 543 544 545
        """Handler for call events.

        If the code block being entered is to be ignored, returns `None',
        else returns self.localtrace.
546 547
        """
        if why == 'call':
Jeremy Hylton's avatar
Jeremy Hylton committed
548
            code = frame.f_code
549
            filename = frame.f_globals.get('__file__', None)
550
            if filename:
551
                # XXX _modname() doesn't work right for packages, so
552
                # the ignore support won't work right for packages
553
                modulename = _modname(filename)
554 555 556 557
                if modulename is not None:
                    ignore_it = self.ignore.names(filename, modulename)
                    if not ignore_it:
                        if self.trace:
558 559
                            print((" --- modulename: %s, funcname: %s"
                                   % (modulename, code.co_name)))
560 561 562 563 564
                        return self.localtrace
            else:
                return None

    def localtrace_trace_and_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
565
        if why == "line":
566
            # record the file name and line number of every trace
Jeremy Hylton's avatar
Jeremy Hylton committed
567 568
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
569 570
            key = filename, lineno
            self.counts[key] = self.counts.get(key, 0) + 1
Tim Peters's avatar
Tim Peters committed
571

Christian Heimes's avatar
Christian Heimes committed
572
            if self.start_time:
573
                print('%.2f' % (_time() - self.start_time), end=' ')
Jeremy Hylton's avatar
Jeremy Hylton committed
574
            bname = os.path.basename(filename)
575
            print("%s(%d): %s" % (bname, lineno,
Georg Brandl's avatar
Georg Brandl committed
576
                                  linecache.getline(filename, lineno)), end='')
577 578 579
        return self.localtrace

    def localtrace_trace(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
580 581 582 583 584
        if why == "line":
            # record the file name and line number of every trace
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno

Christian Heimes's avatar
Christian Heimes committed
585
            if self.start_time:
586
                print('%.2f' % (_time() - self.start_time), end=' ')
Jeremy Hylton's avatar
Jeremy Hylton committed
587
            bname = os.path.basename(filename)
588
            print("%s(%d): %s" % (bname, lineno,
Georg Brandl's avatar
Georg Brandl committed
589
                                  linecache.getline(filename, lineno)), end='')
590 591 592
        return self.localtrace

    def localtrace_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
593
        if why == "line":
594 595 596 597 598 599 600 601 602
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
            key = filename, lineno
            self.counts[key] = self.counts.get(key, 0) + 1
        return self.localtrace

    def results(self):
        return CoverageResults(self.counts, infile=self.infile,
                               outfile=self.outfile,
603 604
                               calledfuncs=self._calledfuncs,
                               callers=self._callers)
605

606
def main():
607
    import argparse
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660

    parser = argparse.ArgumentParser()
    parser.add_argument('--version', action='version', version='trace 2.0')

    grp = parser.add_argument_group('Main options',
            'One of these (or --report) must be given')

    grp.add_argument('-c', '--count', action='store_true',
            help='Count the number of times each line is executed and write '
                 'the counts to <module>.cover for each module executed, in '
                 'the module\'s directory. See also --coverdir, --file, '
                 '--no-report below.')
    grp.add_argument('-t', '--trace', action='store_true',
            help='Print each line to sys.stdout before it is executed')
    grp.add_argument('-l', '--listfuncs', action='store_true',
            help='Keep track of which functions are executed at least once '
                 'and write the results to sys.stdout after the program exits. '
                 'Cannot be specified alongside --trace or --count.')
    grp.add_argument('-T', '--trackcalls', action='store_true',
            help='Keep track of caller/called pairs and write the results to '
                 'sys.stdout after the program exits.')

    grp = parser.add_argument_group('Modifiers')

    _grp = grp.add_mutually_exclusive_group()
    _grp.add_argument('-r', '--report', action='store_true',
            help='Generate a report from a counts file; does not execute any '
                 'code. --file must specify the results file to read, which '
                 'must have been created in a previous run with --count '
                 '--file=FILE')
    _grp.add_argument('-R', '--no-report', action='store_true',
            help='Do not generate the coverage report files. '
                 'Useful if you want to accumulate over several runs.')

    grp.add_argument('-f', '--file',
            help='File to accumulate counts over several runs')
    grp.add_argument('-C', '--coverdir',
            help='Directory where the report files go. The coverage report '
                 'for <package>.<module> will be written to file '
                 '<dir>/<package>/<module>.cover')
    grp.add_argument('-m', '--missing', action='store_true',
            help='Annotate executable lines that were not executed with '
                 '">>>>>> "')
    grp.add_argument('-s', '--summary', action='store_true',
            help='Write a brief summary for each file to sys.stdout. '
                 'Can only be used with --count or --report')
    grp.add_argument('-g', '--timing', action='store_true',
            help='Prefix each line with the time since the program started. '
                 'Only used while tracing')

    grp = parser.add_argument_group('Filters',
            'Can be specified multiple times')
    grp.add_argument('--ignore-module', action='append', default=[],
661
            help='Ignore the given module(s) and its submodules '
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
                 '(if it is a package). Accepts comma separated list of '
                 'module names.')
    grp.add_argument('--ignore-dir', action='append', default=[],
            help='Ignore files in the given directory '
                 '(multiple directories can be joined by os.pathsep).')

    parser.add_argument('filename', nargs='?',
            help='file to run as main program')
    parser.add_argument('arguments', nargs=argparse.REMAINDER,
            help='arguments to the program')

    opts = parser.parse_args()

    if opts.ignore_dir:
        rel_path = 'lib', 'python{0.major}.{0.minor}'.format(sys.version_info)
        _prefix = os.path.join(sys.base_prefix, *rel_path)
        _exec_prefix = os.path.join(sys.base_exec_prefix, *rel_path)

    def parse_ignore_dir(s):
        s = os.path.expanduser(os.path.expandvars(s))
        s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix)
        return os.path.normpath(s)

    opts.ignore_module = [mod.strip()
                          for i in opts.ignore_module for mod in i.split(',')]
    opts.ignore_dir = [parse_ignore_dir(s)
                       for i in opts.ignore_dir for s in i.split(os.pathsep)]

    if opts.report:
        if not opts.file:
            parser.error('-r/--report requires -f/--file')
        results = CoverageResults(infile=opts.file, outfile=opts.file)
        return results.write_results(opts.missing, opts.summary, opts.coverdir)

    if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]):
        parser.error('must specify one of --trace, --count, --report, '
                     '--listfuncs, or --trackcalls')

    if opts.listfuncs and (opts.count or opts.trace):
        parser.error('cannot specify both --listfuncs and (--trace or --count)')

    if opts.summary and not opts.count:
        parser.error('--summary can only be used with --count or --report')

    if opts.filename is None:
        parser.error('filename is missing: required with the main options')

709
    sys.argv = [opts.filename, *opts.arguments]
710 711 712 713 714 715
    sys.path[0] = os.path.dirname(opts.filename)

    t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs,
              countcallers=opts.trackcalls, ignoremods=opts.ignore_module,
              ignoredirs=opts.ignore_dir, infile=opts.file,
              outfile=opts.file, timing=opts.timing)
716
    try:
717 718 719 720 721 722 723 724 725 726 727 728 729 730
        with open(opts.filename) as fp:
            code = compile(fp.read(), opts.filename, 'exec')
        # try to emulate __main__ namespace as much as possible
        globs = {
            '__file__': opts.filename,
            '__name__': '__main__',
            '__package__': None,
            '__cached__': None,
        }
        t.runctx(code, globs, globs)
    except OSError as err:
        sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err))
    except SystemExit:
        pass
731

732
    results = t.results()
733

734 735
    if not opts.no_report:
        results.write_results(opts.missing, opts.summary, opts.coverdir)
736 737 738

if __name__=='__main__':
    main()