Home | History | Annotate | Download | only in Lib
      1 #!/usr/bin/env python3
      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 (at] 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 """program/module to trace Python program or function execution
     31 
     32 Sample use, command line:
     33   trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
     34   trace.py -t --ignore-dir '$prefix' spam.py eggs
     35   trace.py --trackcalls spam.py eggs
     36 
     37 Sample use, programmatically
     38   import sys
     39 
     40   # create a Trace object, telling it what to ignore, and whether to
     41   # do tracing or line-counting or both.
     42   tracer = trace.Trace(ignoredirs=[sys.base_prefix, sys.base_exec_prefix,],
     43                        trace=0, count=1)
     44   # run the new command using the given tracer
     45   tracer.run('main()')
     46   # make a report, placing output in /tmp
     47   r = tracer.results()
     48   r.write_results(show_missing=True, coverdir="/tmp")
     49 """
     50 __all__ = ['Trace', 'CoverageResults']
     51 import argparse
     52 import linecache
     53 import os
     54 import re
     55 import sys
     56 import token
     57 import tokenize
     58 import inspect
     59 import gc
     60 import dis
     61 import pickle
     62 from time import monotonic as _time
     63 
     64 try:
     65     import threading
     66 except ImportError:
     67     _settrace = sys.settrace
     68 
     69     def _unsettrace():
     70         sys.settrace(None)
     71 else:
     72     def _settrace(func):
     73         threading.settrace(func)
     74         sys.settrace(func)
     75 
     76     def _unsettrace():
     77         sys.settrace(None)
     78         threading.settrace(None)
     79 
     80 PRAGMA_NOCOVER = "#pragma NO COVER"
     81 
     82 # Simple rx to find lines with no code.
     83 rx_blank = re.compile(r'^\s*(#.*)?$')
     84 
     85 class _Ignore:
     86     def __init__(self, modules=None, dirs=None):
     87         self._mods = set() if not modules else set(modules)
     88         self._dirs = [] if not dirs else [os.path.normpath(d)
     89                                           for d in dirs]
     90         self._ignore = { '<string>': 1 }
     91 
     92     def names(self, filename, modulename):
     93         if modulename in self._ignore:
     94             return self._ignore[modulename]
     95 
     96         # haven't seen this one before, so see if the module name is
     97         # on the ignore list.
     98         if modulename in self._mods:  # Identical names, so ignore
     99             self._ignore[modulename] = 1
    100             return 1
    101 
    102         # check if the module is a proper submodule of something on
    103         # the ignore list
    104         for mod in self._mods:
    105             # Need to take some care since ignoring
    106             # "cmp" mustn't mean ignoring "cmpcache" but ignoring
    107             # "Spam" must also mean ignoring "Spam.Eggs".
    108             if modulename.startswith(mod + '.'):
    109                 self._ignore[modulename] = 1
    110                 return 1
    111 
    112         # Now check that filename isn't in one of the directories
    113         if filename is None:
    114             # must be a built-in, so we must ignore
    115             self._ignore[modulename] = 1
    116             return 1
    117 
    118         # Ignore a file when it contains one of the ignorable paths
    119         for d in self._dirs:
    120             # The '+ os.sep' is to ensure that d is a parent directory,
    121             # as compared to cases like:
    122             #  d = "/usr/local"
    123             #  filename = "/usr/local.py"
    124             # or
    125             #  d = "/usr/local.py"
    126             #  filename = "/usr/local.py"
    127             if filename.startswith(d + os.sep):
    128                 self._ignore[modulename] = 1
    129                 return 1
    130 
    131         # Tried the different ways, so we don't ignore this module
    132         self._ignore[modulename] = 0
    133         return 0
    134 
    135 def _modname(path):
    136     """Return a plausible module name for the patch."""
    137 
    138     base = os.path.basename(path)
    139     filename, ext = os.path.splitext(base)
    140     return filename
    141 
    142 def _fullmodname(path):
    143     """Return a plausible module name for the path."""
    144 
    145     # If the file 'path' is part of a package, then the filename isn't
    146     # enough to uniquely identify it.  Try to do the right thing by
    147     # looking in sys.path for the longest matching prefix.  We'll
    148     # assume that the rest is the package name.
    149 
    150     comparepath = os.path.normcase(path)
    151     longest = ""
    152     for dir in sys.path:
    153         dir = os.path.normcase(dir)
    154         if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
    155             if len(dir) > len(longest):
    156                 longest = dir
    157 
    158     if longest:
    159         base = path[len(longest) + 1:]
    160     else:
    161         base = path
    162     # the drive letter is never part of the module name
    163     drive, base = os.path.splitdrive(base)
    164     base = base.replace(os.sep, ".")
    165     if os.altsep:
    166         base = base.replace(os.altsep, ".")
    167     filename, ext = os.path.splitext(base)
    168     return filename.lstrip(".")
    169 
    170 class CoverageResults:
    171     def __init__(self, counts=None, calledfuncs=None, infile=None,
    172                  callers=None, outfile=None):
    173         self.counts = counts
    174         if self.counts is None:
    175             self.counts = {}
    176         self.counter = self.counts.copy() # map (filename, lineno) to count
    177         self.calledfuncs = calledfuncs
    178         if self.calledfuncs is None:
    179             self.calledfuncs = {}
    180         self.calledfuncs = self.calledfuncs.copy()
    181         self.callers = callers
    182         if self.callers is None:
    183             self.callers = {}
    184         self.callers = self.callers.copy()
    185         self.infile = infile
    186         self.outfile = outfile
    187         if self.infile:
    188             # Try to merge existing counts file.
    189             try:
    190                 with open(self.infile, 'rb') as f:
    191                     counts, calledfuncs, callers = pickle.load(f)
    192                 self.update(self.__class__(counts, calledfuncs, callers))
    193             except (OSError, EOFError, ValueError) as err:
    194                 print(("Skipping counts file %r: %s"
    195                                       % (self.infile, err)), file=sys.stderr)
    196 
    197     def is_ignored_filename(self, filename):
    198         """Return True if the filename does not refer to a file
    199         we want to have reported.
    200         """
    201         return filename.startswith('<') and filename.endswith('>')
    202 
    203     def update(self, other):
    204         """Merge in the data from another CoverageResults"""
    205         counts = self.counts
    206         calledfuncs = self.calledfuncs
    207         callers = self.callers
    208         other_counts = other.counts
    209         other_calledfuncs = other.calledfuncs
    210         other_callers = other.callers
    211 
    212         for key in other_counts:
    213             counts[key] = counts.get(key, 0) + other_counts[key]
    214 
    215         for key in other_calledfuncs:
    216             calledfuncs[key] = 1
    217 
    218         for key in other_callers:
    219             callers[key] = 1
    220 
    221     def write_results(self, show_missing=True, summary=False, coverdir=None):
    222         """
    223         Write the coverage results.
    224 
    225         :param show_missing: Show lines that had no hits.
    226         :param summary: Include coverage summary per module.
    227         :param coverdir: If None, the results of each module are placed in its
    228                          directory, otherwise it is included in the directory
    229                          specified.
    230         """
    231         if self.calledfuncs:
    232             print()
    233             print("functions called:")
    234             calls = self.calledfuncs
    235             for filename, modulename, funcname in sorted(calls):
    236                 print(("filename: %s, modulename: %s, funcname: %s"
    237                        % (filename, modulename, funcname)))
    238 
    239         if self.callers:
    240             print()
    241             print("calling relationships:")
    242             lastfile = lastcfile = ""
    243             for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \
    244                     in sorted(self.callers):
    245                 if pfile != lastfile:
    246                     print()
    247                     print("***", pfile, "***")
    248                     lastfile = pfile
    249                     lastcfile = ""
    250                 if cfile != pfile and lastcfile != cfile:
    251                     print("  -->", cfile)
    252                     lastcfile = cfile
    253                 print("    %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
    254 
    255         # turn the counts data ("(filename, lineno) = count") into something
    256         # accessible on a per-file basis
    257         per_file = {}
    258         for filename, lineno in self.counts:
    259             lines_hit = per_file[filename] = per_file.get(filename, {})
    260             lines_hit[lineno] = self.counts[(filename, lineno)]
    261 
    262         # accumulate summary info, if needed
    263         sums = {}
    264 
    265         for filename, count in per_file.items():
    266             if self.is_ignored_filename(filename):
    267                 continue
    268 
    269             if filename.endswith(".pyc"):
    270                 filename = filename[:-1]
    271 
    272             if coverdir is None:
    273                 dir = os.path.dirname(os.path.abspath(filename))
    274                 modulename = _modname(filename)
    275             else:
    276                 dir = coverdir
    277                 if not os.path.exists(dir):
    278                     os.makedirs(dir)
    279                 modulename = _fullmodname(filename)
    280 
    281             # If desired, get a list of the line numbers which represent
    282             # executable content (returned as a dict for better lookup speed)
    283             if show_missing:
    284                 lnotab = _find_executable_linenos(filename)
    285             else:
    286                 lnotab = {}
    287             if lnotab:
    288                 source = linecache.getlines(filename)
    289                 coverpath = os.path.join(dir, modulename + ".cover")
    290                 with open(filename, 'rb') as fp:
    291                     encoding, _ = tokenize.detect_encoding(fp.readline)
    292                 n_hits, n_lines = self.write_results_file(coverpath, source,
    293                                                           lnotab, count, encoding)
    294                 if summary and n_lines:
    295                     percent = int(100 * n_hits / n_lines)
    296                     sums[modulename] = n_lines, percent, modulename, filename
    297 
    298 
    299         if summary and sums:
    300             print("lines   cov%   module   (path)")
    301             for m in sorted(sums):
    302                 n_lines, percent, modulename, filename = sums[m]
    303                 print("%5d   %3d%%   %s   (%s)" % sums[m])
    304 
    305         if self.outfile:
    306             # try and store counts and module info into self.outfile
    307             try:
    308                 pickle.dump((self.counts, self.calledfuncs, self.callers),
    309                             open(self.outfile, 'wb'), 1)
    310             except OSError as err:
    311                 print("Can't save counts files because %s" % err, file=sys.stderr)
    312 
    313     def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None):
    314         """Return a coverage results file in path."""
    315 
    316         try:
    317             outfile = open(path, "w", encoding=encoding)
    318         except OSError as err:
    319             print(("trace: Could not open %r for writing: %s"
    320                                   "- skipping" % (path, err)), file=sys.stderr)
    321             return 0, 0
    322 
    323         n_lines = 0
    324         n_hits = 0
    325         with outfile:
    326             for lineno, line in enumerate(lines, 1):
    327                 # do the blank/comment match to try to mark more lines
    328                 # (help the reader find stuff that hasn't been covered)
    329                 if lineno in lines_hit:
    330                     outfile.write("%5d: " % lines_hit[lineno])
    331                     n_hits += 1
    332                     n_lines += 1
    333                 elif rx_blank.match(line):
    334                     outfile.write("       ")
    335                 else:
    336                     # lines preceded by no marks weren't hit
    337                     # Highlight them if so indicated, unless the line contains
    338                     # #pragma: NO COVER
    339                     if lineno in lnotab and not PRAGMA_NOCOVER in line:
    340                         outfile.write(">>>>>> ")
    341                         n_lines += 1
    342                     else:
    343                         outfile.write("       ")
    344                 outfile.write(line.expandtabs(8))
    345 
    346         return n_hits, n_lines
    347 
    348 def _find_lines_from_code(code, strs):
    349     """Return dict where keys are lines in the line number table."""
    350     linenos = {}
    351 
    352     for _, lineno in dis.findlinestarts(code):
    353         if lineno not in strs:
    354             linenos[lineno] = 1
    355 
    356     return linenos
    357 
    358 def _find_lines(code, strs):
    359     """Return lineno dict for all code objects reachable from code."""
    360     # get all of the lineno information from the code of this scope level
    361     linenos = _find_lines_from_code(code, strs)
    362 
    363     # and check the constants for references to other code objects
    364     for c in code.co_consts:
    365         if inspect.iscode(c):
    366             # find another code object, so recurse into it
    367             linenos.update(_find_lines(c, strs))
    368     return linenos
    369 
    370 def _find_strings(filename, encoding=None):
    371     """Return a dict of possible docstring positions.
    372 
    373     The dict maps line numbers to strings.  There is an entry for
    374     line that contains only a string or a part of a triple-quoted
    375     string.
    376     """
    377     d = {}
    378     # If the first token is a string, then it's the module docstring.
    379     # Add this special case so that the test in the loop passes.
    380     prev_ttype = token.INDENT
    381     with open(filename, encoding=encoding) as f:
    382         tok = tokenize.generate_tokens(f.readline)
    383         for ttype, tstr, start, end, line in tok:
    384             if ttype == token.STRING:
    385                 if prev_ttype == token.INDENT:
    386                     sline, scol = start
    387                     eline, ecol = end
    388                     for i in range(sline, eline + 1):
    389                         d[i] = 1
    390             prev_ttype = ttype
    391     return d
    392 
    393 def _find_executable_linenos(filename):
    394     """Return dict where keys are line numbers in the line number table."""
    395     try:
    396         with tokenize.open(filename) as f:
    397             prog = f.read()
    398             encoding = f.encoding
    399     except OSError as err:
    400         print(("Not printing coverage data for %r: %s"
    401                               % (filename, err)), file=sys.stderr)
    402         return {}
    403     code = compile(prog, filename, "exec")
    404     strs = _find_strings(filename, encoding)
    405     return _find_lines(code, strs)
    406 
    407 class Trace:
    408     def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
    409                  ignoremods=(), ignoredirs=(), infile=None, outfile=None,
    410                  timing=False):
    411         """
    412         @param count true iff it should count number of times each
    413                      line is executed
    414         @param trace true iff it should print out each line that is
    415                      being counted
    416         @param countfuncs true iff it should just output a list of
    417                      (filename, modulename, funcname,) for functions
    418                      that were called at least once;  This overrides
    419                      `count' and `trace'
    420         @param ignoremods a list of the names of modules to ignore
    421         @param ignoredirs a list of the names of directories to ignore
    422                      all of the (recursive) contents of
    423         @param infile file from which to read stored counts to be
    424                      added into the results
    425         @param outfile file in which to write the results
    426         @param timing true iff timing information be displayed
    427         """
    428         self.infile = infile
    429         self.outfile = outfile
    430         self.ignore = _Ignore(ignoremods, ignoredirs)
    431         self.counts = {}   # keys are (filename, linenumber)
    432         self.pathtobasename = {} # for memoizing os.path.basename
    433         self.donothing = 0
    434         self.trace = trace
    435         self._calledfuncs = {}
    436         self._callers = {}
    437         self._caller_cache = {}
    438         self.start_time = None
    439         if timing:
    440             self.start_time = _time()
    441         if countcallers:
    442             self.globaltrace = self.globaltrace_trackcallers
    443         elif countfuncs:
    444             self.globaltrace = self.globaltrace_countfuncs
    445         elif trace and count:
    446             self.globaltrace = self.globaltrace_lt
    447             self.localtrace = self.localtrace_trace_and_count
    448         elif trace:
    449             self.globaltrace = self.globaltrace_lt
    450             self.localtrace = self.localtrace_trace
    451         elif count:
    452             self.globaltrace = self.globaltrace_lt
    453             self.localtrace = self.localtrace_count
    454         else:
    455             # Ahem -- do nothing?  Okay.
    456             self.donothing = 1
    457 
    458     def run(self, cmd):
    459         import __main__
    460         dict = __main__.__dict__
    461         self.runctx(cmd, dict, dict)
    462 
    463     def runctx(self, cmd, globals=None, locals=None):
    464         if globals is None: globals = {}
    465         if locals is None: locals = {}
    466         if not self.donothing:
    467             _settrace(self.globaltrace)
    468         try:
    469             exec(cmd, globals, locals)
    470         finally:
    471             if not self.donothing:
    472                 _unsettrace()
    473 
    474     def runfunc(self, func, *args, **kw):
    475         result = None
    476         if not self.donothing:
    477             sys.settrace(self.globaltrace)
    478         try:
    479             result = func(*args, **kw)
    480         finally:
    481             if not self.donothing:
    482                 sys.settrace(None)
    483         return result
    484 
    485     def file_module_function_of(self, frame):
    486         code = frame.f_code
    487         filename = code.co_filename
    488         if filename:
    489             modulename = _modname(filename)
    490         else:
    491             modulename = None
    492 
    493         funcname = code.co_name
    494         clsname = None
    495         if code in self._caller_cache:
    496             if self._caller_cache[code] is not None:
    497                 clsname = self._caller_cache[code]
    498         else:
    499             self._caller_cache[code] = None
    500             ## use of gc.get_referrers() was suggested by Michael Hudson
    501             # all functions which refer to this code object
    502             funcs = [f for f in gc.get_referrers(code)
    503                          if inspect.isfunction(f)]
    504             # require len(func) == 1 to avoid ambiguity caused by calls to
    505             # new.function(): "In the face of ambiguity, refuse the
    506             # temptation to guess."
    507             if len(funcs) == 1:
    508                 dicts = [d for d in gc.get_referrers(funcs[0])
    509                              if isinstance(d, dict)]
    510                 if len(dicts) == 1:
    511                     classes = [c for c in gc.get_referrers(dicts[0])
    512                                    if hasattr(c, "__bases__")]
    513                     if len(classes) == 1:
    514                         # ditto for new.classobj()
    515                         clsname = classes[0].__name__
    516                         # cache the result - assumption is that new.* is
    517                         # not called later to disturb this relationship
    518                         # _caller_cache could be flushed if functions in
    519                         # the new module get called.
    520                         self._caller_cache[code] = clsname
    521         if clsname is not None:
    522             funcname = "%s.%s" % (clsname, funcname)
    523 
    524         return filename, modulename, funcname
    525 
    526     def globaltrace_trackcallers(self, frame, why, arg):
    527         """Handler for call events.
    528 
    529         Adds information about who called who to the self._callers dict.
    530         """
    531         if why == 'call':
    532             # XXX Should do a better job of identifying methods
    533             this_func = self.file_module_function_of(frame)
    534             parent_func = self.file_module_function_of(frame.f_back)
    535             self._callers[(parent_func, this_func)] = 1
    536 
    537     def globaltrace_countfuncs(self, frame, why, arg):
    538         """Handler for call events.
    539 
    540         Adds (filename, modulename, funcname) to the self._calledfuncs dict.
    541         """
    542         if why == 'call':
    543             this_func = self.file_module_function_of(frame)
    544             self._calledfuncs[this_func] = 1
    545 
    546     def globaltrace_lt(self, frame, why, arg):
    547         """Handler for call events.
    548 
    549         If the code block being entered is to be ignored, returns `None',
    550         else returns self.localtrace.
    551         """
    552         if why == 'call':
    553             code = frame.f_code
    554             filename = frame.f_globals.get('__file__', None)
    555             if filename:
    556                 # XXX _modname() doesn't work right for packages, so
    557                 # the ignore support won't work right for packages
    558                 modulename = _modname(filename)
    559                 if modulename is not None:
    560                     ignore_it = self.ignore.names(filename, modulename)
    561                     if not ignore_it:
    562                         if self.trace:
    563                             print((" --- modulename: %s, funcname: %s"
    564                                    % (modulename, code.co_name)))
    565                         return self.localtrace
    566             else:
    567                 return None
    568 
    569     def localtrace_trace_and_count(self, frame, why, arg):
    570         if why == "line":
    571             # record the file name and line number of every trace
    572             filename = frame.f_code.co_filename
    573             lineno = frame.f_lineno
    574             key = filename, lineno
    575             self.counts[key] = self.counts.get(key, 0) + 1
    576 
    577             if self.start_time:
    578                 print('%.2f' % (_time() - self.start_time), end=' ')
    579             bname = os.path.basename(filename)
    580             print("%s(%d): %s" % (bname, lineno,
    581                                   linecache.getline(filename, lineno)), end='')
    582         return self.localtrace
    583 
    584     def localtrace_trace(self, frame, why, arg):
    585         if why == "line":
    586             # record the file name and line number of every trace
    587             filename = frame.f_code.co_filename
    588             lineno = frame.f_lineno
    589 
    590             if self.start_time:
    591                 print('%.2f' % (_time() - self.start_time), end=' ')
    592             bname = os.path.basename(filename)
    593             print("%s(%d): %s" % (bname, lineno,
    594                                   linecache.getline(filename, lineno)), end='')
    595         return self.localtrace
    596 
    597     def localtrace_count(self, frame, why, arg):
    598         if why == "line":
    599             filename = frame.f_code.co_filename
    600             lineno = frame.f_lineno
    601             key = filename, lineno
    602             self.counts[key] = self.counts.get(key, 0) + 1
    603         return self.localtrace
    604 
    605     def results(self):
    606         return CoverageResults(self.counts, infile=self.infile,
    607                                outfile=self.outfile,
    608                                calledfuncs=self._calledfuncs,
    609                                callers=self._callers)
    610 
    611 def main():
    612 
    613     parser = argparse.ArgumentParser()
    614     parser.add_argument('--version', action='version', version='trace 2.0')
    615 
    616     grp = parser.add_argument_group('Main options',
    617             'One of these (or --report) must be given')
    618 
    619     grp.add_argument('-c', '--count', action='store_true',
    620             help='Count the number of times each line is executed and write '
    621                  'the counts to <module>.cover for each module executed, in '
    622                  'the module\'s directory. See also --coverdir, --file, '
    623                  '--no-report below.')
    624     grp.add_argument('-t', '--trace', action='store_true',
    625             help='Print each line to sys.stdout before it is executed')
    626     grp.add_argument('-l', '--listfuncs', action='store_true',
    627             help='Keep track of which functions are executed at least once '
    628                  'and write the results to sys.stdout after the program exits. '
    629                  'Cannot be specified alongside --trace or --count.')
    630     grp.add_argument('-T', '--trackcalls', action='store_true',
    631             help='Keep track of caller/called pairs and write the results to '
    632                  'sys.stdout after the program exits.')
    633 
    634     grp = parser.add_argument_group('Modifiers')
    635 
    636     _grp = grp.add_mutually_exclusive_group()
    637     _grp.add_argument('-r', '--report', action='store_true',
    638             help='Generate a report from a counts file; does not execute any '
    639                  'code. --file must specify the results file to read, which '
    640                  'must have been created in a previous run with --count '
    641                  '--file=FILE')
    642     _grp.add_argument('-R', '--no-report', action='store_true',
    643             help='Do not generate the coverage report files. '
    644                  'Useful if you want to accumulate over several runs.')
    645 
    646     grp.add_argument('-f', '--file',
    647             help='File to accumulate counts over several runs')
    648     grp.add_argument('-C', '--coverdir',
    649             help='Directory where the report files go. The coverage report '
    650                  'for <package>.<module> will be written to file '
    651                  '<dir>/<package>/<module>.cover')
    652     grp.add_argument('-m', '--missing', action='store_true',
    653             help='Annotate executable lines that were not executed with '
    654                  '">>>>>> "')
    655     grp.add_argument('-s', '--summary', action='store_true',
    656             help='Write a brief summary for each file to sys.stdout. '
    657                  'Can only be used with --count or --report')
    658     grp.add_argument('-g', '--timing', action='store_true',
    659             help='Prefix each line with the time since the program started. '
    660                  'Only used while tracing')
    661 
    662     grp = parser.add_argument_group('Filters',
    663             'Can be specified multiple times')
    664     grp.add_argument('--ignore-module', action='append', default=[],
    665             help='Ignore the given module(s) and its submodules'
    666                  '(if it is a package). Accepts comma separated list of '
    667                  'module names.')
    668     grp.add_argument('--ignore-dir', action='append', default=[],
    669             help='Ignore files in the given directory '
    670                  '(multiple directories can be joined by os.pathsep).')
    671 
    672     parser.add_argument('filename', nargs='?',
    673             help='file to run as main program')
    674     parser.add_argument('arguments', nargs=argparse.REMAINDER,
    675             help='arguments to the program')
    676 
    677     opts = parser.parse_args()
    678 
    679     if opts.ignore_dir:
    680         rel_path = 'lib', 'python{0.major}.{0.minor}'.format(sys.version_info)
    681         _prefix = os.path.join(sys.base_prefix, *rel_path)
    682         _exec_prefix = os.path.join(sys.base_exec_prefix, *rel_path)
    683 
    684     def parse_ignore_dir(s):
    685         s = os.path.expanduser(os.path.expandvars(s))
    686         s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix)
    687         return os.path.normpath(s)
    688 
    689     opts.ignore_module = [mod.strip()
    690                           for i in opts.ignore_module for mod in i.split(',')]
    691     opts.ignore_dir = [parse_ignore_dir(s)
    692                        for i in opts.ignore_dir for s in i.split(os.pathsep)]
    693 
    694     if opts.report:
    695         if not opts.file:
    696             parser.error('-r/--report requires -f/--file')
    697         results = CoverageResults(infile=opts.file, outfile=opts.file)
    698         return results.write_results(opts.missing, opts.summary, opts.coverdir)
    699 
    700     if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]):
    701         parser.error('must specify one of --trace, --count, --report, '
    702                      '--listfuncs, or --trackcalls')
    703 
    704     if opts.listfuncs and (opts.count or opts.trace):
    705         parser.error('cannot specify both --listfuncs and (--trace or --count)')
    706 
    707     if opts.summary and not opts.count:
    708         parser.error('--summary can only be used with --count or --report')
    709 
    710     if opts.filename is None:
    711         parser.error('filename is missing: required with the main options')
    712 
    713     sys.argv = opts.filename, *opts.arguments
    714     sys.path[0] = os.path.dirname(opts.filename)
    715 
    716     t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs,
    717               countcallers=opts.trackcalls, ignoremods=opts.ignore_module,
    718               ignoredirs=opts.ignore_dir, infile=opts.file,
    719               outfile=opts.file, timing=opts.timing)
    720     try:
    721         with open(opts.filename) as fp:
    722             code = compile(fp.read(), opts.filename, 'exec')
    723         # try to emulate __main__ namespace as much as possible
    724         globs = {
    725             '__file__': opts.filename,
    726             '__name__': '__main__',
    727             '__package__': None,
    728             '__cached__': None,
    729         }
    730         t.runctx(code, globs, globs)
    731     except OSError as err:
    732         sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err))
    733     except SystemExit:
    734         pass
    735 
    736     results = t.results()
    737 
    738     if not opts.no_report:
    739         results.write_results(opts.missing, opts.summary, opts.coverdir)
    740 
    741 if __name__=='__main__':
    742     main()
    743