summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2023-01-22 09:08:09 -0500
committerNed Batchelder <ned@nedbatchelder.com>2023-01-22 10:58:53 -0500
commit5f65d87b14245d4523bc866a75a16b6c55a7ce70 (patch)
treebd4d404c80baa1c7e6e25e2335ffcee52702ee66 /coverage
parentc51ac463f07e31c87b20f50bd7e6445e4e4e83a2 (diff)
downloadpython-coveragepy-git-5f65d87b14245d4523bc866a75a16b6c55a7ce70.tar.gz
feat: the debug output file can be specified in the config file. #1319
Diffstat (limited to 'coverage')
-rw-r--r--coverage/config.py2
-rw-r--r--coverage/control.py6
-rw-r--r--coverage/debug.py70
3 files changed, 52 insertions, 26 deletions
diff --git a/coverage/config.py b/coverage/config.py
index 046c1b00..e15d2aff 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -199,6 +199,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
self.cover_pylib = False
self.data_file = ".coverage"
self.debug: List[str] = []
+ self.debug_file: Optional[str] = None
self.disable_warnings: List[str] = []
self.dynamic_context: Optional[str] = None
self.parallel = False
@@ -375,6 +376,7 @@ class CoverageConfig(TConfigurable, TPluginConfig):
('cover_pylib', 'run:cover_pylib', 'boolean'),
('data_file', 'run:data_file'),
('debug', 'run:debug', 'list'),
+ ('debug_file', 'run:debug_file'),
('disable_warnings', 'run:disable_warnings', 'list'),
('dynamic_context', 'run:dynamic_context'),
('parallel', 'run:parallel', 'boolean'),
diff --git a/coverage/control.py b/coverage/control.py
index d37c77e3..78e0c70e 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -303,10 +303,8 @@ class Coverage(TConfigurable):
self._inited = True
- # Create and configure the debugging controller. COVERAGE_DEBUG_FILE
- # is an environment variable, the name of a file to append debug logs
- # to.
- self._debug = DebugControl(self.config.debug, self._debug_file)
+ # Create and configure the debugging controller.
+ self._debug = DebugControl(self.config.debug, self._debug_file, self.config.debug_file)
if "multiprocessing" in (self.config.concurrency or ()):
# Multi-processing uses parallel for the subprocesses, so also use
diff --git a/coverage/debug.py b/coverage/debug.py
index 29dd1e7f..d1f27cd8 100644
--- a/coverage/debug.py
+++ b/coverage/debug.py
@@ -39,7 +39,12 @@ class DebugControl:
show_repr_attr = False # For AutoReprMixin
- def __init__(self, options: Iterable[str], output: Optional[IO[str]]) -> None:
+ def __init__(
+ self,
+ options: Iterable[str],
+ output: Optional[IO[str]],
+ file_name: Optional[str] = None,
+ ) -> None:
"""Configure the options and output file for debugging."""
self.options = list(options) + FORCED_DEBUG
self.suppress_callers = False
@@ -49,6 +54,7 @@ class DebugControl:
filters.append(add_pid_and_tid)
self.output = DebugOutputFile.get_one(
output,
+ file_name=file_name,
show_process=self.should('process'),
filters=filters,
)
@@ -306,13 +312,11 @@ class DebugOutputFile: # pragma: debugging
if hasattr(os, 'getppid'):
self.write(f"New process: pid: {os.getpid()!r}, parent pid: {os.getppid()!r}\n")
- SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
- SINGLETON_ATTR = 'the_one_and_is_interim'
-
@classmethod
def get_one(
cls,
fileobj: Optional[IO[str]] = None,
+ file_name: Optional[str] = None,
show_process: bool = True,
filters: Iterable[Callable[[str], str]] = (),
interim: bool = False,
@@ -321,9 +325,9 @@ class DebugOutputFile: # pragma: debugging
If `fileobj` is provided, then a new DebugOutputFile is made with it.
- If `fileobj` isn't provided, then a file is chosen
- (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton
- DebugOutputFile is made.
+ If `fileobj` isn't provided, then a file is chosen (`file_name` if
+ provided, or COVERAGE_DEBUG_FILE, or stderr), and a process-wide
+ singleton DebugOutputFile is made.
`show_process` controls whether the debug file adds process-level
information, and filters is a list of other message filters to apply.
@@ -338,27 +342,49 @@ class DebugOutputFile: # pragma: debugging
# Make DebugOutputFile around the fileobj passed.
return cls(fileobj, show_process, filters)
- # Because of the way igor.py deletes and re-imports modules,
- # this class can be defined more than once. But we really want
- # a process-wide singleton. So stash it in sys.modules instead of
- # on a class attribute. Yes, this is aggressively gross.
- singleton_module = sys.modules.get(cls.SYS_MOD_NAME)
- the_one, is_interim = getattr(singleton_module, cls.SINGLETON_ATTR, (None, True))
+ the_one, is_interim = cls._get_singleton_data()
if the_one is None or is_interim:
- if fileobj is None:
- debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
- if debug_file_name in ("stdout", "stderr"):
- fileobj = getattr(sys, debug_file_name)
- elif debug_file_name:
- fileobj = open(debug_file_name, "a")
+ if file_name is not None:
+ fileobj = open(file_name, "a", encoding="utf-8")
+ else:
+ file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE)
+ if file_name in ("stdout", "stderr"):
+ fileobj = getattr(sys, file_name)
+ elif file_name:
+ fileobj = open(file_name, "a", encoding="utf-8")
else:
fileobj = sys.stderr
the_one = cls(fileobj, show_process, filters)
- singleton_module = types.ModuleType(cls.SYS_MOD_NAME)
- setattr(singleton_module, cls.SINGLETON_ATTR, (the_one, interim))
- sys.modules[cls.SYS_MOD_NAME] = singleton_module
+ cls._set_singleton_data(the_one, interim)
return the_one
+ # Because of the way igor.py deletes and re-imports modules,
+ # this class can be defined more than once. But we really want
+ # a process-wide singleton. So stash it in sys.modules instead of
+ # on a class attribute. Yes, this is aggressively gross.
+
+ SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one'
+ SINGLETON_ATTR = 'the_one_and_is_interim'
+
+ @classmethod
+ def _set_singleton_data(cls, the_one: DebugOutputFile, interim: bool) -> None:
+ """Set the one DebugOutputFile to rule them all."""
+ singleton_module = types.ModuleType(cls.SYS_MOD_NAME)
+ setattr(singleton_module, cls.SINGLETON_ATTR, (the_one, interim))
+ sys.modules[cls.SYS_MOD_NAME] = singleton_module
+
+ @classmethod
+ def _get_singleton_data(cls) -> Tuple[Optional[DebugOutputFile], bool]:
+ """Get the one DebugOutputFile."""
+ singleton_module = sys.modules.get(cls.SYS_MOD_NAME)
+ return getattr(singleton_module, cls.SINGLETON_ATTR, (None, True))
+
+ @classmethod
+ def _del_singleton_data(cls) -> None:
+ """Delete the one DebugOutputFile, just for tests to use."""
+ if cls.SYS_MOD_NAME in sys.modules:
+ del sys.modules[cls.SYS_MOD_NAME]
+
def write(self, text: str) -> None:
"""Just like file.write, but filter through all our filters."""
assert self.outfile is not None