diff options
-rw-r--r-- | CHANGES.rst | 4 | ||||
-rw-r--r-- | coverage/config.py | 14 | ||||
-rw-r--r-- | coverage/control.py | 137 | ||||
-rw-r--r-- | coverage/disposition.py | 21 | ||||
-rw-r--r-- | coverage/inorout.py | 150 | ||||
-rw-r--r-- | coverage/plugin.py | 4 | ||||
-rw-r--r-- | coverage/python.py | 2 | ||||
-rw-r--r-- | coverage/types.py | 5 | ||||
-rw-r--r-- | tests/test_api.py | 2 | ||||
-rw-r--r-- | tests/test_config.py | 4 | ||||
-rw-r--r-- | tox.ini | 2 |
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): @@ -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} |