Skip to content

Commit 5937a62

Browse files
committed
feat: cov.collect() context manager
1 parent da7ee52 commit 5937a62

File tree

11 files changed

+100
-106
lines changed

11 files changed

+100
-106
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ development at the same time, such as 4.5.x and 5.0.
2020
Unreleased
2121
----------
2222

23+
- Added a :meth:`.Coverage.collect` context manager to start and stop coverage
24+
data collection.
25+
2326
- Dropped support for Python 3.7.
2427

2528
- Fix: in unusual circumstances, SQLite cannot be set to asynchronous mode.

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on
3838
.. _GitHub: https://github.com/nedbat/coveragepy
3939

4040
**New in 7.x:**
41+
dropped support for Python 3.7;
42+
added ``Coverage.collect()`` context manager;
4143
improved data combining;
4244
``[run] exclude_also`` setting;
4345
``report --format=``;

coverage/control.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ class Coverage(TConfigurable):
8888
cov.stop()
8989
cov.html_report(directory="covhtml")
9090
91+
A context manager is available to do the same thing::
92+
93+
cov = Coverage()
94+
with cov.collect():
95+
#.. call your code ..
96+
cov.html_report(directory="covhtml")
97+
9198
Note: in keeping with Python custom, names starting with underscore are
9299
not part of the public API. They might stop working at any point. Please
93100
limit yourself to documented methods to avoid problems.
@@ -607,13 +614,16 @@ def _init_data(self, suffix: Optional[Union[str, bool]]) -> None:
607614
def start(self) -> None:
608615
"""Start measuring code coverage.
609616
610-
Coverage measurement only occurs in functions called after
617+
Coverage measurement is only collected in functions called after
611618
:meth:`start` is invoked. Statements in the same scope as
612619
:meth:`start` won't be measured.
613620
614621
Once you invoke :meth:`start`, you must also call :meth:`stop`
615622
eventually, or your process might not shut down cleanly.
616623
624+
The :meth:`collect` method is a context manager to handle both
625+
starting and stopping collection.
626+
617627
"""
618628
self._init()
619629
if not self._inited_for_start:
@@ -649,6 +659,19 @@ def stop(self) -> None:
649659
self._collector.stop()
650660
self._started = False
651661

662+
@contextlib.contextmanager
663+
def collect(self) -> Iterator[None]:
664+
"""A context manager to start/stop coverage measurement collection.
665+
666+
.. versionadded:: 7.3
667+
668+
"""
669+
self.start()
670+
try:
671+
yield
672+
finally:
673+
self.stop()
674+
652675
def _atexit(self, event: str = "atexit") -> None:
653676
"""Clean up on process shutdown."""
654677
if self._debug.should("process"):

coverage/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
# version_info: same semantics as sys.version_info.
1010
# _dev: the .devN suffix if any.
11-
version_info = (7, 2, 8, "alpha", 0)
11+
version_info = (7, 3, 0, "alpha", 0)
1212
_dev = 1
1313

1414

metacov.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ exclude_lines =
4343
#
4444
pragma: nested
4545
cov.stop\(\)
46+
with cov.collect\(\):
4647

4748
# Lines that are only executed when we are debugging coverage.py.
4849
def __repr__

