Skip to content

Commit 6c14ffb

Browse files
committed
Merge branch 'nedbat/contexts'
2 parents 85f63fe + b0f5ac2 commit 6c14ffb

16 files changed

+354
-80
lines changed

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Change history for Coverage.py
1717
Unreleased
1818
----------
1919

20+
- Context support: static contexts let you specify a label for a coverage run,
21+
which is recorded in the data, and retained when you combine files. See
22+
:ref:`contexts` for more information. Currently, only static contexts are
23+
supported, with no reporting features.
24+
2025
- Environment variable substitution in configuration files now supports two
2126
syntaxes for controlling the behavior of undefined variables: if ``VARNAME``
2227
is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default

README.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ library to determine which lines are executable, and which have been executed.
1919

2020
Coverage.py runs on many versions of Python:
2121

22-
* CPython 2.7 and 3.4 through 3.7.
22+
* CPython 2.7.
23+
* CPython 3.4 through 3.7.
2324
* PyPy2 6.0 and PyPy3 6.0.
2425
* Jython 2.7.1, though not for reporting.
2526
* IronPython 2.7.7, though not for reporting.
@@ -31,7 +32,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on
3132
.. _GitHub: https://github.com/nedbat/coveragepy
3233

3334

34-
**New in 5.0:** SQLite data storage, dropped support for Python 2.6 and 3.3.
35+
**New in 5.0:** SQLite data storage, contexts, dropped support for Python 2.6
36+
and 3.3.
3537

3638
New in 4.5: Configurator plug-ins.
3739

coverage/cmdline.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class Opts(object):
4343
"Valid values are: %s."
4444
) % ", ".join(CONCURRENCY_CHOICES),
4545
)
46+
context = optparse.make_option(
47+
'', '--context', action='store', metavar="LABEL",
48+
help="The context label to record for this coverage run",
49+
)
4650
debug = optparse.make_option(
4751
'', '--debug', action='store', metavar="OPTS",
4852
help="Debug options, separated by commas. [env: COVERAGE_DEBUG]",
@@ -160,6 +164,7 @@ def __init__(self, *args, **kwargs):
160164
append=None,
161165
branch=None,
162166
concurrency=None,
167+
context=None,
163168
debug=None,
164169
directory=None,
165170
fail_under=None,
@@ -358,6 +363,7 @@ def get_prog_name(self):
358363
Opts.append,
359364
Opts.branch,
360365
Opts.concurrency,
366+
Opts.context,
361367
Opts.include,
362368
Opts.module,
363369
Opts.omit,
@@ -482,6 +488,7 @@ def command_line(self, argv):
482488
debug=debug,
483489
concurrency=options.concurrency,
484490
check_preimported=True,
491+
context=options.context,
485492
)
486493

487494
if options.action == "debug":

coverage/collector.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency
9999
self.warn = warn
100100
self.branch = branch
101101
self.threading = None
102+
self.covdata = None
103+
104+
self.static_context = None
102105

103106
self.origin = short_stack()
104107

@@ -160,6 +163,12 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency
160163
def __repr__(self):
161164
return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name())
162165

166+
def use_data(self, covdata, context):
167+
"""Use `covdata` for recording data."""
168+
self.covdata = covdata
169+
self.static_context = context
170+
self.covdata.set_context(self.static_context)
171+
163172
def tracer_name(self):
164173
"""Return the class name of the tracer we're using."""
165174
return self._trace_class.__name__
@@ -378,8 +387,11 @@ def cached_abs_file(self, filename):
378387
except KeyError:
379388
return self.abs_file_cache.setdefault(key, abs_file(filename))
380389

