summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2022-12-30 07:05:42 -0500
committerNed Batchelder <ned@nedbatchelder.com>2022-12-30 09:57:46 -0500
commit27990185352f035bafbb0cc7c8ac4159e87fe070 (patch)
treee3f0a1a19f893347c7909294db770e6aa8e514d6
parentc802be289c40f896e910a4f34f1ce27aedc44a0b (diff)
downloadpython-coveragepy-git-27990185352f035bafbb0cc7c8ac4159e87fe070.tar.gz
mypy: inorout.py, disposition.py, and part of control.py
-rw-r--r--CHANGES.rst4
-rw-r--r--coverage/config.py14
-rw-r--r--coverage/control.py137
-rw-r--r--coverage/disposition.py21
-rw-r--r--coverage/inorout.py150
-rw-r--r--coverage/plugin.py4
-rw-r--r--coverage/python.py2
-rw-r--r--coverage/types.py5
-rw-r--r--tests/test_api.py2
-rw-r--r--tests/test_config.py4
-rw-r--r--tox.ini2
11 files changed, 189 insertions, 156 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 54bbe789..9cebd7cb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,10 @@ development at the same time, such as 4.5.x and 5.0.
Unreleased
----------
+- Refactor: a number of refactorings internally due to adding type annotations.
+ This should not affect outward behavior, but they were a bit invasive in some
+ places.
+
- Fix: if Python doesn't provide tomllib, then TOML configuration files can
only be read if coverage.py is installed with the ``[toml]`` extra.
Coverage.py will raise an error if toml support is not installed when it sees
diff --git a/coverage/config.py b/coverage/config.py
index 1846aee4..aae6065b 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -190,20 +190,20 @@ class CoverageConfig(TConfigurable):
# Defaults for [run]
self.branch = False
self.command_line = None
- self.concurrency = None
+ self.concurrency: List[str] = []
self.context = None
self.cover_pylib = False
self.data_file = ".coverage"
self.debug: List[str] = []
self.disable_warnings: List[str] = []
- self.dynamic_context = None
+ self.dynamic_context: Optional[str] = None
self.parallel = False
self.plugins: List[str] = []
self.relative_files = False
- self.run_include = None
- self.run_omit = None
+ self.run_include: List[str] = []
+ self.run_omit: List[str] = []
self.sigterm = False
- self.source = None
+ self.source: Optional[List[str]] = None
self.source_pkgs: List[str] = []
self.timid = False
self._crash = None
@@ -214,8 +214,8 @@ class CoverageConfig(TConfigurable):
self.format = None
self.ignore_errors = False
self.include_namespace_packages = False
- self.report_include = None
- self.report_omit = None
+ self.report_include: Optional[List[str]] = None
+ self.report_omit: Optional[List[str]] = None
self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:]
self.partial_list = DEFAULT_PARTIAL[:]
self.precision = 0
diff --git a/coverage/control.py b/coverage/control.py
index 37e61cfb..69db200b 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -3,18 +3,24 @@
"""Core control stuff for coverage.py."""
+from __future__ import annotations
+
import atexit
import collections
import contextlib
import os
import os.path
import platform
+import re
import signal
import sys
import threading
import time
import warnings
+from types import FrameType
+from typing import Any, Callable, Dict, Generator, List, Optional, Union
+
from coverage import env
from coverage.annotate import AnnotateReporter
from coverage.collector import Collector, CTracer
@@ -22,7 +28,7 @@ from coverage.config import 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.disposition import disposition_debug_msg
+from coverage.disposition import FileDisposition, disposition_debug_msg
from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError
from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
from coverage.html import HtmlReporter
@@ -37,18 +43,20 @@ from coverage.python import PythonFileReporter
from coverage.report import render_report
from coverage.results import Analysis
from coverage.summary import SummaryReporter
+from coverage.types import TConfigurable, TConfigSection, TConfigValue, TSysInfo
from coverage.xmlreport import XmlReporter
try:
from coverage.multiproc import patch_multiprocessing
+ has_patch_multiprocessing = True
except ImportError: # pragma: only jython
# Jython has no multiprocessing module.
- patch_multiprocessing = None
+ has_patch_multiprocessing = False
os = isolate_module(os)
@contextlib.contextmanager
-def override_config(cov, **kwargs):
+def override_config(cov: Coverage, **kwargs: Any) -> Generator[None, None, None]:
"""Temporarily tweak the configuration of `cov`.
The arguments are applied to `cov.config` with the `from_args` method.
@@ -66,7 +74,7 @@ def override_config(cov, **kwargs):
DEFAULT_DATAFILE = DefaultValue("MISSING")
_DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility
-class Coverage:
+class Coverage(TConfigurable):
"""Programmatic access to coverage.py.
To use::
@@ -88,10 +96,10 @@ class Coverage:
"""
# The stack of started Coverage instances.
- _instances = []
+ _instances: List[Coverage] = []
@classmethod
- def current(cls):
+ def current(cls) -> Optional[Coverage]:
"""Get the latest started `Coverage` instance, if any.
Returns: a `Coverage` instance, or None.
@@ -122,7 +130,7 @@ class Coverage:
check_preimported=False,
context=None,
messages=False,
- ): # pylint: disable=too-many-arguments
+ ) -> None: # pylint: disable=too-many-arguments
"""
Many of these arguments duplicate and override values that can be
provided in a configuration file. Parameters that are missing here
@@ -217,8 +225,6 @@ class Coverage:
if data_file is DEFAULT_DATAFILE:
data_file = None
- self.config = None
-
# This is injectable by tests.
self._debug_file = None
@@ -229,20 +235,22 @@ class Coverage:
self._warn_no_data = True
self._warn_unimported_source = True
self._warn_preimported_source = check_preimported
- self._no_warn_slugs = None
+ self._no_warn_slugs: List[str] = []
self._messages = messages
# A record of all the warnings that have been issued.
- self._warnings = []
+ self._warnings: List[str] = []
# Other instance attributes, set later.
- self._data = self._collector = None
- self._plugins = None
- self._inorout = None
+ self._debug: DebugControl
+ self._plugins: Plugins
+ self._inorout: InOrOut
+ self._data: CoverageData
+ self._collector: Collector
+ self._file_mapper: Callable[[str], str]
+
self._data_suffix = self._run_suffix = None
- self._exclude_re = None
- self._debug = None
- self._file_mapper = None
+ self._exclude_re: Dict[str, re.Pattern[str]] = {}
self._old_sigterm = None
# State machine variables:
@@ -282,7 +290,7 @@ class Coverage:
if not env.METACOV:
_prevent_sub_process_measurement()
- def _init(self):
+ def _init(self) -> None:
"""Set all the initial state.
This is called by the public methods to initialize state. This lets us
@@ -322,7 +330,7 @@ class Coverage:
# this is a bit childish. :)
plugin.configure([self, self.config][int(time.time()) % 2])
- def _post_init(self):
+ def _post_init(self) -> None:
"""Stuff to do after everything is initialized."""
if self._should_write_debug:
self._should_write_debug = False
@@ -333,7 +341,7 @@ class Coverage:
if self.config._crash and self.config._crash in short_stack(limit=4):
raise Exception(f"Crashing because called by {self.config._crash}")
- def _write_startup_debug(self):
+ def _write_startup_debug(self) -> None:
"""Write out debug info at startup if needed."""
wrote_any = False
with self._debug.without_callers():
@@ -357,7 +365,7 @@ class Coverage:
if wrote_any:
write_formatted_info(self._debug.write, "end", ())
- def _should_trace(self, filename, frame):
+ def _should_trace(self, filename: str, frame: FrameType) -> FileDisposition:
"""Decide whether to trace execution in `filename`.
Calls `_should_trace_internal`, and returns the FileDisposition.
@@ -368,7 +376,7 @@ class Coverage:
self._debug.write(disposition_debug_msg(disp))
return disp
- def _check_include_omit_etc(self, filename, frame):
+ def _check_include_omit_etc(self, filename: str, frame: FrameType) -> bool:
"""Check a file name against the include/omit/etc, rules, verbosely.
Returns a boolean: True if the file should be traced, False if not.
@@ -384,7 +392,7 @@ class Coverage:
return not reason
- def _warn(self, msg, slug=None, once=False):
+ def _warn(self, msg: str, slug: Optional[str]=None, once: bool=False) -> None:
"""Use `msg` as a warning.
For warning suppression, use `slug` as the shorthand.
@@ -393,31 +401,32 @@ class Coverage:
slug.)
"""
- if self._no_warn_slugs is None:
- if self.config is not None:
+ 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)
- if self._no_warn_slugs is not None:
- if slug in self._no_warn_slugs:
- # Don't issue the warning
- return
+ if slug in self._no_warn_slugs:
+ # Don't issue the warning
+ return
self._warnings.append(msg)
if slug:
msg = f"{msg} ({slug})"
- if self._debug is not None and self._debug.should('pid'):
+ if hasattr(self, "_debug") and self._debug.should('pid'):
msg = f"[{os.getpid()}] {msg}"
warnings.warn(msg, category=CoverageWarning, stacklevel=2)
if once:
+ assert slug is not None
self._no_warn_slugs.append(slug)
- def _message(self, msg):
+ def _message(self, msg: str) -> None:
"""Write a message to the user, if configured to do so."""
if self._messages:
print(msg)
- def get_option(self, option_name):
+ def get_option(self, option_name: str) -> Optional[TConfigValue]:
"""Get an option from the configuration.
`option_name` is a colon-separated string indicating the section and
@@ -428,14 +437,14 @@ class Coverage:
selected.
As a special case, an `option_name` of ``"paths"`` will return an
- OrderedDict with the entire ``[paths]`` section value.
+ dictionary with the entire ``[paths]`` section value.
.. versionadded:: 4.0
"""
return self.config.get_option(option_name)
- def set_option(self, option_name, value):
+ def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection]) -> None:
"""Set an option in the configuration.
`option_name` is a colon-separated string indicating the section and
@@ -460,17 +469,17 @@ class Coverage:
branch = True
As a special case, an `option_name` of ``"paths"`` will replace the
- entire ``[paths]`` section. The value should be an OrderedDict.
+ entire ``[paths]`` section. The value should be a dictionary.
.. versionadded:: 4.0
"""
self.config.set_option(option_name, value)
- def load(self):
+ def load(self) -> None:
"""Load previously-collected coverage data from the data file."""
self._init()
- if self._collector:
+ if hasattr(self, "_collector"):
self._collector.reset()
should_skip = self.config.parallel and not os.path.exists(self.config.data_file)
if not should_skip:
@@ -479,12 +488,12 @@ class Coverage:
if not should_skip:
self._data.read()
- def _init_for_start(self):
+ def _init_for_start(self) -> None:
"""Initialization for start()"""
# Construct the collector.
- concurrency = self.config.concurrency or []
+ concurrency: List[str] = self.config.concurrency or []
if "multiprocessing" in concurrency:
- if not patch_multiprocessing:
+ if not has_patch_multiprocessing:
raise ConfigError( # pragma: only jython
"multiprocessing is not supported on this Python"
)
@@ -550,11 +559,11 @@ class Coverage:
# Create the file classifying substructure.
self._inorout = InOrOut(
+ config=self.config,
warn=self._warn,
debug=(self._debug if self._debug.should('trace') else None),
include_namespace_packages=self.config.include_namespace_packages,
)
- self._inorout.configure(self.config)
self._inorout.plugins = self._plugins
self._inorout.disp_class = self._collector.file_disposition_class
@@ -573,7 +582,7 @@ class Coverage:
def _init_data(self, suffix):
"""Create a data file if we don't have one yet."""
- if self._data is None:
+ if not hasattr(self, "_data"):
# 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.
@@ -586,7 +595,7 @@ class Coverage:
no_disk=self._no_disk,
)
- def start(self):
+ def start(self) -> None:
"""Start measuring code coverage.
Coverage measurement only occurs in functions called after
@@ -618,7 +627,7 @@ class Coverage:
self._started = True
self._instances.append(self)
- def stop(self):
+ def stop(self) -> None:
"""Stop measuring code coverage."""
if self._instances:
if self._instances[-1] is self:
@@ -627,7 +636,7 @@ class Coverage:
self._collector.stop()
self._started = False
- def _atexit(self, event="atexit"):
+ def _atexit(self, event="atexit") -> None:
"""Clean up on process shutdown."""
if self._debug.should("process"):
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
@@ -636,7 +645,7 @@ class Coverage:
if self._auto_save:
self.save()
- def _on_sigterm(self, signum_unused, frame_unused):
+ def _on_sigterm(self, signum_unused, frame_unused) -> None:
"""A handler for signal.SIGTERM."""
self._atexit("sigterm")
# Statements after here won't be seen by metacov because we just wrote
@@ -644,7 +653,7 @@ class Coverage:
signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered
os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered
- def erase(self):
+ def erase(self) -> None:
"""Erase previously collected coverage data.
This removes the in-memory data collected in this session as well as
@@ -653,14 +662,14 @@ class Coverage:
"""
self._init()
self._post_init()
- if self._collector:
+ if hasattr(self, "_collector"):
self._collector.reset()
self._init_data(suffix=None)
self._data.erase(parallel=self.config.parallel)
- self._data = None
+ del self._data
self._inited_for_start = False
- def switch_context(self, new_context):
+ def switch_context(self, new_context) -> None:
"""Switch to a new dynamic context.
`new_context` is a string to use as the :ref:`dynamic context
@@ -681,13 +690,13 @@ class Coverage:
self._collector.switch_context(new_context)
- def clear_exclude(self, which='exclude'):
+ def clear_exclude(self, which='exclude') -> None:
"""Clear the exclude list."""
self._init()
setattr(self.config, which + "_list", [])
self._exclude_regex_stale()
- def exclude(self, regex, which='exclude'):
+ def exclude(self, regex, which='exclude') -> None:
"""Exclude source lines from execution consideration.
A number of lists of regular expressions are maintained. Each list
@@ -707,7 +716,7 @@ class Coverage:
excl_list.append(regex)
self._exclude_regex_stale()
- def _exclude_regex_stale(self):
+ def _exclude_regex_stale(self) -> None:
"""Drop all the compiled exclusion regexes, a list was modified."""
self._exclude_re.clear()
@@ -728,7 +737,7 @@ class Coverage:
self._init()
return getattr(self.config, which + "_list")
- def save(self):
+ def save(self) -> None:
"""Save the collected coverage data to the data file."""
data = self.get_data()
data.write()
@@ -745,7 +754,7 @@ class Coverage:
aliases.add(pattern, result)
return aliases
- def combine(self, data_paths=None, strict=False, keep=False):
+ def combine(self, data_paths=None, strict=False, keep=False) -> None:
"""Combine together a number of similarly-named coverage data files.
All coverage data files whose name starts with `data_file` (from the
@@ -803,12 +812,12 @@ class Coverage:
if not plugin._coverage_enabled:
self._collector.plugin_was_disabled(plugin)
- if self._collector and self._collector.flush_data():
+ if hasattr(self, "_collector") and self._collector.flush_data():
self._post_save_work()
return self._data
- def _post_save_work(self):
+ def _post_save_work(self) -> None:
"""After saving data, look for warnings, post-work, etc.
Warn about things that should have happened but didn't.
@@ -928,7 +937,7 @@ class Coverage:
file_reporters = [self._get_file_reporter(morf) for morf in morfs]
return file_reporters
- def _prepare_data_for_reporting(self):
+ def _prepare_data_for_reporting(self) -> None:
"""Re-map data before reporting, to get implicit 'combine' behavior."""
if self.config.paths:
mapped_data = CoverageData(warn=self._warn, debug=self._debug, no_disk=True)
@@ -1213,7 +1222,7 @@ class Coverage:
):
return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message)
- def sys_info(self):
+ def sys_info(self) -> TSysInfo:
"""Return a list of (key, value) pairs showing internal information."""
import coverage as covmod
@@ -1234,7 +1243,7 @@ class Coverage:
info = [
('coverage_version', covmod.__version__),
('coverage_module', covmod.__file__),
- ('tracer', self._collector.tracer_name() if self._collector else "-none-"),
+ ('tracer', self._collector.tracer_name() if hasattr(self, "_collector") else "-none-"),
('CTracer', 'available' if CTracer else "unavailable"),
('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
('plugins.configurers', plugin_info(self._plugins.configurers)),
@@ -1245,7 +1254,7 @@ class Coverage:
('config_contents',
repr(self.config._config_contents) if self.config._config_contents else '-none-'
),
- ('data_file', self._data.data_filename() if self._data is not None else "-none-"),
+ ('data_file', self._data.data_filename() if hasattr(self, "_data") else "-none-"),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
('implementation', platform.python_implementation()),
@@ -1266,7 +1275,7 @@ class Coverage:
('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
]
- if self._inorout:
+ if hasattr(self, "_inorout"):
info.extend(self._inorout.sys_info())
info.extend(CoverageData.sys_info())
@@ -1282,7 +1291,7 @@ if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugg
Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
-def process_startup():
+def process_startup() -> None:
"""Call this at Python start-up to perhaps measure coverage.
If the environment variable COVERAGE_PROCESS_START is defined, coverage
@@ -1335,7 +1344,7 @@ def process_startup():
return cov
-def _prevent_sub_process_measurement():
+def _prevent_sub_process_measurement() -> None:
"""Stop any subprocess auto-measurement from writing data."""
auto_created_coverage = getattr(process_startup, "coverage", None)
if auto_created_coverage is not None:
diff --git a/coverage/disposition.py b/coverage/disposition.py
index 34819f42..5237c364 100644
--- a/coverage/disposition.py
+++ b/coverage/disposition.py
@@ -3,11 +3,26 @@
"""Simple value objects for tracking what to do with files."""
+from __future__ import annotations
+
+from typing import Optional, Type, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from coverage.plugin import FileTracer
+
class FileDisposition:
"""A simple value type for recording what to do with a file."""
- def __repr__(self):
+ original_filename: str
+ canonical_filename: str
+ source_filename: Optional[str]
+ trace: bool
+ reason: str
+ file_tracer: Optional[FileTracer]
+ has_dynamic_filename: bool
+
+ def __repr__(self) -> str:
return f"<FileDisposition {self.canonical_filename!r}: trace={self.trace}>"
@@ -15,7 +30,7 @@ class FileDisposition:
# be implemented in either C or Python. Acting on them is done with these
# functions.
-def disposition_init(cls, original_filename):
+def disposition_init(cls: Type[FileDisposition], original_filename: str) -> FileDisposition:
"""Construct and initialize a new FileDisposition object."""
disp = cls()
disp.original_filename = original_filename
@@ -28,7 +43,7 @@ def disposition_init(cls, original_filename):
return disp
-def disposition_debug_msg(disp):
+def disposition_debug_msg(disp: FileDisposition) -> str:
"""Make a nice debug message of what the FileDisposition is doing."""
if disp.trace:
msg = f"Tracing {disp.original_filename!r}"
diff --git a/coverage/inorout.py b/coverage/inorout.py
index fcb45974..65aec83c 100644
--- a/coverage/inorout.py
+++ b/coverage/inorout.py
@@ -3,6 +3,8 @@
"""Determining whether files are being measured/reported or not."""
+from __future__ import annotations
+
import importlib.util
import inspect
import itertools
@@ -13,6 +15,9 @@ import sys
import sysconfig
import traceback
+from types import FrameType, ModuleType
+from typing import cast, Iterable, List, Optional, Set, Tuple, TYPE_CHECKING
+
from coverage import env
from coverage.disposition import FileDisposition, disposition_init
from coverage.exceptions import CoverageException, PluginError
@@ -20,26 +25,36 @@ from coverage.files import TreeMatcher, GlobMatcher, ModuleMatcher
from coverage.files import prep_patterns, find_python_files, canonical_filename
from coverage.misc import sys_modules_saved
from coverage.python import source_for_file, source_for_morf
+from coverage.types import TMorf, TWarnFn, TDebugCtl, TSysInfo
+
+if TYPE_CHECKING:
+ from coverage.config import CoverageConfig
+ from coverage.plugin_support import Plugins
# Pypy has some unusual stuff in the "stdlib". Consider those locations
# when deciding where the stdlib is. These modules are not used for anything,
# they are modules importable from the pypy lib directories, so that we can
# find those directories.
-_structseq = _pypy_irc_topic = None
+modules_we_happen_to_have: List[ModuleType] = [
+ inspect, itertools, os, platform, re, sysconfig, traceback,
+]
+
if env.PYPY:
try:
import _structseq
+ modules_we_happen_to_have.append(_structseq)
except ImportError:
pass
try:
import _pypy_irc_topic
+ modules_we_happen_to_have.append(_pypy_irc_topic)
except ImportError:
pass
-def canonical_path(morf, directory=False):
+def canonical_path(morf: TMorf, directory: bool=False) -> str:
"""Return the canonical path of the module or file `morf`.
If the module is a package, then return its directory. If it is a
@@ -53,7 +68,7 @@ def canonical_path(morf, directory=False):
return morf_path
-def name_for_module(filename, frame):
+def name_for_module(filename: str, frame: Optional[FrameType]) -> str:
"""Get the name of the module for a filename and frame.
For configurability's sake, we allow __main__ modules to be matched by
@@ -68,9 +83,9 @@ def name_for_module(filename, frame):
module_globals = frame.f_globals if frame is not None else {}
if module_globals is None: # pragma: only ironpython
# IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296
- module_globals = {}
+ module_globals = {} # type: ignore[unreachable]
- dunder_name = module_globals.get('__name__', None)
+ dunder_name: str = module_globals.get('__name__', None)
if isinstance(dunder_name, str) and dunder_name != '__main__':
# This is the usual case: an imported module.
@@ -95,12 +110,12 @@ def name_for_module(filename, frame):
return dunder_name
-def module_is_namespace(mod):
+def module_is_namespace(mod: ModuleType) -> bool:
"""Is the module object `mod` a PEP420 namespace module?"""
return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None
-def module_has_file(mod):
+def module_has_file(mod: ModuleType) -> bool:
"""Does the module object `mod` have an existing __file__ ?"""
mod__file__ = getattr(mod, '__file__', None)
if mod__file__ is None:
@@ -108,7 +123,7 @@ def module_has_file(mod):
return os.path.exists(mod__file__)
-def file_and_path_for_module(modulename):
+def file_and_path_for_module(modulename: str) -> Tuple[Optional[str], List[str]]:
"""Find the file and search path for `modulename`.
Returns:
@@ -129,32 +144,19 @@ def file_and_path_for_module(modulename):
return filename, path
-def add_stdlib_paths(paths):
+def add_stdlib_paths(paths: Set[str]) -> None:
"""Add paths where the stdlib can be found to the set `paths`."""
# Look at where some standard modules are located. That's the
# indication for "installed with the interpreter". In some
# environments (virtualenv, for example), these modules may be
# spread across a few locations. Look at all the candidate modules
# we've imported, and take all the different ones.
- modules_we_happen_to_have = [
- inspect, itertools, os, platform, re, sysconfig, traceback,
- _pypy_irc_topic, _structseq,
- ]
for m in modules_we_happen_to_have:
- if m is not None and hasattr(m, "__file__"):
+ if hasattr(m, "__file__"):
paths.add(canonical_path(m, directory=True))
- if _structseq and not hasattr(_structseq, '__file__'):
- # PyPy 2.4 has no __file__ in the builtin modules, but the code
- # objects still have the file names. So dig into one to find
- # the path to exclude. The "filename" might be synthetic,
- # don't be fooled by those.
- structseq_file = _structseq.structseq_new.__code__.co_filename
- if not structseq_file.startswith("<"):
- paths.add(canonical_path(structseq_file))
-
-def add_third_party_paths(paths):
+def add_third_party_paths(paths: Set[str]) -> None:
"""Add locations for third-party packages to the set `paths`."""
# Get the paths that sysconfig knows about.
scheme_names = set(sysconfig.get_scheme_names())
@@ -168,7 +170,7 @@ def add_third_party_paths(paths):
paths.add(config_paths[path_name])
-def add_coverage_paths(paths):
+def add_coverage_paths(paths: Set[str]) -> None:
"""Add paths where coverage.py code can be found to the set `paths`."""
cover_path = canonical_path(__file__, directory=True)
paths.add(cover_path)
@@ -180,31 +182,19 @@ def add_coverage_paths(paths):
class InOrOut:
"""Machinery for determining what files to measure."""
- def __init__(self, warn, debug, include_namespace_packages):
+ def __init__(
+ self,
+ config: CoverageConfig,
+ warn: TWarnFn,
+ debug: Optional[TDebugCtl],
+ include_namespace_packages: bool,
+ ) -> None:
self.warn = warn
self.debug = debug
self.include_namespace_packages = include_namespace_packages
- # The matchers for should_trace.
- self.source_match = None
- self.source_pkgs_match = None
- self.pylib_paths = self.cover_paths = self.third_paths = None
- self.pylib_match = self.cover_match = self.third_match = None
- self.include_match = self.omit_match = None
- self.plugins = []
- self.disp_class = FileDisposition
-
- # The source argument can be directories or package names.
- self.source = []
- self.source_pkgs = []
- self.source_pkgs_unmatched = []
- self.omit = self.include = None
-
- # Is the source inside a third-party area?
- self.source_in_third = False
-
- def configure(self, config):
- """Apply the configuration to get ready for decision-time."""
+ self.source: List[str] = []
+ self.source_pkgs: List[str] = []
self.source_pkgs.extend(config.source_pkgs)
for src in config.source or []:
if os.path.isdir(src):
@@ -217,27 +207,34 @@ class InOrOut:
self.include = prep_patterns(config.run_include)
# The directories for files considered "installed with the interpreter".
- self.pylib_paths = set()
+ self.pylib_paths: Set[str] = set()
if not config.cover_pylib:
add_stdlib_paths(self.pylib_paths)
# To avoid tracing the coverage.py code itself, we skip anything
# located where we are.
- self.cover_paths = set()
+ self.cover_paths: Set[str] = set()
add_coverage_paths(self.cover_paths)
# Find where third-party packages are installed.
- self.third_paths = set()
+ self.third_paths: Set[str] = set()
add_third_party_paths(self.third_paths)
- def debug(msg):
+ def _debug(msg: str) -> None:
if self.debug:
self.debug.write(msg)
+ # The matchers for should_trace.
+
# Generally useful information
- debug("sys.path:" + "".join(f"\n {p}" for p in sys.path))
+ _debug("sys.path:" + "".join(f"\n {p}" for p in sys.path))
# Create the matchers we need for should_trace
+ self.source_match = None
+ self.source_pkgs_match = None
+ self.pylib_match = None
+ self.include_match = self.omit_match = None
+
if self.source or self.source_pkgs:
against = []
if self.source:
@@ -246,44 +243,46 @@ class InOrOut:
if self.source_pkgs:
self.source_pkgs_match = ModuleMatcher(self.source_pkgs, "source_pkgs")
against.append(f"modules {self.source_pkgs_match!r}")
- debug("Source matching against " + " and ".join(against))
+ _debug("Source matching against " + " and ".join(against))
else:
if self.pylib_paths:
self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
- debug(f"Python stdlib matching: {self.pylib_match!r}")
+ _debug(f"Python stdlib matching: {self.pylib_match!r}")
if self.include:
self.include_match = GlobMatcher(self.include, "include")
- debug(f"Include matching: {self.include_match!r}")
+ _debug(f"Include matching: {self.include_match!r}")
if self.omit:
self.omit_match = GlobMatcher(self.omit, "omit")
- debug(f"Omit matching: {self.omit_match!r}")
+ _debug(f"Omit matching: {self.omit_match!r}")
self.cover_match = TreeMatcher(self.cover_paths, "coverage")
- debug(f"Coverage code matching: {self.cover_match!r}")
+ _debug(f"Coverage code matching: {self.cover_match!r}")
self.third_match = TreeMatcher(self.third_paths, "third")
- debug(f"Third-party lib matching: {self.third_match!r}")
+ _debug(f"Third-party lib matching: {self.third_match!r}")
# Check if the source we want to measure has been installed as a
# third-party package.
+ # Is the source inside a third-party area?
+ self.source_in_third = False
with sys_modules_saved():
for pkg in self.source_pkgs:
try:
modfile, path = file_and_path_for_module(pkg)
- debug(f"Imported source package {pkg!r} as {modfile!r}")
+ _debug(f"Imported source package {pkg!r} as {modfile!r}")
except CoverageException as exc:
- debug(f"Couldn't import source package {pkg!r}: {exc}")
+ _debug(f"Couldn't import source package {pkg!r}: {exc}")
continue
if modfile:
if self.third_match.match(modfile):
- debug(
+ _debug(
f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}"
)
self.source_in_third = True
else:
for pathdir in path:
if self.third_match.match(pathdir):
- debug(
+ _debug(
f"Source is in third-party because of {pkg!r} path directory " +
f"at {pathdir!r}"
)
@@ -291,10 +290,13 @@ class InOrOut:
for src in self.source:
if self.third_match.match(src):
- debug(f"Source is in third-party because of source directory {src!r}")
+ _debug(f"Source is in third-party because of source directory {src!r}")
self.source_in_third = True
- def should_trace(self, filename, frame=None):
+ self.plugins: Plugins
+ self.disp_class = FileDisposition
+
+ def should_trace(self, filename: str, frame: Optional[FrameType]=None) -> FileDisposition:
"""Decide whether to trace execution in `filename`, with a reason.
This function is called from the trace function. As each new file name
@@ -306,7 +308,7 @@ class InOrOut:
original_filename = filename
disp = disposition_init(self.disp_class, filename)
- def nope(disp, reason):
+ def nope(disp: FileDisposition, reason: str) -> FileDisposition:
"""Simple helper to make it easy to return NO."""
disp.trace = False
disp.reason = reason
@@ -395,7 +397,7 @@ class InOrOut:
return disp
- def check_include_omit_etc(self, filename, frame):
+ def check_include_omit_etc(self, filename: str, frame: Optional[FrameType]) -> Optional[str]:
"""Check a file name against the include, omit, etc, rules.
Returns a string or None. String means, don't trace, and is the reason
@@ -457,13 +459,13 @@ class InOrOut:
# No reason found to skip this file.
return None
- def warn_conflicting_settings(self):
+ def warn_conflicting_settings(self) -> None:
"""Warn if there are settings that conflict."""
if self.include:
if self.source or self.source_pkgs:
self.warn("--include is ignored because --source is set", slug="include-ignored")
- def warn_already_imported_files(self):
+ def warn_already_imported_files(self) -> None:
"""Warn if files have already been imported that we will be measuring."""
if self.include or self.source or self.source_pkgs:
warned = set()
@@ -495,12 +497,12 @@ class InOrOut:
)
)
- def warn_unimported_source(self):
+ def warn_unimported_source(self) -> None:
"""Warn about source packages that were of interest, but never traced."""
for pkg in self.source_pkgs_unmatched:
self._warn_about_unmeasured_code(pkg)
- def _warn_about_unmeasured_code(self, pkg):
+ def _warn_about_unmeasured_code(self, pkg: str) -> None:
"""Warn about a package or module that we never traced.
`pkg` is a string, the name of the package or module.
@@ -526,7 +528,7 @@ class InOrOut:
msg = f"Module {pkg} was previously imported, but not measured"
self.warn(msg, slug="module-not-measured")
- def find_possibly_unexecuted_files(self):
+ def find_possibly_unexecuted_files(self) -> Iterable[Tuple[str, Optional[str]]]:
"""Find files in the areas of interest that might be untraced.
Yields pairs: file path, and responsible plug-in name.
@@ -535,19 +537,19 @@ class InOrOut:
if (not pkg in sys.modules or
not module_has_file(sys.modules[pkg])):
continue
- pkg_file = source_for_file(sys.modules[pkg].__file__)
+ pkg_file = source_for_file(cast(str, sys.modules[pkg].__file__))
yield from self._find_executable_files(canonical_path(pkg_file))
for src in self.source:
yield from self._find_executable_files(src)
- def _find_plugin_files(self, src_dir):
+ def _find_plugin_files(self, src_dir: str) -> Iterable[Tuple[str, str]]:
"""Get executable files from the plugins."""
for plugin in self.plugins.file_tracers:
for x_file in plugin.find_executable_files(src_dir):
yield x_file, plugin._coverage_plugin_name
- def _find_executable_files(self, src_dir):
+ def _find_executable_files(self, src_dir: str) -> Iterable[Tuple[str, Optional[str]]]:
"""Find executable files in `src_dir`.
Search for files in `src_dir` that can be executed because they
@@ -571,7 +573,7 @@ class InOrOut:
continue
yield file_path, plugin_name
- def sys_info(self):
+ def sys_info(self) -> TSysInfo:
"""Our information for Coverage.sys_info.
Returns a list of (key, value) pairs.
diff --git a/coverage/plugin.py b/coverage/plugin.py
index ee1ae365..8f309f42 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -121,7 +121,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from coverage import files
from coverage.misc import _needs_to_implement
-from coverage.types import TArc, TConfigurable, TLineNo, TSourceTokenLines
+from coverage.types import TArc, TConfigurable, TLineNo, TSourceTokenLines, TSysInfo
class CoveragePlugin:
@@ -235,7 +235,7 @@ class CoveragePlugin:
"""
pass
- def sys_info(self) -> List[Tuple[str, str]]:
+ def sys_info(self) -> TSysInfo:
"""Get a list of information useful for debugging.
Plug-in type: any.
diff --git a/coverage/python.py b/coverage/python.py
index 5716eb27..70d38fe3 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -160,7 +160,6 @@ class PythonFileReporter(FileReporter):
fname = filename
canonicalize = True
if self.coverage is not None:
- assert self.coverage.config is not None
if self.coverage.config.relative_files:
canonicalize = False
if canonicalize:
@@ -215,7 +214,6 @@ class PythonFileReporter(FileReporter):
@expensive
def no_branch_lines(self) -> Set[TLineNo]:
assert self.coverage is not None
- assert self.coverage.config is not None
no_branch = self.parser.lines_matching(
join_regex(self.coverage.config.partial_list),
join_regex(self.coverage.config.partial_always_list),
diff --git a/coverage/types.py b/coverage/types.py
index d138b2f2..b7390962 100644
--- a/coverage/types.py
+++ b/coverage/types.py
@@ -6,7 +6,7 @@ Types for use throughout coverage.py.
"""
from types import ModuleType
-from typing import Dict, Iterable, List, Optional, Tuple, Union, TYPE_CHECKING
+from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union, TYPE_CHECKING
if TYPE_CHECKING:
# Protocol is new in 3.8. PYVERSIONS
@@ -74,3 +74,6 @@ class TDebugCtl(Protocol):
def write(self, msg: str) -> None:
"""Write a line of debug output."""
+
+# Data returned from sys_info()
+TSysInfo = Sequence[Tuple[str, Union[str, Iterable[str]]]]
diff --git a/tests/test_api.py b/tests/test_api.py
index ee24aa8f..71712f8e 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -32,6 +32,8 @@ BAD_SQLITE_REGEX = r"file( is encrypted or)? is not a database"
class ApiTest(CoverageTest):
"""Api-oriented tests for coverage.py."""
+ # pylint: disable=use-implicit-booleaness-not-comparison
+
def clean_files(self, files, pats):
"""Remove names matching `pats` from `files`, a list of file names."""
good = []
diff --git a/tests/test_config.py b/tests/test_config.py
index eb0733dd..26276c47 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -638,7 +638,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest):
""")
cov = coverage.Coverage()
assert cov.config.run_include == ["foo"]
- assert cov.config.run_omit is None
+ assert cov.config.run_omit == []
assert cov.config.branch is False
def test_setupcfg_only_if_not_coveragerc(self):
@@ -655,7 +655,7 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest):
branch = true
""")
cov = coverage.Coverage()
- assert cov.config.run_omit is None
+ assert cov.config.run_omit == []
assert cov.config.branch is False
def test_setupcfg_only_if_prefixed(self):
diff --git a/tox.ini b/tox.ini
index 4b641842..b306a6d2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -97,7 +97,7 @@ deps =
setenv =
{[testenv]setenv}
- T_AN=coverage/config.py coverage/files.py coverage/multiproc.py coverage/numbits.py
+ T_AN=coverage/config.py coverage/disposition.py coverage/files.py coverage/inorout.py coverage/multiproc.py coverage/numbits.py
T_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py
T_QZ=coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py
TYPEABLE={env:T_AN} {env:T_OP} {env:T_QZ}