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