381-
def save_data(self, covdata):
382-
"""Save the collected data to a `CoverageData`.
390+
def flush_data(self):
391+
"""Save the collected data to our associated `CoverageData`.
392+
393+
Data may have also been saved along the way. This forces the
394+
last of the data to be saved.
383395
384396
Returns True if there was data to save, False if not.
385397
"""
@@ -406,10 +418,10 @@ def abs_file_dict(d):
406418
return dict((self.cached_abs_file(k), v) for k, v in items)
407419

408420
if self.branch:
409-
covdata.add_arcs(abs_file_dict(self.data))
421+
self.covdata.add_arcs(abs_file_dict(self.data))
410422
else:
411-
covdata.add_lines(abs_file_dict(self.data))
412-
covdata.add_file_tracers(abs_file_dict(self.file_tracers))
423+
self.covdata.add_lines(abs_file_dict(self.data))
424+
self.covdata.add_file_tracers(abs_file_dict(self.file_tracers))
413425

414426
if self.wtw:
415427
# Just a hack, so just hack it.

coverage/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def __init__(self):
175175
# Defaults for [run]
176176
self.branch = False
177177
self.concurrency = None
178+
self.context = None
178179
self.cover_pylib = False
179180
self.data_file = ".coverage"
180181
self.debug = []
@@ -318,6 +319,7 @@ def from_file(self, filename, our_file):
318319
# [run]
319320
('branch', 'run:branch', 'boolean'),
320321
('concurrency', 'run:concurrency', 'list'),
322+
('context', 'run:context'),
321323
('cover_pylib', 'run:cover_pylib', 'boolean'),
322324
('data_file', 'run:data_file'),
323325
('debug', 'run:debug', 'list'),

coverage/control.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(
5757
self, data_file=None, data_suffix=None, cover_pylib=None,
5858
auto_data=False, timid=None, branch=None, config_file=True,
5959
source=None, omit=None, include=None, debug=None,
60-
concurrency=None, check_preimported=False,
60+
concurrency=None, check_preimported=False, context=None,
6161
):
6262
"""
6363
`data_file` is the base name of the data file to use, defaulting to
@@ -116,6 +116,8 @@ def __init__(
116116
by coverage. Importing measured files before coverage is started can
117117
mean that code is missed.
118118
119+
`context` is a string to use as the context label for collected data.
120+
119121
.. versionadded:: 4.0
120122
The `concurrency` parameter.
121123
@@ -133,7 +135,7 @@ def __init__(
133135
branch=branch, parallel=bool_or_none(data_suffix),
134136
source=source, run_omit=omit, run_include=include, debug=debug,
135137
report_omit=omit, report_include=include,
136-
concurrency=concurrency,
138+
concurrency=concurrency, context=context,
137139
)
138140

139141
# This is injectable by tests.
@@ -333,6 +335,7 @@ def load(self):
333335

334336
def _init_for_start(self):
335337
"""Initialization for start()"""
338+
# Construct the collector.
336339
concurrency = self.config.concurrency or []
337340
if "multiprocessing" in concurrency:
338341
if not patch_multiprocessing:
@@ -363,6 +366,8 @@ def _init_for_start(self):
363366

364367
self._init_data(suffix)
365368

369+
self._collector.use_data(self._data, self.config.context)
370+
366371
# Early warning if we aren't going to be able to support plugins.
367372
if self._plugins.file_tracers and not self._collector.supports_plugins:
368373
self._warn(
@@ -562,7 +567,7 @@ def get_data(self):
562567
self._init_data(suffix=None)
563568
self._post_init()
564569

565-
if self._collector and self._collector.save_data(self._data):
570+
if self._collector and self._collector.flush_data():
566571
self._post_save_work()
567572

568573
return self._data
@@ -678,15 +683,11 @@ def _get_file_reporters(self, morfs=None):
678683
if not morfs:
679684
morfs = self._data.measured_files()
680685

681-
# Be sure we have a list.
682-
if not isinstance(morfs, (list, tuple)):
686+
# Be sure we have a collection.
687+
if not isinstance(morfs, (list, tuple, set)):
683688
morfs = [morfs]
684689

685-
file_reporters = []
686-
for morf in morfs:
687-
file_reporter = self._get_file_reporter(morf)
688-
file_reporters.append(file_reporter)
689-
690+
file_reporters = [self._get_file_reporter(morf) for morf in morfs]
690691
return file_reporters
691692

692693
def report(

coverage/data.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ def run_infos(self):
252252
return self._runs
253253

254254
def measured_files(self):
255-
"""A list of all files that had been measured."""
256-
return list(self._arcs or self._lines or {})
255+
"""A set of all files that had been measured."""
256+
return set(self._arcs or self._lines or {})
257257

258258
def __nonzero__(self):
259259
return bool(self._lines or self._arcs)
@@ -445,6 +445,11 @@ def touch_file(self, filename, plugin_name=""):
445445

446446
self._validate()
447447

448+
def set_context(self, context):
449+
"""Set the context. Not implemented for JSON storage."""
450+
if context:
451+
raise CoverageException("JSON storage doesn't support contexts")
452+
448453
def write(self):
449454
"""Write the collected coverage data to a file.
450455
@@ -722,6 +727,8 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False):
722727

723728
files_combined = 0
724729
for f in files_to_combine:
730+
if data._debug and data._debug.should('dataio'):
731+
data._debug.write("Combining data file %r" % (f,))
725732
try:
726733
new_data = CoverageData(f, debug=data._debug)
727734
new_data.read()

0 commit comments

Comments
 (0)