summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2023-02-11 17:59:25 -0500
committerNed Batchelder <ned@nedbatchelder.com>2023-02-11 17:59:25 -0500
commit6bc043981f6548852844ea6b16d5ef7d37c0417d (patch)
tree5d10b3c3063feae47d909d2b722d3ff8624a49ae
parent151d3a74857b174ee4d852b98a0e1a7ef4f73427 (diff)
downloadpython-coveragepy-git-6bc043981f6548852844ea6b16d5ef7d37c0417d.tar.gz
refactor: use placebos instead of non-existent attributes
Details of the problem and thought process: https://nedbatchelder.com/blog/202302/late_initialization_with_mypy.html
-rw-r--r--coverage/control.py99
-rw-r--r--coverage/debug.py8
-rw-r--r--coverage/sqldata.py2
3 files changed, 72 insertions, 37 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 730ba554..cdea42ee 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -26,10 +26,10 @@ from typing import (
from coverage import env
from coverage.annotate import AnnotateReporter
from coverage.collector import Collector, HAS_CTRACER
-from coverage.config import read_coverage_config
+from coverage.config import CoverageConfig, read_coverage_config
from coverage.context import should_start_context_test_function, combine_context_switchers
from coverage.data import CoverageData, combine_parallel_data
-from coverage.debug import DebugControl, short_stack, write_formatted_info
+from coverage.debug import DebugControl, NoDebugging, short_stack, write_formatted_info
from coverage.disposition import disposition_debug_msg
from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
@@ -52,7 +52,6 @@ from coverage.types import (
)
from coverage.xmlreport import XmlReporter
-
os = isolate_module(os)
@contextlib.contextmanager
@@ -219,6 +218,10 @@ class Coverage(TConfigurable):
The `messages` parameter.
"""
+ # Start self.config as a usable default configuration. It will soon be
+ # replaced with the real configuration.
+ self.config = CoverageConfig()
+
# data_file=None means no disk file at all. data_file missing means
# use the value from the config file.
self._no_disk = data_file is None
@@ -241,14 +244,15 @@ class Coverage(TConfigurable):
# A record of all the warnings that have been issued.
self._warnings: List[str] = []
- # Other instance attributes, set later.
- self._debug: DebugControl
- self._plugins: Plugins
- self._inorout: InOrOut
- self._data: CoverageData
- self._collector: Collector
- self._file_mapper: Callable[[str], str]
+ # Other instance attributes, set with placebos or placeholders.
+ # More useful objects will be created later.
+ self._debug: DebugControl = NoDebugging()
+ self._inorout: InOrOut = InOrOutPlacebo()
+ self._plugins: Plugins = Plugins()
+ self._data: CoverageData = CoverageDataPlacebo()
+ self._collector: Collector = CollectorPlacebo()
+ self._file_mapper: Callable[[str], str] = abs_file
self._data_suffix = self._run_suffix = None
self._exclude_re: Dict[str, str] = {}
self._old_sigterm: Optional[Callable[[int, Optional[FrameType]], Any]] = None
@@ -315,7 +319,8 @@ class Coverage(TConfigurable):
self._exclude_re = {}
set_relative_directory()
- self._file_mapper = relative_filename if self.config.relative_files else abs_file
+ if self.config.relative_files:
+ self._file_mapper = relative_filename
# Load plugins
self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug)
@@ -400,9 +405,7 @@ class Coverage(TConfigurable):
"""
if not self._no_warn_slugs:
- # _warn() can be called before self.config is set in __init__...
- if hasattr(self, "config"):
- self._no_warn_slugs = list(self.config.disable_warnings)
+ self._no_warn_slugs = list(self.config.disable_warnings)
if slug in self._no_warn_slugs:
# Don't issue the warning
@@ -411,7 +414,7 @@ class Coverage(TConfigurable):
self._warnings.append(msg)
if slug:
msg = f"{msg} ({slug})"
- if hasattr(self, "_debug") and self._debug.should('pid'):
+ if self._debug.should('pid'):
msg = f"[{os.getpid()}] {msg}"
warnings.warn(msg, category=CoverageWarning, stacklevel=2)
@@ -477,8 +480,7 @@ class Coverage(TConfigurable):
def load(self) -> None:
"""Load previously-collected coverage data from the data file."""
self._init()
- if hasattr(self, "_collector"):
- self._collector.reset()
+ self._collector.reset()
should_skip = self.config.parallel and not os.path.exists(self.config.data_file)
if not should_skip:
self._init_data(suffix=None)
@@ -578,7 +580,7 @@ class Coverage(TConfigurable):
def _init_data(self, suffix: Optional[Union[str, bool]]) -> None:
"""Create a data file if we don't have one yet."""
- if not hasattr(self, "_data"):
+ if not self._data._real:
# Create the data file. We do this at construction time so that the
# data file will be written into the directory where the process
# started rather than wherever the process eventually chdir'd to.
@@ -658,11 +660,10 @@ class Coverage(TConfigurable):
"""
self._init()
self._post_init()
- if hasattr(self, "_collector"):
- self._collector.reset()
+ self._collector.reset()
self._init_data(suffix=None)
self._data.erase(parallel=self.config.parallel)
- del self._data
+ self._data = CoverageDataPlacebo()
self._inited_for_start = False
def switch_context(self, new_context: str) -> None:
@@ -709,7 +710,6 @@ class Coverage(TConfigurable):
"""
self._init()
excl_list = getattr(self.config, which + "_list")
- assert isinstance(regex, str)
excl_list.append(regex)
self._exclude_regex_stale()
@@ -814,7 +814,7 @@ class Coverage(TConfigurable):
if not plugin._coverage_enabled:
self._collector.plugin_was_disabled(plugin)
- if hasattr(self, "_collector") and self._collector.flush_data():
+ if self._collector.flush_data():
self._post_save_work()
return self._data
@@ -837,13 +837,12 @@ class Coverage(TConfigurable):
# Touch all the files that could have executed, so that we can
# mark completely un-executed files as 0% covered.
- if self._data is not None:
- file_paths = collections.defaultdict(list)
- for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files():
- file_path = self._file_mapper(file_path)
- file_paths[plugin_name].append(file_path)
- for plugin_name, paths in file_paths.items():
- self._data.touch_files(paths, plugin_name)
+ file_paths = collections.defaultdict(list)
+ for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files():
+ file_path = self._file_mapper(file_path)
+ file_paths[plugin_name].append(file_path)
+ for plugin_name, paths in file_paths.items():
+ self._data.touch_files(paths, plugin_name)
# Backward compatibility with version 1.
def analysis(self, morf: TMorf) -> Tuple[str, List[TLineNo], List[TLineNo], str]:
@@ -1253,7 +1252,7 @@ class Coverage(TConfigurable):
info = [
('coverage_version', covmod.__version__),
('coverage_module', covmod.__file__),
- ('tracer', self._collector.tracer_name() if hasattr(self, "_collector") else "-none-"),
+ ('tracer', self._collector.tracer_name()),
('CTracer', 'available' if HAS_CTRACER else "unavailable"),
('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
('plugins.configurers', plugin_info(self._plugins.configurers)),
@@ -1264,7 +1263,7 @@ class Coverage(TConfigurable):
('config_contents',
repr(self.config._config_contents) if self.config._config_contents else '-none-'
),
- ('data_file', self._data.data_filename() if hasattr(self, "_data") else "-none-"),
+ ('data_file', self._data.data_filename()),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
('implementation', platform.python_implementation()),
@@ -1285,14 +1284,44 @@ class Coverage(TConfigurable):
('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
]
- if hasattr(self, "_inorout"):
- info.extend(self._inorout.sys_info())
-
+ info.extend(self._inorout.sys_info())
info.extend(CoverageData.sys_info())
return info
+class _Placebo:
+ """Base class for placebos, to prevent calling the real base class __init__."""
+ def __init__(self) -> None:
+ ...
+
+
+class CoverageDataPlacebo(_Placebo, CoverageData):
+ """Just enough of a CoverageData to be used when we don't have a real one."""
+ _real = False
+
+ def data_filename(self) -> str:
+ return "-none-"
+
+
+class CollectorPlacebo(_Placebo, Collector):
+ """Just enough of a Collector to be used when we don't have a real one."""
+ def reset(self) -> None:
+ ...
+
+ def flush_data(self) -> bool:
+ return False
+
+ def tracer_name(self) -> str:
+ return "-none-"
+
+
+class InOrOutPlacebo(_Placebo, InOrOut):
+ """Just enough of an InOrOut to be used when we don't have a real one."""
+ def sys_info(self) -> Iterable[Tuple[str, Any]]:
+ return []
+
+
# Mega debugging...
# $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage.
if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging
diff --git a/coverage/debug.py b/coverage/debug.py
index 12233959..d56a66bb 100644
--- a/coverage/debug.py
+++ b/coverage/debug.py
@@ -105,9 +105,13 @@ class DebugControlString(DebugControl):
return cast(str, self.raw_output.getvalue()) # type: ignore
-class NoDebugging:
+class NoDebugging(DebugControl):
"""A replacement for DebugControl that will never try to do anything."""
- def should(self, option: str) -> bool: # pylint: disable=unused-argument
+ def __init__(self) -> None:
+ # pylint: disable=super-init-not-called
+ ...
+
+ def should(self, option: str) -> bool:
"""Should we write debug messages? Never."""
return False
diff --git a/coverage/sqldata.py b/coverage/sqldata.py
index 9aa2b129..bb26e4d5 100644
--- a/coverage/sqldata.py
+++ b/coverage/sqldata.py
@@ -212,6 +212,8 @@ class CoverageData(AutoReprMixin):
"""
+ _real = True # to distinguish from a placebo in control.py
+
def __init__(
self,
basename: Optional[str] = None,