1"""Source file annotation for Coverage."""
2
3import os, re
4
5from coverage.report import Reporter
6
7class 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