tests/coveragetest.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,9 @@ def start_import_stop(
8787
The imported module is returned.
8888
8989
"""
90-
cov.start()
91-
try: # pragma: nested
90+
with cov.collect():
9291
# Import the Python file, executing it.
93-
mod = import_local_file(modname, modfile)
94-
finally: # pragma: nested
95-
# Stop coverage.py.
96-
cov.stop()
97-
return mod
92+
return import_local_file(modname, modfile)
9893

9994
def get_report(self, cov: Coverage, squeeze: bool = True, **kwargs: Any) -> str:
10095
"""Get the report from `cov`, and canonicalize it."""

tests/test_api.py

Lines changed: 40 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,8 @@ def f1() -> None: # pragma: nested
282282

283283
def run_one_function(f: Callable[[], None]) -> None:
284284
cov.erase()
285-
cov.start()
286-
f() # pragma: nested
287-
cov.stop()
285+
with cov.collect():
286+
f()
288287

289288
fs = cov.get_data().measured_files()
290289
lines.append(cov.get_data().lines(list(fs)[0]))
@@ -363,30 +362,26 @@ def test_start_stop_start_stop(self) -> None:
363362
def test_start_save_stop(self) -> None:
364363
self.make_code1_code2()
365364
cov = coverage.Coverage()
366-
cov.start()
367-
import_local_file("code1") # pragma: nested
368-
cov.save() # pragma: nested
369-
import_local_file("code2") # pragma: nested
370-
cov.stop()
365+
with cov.collect():
366+
import_local_file("code1")
367+
cov.save()
368+
import_local_file("code2")
371369
self.check_code1_code2(cov)
372370

373371
def test_start_save_nostop(self) -> None:
374372
self.make_code1_code2()
375373
cov = coverage.Coverage()
376-
cov.start()
377-
import_local_file("code1") # pragma: nested
378-
cov.save() # pragma: nested
379-
import_local_file("code2") # pragma: nested
380-
self.check_code1_code2(cov) # pragma: nested
381-
# Then stop it, or the test suite gets out of whack.
382-
cov.stop()
374+
with cov.collect():
375+
import_local_file("code1")
376+
cov.save()
377+
import_local_file("code2")
378+
self.check_code1_code2(cov)
383379

384380
def test_two_getdata_only_warn_once(self) -> None:
385381
self.make_code1_code2()
386382
cov = coverage.Coverage(source=["."], omit=["code1.py"])
387-
cov.start()
388-
import_local_file("code1") # pragma: nested
389-
cov.stop()
383+
with cov.collect():
384+
import_local_file("code1")
390385
# We didn't collect any data, so we should get a warning.
391386
with self.assert_warnings(cov, ["No data was collected"]):
392387
cov.get_data()
@@ -398,17 +393,15 @@ def test_two_getdata_only_warn_once(self) -> None:
398393
def test_two_getdata_warn_twice(self) -> None:
399394
self.make_code1_code2()
400395
cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"])
401-
cov.start()
402-
import_local_file("code1") # pragma: nested
403-
# We didn't collect any data, so we should get a warning.
404-
with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested
405-
cov.save() # pragma: nested
406-
import_local_file("code2") # pragma: nested
407-
# Calling get_data a second time after tracing some more will warn again.
408-
with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested
409-
cov.get_data() # pragma: nested
410-
# Then stop it, or the test suite gets out of whack.
411-
cov.stop()
396+
with cov.collect():
397+
import_local_file("code1")
398+
# We didn't collect any data, so we should get a warning.
399+
with self.assert_warnings(cov, ["No data was collected"]):
400+
cov.save()
401+
import_local_file("code2")
402+
# Calling get_data a second time after tracing some more will warn again.
403+
with self.assert_warnings(cov, ["No data was collected"]):
404+
cov.get_data()
412405

413406
def make_good_data_files(self) -> None:
414407
"""Make some good data files."""
@@ -632,9 +625,7 @@ def test_switch_context_testrunner(self) -> None:
632625

633626
# Test runner starts
634627
cov = coverage.Coverage()
635-
cov.start()
636-
637-
if "pragma: nested":
628+
with cov.collect():
638629
# Imports the test suite
639630
suite = import_local_file("testsuite")
640631

@@ -648,7 +639,6 @@ def test_switch_context_testrunner(self) -> None:
648639

649640
# Runner finishes
650641
cov.save()
651-
cov.stop()
652642

653643
# Labeled data is collected
654644
data = cov.get_data()
@@ -670,9 +660,7 @@ def test_switch_context_with_static(self) -> None:
670660

671661
# Test runner starts
672662
cov = coverage.Coverage(context="mysuite")
673-
cov.start()
674-
675-
if "pragma: nested":
663+
with cov.collect():
676664
# Imports the test suite
677665
suite = import_local_file("testsuite")
678666

@@ -686,7 +674,6 @@ def test_switch_context_with_static(self) -> None:
686674

687675
# Runner finishes
688676
cov.save()
689-
cov.stop()
690677

691678
# Labeled data is collected
692679
data = cov.get_data()
@@ -704,12 +691,11 @@ def test_switch_context_with_static(self) -> None:
704691
def test_dynamic_context_conflict(self) -> None:
705692
cov = coverage.Coverage(source=["."])
706693
cov.set_option("run:dynamic_context", "test_function")
707-
cov.start()
708-
with pytest.warns(Warning) as warns: # pragma: nested
709-
# Switch twice, but only get one warning.
710-
cov.switch_context("test1")
711-
cov.switch_context("test2")
712-
cov.stop()
694+
with cov.collect():
695+
with pytest.warns(Warning) as warns:
696+
# Switch twice, but only get one warning.
697+
cov.switch_context("test1")
698+
cov.switch_context("test2")
713699
assert_coverage_warnings(warns, "Conflicting dynamic contexts (dynamic-conflict)")
714700

715701
def test_unknown_dynamic_context(self) -> None:
@@ -725,10 +711,9 @@ def test_switch_context_unstarted(self) -> None:
725711
with pytest.raises(CoverageException, match=msg):
726712
cov.switch_context("test1")
727713

728-
cov.start()
729-
cov.switch_context("test2") # pragma: nested
714+
with cov.collect():
715+
cov.switch_context("test2")
730716

731-
cov.stop()
732717
with pytest.raises(CoverageException, match=msg):
733718
cov.switch_context("test3")
734719

@@ -750,9 +735,8 @@ def test_config_crash_no_crash(self) -> None:
750735
def test_run_debug_sys(self) -> None:
751736
# https://github.com/nedbat/coveragepy/issues/907
752737
cov = coverage.Coverage()
753-
cov.start()
754-
d = dict(cov.sys_info()) # pragma: nested
755-
cov.stop()
738+
with cov.collect():
739+
d = dict(cov.sys_info())
756740
assert cast(str, d['data_file']).endswith(".coverage")
757741

758742

@@ -779,12 +763,10 @@ def test_current(self) -> None:
779763
self.assert_current_is_none(cur1)
780764
assert cur0 is cur1
781765
# Starting the instance makes it current.
782-
cov.start()
783-
if "# pragma: nested":
766+
with cov.collect():
784767
cur2 = coverage.Coverage.current()
785768
assert cur2 is cov
786769
# Stopping the instance makes current None again.
787-
cov.stop()
788770

789771
cur3 = coverage.Coverage.current()
790772
self.assert_current_is_none(cur3)
@@ -900,9 +882,8 @@ def coverage_usepkgs_counts(self, **kwargs: TCovKwargs) -> Dict[str, int]:
900882
901883
"""
902884
cov = coverage.Coverage(**kwargs)
903-
cov.start()
904-
import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
905-
cov.stop()
885+
with cov.collect():
886+
import usepkgs # pylint: disable=import-error, unused-import
906887
with self.assert_warnings(cov, []):
907888
data = cov.get_data()
908889
summary = line_counts(data)
@@ -993,9 +974,8 @@ class ReportIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest):
993974
def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]:
994975
"""Try coverage.report()."""
995976
cov = coverage.Coverage()
996-
cov.start()
997-
import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
998-
cov.stop()
977+
with cov.collect():
978+
import usepkgs # pylint: disable=import-error, unused-import
999979
report = io.StringIO()
1000980
cov.report(file=report, **kwargs)
1001981
return report.getvalue()
@@ -1012,9 +992,8 @@ class XmlIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest):
1012992
def coverage_usepkgs(self, **kwargs: TCovKwargs) -> Iterable[str]:
1013993
"""Try coverage.xml_report()."""
1014994
cov = coverage.Coverage()
1015-
cov.start()
1016-
import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
1017-
cov.stop()
995+
with cov.collect():
996+
import usepkgs # pylint: disable=import-error, unused-import
1018997
cov.xml_report(outfile="-", **kwargs)
1019998
return self.stdout()
1020999

