Skip to content

Commit 6bf9e96

Browse files
authored
Merge pull request #134 from vidartf/cov
Fix coverage for coverage.py >=5
2 parents ff8f897 + 9a81893 commit 6bf9e96

File tree

4 files changed

+280
-195
lines changed

4 files changed

+280
-195
lines changed

nbval/_cover4.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
Code to enable coverage of any external code called by the
3+
notebook.
4+
"""
5+
6+
import os
7+
import coverage
8+
import warnings
9+
10+
11+
# Coverage setup/teardown code to run in kernel
12+
# Inspired by pytest-cov code.
13+
_python_setup = """\
14+
import coverage
15+
16+
__cov = coverage.Coverage(
17+
data_file=%r,
18+
source=%r,
19+
config_file=%r,
20+
auto_data=True,
21+
data_suffix='nbval',
22+
)
23+
__cov.load()
24+
__cov.start()
25+
__cov._warn_no_data = False
26+
__cov._warn_unimported_source = False
27+
"""
28+
_python_teardown = """\
29+
__cov.stop()
30+
__cov.save()
31+
"""
32+
33+
34+
def setup_coverage(config, kernel, floc, output_loc=None):
35+
"""Start coverage reporting in kernel.
36+
37+
Currently supported kernel languages are:
38+
- Python
39+
"""
40+
41+
language = kernel.language
42+
if language.startswith('python'):
43+
# Get the pytest-cov coverage object
44+
cov = get_cov(config)
45+
if cov:
46+
# If present, copy the data file location used by pytest-cov
47+
data_file = os.path.abspath(cov.config.data_file)
48+
else:
49+
# Fall back on output_loc and current dir if not
50+
data_file = os.path.abspath(os.path.join(output_loc or os.getcwd(), '.coverage'))
51+
52+
# Get options from pytest-cov's command line arguments:
53+
source = config.option.cov_source
54+
config_file = config.option.cov_config
55+
if isinstance(config_file, str) and os.path.isfile(config_file):
56+
config_file = os.path.abspath(config_file)
57+
58+
# Build setup command and execute in kernel:
59+
cmd = _python_setup % (data_file, source, config_file)
60+
msg_id = kernel.kc.execute(cmd, stop_on_error=False)
61+
kernel.await_idle(msg_id, 60) # A minute should be plenty to enable coverage
62+
else:
63+
warnings.warn_explicit(
64+
'Coverage currently not supported for language %r.' % language,
65+
category=UserWarning,
66+
filename=floc[0] if floc else '',
67+
lineno=0
68+
)
69+
return
70+
71+
72+
def teardown_coverage(config, kernel, output_loc=None):
73+
"""Finish coverage reporting in kernel.
74+
75+
The coverage should previously have been started with
76+
setup_coverage.
77+
"""
78+
language = kernel.language
79+
if language.startswith('python'):
80+
# Teardown code does not require any input, simply execute:
81+
msg_id = kernel.kc.execute(_python_teardown)
82+
kernel.await_idle(msg_id, 60) # A minute should be plenty to write out coverage
83+
84+
# Ensure we merge our data into parent data of pytest-cov, if possible
85+
cov = get_cov(config)
86+
_merge_nbval_coverage_data(cov)
87+
88+
else:
89+
# Warnings should be given on setup, or there might be no teardown
90+
# for a specific language, so do nothing here
91+
pass
92+
93+
94+
def get_cov(config):
95+
"""Returns the coverage object of pytest-cov."""
96+
97+
# Check with hasplugin to avoid getplugin exception in older pytest.
98+
if config.pluginmanager.hasplugin('_cov'):
99+
plugin = config.pluginmanager.getplugin('_cov')
100+
if plugin.cov_controller:
101+
return plugin.cov_controller.cov
102+
return None
103+
104+
def _merge_nbval_coverage_data(cov):
105+
"""Merge nbval coverage data into pytest-cov data."""
106+
if not cov:
107+
return
108+
109+
if coverage.version_info > (5, 0):
110+
data = cov.get_data()
111+
nbval_data = coverage.CoverageData(data.data_filename(), suffix='.nbval', debug=cov.debug)
112+
nbval_data.read()
113+
cov.get_data().update(nbval_data, aliases=aliases)
114+
else:
115+
# Get the filename of the nbval coverage:
116+
filename = cov.data_files.filename + '.nbval'
117+
118+
# Read coverage generated by nbval in this run:
119+
nbval_data = coverage.CoverageData(debug=cov.debug)
120+
try:
121+
nbval_data.read_file(os.path.abspath(filename))
122+
except coverage.CoverageException:
123+
return
124+
125+
# Set up aliases (following internal coverage.py code here)
126+
aliases = None
127+
if cov.config.paths:
128+
aliases = coverage.files.PathAliases()
129+
for paths in cov.config.paths.values():
130+
result = paths[0]
131+
for pattern in paths[1:]:
132+
aliases.add(pattern, result)
133+
134+
# Merge nbval data into pytest-cov data:
135+
cov.data.update(nbval_data, aliases=aliases)
136+
# Delete our nbval coverage data
137+
coverage.misc.file_be_gone(filename)
138+
139+
140+
"""
141+
Note about coverage data/datafiles:
142+
143+
When pytest is running, we get the pytest-cov coverage object.
144+
This object tracks its own coverage data, which is stored in its
145+
data file. For several reasons detailed below, we cannot use the
146+
same file in the kernel, so we have to ensure our own, and then
147+
ensure that they are all merged correctly at the end. The important
148+
factor here is the data_suffix attribute which might be set.
149+
150+
Cases:
151+
1. data_suffix is set to None:
152+
No suffix is used by pytest-cov. We need to create a new file,
153+
so we add a suffix for kernel, and then merge this file into
154+
the pytest-cov data at teardown.
155+
2. data_suffix is set to a string:
156+
We need to create a new file, so we append a string to the
157+
suffix passed to the kernel. We merge this file into the
158+
pytest-cov data at teardown.
159+
3. data_suffix is set to True:
160+
The suffix will be autogenerated by coverage.py, along the lines
161+
of 'hostname.pid.random'. This is typically used for parallel
162+
tests. We pass True as suffix to kernel, ensuring a unique
163+
auto-suffix later. We cannot merge this data into the pytest-cov
164+
one, as we do not know the suffix, but we can just leave the data
165+
for automatic collection. However, this might lead to a warning
166+
about no coverage data being collected by the pytest-cov
167+
collector.
168+
169+
Why do we need our own coverage data file?
170+
Coverage data can get lost if we try to sync via load/save/load cycles
171+
between the two. By having our own file, we can do an in-memory merge
172+
of the data afterwards using the official API. Either way, the data
173+
will always be merged to one coverage file in the end, so these files
174+
are transient.
175+
"""

nbval/_cover5.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Code to enable coverage of any external code called by the
3+
notebook.
4+
5+
For coverage.py >= v 5.0.0
6+
"""
7+
8+
import os
9+
import coverage
10+
import warnings
11+
12+
13+
# Coverage setup/teardown code to run in kernel
14+
# Inspired by pytest-cov code.
15+
_python_setup = """\
16+
import coverage
17+
18+
__cov = coverage.Coverage(
19+
data_file=%r,
20+
source=%r,
21+
config_file=%r,
22+
auto_data=True,
23+
data_suffix='.nbval',
24+
)
25+
__cov.load()
26+
__cov.start()
27+
__cov._warn_no_data = False
28+
__cov._warn_unimported_source = False
29+
"""
30+
_python_teardown = """\
31+
__cov.stop()
32+
__cov.save()
33+
"""
34+
35+
36+
def setup_coverage(config, kernel, floc, output_loc=None):
37+
"""Start coverage reporting in kernel.
38+
39+
Currently supported kernel languages are:
40+
- Python
41+
"""
42+
43+
language = kernel.language
44+
if language.startswith('python'):
45+
# Get the pytest-cov coverage object
46+
cov = get_cov(config)
47+
if cov:
48+
# If present, copy the data file location used by pytest-cov
49+
data_file = os.path.abspath(cov.get_data().data_filename())
50+
else:
51+
# Fall back on output_loc and current dir if not
52+
data_file = os.path.abspath(os.path.join(output_loc or os.getcwd(), '.coverage'))
53+
54+
# Get options from pytest-cov's command line arguments:
55+
source = config.option.cov_source
56+
config_file = config.option.cov_config
57+
if isinstance(config_file, str) and os.path.isfile(config_file):
58+
config_file = os.path.abspath(config_file)
59+
60+
# Build setup command and execute in kernel:
61+
cmd = _python_setup % (data_file, source, config_file)
62+
msg_id = kernel.kc.execute(cmd, stop_on_error=False)
63+
kernel.await_idle(msg_id, 60) # A minute should be plenty to enable coverage
64+
else:
65+
warnings.warn_explicit(
66+
'Coverage currently not supported for language %r.' % language,
67+
category=UserWarning,
68+
filename=floc[0] if floc else '',
69+
lineno=0
70+
)
71+
return
72+
73+
74+
def teardown_coverage(config, kernel, output_loc=None):
75+
"""Finish coverage reporting in kernel.
76+
77+
The coverage should previously have been started with
78+
setup_coverage.
79+
"""
80+
language = kernel.language
81+
if language.startswith('python'):
82+
# Teardown code does not require any input, simply execute:
83+
msg_id = kernel.kc.execute(_python_teardown)
84+
kernel.await_idle(msg_id, 60) # A minute should be plenty to write out coverage
85+
86+
else:
87+
# Warnings should be given on setup, or there might be no teardown
88+
# for a specific language, so do nothing here
89+
pass
90+
91+
92+
def get_cov(config):
93+
"""Returns the coverage object of pytest-cov."""
94+
95+
# Check with hasplugin to avoid getplugin exception in older pytest.
96+
if config.pluginmanager.hasplugin('_cov'):
97+
plugin = config.pluginmanager.getplugin('_cov')
98+
if plugin.cov_controller:
99+
return plugin.cov_controller.cov
100+
return None

0 commit comments

Comments
 (0)