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