tests/test_concurrency.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -624,13 +624,11 @@ def run_thread() -> None: # pragma: nested
624624
has_stopped_coverage.append(ident)
625625

626626
cov = coverage.Coverage()
627-
cov.start()
627+
with cov.collect():
628+
t = threading.Thread(target=run_thread)
629+
t.start()
628630

629-
t = threading.Thread(target=run_thread) # pragma: nested
630-
t.start() # pragma: nested
631-
632-
time.sleep(0.1) # pragma: nested
633-
cov.stop() # pragma: nested
631+
time.sleep(0.1)
634632
t.join()
635633

636634
assert has_started_coverage == [t.ident]
@@ -672,16 +670,13 @@ def random_load() -> None: # pragma: nested
672670
duration = 0.01
673671
for _ in range(3):
674672
cov = coverage.Coverage()
675-
cov.start()
676-
677-
threads = [threading.Thread(target=random_load) for _ in range(10)] # pragma: nested
678-
should_run[0] = True # pragma: nested
679-
for t in threads: # pragma: nested
680-
t.start()
681-
682-
time.sleep(duration) # pragma: nested
673+
with cov.collect():
674+
threads = [threading.Thread(target=random_load) for _ in range(10)]
675+
should_run[0] = True
676+
for t in threads:
677+
t.start()
683678

684-
cov.stop() # pragma: nested
679+
time.sleep(duration)
685680

686681
# The following call used to crash with running background threads.
687682
cov.get_data()

tests/test_oddball.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,10 @@ def doit(calls):
374374
calls = [getattr(sys.modules[cn], cn) for cn in callnames_list]
375375

376376
cov = coverage.Coverage()
377-
cov.start()
378-
# Call our list of functions: invoke the first, with the rest as
379-
# an argument.
380-
calls[0](calls[1:]) # pragma: nested
381-
cov.stop() # pragma: nested
377+
with cov.collect():
378+
# Call our list of functions: invoke the first, with the rest as
379+
# an argument.
380+
calls[0](calls[1:])
382381

383382
# Clean the line data and compare to expected results.
384383
# The file names are absolute, so keep just the base.

0 commit comments

Comments
 (0)