Home | History | Annotate | Download | only in coverage
      1 """Source file annotation for Coverage."""
      2 
      3 import os, re
      4 
      5 from coverage.report import Reporter
      6 
      7 class AnnotateReporter(Reporter):
      8     """Generate annotated source files showing line coverage.
      9 
     10     This reporter creates annotated copies of the measured source files. Each
     11     .py file is copied as a .py,cover file, with a left-hand margin annotating
     12     each line::
     13 
     14         > def h(x):
     15         -     if 0:   #pragma: no cover
     16         -         pass
     17         >     if x == 1:
     18         !         a = 1
     19         >     else:
     20         >         a = 2
     21 
     22         > h(2)
     23 
     24     Executed lines use '>', lines not executed use '!', lines excluded from
     25     consideration use '-'.
     26 
     27     """
     28 
     29     def __init__(self, coverage, ignore_errors=False):
     30         super(AnnotateReporter, self).__init__(coverage, ignore_errors)
     31         self.directory = None
     32 
     33     blank_re = re.compile(r"\s*(#|$)")
     34     else_re = re.compile(r"\s*else\s*:\s*(#|$)")
     35 
     36     def report(self, morfs, config, directory=None):
     37         """Run the report.
     38 
     39         See `coverage.report()` for arguments.
     40 
     41         """
     42         self.report_files(self.annotate_file, morfs, config, directory)
     43 
     44     def annotate_file(self, cu, analysis):
     45         """Annotate a single file.
     46 
     47         `cu` is the CodeUnit for the file to annotate.
     48 
     49         """
     50         if not cu.relative:
     51             return
     52 
     53         filename = cu.filename
     54         source = cu.source_file()
     55         if self.directory:
     56             dest_file = os.path.join(self.directory, cu.flat_rootname())
     57             dest_file += ".py,cover"
     58         else:
     59             dest_file = filename + ",cover"
     60         dest = open(dest_file, 'w')
     61 
     62         statements = analysis.statements
     63         missing = analysis.missing
     64         excluded = analysis.excluded
     65 
     66         lineno = 0
     67         i = 0
     68         j = 0
     69         covered = True
     70         while True:
     71             line = source.readline()
     72             if line == '':
     73                 break
     74             lineno += 1
     75             while i < len(statements) and statements[i] < lineno:
     76                 i += 1
     77             while j < len(missing) and missing[j] < lineno:
     78                 j += 1
     79             if i < len(statements) and statements[i] == lineno:
     80                 covered = j >= len(missing) or missing[j] > lineno
     81             if self.blank_re.match(line):
     82                 dest.write('  ')
     83             elif self.else_re.match(line):
     84                 # Special logic for lines containing only 'else:'.
     85                 if i >= len(statements) and j >= len(missing):
     86                     dest.write('! ')
     87                 elif i >= len(statements) or j >= len(missing):
     88                     dest.write('> ')
     89                 elif statements[i] == missing[j]:
     90                     dest.write('! ')
     91                 else:
     92                     dest.write('> ')
     93             elif lineno in excluded:
     94                 dest.write('- ')
     95             elif covered:
     96                 dest.write('> ')
     97             else:
     98                 dest.write('! ')
     99             dest.write(line)
    100         source.close()
    101         dest.close()
    102