diff options
-rw-r--r-- | coverage/config.py | 20 | ||||
-rw-r--r-- | coverage/control.py | 21 | ||||
-rw-r--r-- | coverage/files.py | 2 | ||||
-rw-r--r-- | coverage/tomlconfig.py | 8 | ||||
-rw-r--r-- | coverage/types.py | 10 | ||||
-rw-r--r-- | tests/test_cmdline.py | 10 | ||||
-rw-r--r-- | tests/test_debug.py | 50 | ||||
-rw-r--r-- | tests/test_execfile.py | 56 | ||||
-rw-r--r-- | tests/test_filereporter.py | 12 | ||||
-rw-r--r-- | tests/test_files.py | 121 | ||||
-rw-r--r-- | tests/test_summary.py | 4 | ||||
-rw-r--r-- | tox.ini | 5 |
12 files changed, 175 insertions, 144 deletions
diff --git a/coverage/config.py b/coverage/config.py index 04bde26f..8ab68741 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -18,7 +18,9 @@ from typing import ( from coverage.exceptions import ConfigError from coverage.misc import isolate_module, human_sorted_items, substitute_variables from coverage.tomlconfig import TomlConfigParser, TomlDecodeError -from coverage.types import TConfigurable, TConfigSection, TConfigValue +from coverage.types import ( + TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigSectionOut, TConfigValueOut, +) os = isolate_module(os) @@ -71,9 +73,9 @@ class HandyConfigParser(configparser.ConfigParser): return super().options(real_section) raise ConfigError(f"No section: {section!r}") - def get_section(self, section: str) -> TConfigSection: + def get_section(self, section: str) -> TConfigSectionOut: """Get the contents of a section, as a dictionary.""" - d: Dict[str, TConfigValue] = {} + d: Dict[str, TConfigValueOut] = {} for opt in self.options(section): d[opt] = self.get(section, opt) return d @@ -249,7 +251,7 @@ class CoverageConfig(TConfigurable): self.paths: Dict[str, List[str]] = {} # Options for plugins - self.plugin_options: Dict[str, TConfigSection] = {} + self.plugin_options: Dict[str, TConfigSectionOut] = {} MUST_BE_LIST = { "debug", "concurrency", "plugins", @@ -257,7 +259,7 @@ class CoverageConfig(TConfigurable): "run_omit", "run_include", } - def from_args(self, **kwargs: TConfigValue) -> None: + def from_args(self, **kwargs: TConfigValueIn) -> None: """Read config values from `kwargs`.""" for k, v in kwargs.items(): if v is not None: @@ -441,11 +443,11 @@ class CoverageConfig(TConfigurable): return True return False - def get_plugin_options(self, plugin: str) -> TConfigSection: + def get_plugin_options(self, plugin: str) -> TConfigSectionOut: """Get a dictionary of options for the plugin named `plugin`.""" return self.plugin_options.get(plugin, {}) - def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection]) -> None: + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: """Set an option in the configuration. `option_name` is a colon-separated string indicating the section and @@ -476,7 +478,7 @@ class CoverageConfig(TConfigurable): # If we get here, we didn't find the option. raise ConfigError(f"No such option: {option_name!r}") - def get_option(self, option_name: str) -> Optional[TConfigValue]: + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: """Get an option from the configuration. `option_name` is a colon-separated string indicating the section and @@ -559,7 +561,7 @@ def config_files_to_try(config_file: Union[bool, str]) -> List[Tuple[str, bool, def read_coverage_config( config_file: Union[bool, str], warn: Callable[[str], None], - **kwargs: TConfigValue, + **kwargs: TConfigValueIn, ) -> CoverageConfig: """Read the coverage.py configuration. diff --git a/coverage/control.py b/coverage/control.py index acd89b94..4e97ce9c 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -47,7 +47,8 @@ from coverage.report import render_report from coverage.results import Analysis from coverage.summary import SummaryReporter from coverage.types import ( - TConfigurable, TConfigSection, TConfigValue, TFileDisposition, TLineNo, TMorf, + TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut, + TFileDisposition, TLineNo, TMorf, ) from coverage.xmlreport import XmlReporter @@ -55,7 +56,7 @@ from coverage.xmlreport import XmlReporter os = isolate_module(os) @contextlib.contextmanager -def override_config(cov: Coverage, **kwargs: TConfigValue) -> Generator[None, None, None]: +def override_config(cov: Coverage, **kwargs: TConfigValueIn) -> Generator[None, None, None]: """Temporarily tweak the configuration of `cov`. The arguments are applied to `cov.config` with the `from_args` method. @@ -120,12 +121,12 @@ class Coverage(TConfigurable): timid: Optional[bool]=None, branch: Optional[bool]=None, config_file: Union[str, bool]=True, - source: Optional[List[str]]=None, - source_pkgs: Optional[List[str]]=None, - omit: Optional[Union[str, List[str]]]=None, - include: Optional[Union[str, List[str]]]=None, - debug: Optional[List[str]]=None, - concurrency: Optional[Union[str, List[str]]]=None, + source: Optional[Iterable[str]]=None, + source_pkgs: Optional[Iterable[str]]=None, + omit: Optional[Union[str, Iterable[str]]]=None, + include: Optional[Union[str, Iterable[str]]]=None, + debug: Optional[Iterable[str]]=None, + concurrency: Optional[Union[str, Iterable[str]]]=None, check_preimported: bool=False, context: Optional[str]=None, messages: bool=False, @@ -425,7 +426,7 @@ class Coverage(TConfigurable): if self._messages: print(msg) - def get_option(self, option_name: str) -> Optional[TConfigValue]: + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: """Get an option from the configuration. `option_name` is a colon-separated string indicating the section and @@ -443,7 +444,7 @@ class Coverage(TConfigurable): """ return self.config.get_option(option_name) - def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection]) -> None: + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: """Set an option in the configuration. `option_name` is a colon-separated string indicating the section and diff --git a/coverage/files.py b/coverage/files.py index 2aca85ed..e2800bf2 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -351,7 +351,7 @@ def _glob_to_regex(pattern: str) -> str: def globs_to_regex( patterns: Iterable[str], case_insensitive: bool=False, - partial: bool=False + partial: bool=False, ) -> re.Pattern[str]: """Convert glob patterns to a compiled regex that matches any of them. diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 737c728c..3b8ff347 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -11,7 +11,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, T from coverage import env from coverage.exceptions import ConfigError from coverage.misc import import_third_party, substitute_variables -from coverage.types import TConfigSection, TConfigValue +from coverage.types import TConfigSectionOut, TConfigValueOut if env.PYVERSION >= (3, 11, 0, "alpha", 7): @@ -65,7 +65,7 @@ class TomlConfigParser: raise ConfigError(msg.format(filename)) return [] - def _get_section(self, section: str) -> Tuple[Optional[str], Optional[TConfigSection]]: + def _get_section(self, section: str) -> Tuple[Optional[str], Optional[TConfigSectionOut]]: """Get a section from the data. Arguments: @@ -92,7 +92,7 @@ class TomlConfigParser: return None, None return real_section, data - def _get(self, section: str, option: str) -> Tuple[str, TConfigValue]: + def _get(self, section: str, option: str) -> Tuple[str, TConfigValueOut]: """Like .get, but returns the real section name and the value.""" name, data = self._get_section(section) if data is None: @@ -135,7 +135,7 @@ class TomlConfigParser: raise ConfigError(f"No section: {section!r}") return list(data.keys()) - def get_section(self, section: str) -> TConfigSection: + def get_section(self, section: str) -> TConfigSectionOut: _, data = self._get_section(section) return data or {} diff --git a/coverage/types.py b/coverage/types.py index ed22e699..8ec4f37a 100644 --- a/coverage/types.py +++ b/coverage/types.py @@ -107,14 +107,16 @@ TCovKwargs = Any ## Configuration # One value read from a config file. -TConfigValue = Optional[Union[bool, int, float, str, List[str]]] +TConfigValueIn = Optional[Union[bool, int, float, str, Iterable[str]]] +TConfigValueOut = Optional[Union[bool, int, float, str, List[str]]] # An entire config section, mapping option names to values. -TConfigSection = Mapping[str, TConfigValue] +TConfigSectionIn = Mapping[str, TConfigValueIn] +TConfigSectionOut = Mapping[str, TConfigValueOut] class TConfigurable(Protocol): """Something that can proxy to the coverage configuration settings.""" - def get_option(self, option_name: str) -> Optional[TConfigValue]: + def get_option(self, option_name: str) -> Optional[TConfigValueOut]: """Get an option from the configuration. `option_name` is a colon-separated string indicating the section and @@ -125,7 +127,7 @@ class TConfigurable(Protocol): """ - def set_option(self, option_name: str, value: Union[TConfigValue, TConfigSection]) -> None: + def set_option(self, option_name: str, value: Union[TConfigValueIn, TConfigSectionIn]) -> None: """Set an option in the configuration. `option_name` is a colon-separated string indicating the section and diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 96bd3bbf..f20cfe74 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -20,7 +20,7 @@ from coverage import env from coverage.control import DEFAULT_DATAFILE from coverage.config import CoverageConfig from coverage.exceptions import _ExceptionDuringRun -from coverage.types import TConfigValue +from coverage.types import TConfigValueIn, TConfigValueOut from coverage.version import __url__ from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -96,7 +96,7 @@ class BaseCmdLineTest(CoverageTest): def mock_command_line( self, args: str, - options: Optional[Mapping[str, TConfigValue]]=None, + options: Optional[Mapping[str, TConfigValueIn]]=None, ) -> Tuple[mock.Mock, int]: """Run `args` through the command line, with a Mock. @@ -130,7 +130,7 @@ class BaseCmdLineTest(CoverageTest): args: str, code: str, ret: int=OK, - options: Optional[Mapping[str, TConfigValue]]=None, + options: Optional[Mapping[str, TConfigValueIn]]=None, ) -> None: """Assert that the `args` end up executing the sequence in `code`.""" called, status = self.mock_command_line(args, options=options) @@ -1139,10 +1139,10 @@ class CoverageReportingFake: self.json_result = json_report self.lcov_result = lcov_result - def set_option(self, optname: str, optvalue: TConfigValue) -> None: + def set_option(self, optname: str, optvalue: TConfigValueIn) -> None: self.config.set_option(optname, optvalue) - def get_option(self, optname: str) -> TConfigValue: + def get_option(self, optname: str) -> TConfigValueOut: return self.config.get_option(optname) def load(self) -> None: diff --git a/tests/test_debug.py b/tests/test_debug.py index b4ce6d92..73217f1a 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -9,6 +9,8 @@ import os import re import sys +from typing import Any, Callable, Iterable + import pytest import coverage @@ -25,7 +27,7 @@ class InfoFormatterTest(CoverageTest): run_in_temp_dir = False - def test_info_formatter(self): + def test_info_formatter(self) -> None: lines = list(info_formatter([ ('x', 'hello there'), ('very long label', ['one element']), @@ -43,7 +45,7 @@ class InfoFormatterTest(CoverageTest): ] assert expected == lines - def test_info_formatter_with_generator(self): + def test_info_formatter_with_generator(self) -> None: lines = list(info_formatter(('info%d' % i, i) for i in range(3))) expected = [ ' info0: 0', @@ -52,7 +54,7 @@ class InfoFormatterTest(CoverageTest): ] assert expected == lines - def test_too_long_label(self): + def test_too_long_label(self) -> None: with pytest.raises(AssertionError): list(info_formatter([('this label is way too long and will not fit', 23)])) @@ -61,7 +63,7 @@ class InfoFormatterTest(CoverageTest): ("x", "-- x ---------------------------------------------------------"), ("hello there", "-- hello there -----------------------------------------------"), ]) -def test_info_header(label, header): +def test_info_header(label: str, header: str) -> None: assert info_header(label) == header @@ -71,7 +73,7 @@ def test_info_header(label, header): (0xA5A55A5A, 0xFFFF), (0x1234cba956780fed, 0x8008), ]) -def test_short_id(id64, id16): +def test_short_id(id64: int, id16: int) -> None: assert short_id(id64) == id16 @@ -79,7 +81,7 @@ def test_short_id(id64, id16): ("hello", 10, "'hello'"), ("0123456789abcdefghijklmnopqrstuvwxyz", 15, "'01234...vwxyz'"), ]) -def test_clipped_repr(text, numchars, result): +def test_clipped_repr(text: str, numchars: int, result: str) -> None: assert clipped_repr(text, numchars) == result @@ -90,14 +92,18 @@ def test_clipped_repr(text, numchars, result): ("hello\nbye\n", [lambda x: "="+x], "=hello\n=bye\n"), ("hello\nbye\n", [lambda x: "="+x, lambda x: x+"\ndone\n"], "=hello\ndone\n=bye\ndone\n"), ]) -def test_filter_text(text, filters, result): +def test_filter_text( + text: str, + filters: Iterable[Callable[[str], str]], + result: str, +) -> None: assert filter_text(text, filters) == result class DebugTraceTest(CoverageTest): """Tests of debug output.""" - def f1_debug_output(self, debug): + def f1_debug_output(self, debug: Iterable[str]) -> str: """Runs some code with `debug` option, returns the debug output.""" # Make code to run. self.make_file("f1.py", """\ @@ -116,13 +122,13 @@ class DebugTraceTest(CoverageTest): return debug_out.getvalue() - def test_debug_no_trace(self): + def test_debug_no_trace(self) -> None: out_text = self.f1_debug_output([]) # We should have no output at all. assert not out_text - def test_debug_trace(self): + def test_debug_trace(self) -> None: out_text = self.f1_debug_output(["trace"]) # We should have a line like "Tracing 'f1.py'", perhaps with an @@ -132,7 +138,7 @@ class DebugTraceTest(CoverageTest): # We should have lines like "Not tracing 'collector.py'..." assert re_lines(r"^Not tracing .*: is part of coverage.py$", out_text) - def test_debug_trace_pid(self): + def test_debug_trace_pid(self) -> None: out_text = self.f1_debug_output(["trace", "pid"]) # Now our lines are always prefixed with the process id. @@ -144,7 +150,7 @@ class DebugTraceTest(CoverageTest): assert re_lines(pid_prefix + "Tracing ", out_text) assert re_lines(pid_prefix + "Not tracing ", out_text) - def test_debug_callers(self): + def test_debug_callers(self) -> None: out_text = self.f1_debug_output(["pid", "dataop", "dataio", "callers", "lock"]) # For every real message, there should be a stack trace with a line like # "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" @@ -161,7 +167,7 @@ class DebugTraceTest(CoverageTest): assert re_lines(r"^\s*\d+\.\w{4}: Adding file tracers: 0 files", real_messages[-1]) assert re_lines(r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$", last_line) - def test_debug_config(self): + def test_debug_config(self) -> None: out_text = self.f1_debug_output(["config"]) labels = """ @@ -176,7 +182,7 @@ class DebugTraceTest(CoverageTest): msg = f"Incorrect lines for {label!r}" assert 1 == len(re_lines(label_pat, out_text)), msg - def test_debug_sys(self): + def test_debug_sys(self) -> None: out_text = self.f1_debug_output(["sys"]) labels = """ @@ -190,7 +196,7 @@ class DebugTraceTest(CoverageTest): msg = f"Incorrect lines for {label!r}" assert 1 == len(re_lines(label_pat, out_text)), msg - def test_debug_sys_ctracer(self): + def test_debug_sys_ctracer(self) -> None: out_text = self.f1_debug_output(["sys"]) tracer_line = re_line(r"CTracer:", out_text).strip() if env.C_TRACER: @@ -199,7 +205,7 @@ class DebugTraceTest(CoverageTest): expected = "CTracer: unavailable" assert expected == tracer_line - def test_debug_pybehave(self): + def test_debug_pybehave(self) -> None: out_text = self.f1_debug_output(["pybehave"]) out_lines = out_text.splitlines() assert 10 < len(out_lines) < 40 @@ -208,15 +214,15 @@ class DebugTraceTest(CoverageTest): assert vtuple[:5] == sys.version_info -def f_one(*args, **kwargs): +def f_one(*args: Any, **kwargs: Any) -> str: """First of the chain of functions for testing `short_stack`.""" return f_two(*args, **kwargs) -def f_two(*args, **kwargs): +def f_two(*args: Any, **kwargs: Any) -> str: """Second of the chain of functions for testing `short_stack`.""" return f_three(*args, **kwargs) -def f_three(*args, **kwargs): +def f_three(*args: Any, **kwargs: Any) -> str: """Third of the chain of functions for testing `short_stack`.""" return short_stack(*args, **kwargs) @@ -226,17 +232,17 @@ class ShortStackTest(CoverageTest): run_in_temp_dir = False - def test_short_stack(self): + def test_short_stack(self) -> None: stack = f_one().splitlines() assert len(stack) > 10 assert "f_three" in stack[-1] assert "f_two" in stack[-2] assert "f_one" in stack[-3] - def test_short_stack_limit(self): + def test_short_stack_limit(self) -> None: stack = f_one(limit=5).splitlines() assert len(stack) == 5 - def test_short_stack_skip(self): + def test_short_stack_skip(self) -> None: stack = f_one(skip=1).splitlines() assert "f_two" in stack[-1] diff --git a/tests/test_execfile.py b/tests/test_execfile.py index e1db7bb5..229d8d95 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -12,6 +12,8 @@ import py_compile import re import sys +from typing import Any, Generator + import pytest from coverage.exceptions import NoCode, NoSource, _ExceptionDuringRun @@ -27,12 +29,12 @@ class RunFileTest(CoverageTest): """Test cases for `run_python_file`.""" @pytest.fixture(autouse=True) - def clean_up(self): + def clean_up(self) -> Generator[None, None, None]: """These tests all run in-process. Clean up global changes.""" yield sys.excepthook = sys.__excepthook__ - def test_run_python_file(self): + def test_run_python_file(self) -> None: run_python_file([TRY_EXECFILE, "arg1", "arg2"]) mod_globs = json.loads(self.stdout()) @@ -58,7 +60,7 @@ class RunFileTest(CoverageTest): # __builtins__ should have the right values, like open(). assert mod_globs['__builtins__.has_open'] is True - def test_no_extra_file(self): + def test_no_extra_file(self) -> None: # Make sure that running a file doesn't create an extra compiled file. self.make_file("xxx", """\ desc = "a non-.py file!" @@ -68,7 +70,7 @@ class RunFileTest(CoverageTest): run_python_file(["xxx"]) assert os.listdir(".") == ["xxx"] - def test_universal_newlines(self): + def test_universal_newlines(self) -> None: # Make sure we can read any sort of line ending. pylines = """# try newlines|print('Hello, world!')|""".split('|') for nl in ('\n', '\r\n', '\r'): @@ -77,7 +79,7 @@ class RunFileTest(CoverageTest): run_python_file(['nl.py']) assert self.stdout() == "Hello, world!\n"*3 - def test_missing_final_newline(self): + def test_missing_final_newline(self) -> None: # Make sure we can deal with a Python file with no final newline. self.make_file("abrupt.py", """\ if 1: @@ -90,25 +92,25 @@ class RunFileTest(CoverageTest): run_python_file(["abrupt.py"]) assert self.stdout() == "a is 1\n" - def test_no_such_file(self): + def test_no_such_file(self) -> None: path = python_reported_file('xyzzy.py') msg = re.escape(f"No file to run: '{path}'") with pytest.raises(NoSource, match=msg): run_python_file(["xyzzy.py"]) - def test_directory_with_main(self): + def test_directory_with_main(self) -> None: self.make_file("with_main/__main__.py", """\ print("I am __main__") """) run_python_file(["with_main"]) assert self.stdout() == "I am __main__\n" - def test_directory_without_main(self): + def test_directory_without_main(self) -> None: self.make_file("without_main/__init__.py", "") with pytest.raises(NoSource, match="Can't find '__main__' module in 'without_main'"): run_python_file(["without_main"]) - def test_code_throws(self): + def test_code_throws(self) -> None: self.make_file("throw.py", """\ class MyException(Exception): pass @@ -129,7 +131,7 @@ class RunFileTest(CoverageTest): assert self.stdout() == "about to raise..\n" assert self.stderr() == "" - def test_code_exits(self): + def test_code_exits(self) -> None: self.make_file("exit.py", """\ import sys def f1(): @@ -148,7 +150,7 @@ class RunFileTest(CoverageTest): assert self.stdout() == "about to exit..\n" assert self.stderr() == "" - def test_excepthook_exit(self): + def test_excepthook_exit(self) -> None: self.make_file("excepthook_exit.py", """\ import sys @@ -165,7 +167,7 @@ class RunFileTest(CoverageTest): cov_out = self.stdout() assert cov_out == "in excepthook\n" - def test_excepthook_throw(self): + def test_excepthook_throw(self) -> None: self.make_file("excepthook_throw.py", """\ import sys @@ -193,7 +195,7 @@ class RunFileTest(CoverageTest): class RunPycFileTest(CoverageTest): """Test cases for `run_python_file`.""" - def make_pyc(self, **kwargs): + def make_pyc(self, **kwargs: Any) -> str: """Create a .pyc file, and return the path to it.""" self.make_file("compiled.py", """\ def doit(): @@ -207,12 +209,12 @@ class RunPycFileTest(CoverageTest): # Find the .pyc file! return str(next(pathlib.Path(".").rglob("compiled*.pyc"))) - def test_running_pyc(self): + def test_running_pyc(self) -> None: pycfile = self.make_pyc() run_python_file([pycfile]) assert self.stdout() == "I am here!\n" - def test_running_pyo(self): + def test_running_pyo(self) -> None: pycfile = self.make_pyc() pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile) assert pycfile != pyofile @@ -220,7 +222,7 @@ class RunPycFileTest(CoverageTest): run_python_file([pyofile]) assert self.stdout() == "I am here!\n" - def test_running_pyc_from_wrong_python(self): + def test_running_pyc_from_wrong_python(self) -> None: pycfile = self.make_pyc() # Jam Python 2.1 magic number into the .pyc file. @@ -234,18 +236,18 @@ class RunPycFileTest(CoverageTest): # In some environments, the pycfile persists and pollutes another test. os.remove(pycfile) - def test_running_hashed_pyc(self): + def test_running_hashed_pyc(self) -> None: pycfile = self.make_pyc(invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH) run_python_file([pycfile]) assert self.stdout() == "I am here!\n" - def test_no_such_pyc_file(self): + def test_no_such_pyc_file(self) -> None: path = python_reported_file('xyzzy.pyc') msg = re.escape(f"No file to run: '{path}'") with pytest.raises(NoCode, match=msg): run_python_file(["xyzzy.pyc"]) - def test_running_py_from_binary(self): + def test_running_py_from_binary(self) -> None: # Use make_file to get the bookkeeping. Ideally, it would # be able to write binary files. bf = self.make_file("binary") @@ -266,43 +268,43 @@ class RunModuleTest(UsingModulesMixin, CoverageTest): run_in_temp_dir = False - def test_runmod1(self): + def test_runmod1(self) -> None: run_python_module(["runmod1", "hello"]) out, err = self.stdouterr() assert out == "runmod1: passed hello\n" assert err == "" - def test_runmod2(self): + def test_runmod2(self) -> None: run_python_module(["pkg1.runmod2", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\nrunmod2: passed hello\n" assert err == "" - def test_runmod3(self): + def test_runmod3(self) -> None: run_python_module(["pkg1.sub.runmod3", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\nrunmod3: passed hello\n" assert err == "" - def test_pkg1_main(self): + def test_pkg1_main(self) -> None: run_python_module(["pkg1", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n" assert err == "" - def test_pkg1_sub_main(self): + def test_pkg1_sub_main(self) -> None: run_python_module(["pkg1.sub", "hello"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n" assert err == "" - def test_pkg1_init(self): + def test_pkg1_init(self) -> None: run_python_module(["pkg1.__init__", "wut?"]) out, err = self.stdouterr() assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_no_such_module(self): + def test_no_such_module(self) -> None: with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) with pytest.raises(NoSource, match="No module named '?i'?"): @@ -310,6 +312,6 @@ class RunModuleTest(UsingModulesMixin, CoverageTest): with pytest.raises(NoSource, match="No module named '?i'?"): run_python_module(["i.dont.exist"]) - def test_no_main(self): + def test_no_main(self) -> None: with pytest.raises(NoSource): run_python_module(["pkg2", "hi"]) diff --git a/tests/test_filereporter.py b/tests/test_filereporter.py index 227cc458..59335309 100644 --- a/tests/test_filereporter.py +++ b/tests/test_filereporter.py @@ -20,7 +20,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): run_in_temp_dir = False - def test_filenames(self): + def test_filenames(self) -> None: acu = PythonFileReporter("aa/afile.py") bcu = PythonFileReporter("aa/bb/bfile.py") ccu = PythonFileReporter("aa/bb/cc/cfile.py") @@ -31,7 +31,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): assert bcu.source() == "# bfile.py\n" assert ccu.source() == "# cfile.py\n" - def test_odd_filenames(self): + def test_odd_filenames(self) -> None: acu = PythonFileReporter("aa/afile.odd.py") bcu = PythonFileReporter("aa/bb/bfile.odd.py") b2cu = PythonFileReporter("aa/bb.odd/bfile.py") @@ -42,7 +42,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): assert bcu.source() == "# bfile.odd.py\n" assert b2cu.source() == "# bfile.py\n" - def test_modules(self): + def test_modules(self) -> None: import aa import aa.bb import aa.bb.cc @@ -57,7 +57,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): assert bcu.source() == "# bb\n" assert ccu.source() == "" # yes, empty - def test_module_files(self): + def test_module_files(self) -> None: import aa.afile import aa.bb.bfile import aa.bb.cc.cfile @@ -72,7 +72,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): assert bcu.source() == "# bfile.py\n" assert ccu.source() == "# cfile.py\n" - def test_comparison(self): + def test_comparison(self) -> None: acu = FileReporter("aa/afile.py") acu2 = FileReporter("aa/afile.py") zcu = FileReporter("aa/zfile.py") @@ -83,7 +83,7 @@ class FileReporterTest(UsingModulesMixin, CoverageTest): assert acu < bcu and acu <= bcu and acu != bcu assert bcu > acu and bcu >= acu and bcu != acu - def test_zipfile(self): + def test_zipfile(self) -> None: sys.path.append("tests/zip1.zip") # Test that we can get files out of zipfiles, and read their source files. diff --git a/tests/test_files.py b/tests/test_files.py index 2d029a04..ff02e0e9 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -7,6 +7,8 @@ import itertools import os import os.path import re + +from typing import Any, Generator, Iterable, List from unittest import mock import pytest @@ -17,6 +19,7 @@ from coverage.files import ( GlobMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file, actual_path, find_python_files, flat_rootname, globs_to_regex, ) +from coverage.types import Protocol from tests.coveragetest import CoverageTest from tests.helpers import os_sep @@ -25,11 +28,11 @@ from tests.helpers import os_sep class FilesTest(CoverageTest): """Tests of coverage.files.""" - def abs_path(self, p): + def abs_path(self, p: str) -> str: """Return the absolute path for `p`.""" return os.path.join(abs_file(os.getcwd()), os.path.normpath(p)) - def test_simple(self): + def test_simple(self) -> None: self.make_file("hello.py") files.set_relative_directory() assert files.relative_filename("hello.py") == "hello.py" @@ -37,7 +40,7 @@ class FilesTest(CoverageTest): assert a != "hello.py" assert files.relative_filename(a) == "hello.py" - def test_peer_directories(self): + def test_peer_directories(self) -> None: self.make_file("sub/proj1/file1.py") self.make_file("sub/proj2/file2.py") a1 = self.abs_path("sub/proj1/file1.py") @@ -48,7 +51,7 @@ class FilesTest(CoverageTest): assert files.relative_filename(a1) == "file1.py" assert files.relative_filename(a2) == a2 - def test_filepath_contains_absolute_prefix_twice(self): + def test_filepath_contains_absolute_prefix_twice(self) -> None: # https://github.com/nedbat/coveragepy/issues/194 # Build a path that has two pieces matching the absolute path prefix. # Technically, this test doesn't do that on Windows, but drive @@ -59,7 +62,7 @@ class FilesTest(CoverageTest): rel = os.path.join('sub', trick, 'file1.py') assert files.relative_filename(abs_file(rel)) == rel - def test_canonical_filename_ensure_cache_hit(self): + def test_canonical_filename_ensure_cache_hit(self) -> None: self.make_file("sub/proj1/file1.py") d = actual_path(self.abs_path("sub/proj1")) os.chdir(d) @@ -76,7 +79,7 @@ class FilesTest(CoverageTest): ("X:\\", "\\"), ] ) - def test_relative_dir_for_root(self, curdir, sep): + def test_relative_dir_for_root(self, curdir: str, sep: str) -> None: with mock.patch.object(files.os, 'curdir', new=curdir): with mock.patch.object(files.os, 'sep', new=sep): with mock.patch('coverage.files.os.path.normcase', return_value=curdir): @@ -95,7 +98,7 @@ class FilesTest(CoverageTest): ("src/files.pex", "src/files.pex/zipfiles/files.zip/foo.py", True), ] ) - def test_source_exists(self, to_make, to_check, answer): + def test_source_exists(self, to_make: str, to_check: str, answer: bool) -> None: # source_exists won't look inside the zipfile, so it's fine to make # an empty file with the zipfile name. self.make_file(to_make, "") @@ -122,13 +125,17 @@ class FilesTest(CoverageTest): "d_e597dfacb73a23d5_my_program_py" ), ]) -def test_flat_rootname(original, flat): +def test_flat_rootname(original: str, flat: str) -> None: assert flat_rootname(original) == flat def globs_to_regex_params( - patterns, case_insensitive=False, partial=False, matches=(), nomatches=(), -): + patterns: Iterable[str], + case_insensitive: bool=False, + partial: bool=False, + matches: Iterable[str]=(), + nomatches: Iterable[str]=(), +) -> Generator[Any, None, None]: """Generate parameters for `test_globs_to_regex`. `patterns`, `case_insensitive`, and `partial` are arguments for @@ -242,11 +249,14 @@ def globs_to_regex_params( ), ])) ) -def test_globs_to_regex(patterns, case_insensitive, partial, text, result): +def test_globs_to_regex( + patterns: Iterable[str], + case_insensitive: bool, + partial: bool, + text: str, + result: bool, +) -> None: regex = globs_to_regex(patterns, case_insensitive=case_insensitive, partial=partial) - print(patterns) - print(regex) - print(text) assert bool(regex.match(text)) == result @@ -261,26 +271,33 @@ def test_globs_to_regex(patterns, case_insensitive, partial, text, result): ("x/**a/b.py", "**a"), ("x/**/**/b.py", "**/**"), ]) -def test_invalid_globs(pattern, bad_word): +def test_invalid_globs(pattern: str, bad_word: str) -> None: msg = f"File pattern can't include {bad_word!r}" with pytest.raises(ConfigError, match=re.escape(msg)): globs_to_regex([pattern]) +class TMatcher(Protocol): + """The shape all Matchers have.""" + + def match(self, s: str) -> bool: + """Does this string match?""" + + class MatcherTest(CoverageTest): """Tests of file matchers.""" - def setUp(self): + def setUp(self) -> None: super().setUp() files.set_relative_directory() - def assertMatches(self, matcher, filepath, matches): + def assertMatches(self, matcher: TMatcher, filepath: str, matches: bool) -> None: """The `matcher` should agree with `matches` about `filepath`.""" canonical = files.canonical_filename(filepath) msg = f"File {filepath} should have matched as {matches}" assert matches == matcher.match(canonical), msg - def test_tree_matcher(self): + def test_tree_matcher(self) -> None: case_folding = env.WINDOWS matches_to_try = [ (self.make_file("sub/file1.py"), True), @@ -302,7 +319,7 @@ class MatcherTest(CoverageTest): for filepath, matches in matches_to_try: self.assertMatches(tm, filepath, matches) - def test_module_matcher(self): + def test_module_matcher(self) -> None: matches_to_try = [ ('test', True), ('trash', False), @@ -325,7 +342,7 @@ class MatcherTest(CoverageTest): for modulename, matches in matches_to_try: assert mm.match(modulename) == matches, modulename - def test_glob_matcher(self): + def test_glob_matcher(self) -> None: matches_to_try = [ (self.make_file("sub/file1.py"), True), (self.make_file("sub/file2.c"), False), @@ -338,7 +355,7 @@ class MatcherTest(CoverageTest): for filepath, matches in matches_to_try: self.assertMatches(fnm, filepath, matches) - def test_glob_matcher_overload(self): + def test_glob_matcher_overload(self) -> None: fnm = GlobMatcher(["*x%03d*.txt" % i for i in range(500)]) self.assertMatches(fnm, "x007foo.txt", True) self.assertMatches(fnm, "x123foo.txt", True) @@ -346,7 +363,7 @@ class MatcherTest(CoverageTest): self.assertMatches(fnm, "x499.txt", True) self.assertMatches(fnm, "x500.txt", False) - def test_glob_windows_paths(self): + def test_glob_windows_paths(self) -> None: # We should be able to match Windows paths even if we are running on # a non-Windows OS. fnm = GlobMatcher(["*/foo.py"]) @@ -356,9 +373,9 @@ class MatcherTest(CoverageTest): @pytest.fixture(params=[False, True], name="rel_yn") -def relative_setting(request): +def relative_setting(request: pytest.FixtureRequest) -> bool: """Parameterized fixture to choose whether PathAliases is relative or not.""" - return request.param + return request.param # type: ignore[no-any-return] class PathAliasesTest(CoverageTest): @@ -366,7 +383,7 @@ class PathAliasesTest(CoverageTest): run_in_temp_dir = False - def assert_mapped(self, aliases, inp, out): + def assert_mapped(self, aliases: PathAliases, inp: str, out: str) -> None: """Assert that `inp` mapped through `aliases` produces `out`. If the aliases are not relative, then `out` is canonicalized first, @@ -380,20 +397,20 @@ class PathAliasesTest(CoverageTest): expected = files.canonical_filename(out) assert mapped == expected - def assert_unchanged(self, aliases, inp, exists=True): + def assert_unchanged(self, aliases: PathAliases, inp: str, exists: bool=True) -> None: """Assert that `inp` mapped through `aliases` is unchanged.""" assert aliases.map(inp, exists=lambda p: exists) == inp - def test_noop(self, rel_yn): + def test_noop(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) self.assert_unchanged(aliases, '/ned/home/a.py') - def test_nomatch(self, rel_yn): + def test_nomatch(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/*/src', './mysrc') self.assert_unchanged(aliases, '/home/foo/a.py') - def test_wildcard(self, rel_yn): + def test_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/ned/home/*/src', './mysrc') self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py') @@ -402,24 +419,24 @@ class PathAliasesTest(CoverageTest): aliases.add('/ned/home/*/src/', './mysrc') self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py') - def test_no_accidental_match(self, rel_yn): + def test_no_accidental_match(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/*/src', './mysrc') self.assert_unchanged(aliases, '/home/foo/srcetc') - def test_no_map_if_not_exist(self, rel_yn): + def test_no_map_if_not_exist(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/ned/home/*/src', './mysrc') self.assert_unchanged(aliases, '/ned/home/foo/src/a.py', exists=False) self.assert_unchanged(aliases, 'foo/src/a.py', exists=False) - def test_no_dotslash(self, rel_yn): + def test_no_dotslash(self, rel_yn: bool) -> None: # The result shouldn't start with "./" if the map result didn't. aliases = PathAliases(relative=rel_yn) aliases.add('*/project', '.') self.assert_mapped(aliases, '/ned/home/project/src/a.py', os_sep('src/a.py')) - def test_relative_pattern(self): + def test_relative_pattern(self) -> None: aliases = PathAliases(relative=True) aliases.add(".tox/*/site-packages", "src") self.assert_mapped( @@ -428,9 +445,9 @@ class PathAliasesTest(CoverageTest): os_sep("src/proj/a.py"), ) - def test_multiple_patterns(self, rel_yn): + def test_multiple_patterns(self, rel_yn: bool) -> None: # also test the debugfn... - msgs = [] + msgs: List[str] = [] aliases = PathAliases(debugfn=msgs.append, relative=rel_yn) aliases.add('/home/*/src', './mysrc') aliases.add('/lib/*/libsrc', './mylib') @@ -466,20 +483,20 @@ class PathAliasesTest(CoverageTest): "/ned/home/*/", "/ned/home/*/*/", ]) - def test_cant_have_wildcard_at_end(self, badpat): + def test_cant_have_wildcard_at_end(self, badpat: str) -> None: aliases = PathAliases() msg = "Pattern must not end with wildcards." with pytest.raises(ConfigError, match=msg): aliases.add(badpat, "fooey") - def test_no_accidental_munging(self): + def test_no_accidental_munging(self) -> None: aliases = PathAliases() aliases.add(r'c:\Zoo\boo', 'src/') aliases.add('/home/ned$', 'src/') self.assert_mapped(aliases, r'c:\Zoo\boo\foo.py', 'src/foo.py') self.assert_mapped(aliases, r'/home/ned$/foo.py', 'src/foo.py') - def test_paths_are_os_corrected(self, rel_yn): + def test_paths_are_os_corrected(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/ned/*/src', './mysrc') aliases.add(r'c:\ned\src', './mysrc') @@ -500,7 +517,7 @@ class PathAliasesTest(CoverageTest): lin_win_paths = [[lin, win], [win, lin]] @pytest.mark.parametrize("paths", lin_win_paths) - def test_windows_on_linux(self, paths, rel_yn): + def test_windows_on_linux(self, paths: Iterable[str], rel_yn: bool) -> None: # https://github.com/nedbat/coveragepy/issues/618 aliases = PathAliases(relative=rel_yn) for path in paths: @@ -512,7 +529,7 @@ class PathAliasesTest(CoverageTest): ) @pytest.mark.parametrize("paths", lin_win_paths) - def test_linux_on_windows(self, paths, rel_yn): + def test_linux_on_windows(self, paths: Iterable[str], rel_yn: bool) -> None: # https://github.com/nedbat/coveragepy/issues/618 aliases = PathAliases(relative=rel_yn) for path in paths: @@ -524,7 +541,7 @@ class PathAliasesTest(CoverageTest): ) @pytest.mark.parametrize("paths", lin_win_paths) - def test_relative_windows_on_linux(self, paths): + def test_relative_windows_on_linux(self, paths: Iterable[str]) -> None: # https://github.com/nedbat/coveragepy/issues/991 aliases = PathAliases(relative=True) for path in paths: @@ -536,7 +553,7 @@ class PathAliasesTest(CoverageTest): ) @pytest.mark.parametrize("paths", lin_win_paths) - def test_relative_linux_on_windows(self, paths): + def test_relative_linux_on_windows(self, paths: Iterable[str]) -> None: # https://github.com/nedbat/coveragepy/issues/991 aliases = PathAliases(relative=True) for path in paths: @@ -548,7 +565,7 @@ class PathAliasesTest(CoverageTest): ) @pytest.mark.skipif(env.WINDOWS, reason="This test assumes Unix file system") - def test_implicit_relative_windows_on_linux(self): + def test_implicit_relative_windows_on_linux(self) -> None: # https://github.com/nedbat/coveragepy/issues/991 aliases = PathAliases(relative=True) self.assert_mapped( @@ -558,7 +575,7 @@ class PathAliasesTest(CoverageTest): ) @pytest.mark.skipif(not env.WINDOWS, reason="This test assumes Windows file system") - def test_implicit_relative_linux_on_windows(self): + def test_implicit_relative_linux_on_windows(self) -> None: # https://github.com/nedbat/coveragepy/issues/991 aliases = PathAliases(relative=True) self.assert_mapped( @@ -567,7 +584,7 @@ class PathAliasesTest(CoverageTest): r"project\module\tests\file.py", ) - def test_multiple_wildcard(self, rel_yn): + def test_multiple_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('/home/jenkins/*/a/*/b/*/django', './django') self.assert_mapped( @@ -576,7 +593,7 @@ class PathAliasesTest(CoverageTest): './django/foo/bar.py', ) - def test_windows_root_paths(self, rel_yn): + def test_windows_root_paths(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('X:\\', '/tmp/src') self.assert_mapped( @@ -590,7 +607,7 @@ class PathAliasesTest(CoverageTest): "/tmp/src/file.py", ) - def test_leading_wildcard(self, rel_yn): + def test_leading_wildcard(self, rel_yn: bool) -> None: aliases = PathAliases(relative=rel_yn) aliases.add('*/d1', './mysrc1') aliases.add('*/d2', './mysrc2') @@ -598,7 +615,7 @@ class PathAliasesTest(CoverageTest): self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py') @pytest.mark.parametrize("dirname", [".", "..", "../other", "/"]) - def test_dot(self, dirname): + def test_dot(self, dirname: str) -> None: if env.WINDOWS and dirname == "/": # The root test case was added for the manylinux Docker images, # and I'm not sure how it should work on Windows, so skip it. @@ -616,7 +633,7 @@ class PathAliasesTest(CoverageTest): class PathAliasesRealFilesTest(CoverageTest): """Tests for coverage/files.py:PathAliases using real files.""" - def test_aliasing_zip_files(self): + def test_aliasing_zip_files(self) -> None: self.make_file("src/zipfiles/code.zip", "fake zip, doesn't matter") aliases = PathAliases() aliases.add("*/d1", "./src") @@ -629,7 +646,7 @@ class PathAliasesRealFilesTest(CoverageTest): class FindPythonFilesTest(CoverageTest): """Tests of `find_python_files`.""" - def test_find_python_files(self): + def test_find_python_files(self) -> None: self.make_file("sub/a.py") self.make_file("sub/b.py") self.make_file("sub/x.c") # nope: not .py @@ -645,7 +662,7 @@ class FindPythonFilesTest(CoverageTest): "sub/windows.pyw", ]) - def test_find_python_files_include_namespace_packages(self): + def test_find_python_files_include_namespace_packages(self) -> None: self.make_file("sub/a.py") self.make_file("sub/b.py") self.make_file("sub/x.c") # nope: not .py @@ -669,5 +686,5 @@ class WindowsFileTest(CoverageTest): run_in_temp_dir = False - def test_actual_path(self): + def test_actual_path(self) -> None: assert actual_path(r'c:\Windows') == actual_path(r'C:\wINDOWS') diff --git a/tests/test_summary.py b/tests/test_summary.py index f3c7ed3f..15c75348 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -22,7 +22,7 @@ from coverage.data import CoverageData from coverage.exceptions import ConfigError, NoDataError, NotPython from coverage.files import abs_file from coverage.summary import SummaryReporter -from coverage.types import TConfigValue +from coverage.types import TConfigValueIn from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin from tests.helpers import assert_coverage_warnings @@ -900,7 +900,7 @@ class SummaryReporterConfigurationTest(CoverageTest): source += " a = 2\n" * dont_run self.make_file(filename, source) - def get_summary_text(self, *options: Tuple[str, TConfigValue]) -> str: + def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str: """Get text output from the SummaryReporter. The arguments are tuples: (name, value) for Coverage.set_option. @@ -103,8 +103,9 @@ setenv = C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py T1=tests/conftest.py tests/coveragetest.py tests/goldtest.py tests/helpers.py tests/mixins.py tests/osinfo.py T2=tests/test_annotate.py tests/test_api.py tests/test_arcs.py tests/test_cmdline.py tests/test_collector.py tests/test_concurrency.py - T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_html.py tests/test_misc.py tests/test_python.py tests/test_summary.py tests/test_xml.py - TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} + T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_debug.py tests/test_execfile.py + T4=tests/test_filereporter.py tests/test_files.py tests/test_html.py tests/test_misc.py tests/test_python.py tests/test_summary.py tests/test_xml.py + TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} commands = # PYVERSIONS |