summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2021-11-23 06:35:45 -0500
committerNed Batchelder <ned@nedbatchelder.com>2021-11-25 15:03:08 -0500
commitc9d821deba6f7ee5eef30fef5355f7c93808b4f9 (patch)
treefb2d4d88de781e203d8beae8260e17380c5553f9 /coverage
parent97fdd550020384d2eedaf72ff0cd46a4efcb7d05 (diff)
downloadpython-coveragepy-git-nedbat/multi-concurrency.tar.gz
feat: multiple --concurrency values. #1012 #1082nedbat/multi-concurrency
Diffstat (limited to 'coverage')
-rw-r--r--coverage/cmdline.py16
-rw-r--r--coverage/collector.py101
-rw-r--r--coverage/config.py2
-rw-r--r--coverage/control.py2
-rw-r--r--coverage/version.py2
5 files changed, 69 insertions, 54 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index ae20acc5..ec809330 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -17,6 +17,7 @@ import coverage
from coverage import Coverage
from coverage import env
from coverage.collector import CTracer
+from coverage.config import CoverageConfig
from coverage.data import combinable_files, debug_data_file
from coverage.debug import info_formatter, info_header, short_stack
from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource
@@ -39,16 +40,12 @@ class Opts:
'', '--branch', action='store_true',
help="Measure branch coverage in addition to statement coverage.",
)
- CONCURRENCY_CHOICES = [
- "thread", "gevent", "greenlet", "eventlet", "multiprocessing",
- ]
concurrency = optparse.make_option(
- '', '--concurrency', action='store', metavar="LIB",
- choices=CONCURRENCY_CHOICES,
+ '', '--concurrency', action='store', metavar="LIBS",
help=(
"Properly measure code using a concurrency library. " +
"Valid values are: {}."
- ).format(", ".join(CONCURRENCY_CHOICES)),
+ ).format(", ".join(sorted(CoverageConfig.CONCURRENCY_CHOICES))),
)
context = optparse.make_option(
'', '--context', action='store', metavar="LABEL",
@@ -570,6 +567,11 @@ class CoverageScript:
debug = unshell_list(options.debug)
contexts = unshell_list(options.contexts)
+ if options.concurrency is not None:
+ concurrency = options.concurrency.split(",")
+ else:
+ concurrency = None
+
# Do something.
self.coverage = Coverage(
data_suffix=options.parallel_mode,
@@ -581,7 +583,7 @@ class CoverageScript:
omit=omit,
include=include,
debug=debug,
- concurrency=options.concurrency,
+ concurrency=concurrency,
check_preimported=True,
context=options.context,
messages=not options.quiet,
diff --git a/coverage/collector.py b/coverage/collector.py
index 89ba66ba..0397031a 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -7,6 +7,7 @@ import os
import sys
from coverage import env
+from coverage.config import CoverageConfig
from coverage.debug import short_stack
from coverage.disposition import FileDisposition
from coverage.exceptions import ConfigError
@@ -55,7 +56,7 @@ class Collector:
_collectors = []
# The concurrency settings we support here.
- SUPPORTED_CONCURRENCIES = {"greenlet", "eventlet", "gevent", "thread"}
+ LIGHT_THREADS = {"greenlet", "eventlet", "gevent"}
def __init__(
self, should_trace, check_include, should_start_context, file_mapper,
@@ -93,19 +94,21 @@ class Collector:
`concurrency` is a list of strings indicating the concurrency libraries
in use. Valid values are "greenlet", "eventlet", "gevent", or "thread"
- (the default). Of these four values, only one can be supplied. Other
- values are ignored.
+ (the default). "thread" can be combined with one of the other three.
+ Other values are ignored.
"""
self.should_trace = should_trace
self.check_include = check_include
self.should_start_context = should_start_context
self.file_mapper = file_mapper
- self.warn = warn
self.branch = branch
+ self.warn = warn
+ self.concurrency = concurrency
+ assert isinstance(self.concurrency, list), f"Expected a list: {self.concurrency!r}"
+
self.threading = None
self.covdata = None
-
self.static_context = None
self.origin = short_stack()
@@ -113,39 +116,6 @@ class Collector:
self.concur_id_func = None
self.mapped_file_cache = {}
- # We can handle a few concurrency options here, but only one at a time.
- these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency)
- if len(these_concurrencies) > 1:
- raise ConfigError(f"Conflicting concurrency settings: {concurrency}")
- self.concurrency = these_concurrencies.pop() if these_concurrencies else ''
-
- try:
- if self.concurrency == "greenlet":
- import greenlet
- self.concur_id_func = greenlet.getcurrent
- elif self.concurrency == "eventlet":
- import eventlet.greenthread # pylint: disable=import-error,useless-suppression
- self.concur_id_func = eventlet.greenthread.getcurrent
- elif self.concurrency == "gevent":
- import gevent # pylint: disable=import-error,useless-suppression
- self.concur_id_func = gevent.getcurrent
- elif self.concurrency == "thread" or not self.concurrency:
- # It's important to import threading only if we need it. If
- # it's imported early, and the program being measured uses
- # gevent, then gevent's monkey-patching won't work properly.
- import threading
- self.threading = threading
- else:
- raise ConfigError(f"Don't understand concurrency={concurrency}")
- except ImportError as ex:
- raise ConfigError(
- "Couldn't trace with concurrency={}, the module isn't installed.".format(
- self.concurrency,
- )
- ) from ex
-
- self.reset()
-
if timid:
# Being timid: use the simple Python trace function.
self._trace_class = PyTracer
@@ -163,6 +133,54 @@ class Collector:
self.supports_plugins = False
self.packed_arcs = False
+ # We can handle a few concurrency options here, but only one at a time.
+ concurrencies = set(self.concurrency)
+ unknown = concurrencies - CoverageConfig.CONCURRENCY_CHOICES
+ if unknown:
+ show = ", ".join(sorted(unknown))
+ raise ConfigError(f"Unknown concurrency choices: {show}")
+ light_threads = concurrencies & self.LIGHT_THREADS
+ if len(light_threads) > 1:
+ show = ", ".join(sorted(light_threads))
+ raise ConfigError(f"Conflicting concurrency settings: {show}")
+ do_threading = False
+
+ try:
+ if "greenlet" in concurrencies:
+ tried = "greenlet"
+ import greenlet
+ self.concur_id_func = greenlet.getcurrent
+ elif "eventlet" in concurrencies:
+ tried = "eventlet"
+ import eventlet.greenthread # pylint: disable=import-error,useless-suppression
+ self.concur_id_func = eventlet.greenthread.getcurrent
+ elif "gevent" in concurrencies:
+ tried = "gevent"
+ import gevent # pylint: disable=import-error,useless-suppression
+ self.concur_id_func = gevent.getcurrent
+
+ if "thread" in concurrencies:
+ do_threading = True
+ except ImportError as ex:
+ msg = f"Couldn't trace with concurrency={tried}, the module isn't installed."
+ raise ConfigError(msg) from ex
+
+ if self.concur_id_func and not hasattr(self._trace_class, "concur_id_func"):
+ raise ConfigError(
+ "Can't support concurrency={} with {}, only threads are supported.".format(
+ tried, self.tracer_name(),
+ )
+ )
+
+ if do_threading or not concurrencies:
+ # It's important to import threading only if we need it. If
+ # it's imported early, and the program being measured uses
+ # gevent, then gevent's monkey-patching won't work properly.
+ import threading
+ self.threading = threading
+
+ self.reset()
+
def __repr__(self):
return f"<Collector at 0x{id(self):x}: {self.tracer_name()}>"
@@ -244,13 +262,6 @@ class Collector:
if hasattr(tracer, 'concur_id_func'):
tracer.concur_id_func = self.concur_id_func
- elif self.concur_id_func:
- raise ConfigError(
- "Can't support concurrency={} with {}, only threads are supported".format(
- self.concurrency, self.tracer_name(),
- )
- )
-
if hasattr(tracer, 'file_tracers'):
tracer.file_tracers = self.file_tracers
if hasattr(tracer, 'threading'):
diff --git a/coverage/config.py b/coverage/config.py
index 8ed2dee7..9835e341 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -334,6 +334,8 @@ class CoverageConfig:
"""Return a copy of the configuration."""
return copy.deepcopy(self)
+ CONCURRENCY_CHOICES = {"thread", "gevent", "greenlet", "eventlet", "multiprocessing"}
+
CONFIG_FILE_OPTIONS = [
# These are *args for _set_attr_from_config_option:
# (attr, where, type_="")
diff --git a/coverage/control.py b/coverage/control.py
index 00836b3c..99319c05 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -448,7 +448,7 @@ class Coverage:
def _init_for_start(self):
"""Initialization for start()"""
# Construct the collector.
- concurrency = self.config.concurrency or ()
+ concurrency = self.config.concurrency or []
if "multiprocessing" in concurrency:
if not patch_multiprocessing:
raise ConfigError( # pragma: only jython
diff --git a/coverage/version.py b/coverage/version.py
index 394cd076..110eceb7 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -5,7 +5,7 @@
# This file is exec'ed in setup.py, don't import anything!
# Same semantics as sys.version_info.
-version_info = (6, 1, 3, "alpha", 0)
+version_info = (6, 2, 0, "alpha", 0)
def _make_version(major, minor, micro, releaselevel, serial):