• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TheKevJames / coveralls-python / 36ccccef-eea6-4e8d-9965-1fb2bf22a852

26 Apr 2024 12:52PM UTC coverage: 90.893%. Remained the same
36ccccef-eea6-4e8d-9965-1fb2bf22a852

push

circleci

TheKevJames
feat(compat): drop support for python 3.7

And migrate to poetry for package management.

138 of 157 branches covered (87.9%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

6 existing lines in 2 files now uncovered.

381 of 414 relevant lines covered (92.03%)

5.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.25
/coveralls/reporter.py
1
import logging
6✔
2
import os
6✔
3

4
from coverage.files import FnmatchMatcher
6✔
5
from coverage.files import prep_patterns
6✔
6

7
try:
6✔
8
    # coverage v6.x
9
    from coverage.exceptions import NoSource
6✔
10
    from coverage.exceptions import NotPython
6✔
11
except ImportError:
×
12
    # coverage v5.x
13
    from coverage.misc import NoSource
×
14
    from coverage.misc import NotPython
×
15

16
log = logging.getLogger('coveralls.reporter')
6✔
17

18

19
class CoverallReporter:
6✔
20
    """Custom coverage.py reporter for coveralls.io."""
21

22
    def __init__(self, cov, base_dir='', src_dir=''):
6✔
23
        self.coverage = []
6✔
24
        self.base_dir = self.sanitize_dir(base_dir)
6✔
25
        self.src_dir = self.sanitize_dir(src_dir)
6✔
26
        self.report(cov)
6✔
27

28
    @staticmethod
6✔
29
    def sanitize_dir(directory):
5✔
30
        if directory:
6✔
31
            directory = directory.replace(os.path.sep, '/')
6✔
32
            if directory[-1] != '/':
6✔
33
                directory += '/'
6✔
34
        return directory
6✔
35

36
    def report(self, cov):
6✔
37
        # N.B. this method is 99% copied from the coverage source code;
38
        # unfortunately, the coverage v5 style of `get_analysis_to_report`
39
        # errors out entirely if any source file has issues -- which would be a
40
        # breaking change for us. In the interest of backwards compatibility,
41
        # I've copied their code here so we can maintain the same `coveralls`
42
        # API regardless of which `coverage` version is being used.
43
        #
44
        # TODO: deprecate the relevant APIs so we can just use the coverage
45
        # public API directly.
46
        #
47
        # from coverage.report import get_analysis_to_report
48
        # try:
49
        #     for cu, analyzed in get_analysis_to_report(cov, None):
50
        #         self.parse_file(cu, analyzed)
51
        # except NoSource:
52
        #     # Note that this behavior must necessarily change between
53
        #     # coverage<5 and coverage>=5, as we are no longer interweaving
54
        #     # with get_analysis_to_report (a single exception breaks the
55
        #     # whole loop)
56
        #     log.warning('No source for at least one file')
57
        # except NotPython:
58
        #     # Note that this behavior must necessarily change between
59
        #     # coverage<5 and coverage>=5, as we are no longer interweaving
60
        #     # with get_analysis_to_report (a single exception breaks the
61
        #     # whole loop)
62
        #     log.warning('A source file is not python')
63
        # except CoverageException as e:
64
        #     if str(e) != 'No data to report.':
65
        #         raise
66

67
        # get_analysis_to_report starts here; changes marked with TODOs
68
        file_reporters = cov._get_file_reporters(None)  # pylint: disable=W0212
6✔
69
        config = cov.config
6✔
70

71
        if config.report_include:
6!
UNCOV
72
            matcher = FnmatchMatcher(prep_patterns(config.report_include))
×
73
            file_reporters = [
×
74
                fr for fr in file_reporters
75
                if matcher.match(fr.filename)
76
            ]
77

78
        if config.report_omit:
6!
UNCOV
79
            matcher = FnmatchMatcher(prep_patterns(config.report_omit))
×
UNCOV
80
            file_reporters = [
×
81
                fr for fr in file_reporters
82
                if not matcher.match(fr.filename)
83
            ]
84

85
        # TODO: deprecate changes
86
        # if not file_reporters:
87
        #     raise CoverageException("No data to report.")
88

89
        for fr in sorted(file_reporters):
6✔
90
            try:
6✔
91
                analysis = cov._analyze(fr)  # pylint: disable=W0212
6✔
92
            except NoSource:
6✔
93
                if not config.ignore_errors:
6!
94
                    # TODO: deprecate changes
95
                    # raise
96
                    log.warning('No source for %s', fr.filename)
6✔
97
            except NotPython:
6✔
98
                # Only report errors for .py files, and only if we didn't
99
                # explicitly suppress those errors.
100
                # NotPython is only raised by PythonFileReporter, which has a
101
                # should_be_python() method.
102
                if fr.should_be_python():
6!
103
                    if config.ignore_errors:
6!
UNCOV
104
                        msg = f"Couldn't parse Python file '{fr.filename}'"
×
UNCOV
105
                        cov._warn(  # pylint: disable=W0212
×
106
                            msg, slug='couldnt-parse',
107
                        )
108
                    else:
109
                        # TODO: deprecate changes
110
                        # raise
111
                        log.warning(
6✔
112
                            'Source file is not python %s', fr.filename,
113
                        )
114
            else:
115
                # TODO: deprecate changes (well, this one is fine /shrug)
116
                # yield (fr, analysis)
117
                self.parse_file(fr, analysis)
6✔
118

119
    @staticmethod
6✔
120
    def get_hits(line_num, analysis):
5✔
121
        """
122
        Source file stats for each line.
123

124
        * A positive integer if the line is covered, representing the number
125
          of times the line is hit during the test suite.
126
        * 0 if the line is not covered by the test suite.
127
        * null to indicate the line is not relevant to code coverage (it may
128
          be whitespace or a comment).
129
        """
130
        if line_num in analysis.missing:
6✔
131
            return 0
6✔
132

133
        if line_num not in analysis.statements:
6✔
134
            return None
6✔
135

136
        return 1
6✔
137

138
    @staticmethod
6✔
139
    def get_arcs(analysis):
5✔
140
        """
141
        Hit stats for each branch.
142

143
        Returns a flat list where every four values represent a branch:
144
        1. line-number
145
        2. block-number (not used)
146
        3. branch-number
147
        4. hits (we only get 1/0 from coverage.py)
148
        """
149
        if not analysis.has_arcs():
6✔
150
            return None
6✔
151

152
        # N.B. switching to the public method analysis.missing_branch_arcs
153
        # would work for half of what we need, but there doesn't seem to be an
154
        # equivalent analysis.executed_branch_arcs
155
        branch_lines = analysis._branch_lines()  # pylint: disable=W0212
6✔
156

157
        branches = []
6✔
158

159
        for l1, l2 in analysis.arcs_executed():
6✔
160
            if l1 in branch_lines:
6✔
161
                branches.extend((l1, 0, abs(l2), 1))
6✔
162

163
        for l1, l2 in analysis.arcs_missing():
6✔
164
            if l1 in branch_lines:
6✔
165
                branches.extend((l1, 0, abs(l2), 0))
6✔
166

167
        return branches
6✔
168

169
    def parse_file(self, cu, analysis):
6✔
170
        """Generate data for single file."""
171
        filename = cu.relative_filename()
6✔
172

173
        # ensure results are properly merged between platforms
174
        posix_filename = filename.replace(os.path.sep, '/')
6✔
175

176
        if self.base_dir and posix_filename.startswith(self.base_dir):
6✔
177
            posix_filename = posix_filename[len(self.base_dir):]
6✔
178
        posix_filename = self.src_dir + posix_filename
6✔
179

180
        source = analysis.file_reporter.source()
6✔
181

182
        token_lines = analysis.file_reporter.source_token_lines()
6✔
183
        coverage_lines = [
6✔
184
            self.get_hits(i, analysis)
185
            for i, _ in enumerate(token_lines, 1)
186
        ]
187

188
        results = {
6✔
189
            'name': posix_filename,
190
            'source': source,
191
            'coverage': coverage_lines,
192
        }
193

194
        branches = self.get_arcs(analysis)
6✔
195
        if branches:
6✔
196
            results['branches'] = branches
6✔
197

198
        self.coverage.append(results)
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc