-
Notifications
You must be signed in to change notification settings - Fork 0
/
pytest_interrogate.py
282 lines (261 loc) · 10.3 KB
/
pytest_interrogate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
"""Docstring coverage plugin for pytest using interrogate."""
import os
import re
import warnings
import pytest
import attr
from interrogate import config as interrogate_conf
from interrogate import coverage as interrogate_cov
def validate_fail_under(num_str):
"""Fail under value from args.
Should be under 100 as anything over 100 will be converted to 100.
Args:
num_str (str): string representation for integer.
Returns:
Any[float,int]: minimum of 100 or converted num_str
"""
try:
value = int(num_str)
except ValueError:
value = float(num_str)
if 1 <= value <= 100:
return value
raise ValueError(
"Not allowing docstring coverage below 1% to be tested. Should be between 1 and 100, not: {0}".format(
value
)
)
def pytest_addoption(parser):
"""Add options to control interrogate."""
group = parser.getgroup("interrogate", "Docstring coverage reporting with interrogate.")
group.addoption(
"--interrogate",
action="append",
default=[],
metavar="SOURCE",
nargs="?",
const=True,
dest="interrogate_source",
help="Path or package name to measure during execution (multi-allowed)"
"--interrogate supported for using CWD. "
"Use --intterogate=PKGNAME for filtering."
"--interrogate takes precedence.",
)
group.addoption(
"--interrogate-verbose",
action="store",
metavar="VERBOSITY",
default=0,
choices=(0, 1, 2),
type=int,
)
group.addoption(
"--interrogate-exclude",
action="append",
metavar="PATH",
default=(),
nargs="?",
)
group.addoption("--interrogate-tofile", action="store", default=None, type=str)
group.addoption(
"--interrogate-noreport-on-fail",
action="store_true",
default=False,
help="Do not report interrogate if test run fails. " "Default: False",
)
group.addoption(
"--interrogate-quiet",
action="store_true",
default=False,
help="Disable interrogate completely (useful for debuggers). " "Default: False",
)
group.addoption(
"--interrogate-fail-under",
action="store",
metavar="MIN",
type=validate_fail_under,
default=80.0,
help="Fail if the total interrogate is less than MIN.",
)
group.addoption(
"--interrogate-no-color",
action="store_false",
help="Disable color output.",
dest="interrogate_color",
)
group.addoption(
"--interrogate-no-pyproject",
action="store_true",
help="Disable pyproject.toml if present. Default it uses pyproject.toml in cwd.",
dest="interrogate_nopyproject",
)
for option in attr.fields_dict(interrogate_conf.InterrogateConfig):
if option in ("color", "fail_under"):
continue
iattr = "--interrogate-{0}".format(option.replace("_", "-"))
if option.endswith("_regex"):
group.addoption(
iattr,
action="append",
metavar="PATH",
default=[],
nargs="?",
)
else:
group.addoption(
iattr,
action="store_true",
default=False,
help="Interrogate {0} option. " "Default: False".format(option),
)
@pytest.mark.tryfirst
def pytest_load_initial_conftests(early_config, parser, args):
"""Pytest API for config loader."""
if early_config.known_args_namespace.interrogate_source:
plugin = PytestInterrogatePlugin(
early_config.known_args_namespace, early_config.pluginmanager
)
early_config.pluginmanager.register(plugin, "_interrogate")
class PytestInterrogatePlugin(object):
"""Use interrogate package to produce code docstring reports."""
def __init__(self, options, pluginmanager):
"""Create a interrogate pytest plugin."""
self.interrogate = None
self.interrogate_covered = None
self.options = options
self.failed = False
if not self.options.interrogate_source:
return
opts = {}
current_wd = os.getcwd()
if not options.interrogate_nopyproject:
_ppf = os.path.join(current_wd, "pyproject.toml")
pyproject_file = _ppf if os.path.exists(_ppf) else None
if pyproject_file:
opts = interrogate_conf.parse_pyproject_toml(pyproject_file)
for option in attr.fields_dict(interrogate_conf.InterrogateConfig):
iattr = "interrogate_{0}".format(option)
if not hasattr(self.options, iattr):
warnings.warn("Didnot find option: {0} in interrogate".format(option))
continue
opt_val = getattr(self.options, iattr)
if option.startswith("ignore") or option == "include_regex":
if option.endswith("_regex"):
cur_val = opts.get(option)
if opt_val or cur_val:
opt_val = [re.compile(regex) for regex in (opt_val or cur_val)]
if opt_val:
opts[option] = opt_val
elif option == "color":
if option not in opts:
opts[option] = True
if not opt_val:
opts[option] = opt_val
elif opt_val != 80.0:
opts[option] = opt_val
setattr(self.options, iattr, opts.get(option) or opt_val)
self._disabled = getattr(options, "interrogate_quiet", opts.pop("quiet", False))
if self._disabled:
return
exclude = tuple(self.options.interrogate_exclude or opts.pop("exclude", ()))
self.options.interrogate_exclue = exclude
if options.interrogate_source:
if True in options.interrogate_source:
all_source = [current_wd]
else:
all_source = [pth for pth in options.interrogate_source if pth is not True]
print(all_source)
self.options.interrogate_source = all_source
self.options.interrogate_verbose = opts.pop("verbose", self.options.interrogate_verbose)
self.interrogate_verbosity = self.options.interrogate_verbose
interrogate_config = interrogate_conf.InterrogateConfig(**opts)
self.interrogate = interrogate_cov.InterrogateCoverage(
paths=all_source,
conf=interrogate_config,
excluded=exclude,
)
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, session):
"""Pytest API function."""
yield
if self.interrogate:
self.interrogate_results = self.interrogate.get_coverage()
self.interrogate_covered = self.interrogate_results.perc_covered
self.failed = self.interrogate_covered < self.options.interrogate_fail_under
def pytest_terminal_summary(self, terminalreporter):
"""Pytest API function to write summary."""
if self.failed:
markup = {"red": True, "bold": True}
else:
markup = {"green": True}
if self._disabled:
message = "Interrogate disabled via --interrogate-disable switch!"
terminalreporter.write("WARNING: {0}\n".format(message), **markup)
warnings.warn(pytest.PytestWarning(message))
return
if (
self.options.interrogate_noreport_on_fail
or self.interrogate is None
or self.interrogate_covered is None
):
return
_isatty = terminalreporter.isatty
terminalreporter.isatty = lambda: _isatty
terminalreporter.flush = lambda: None
output_formatter = interrogate_cov.utils.OutputFormatter(
self.interrogate.config, terminalreporter
)
base = self.interrogate._get_header_base()
if self.options.interrogate_tofile:
self.interrogate.print_results(
self.interrogate_results,
self.options.interrogate_tofile,
self.interrogate_verbosity,
)
output_formatter.tw.sep(
"=",
"Interrogate docstring coverage report for {0} written to file {1}".format(
base, self.options.interrogate_tofile
),
fullwidth=output_formatter.TERMINAL_WIDTH,
**markup
)
else:
self.interrogate.output_formatter = output_formatter
output_formatter = interrogate_cov.utils.OutputFormatter(
self.interrogate.config, terminalreporter
)
self.interrogate.output_formatter = output_formatter
output_formatter.tw.sep(
"=",
"Interrogate docstring coverage for {0}: {1}".format(
base, "FAILED" if self.failed else "SUCCESS"
),
fullwidth=output_formatter.TERMINAL_WIDTH,
**markup
)
if self.interrogate_verbosity > 1:
self.interrogate._print_detailed_table(self.interrogate_results)
elif self.interrogate_verbosity > 0:
self.interrogate._print_summary_table(self.interrogate_results)
output_formatter.tw.sep("-", title="", fullwidth=output_formatter.TERMINAL_WIDTH)
message = (
"REPORT: Required docstring coverage of {required}% {reached}. "
"Total docstring coverage is {actual:.2f}%.\n".format(
required=self.options.interrogate_fail_under,
actual=self.interrogate_covered,
reached="not fulfilled" if self.failed else "fulfilled",
)
)
terminalreporter.write(message, **markup)
terminalreporter.isatty = _isatty
terminalreporter.flush = lambda: AttributeError(
"'{0}' has no attribute called 'flush'".format(terminalreporter.__class__.__name__)
)
@pytest.fixture
def interrogate(request):
"""Pytest fixture to provide access to the underlying interrogate object."""
if request.config.pluginmanager.hasplugin("_interrogate"):
plugin = request.config.pluginmanager.getplugin("_interrogate")
return plugin.interrogate
return None