From a3f3841b746a1789ff8f7fea0cc0715c45770996 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 Dec 2022 23:33:31 -0500 Subject: mypy: add cmdline.py and test_cmdline.py --- coverage/cmdline.py | 84 +++++++++++++++--------- coverage/control.py | 4 +- coverage/types.py | 2 +- tests/coveragetest.py | 4 +- tests/test_cmdline.py | 176 +++++++++++++++++++++++++++++--------------------- tox.ini | 11 ++-- 6 files changed, 166 insertions(+), 115 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index b15a66f7..4a00105a 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -12,6 +12,8 @@ import sys import textwrap import traceback +from typing import cast, Any, List, NoReturn, Optional, Tuple + import coverage from coverage import Coverage from coverage import env @@ -235,8 +237,9 @@ class CoverageOptionParser(optparse.OptionParser): """ - def __init__(self, *args, **kwargs): - super().__init__(add_help_option=False, *args, **kwargs) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs["add_help_option"] = False + super().__init__(*args, **kwargs) self.set_defaults( # Keep these arguments alphabetized by their names. action=None, @@ -278,19 +281,19 @@ class CoverageOptionParser(optparse.OptionParser): """Used to stop the optparse error handler ending the process.""" pass - def parse_args_ok(self, args=None, options=None): + def parse_args_ok(self, args: List[str]) -> Tuple[bool, Optional[optparse.Values], List[str]]: """Call optparse.parse_args, but return a triple: (ok, options, args) """ try: - options, args = super().parse_args(args, options) + options, args = super().parse_args(args) except self.OptionParserError: - return False, None, None + return False, None, [] return True, options, args - def error(self, msg): + def error(self, msg: str) -> NoReturn: """Override optparse.error so sys.exit doesn't get called.""" show_help(msg) raise self.OptionParserError @@ -299,7 +302,7 @@ class CoverageOptionParser(optparse.OptionParser): class GlobalOptionParser(CoverageOptionParser): """Command-line parser for coverage.py global option arguments.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.add_options([ @@ -311,14 +314,19 @@ class GlobalOptionParser(CoverageOptionParser): class CmdOptionParser(CoverageOptionParser): """Parse one of the new-style commands for coverage.py.""" - def __init__(self, action, options, defaults=None, usage=None, description=None): + def __init__( + self, + action: str, + options: List[optparse.Option], + description: str, + usage: Optional[str]=None, + ): """Create an OptionParser for a coverage.py command. `action` is the slug to put into `options.action`. `options` is a list of Option's for the command. - `defaults` is a dict of default value for options. - `usage` is the usage string to display in help. `description` is the description of the command, for the help text. + `usage` is the usage string to display in help. """ if usage: @@ -327,18 +335,18 @@ class CmdOptionParser(CoverageOptionParser): usage=usage, description=description, ) - self.set_defaults(action=action, **(defaults or {})) + self.set_defaults(action=action) self.add_options(options) self.cmd = action - def __eq__(self, other): + def __eq__(self, other: str) -> bool: # type: ignore[override] # A convenience equality, so that I can put strings in unit test # results, and they will compare equal to objects. return (other == f"") - __hash__ = None # This object doesn't need to be hashed. + __hash__ = None # type: ignore[assignment] - def get_prog_name(self): + def get_prog_name(self) -> str: """Override of an undocumented function in optparse.OptionParser.""" program_name = super().get_prog_name() @@ -540,7 +548,11 @@ COMMANDS = { } -def show_help(error=None, topic=None, parser=None): +def show_help( + error: Optional[str]=None, + topic: Optional[str]=None, + parser: Optional[optparse.OptionParser]=None, +) -> None: """Display an error message, or the named topic.""" assert error or topic or parser @@ -573,6 +585,7 @@ def show_help(error=None, topic=None, parser=None): print(parser.format_help().strip()) print() else: + assert topic is not None help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() if help_msg: print(help_msg.format(**help_params)) @@ -587,11 +600,11 @@ OK, ERR, FAIL_UNDER = 0, 1, 2 class CoverageScript: """The command-line interface to coverage.py.""" - def __init__(self): + def __init__(self) -> None: self.global_option = False - self.coverage = None + self.coverage: Coverage - def command_line(self, argv): + def command_line(self, argv: List[str]) -> int: """The bulk of the command line interface to coverage.py. `argv` is the argument list to process. @@ -606,6 +619,7 @@ class CoverageScript: # The command syntax we parse depends on the first argument. Global # switch syntax always starts with an option. + parser: Optional[optparse.OptionParser] self.global_option = argv[0].startswith('-') if self.global_option: parser = GlobalOptionParser() @@ -619,6 +633,7 @@ class CoverageScript: ok, options, args = parser.parse_args_ok(argv) if not ok: return ERR + assert options is not None # Handle help and version. if self.do_help(options, args, parser): @@ -740,8 +755,8 @@ class CoverageScript: if options.precision is not None: self.coverage.set_option("report:precision", options.precision) - fail_under = self.coverage.get_option("report:fail_under") - precision = self.coverage.get_option("report:precision") + fail_under = cast(float, self.coverage.get_option("report:fail_under")) + precision = cast(int, self.coverage.get_option("report:precision")) if should_fail_under(total, fail_under, precision): msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format( total=Numbers(precision=precision).display_covered(total), @@ -753,7 +768,12 @@ class CoverageScript: return OK - def do_help(self, options, args, parser): + def do_help( + self, + options: optparse.Values, + args: List[str], + parser: optparse.OptionParser, + ) -> bool: """Deal with help requests. Return True if it handled the request, False if not. @@ -770,9 +790,9 @@ class CoverageScript: if options.action == "help": if args: for a in args: - parser = COMMANDS.get(a) - if parser: - show_help(parser=parser) + parser_maybe = COMMANDS.get(a) + if parser_maybe is not None: + show_help(parser=parser_maybe) else: show_help(topic=a) else: @@ -786,7 +806,7 @@ class CoverageScript: return False - def do_run(self, options, args): + def do_run(self, options: optparse.Values, args: List[str]) -> int: """Implementation of 'coverage run'.""" if not args: @@ -794,7 +814,7 @@ class CoverageScript: # Specified -m with nothing else. show_help("No module specified for -m") return ERR - command_line = self.coverage.get_option("run:command_line") + command_line = cast(str, self.coverage.get_option("run:command_line")) if command_line is not None: args = shlex.split(command_line) if args and args[0] in {"-m", "--module"}: @@ -845,7 +865,7 @@ class CoverageScript: return OK - def do_debug(self, args): + def do_debug(self, args: List[str]) -> int: """Implementation of 'coverage debug'.""" if not args: @@ -878,7 +898,7 @@ class CoverageScript: return OK -def unshell_list(s): +def unshell_list(s: str) -> Optional[List[str]]: """Turn a command-line argument into a list.""" if not s: return None @@ -892,7 +912,7 @@ def unshell_list(s): return s.split(',') -def unglob_args(args): +def unglob_args(args: List[str]) -> List[str]: """Interpret shell wildcards for platforms that need it.""" if env.WINDOWS: globbed = [] @@ -938,7 +958,7 @@ HELP_TOPICS = { } -def main(argv=None): +def main(argv: Optional[List[str]]=None) -> Optional[int]: """The main entry point to coverage.py. This is installed as the script entry point. @@ -976,7 +996,9 @@ if _profile: # pragma: debugging from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error original_main = main - def main(argv=None): # pylint: disable=function-redefined + def main( # pylint: disable=function-redefined + argv: Optional[List[str]]=None, + ) -> Optional[int]: """A wrapper around main that profiles.""" profiler = SimpleLauncher.launch() try: diff --git a/coverage/control.py b/coverage/control.py index 5aa312d3..4306fea7 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -111,7 +111,7 @@ class Coverage(TConfigurable): def __init__( # pylint: disable=too-many-arguments self, - data_file: Optional[str]=DEFAULT_DATAFILE, # type: ignore[assignment] + data_file: Optional[Union[str, DefaultValue]]=DEFAULT_DATAFILE, data_suffix: Optional[Union[str, bool]]=None, cover_pylib: Optional[bool]=None, auto_data: bool=False, @@ -219,7 +219,7 @@ class Coverage(TConfigurable): # data_file=None means no disk file at all. data_file missing means # use the value from the config file. self._no_disk = data_file is None - if data_file is DEFAULT_DATAFILE: + if isinstance(data_file, DefaultValue): data_file = None # This is injectable by tests. diff --git a/coverage/types.py b/coverage/types.py index 416b0b5d..79cf5d3a 100644 --- a/coverage/types.py +++ b/coverage/types.py @@ -25,7 +25,7 @@ TCovKwargs = Any ## Configuration # One value read from a config file. -TConfigValue = Optional[Union[bool, int, str, List[str]]] +TConfigValue = Optional[Union[bool, int, float, str, List[str]]] # An entire config section, mapping option names to values. TConfigSection = Dict[str, TConfigValue] diff --git a/tests/coveragetest.py b/tests/coveragetest.py index e718dd31..47a124c1 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -310,7 +310,7 @@ class CoverageTest( assert age.total_seconds() >= 0, msg assert age.total_seconds() <= seconds, msg - def command_line(self, args, ret=OK): + def command_line(self, args: str, ret: int=OK) -> None: """Run `args` through the command line. Use this when you want to run the full coverage machinery, but in the @@ -467,7 +467,7 @@ class UsingModulesMixin: sys.path.append(nice_file(TESTS_DIR, "zipmods.zip")) -def command_line(args): +def command_line(args: str) -> int: """Run `args` through the CoverageScript command line. Returns the return code from CoverageScript.command_line. diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 67899b75..85e99ad5 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -10,6 +10,8 @@ import sys import textwrap from unittest import mock +from typing import Any, List, Mapping, Optional, Tuple + import pytest import coverage @@ -18,6 +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.version import __url__ from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -67,7 +70,7 @@ class BaseCmdLineTest(CoverageTest): DEFAULT_KWARGS = {name: kw for name, _, kw in _defaults.mock_calls} - def model_object(self): + def model_object(self) -> mock.Mock: """Return a Mock suitable for use in CoverageScript.""" mk = mock.Mock() @@ -90,7 +93,11 @@ class BaseCmdLineTest(CoverageTest): # Global names in cmdline.py that will be mocked during the tests. MOCK_GLOBALS = ['Coverage', 'PyRunner', 'show_help'] - def mock_command_line(self, args, options=None): + def mock_command_line( + self, + args: str, + options: Optional[Mapping[str, TConfigValue]]=None, + ) -> Tuple[mock.Mock, int]: """Run `args` through the command line, with a Mock. `options` is a dict of names and values to pass to `set_option`. @@ -118,7 +125,13 @@ class BaseCmdLineTest(CoverageTest): return mk, ret - def cmd_executes(self, args, code, ret=OK, options=None): + def cmd_executes( + self, + args: str, + code: str, + ret: int=OK, + options: Optional[Mapping[str, TConfigValue]]=None, + ) -> None: """Assert that the `args` end up executing the sequence in `code`.""" called, status = self.mock_command_line(args, options=options) assert status == ret, f"Wrong status: got {status!r}, wanted {ret!r}" @@ -140,14 +153,14 @@ class BaseCmdLineTest(CoverageTest): self.assert_same_mock_calls(expected, called) - def cmd_executes_same(self, args1, args2): + def cmd_executes_same(self, args1: str, args2: str) -> None: """Assert that the `args1` executes the same as `args2`.""" m1, r1 = self.mock_command_line(args1) m2, r2 = self.mock_command_line(args2) assert r1 == r2 self.assert_same_mock_calls(m1, m2) - def assert_same_mock_calls(self, m1, m2): + def assert_same_mock_calls(self, m1: mock.Mock, m2: mock.Mock) -> None: """Assert that `m1.mock_calls` and `m2.mock_calls` are the same.""" # Use a real equality comparison, but if it fails, use a nicer assert # so we can tell what's going on. We have to use the real == first due @@ -157,7 +170,13 @@ class BaseCmdLineTest(CoverageTest): pp2 = pprint.pformat(m2.mock_calls) assert pp1+'\n' == pp2+'\n' - def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): + def cmd_help( + self, + args: str, + help_msg: Optional[str]=None, + topic: Optional[str]=None, + ret: int=ERR, + ) -> None: """Run a command line, and check that it prints the right help. Only the last function call in the mock is checked, which should be the @@ -174,7 +193,7 @@ class BaseCmdLineTest(CoverageTest): class BaseCmdLineTestTest(BaseCmdLineTest): """Tests that our BaseCmdLineTest helpers work.""" - def test_cmd_executes_same(self): + def test_cmd_executes_same(self) -> None: # All the other tests here use self.cmd_executes_same in successful # ways, so here we just check that it fails. with pytest.raises(AssertionError): @@ -184,7 +203,7 @@ class BaseCmdLineTestTest(BaseCmdLineTest): class CmdLineTest(BaseCmdLineTest): """Tests of the coverage.py command line.""" - def test_annotate(self): + def test_annotate(self) -> None: # coverage annotate [-d DIR] [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("annotate", """\ cov = Coverage() @@ -222,7 +241,7 @@ class CmdLineTest(BaseCmdLineTest): cov.annotate(morfs=["mod1", "mod2", "mod3"]) """) - def test_combine(self): + def test_combine(self) -> None: # coverage combine with args self.cmd_executes("combine datadir1", """\ cov = Coverage() @@ -259,7 +278,7 @@ class CmdLineTest(BaseCmdLineTest): cov.save() """) - def test_combine_doesnt_confuse_options_with_args(self): + def test_combine_doesnt_confuse_options_with_args(self) -> None: # https://github.com/nedbat/coveragepy/issues/385 self.cmd_executes("combine --rcfile cov.ini", """\ cov = Coverage(config_file='cov.ini') @@ -277,23 +296,23 @@ class CmdLineTest(BaseCmdLineTest): ("debug foo", "Don't know what you mean by 'foo'"), ("debug sys config", "Only one topic at a time, please"), ]) - def test_debug(self, cmd, output): + def test_debug(self, cmd: str, output: str) -> None: self.cmd_help(cmd, output) - def test_debug_sys(self): + def test_debug_sys(self) -> None: self.command_line("debug sys") out = self.stdout() assert "version:" in out assert "data_file:" in out - def test_debug_config(self): + def test_debug_config(self) -> None: self.command_line("debug config") out = self.stdout() assert "cover_pylib:" in out assert "skip_covered:" in out assert "skip_empty:" in out - def test_debug_pybehave(self): + def test_debug_pybehave(self) -> None: self.command_line("debug pybehave") out = self.stdout() assert " CPYTHON:" in out @@ -303,7 +322,7 @@ class CmdLineTest(BaseCmdLineTest): vtuple = ast.literal_eval(pyversion.partition(":")[-1].strip()) assert vtuple[:5] == sys.version_info - def test_debug_premain(self): + def test_debug_premain(self) -> None: self.command_line("debug premain") out = self.stdout() # ... many lines ... @@ -317,7 +336,7 @@ class CmdLineTest(BaseCmdLineTest): assert re.search(r"(?m)^\s+command_line : .*[/\\]coverage[/\\]cmdline.py:\d+$", out) assert re.search(r"(?m)^\s+do_debug : .*[/\\]coverage[/\\]cmdline.py:\d+$", out) - def test_erase(self): + def test_erase(self) -> None: # coverage erase self.cmd_executes("erase", """\ cov = Coverage() @@ -328,23 +347,23 @@ class CmdLineTest(BaseCmdLineTest): cov.erase() """) - def test_version(self): + def test_version(self) -> None: # coverage --version self.cmd_help("--version", topic="version", ret=OK) - def test_help_option(self): + def test_help_option(self) -> None: # coverage -h self.cmd_help("-h", topic="help", ret=OK) self.cmd_help("--help", topic="help", ret=OK) - def test_help_command(self): + def test_help_command(self) -> None: self.cmd_executes("help", "show_help(topic='help')") - def test_cmd_help(self): + def test_cmd_help(self) -> None: self.cmd_executes("run --help", "show_help(parser='')") self.cmd_executes_same("help run", "run --help") - def test_html(self): + def test_html(self) -> None: # coverage html -d DIR [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("html", """\ cov = Coverage() @@ -402,7 +421,7 @@ class CmdLineTest(BaseCmdLineTest): cov.html_report() """) - def test_json(self): + def test_json(self) -> None: # coverage json [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("json", """\ cov = Coverage() @@ -465,7 +484,7 @@ class CmdLineTest(BaseCmdLineTest): cov.json_report() """) - def test_lcov(self): + def test_lcov(self) -> None: # coverage lcov [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("lcov", """\ cov = Coverage() @@ -508,7 +527,7 @@ class CmdLineTest(BaseCmdLineTest): cov.lcov_report() """) - def test_report(self): + def test_report(self) -> None: # coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] self.cmd_executes("report", """\ cov = Coverage() @@ -591,7 +610,7 @@ class CmdLineTest(BaseCmdLineTest): cov.report(output_format="markdown") """) - def test_run(self): + def test_run(self) -> None: # coverage run [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] # run calls coverage.erase first. @@ -726,7 +745,7 @@ class CmdLineTest(BaseCmdLineTest): cov.save() """) - def test_multiprocessing_needs_config_file(self): + def test_multiprocessing_needs_config_file(self) -> None: # You can't use command-line args to add options to multiprocessing # runs, since they won't make it to the subprocesses. You need to use a # config file. @@ -736,7 +755,7 @@ class CmdLineTest(BaseCmdLineTest): assert msg in err assert "Remove --branch from the command line." in err - def test_run_debug(self): + def test_run_debug(self) -> None: self.cmd_executes("run --debug=opt1 foo.py", """\ cov = Coverage(debug=["opt1"]) runner = PyRunner(['foo.py'], as_module=False) @@ -756,7 +775,7 @@ class CmdLineTest(BaseCmdLineTest): cov.save() """) - def test_run_module(self): + def test_run_module(self) -> None: self.cmd_executes("run -m mymodule", """\ cov = Coverage() runner = PyRunner(['mymodule'], as_module=True) @@ -786,11 +805,11 @@ class CmdLineTest(BaseCmdLineTest): """) self.cmd_executes_same("run -m mymodule", "run --module mymodule") - def test_run_nothing(self): + def test_run_nothing(self) -> None: self.command_line("run", ret=ERR) assert "Nothing to do" in self.stderr() - def test_run_from_config(self): + def test_run_from_config(self) -> None: options = {"run:command_line": "myprog.py a 123 'a quoted thing' xyz"} self.cmd_executes("run", """\ cov = Coverage() @@ -804,7 +823,7 @@ class CmdLineTest(BaseCmdLineTest): options=options, ) - def test_run_module_from_config(self): + def test_run_module_from_config(self) -> None: self.cmd_executes("run", """\ cov = Coverage() runner = PyRunner(['mymodule', 'thing1', 'thing2'], as_module=True) @@ -817,7 +836,7 @@ class CmdLineTest(BaseCmdLineTest): options={"run:command_line": "-m mymodule thing1 thing2"}, ) - def test_run_from_config_but_empty(self): + def test_run_from_config_but_empty(self) -> None: self.cmd_executes("run", """\ cov = Coverage() show_help('Nothing to do.') @@ -826,7 +845,7 @@ class CmdLineTest(BaseCmdLineTest): options={"run:command_line": ""}, ) - def test_run_dashm_only(self): + def test_run_dashm_only(self) -> None: self.cmd_executes("run -m", """\ cov = Coverage() show_help('No module specified for -m') @@ -841,11 +860,11 @@ class CmdLineTest(BaseCmdLineTest): options={"run:command_line": "myprog.py"} ) - def test_cant_append_parallel(self): + def test_cant_append_parallel(self) -> None: self.command_line("run --append --parallel-mode foo.py", ret=ERR) assert "Can't append to data files in parallel mode." in self.stderr() - def test_xml(self): + def test_xml(self) -> None: # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("xml", """\ cov = Coverage() @@ -898,10 +917,10 @@ class CmdLineTest(BaseCmdLineTest): cov.xml_report() """) - def test_no_arguments_at_all(self): + def test_no_arguments_at_all(self) -> None: self.cmd_help("", topic="minimum_help", ret=OK) - def test_bad_command(self): + def test_bad_command(self) -> None: self.cmd_help("xyzzy", "Unknown command: 'xyzzy'") @@ -910,7 +929,7 @@ class CmdLineWithFilesTest(BaseCmdLineTest): run_in_temp_dir = True - def test_debug_data(self): + def test_debug_data(self) -> None: data = self.make_data_file( lines={ "file1.py": range(1, 18), @@ -929,7 +948,7 @@ class CmdLineWithFilesTest(BaseCmdLineTest): file2.py: 23 lines """) - def test_debug_data_with_no_data_file(self): + def test_debug_data_with_no_data_file(self) -> None: data = self.make_data_file() self.command_line("debug data") assert self.stdout() == textwrap.dedent(f"""\ @@ -938,7 +957,7 @@ class CmdLineWithFilesTest(BaseCmdLineTest): No data collected: file doesn't exist """) - def test_debug_combinable_data(self): + def test_debug_combinable_data(self) -> None: data1 = self.make_data_file(lines={"file1.py": range(1, 18), "file2.py": [1]}) data2 = self.make_data_file(suffix="123", lines={"file2.py": range(1, 10)}) @@ -961,13 +980,13 @@ class CmdLineWithFilesTest(BaseCmdLineTest): class CmdLineStdoutTest(BaseCmdLineTest): """Test the command line with real stdout output.""" - def test_minimum_help(self): + def test_minimum_help(self) -> None: self.command_line("") out = self.stdout() assert "Code coverage for Python" in out assert out.count("\n") < 4 - def test_version(self): + def test_version(self) -> None: self.command_line("--version") out = self.stdout() assert "ersion " in out @@ -977,7 +996,7 @@ class CmdLineStdoutTest(BaseCmdLineTest): assert "without C extension" in out assert out.count("\n") < 4 - def test_help_contains_command_name(self): + def test_help_contains_command_name(self) -> None: # Command name should be present in help output. fake_command_path = os_sep("lorem/ipsum/dolor") expected_command_name = "dolor" @@ -987,7 +1006,7 @@ class CmdLineStdoutTest(BaseCmdLineTest): out = self.stdout() assert expected_command_name in out - def test_help_contains_command_name_from_package(self): + def test_help_contains_command_name_from_package(self) -> None: # Command package name should be present in help output. # # When the main module is actually a package's `__main__` module, the resulting command line @@ -1002,13 +1021,13 @@ class CmdLineStdoutTest(BaseCmdLineTest): out = self.stdout() assert expected_command_name in out - def test_help(self): + def test_help(self) -> None: self.command_line("help") lines = self.stdout().splitlines() assert len(lines) > 10 assert lines[-1] == f"Full documentation is at {__url__}" - def test_cmd_help(self): + def test_cmd_help(self) -> None: self.command_line("help run") out = self.stdout() lines = out.splitlines() @@ -1017,26 +1036,26 @@ class CmdLineStdoutTest(BaseCmdLineTest): assert len(lines) > 20 assert lines[-1] == f"Full documentation is at {__url__}" - def test_unknown_topic(self): + def test_unknown_topic(self) -> None: # Should probably be an ERR return, but meh. self.command_line("help foobar") lines = self.stdout().splitlines() assert lines[0] == "Don't know topic 'foobar'" assert lines[-1] == f"Full documentation is at {__url__}" - def test_error(self): + def test_error(self) -> None: self.command_line("fooey kablooey", ret=ERR) err = self.stderr() assert "fooey" in err assert "help" in err - def test_option_error(self): + def test_option_error(self) -> None: self.command_line("run --fooey", ret=ERR) err = self.stderr() assert "fooey" in err assert "help" in err - def test_doc_url(self): + def test_doc_url(self) -> None: assert __url__.startswith("https://coverage.readthedocs.io") @@ -1048,7 +1067,7 @@ class CmdMainTest(CoverageTest): class CoverageScriptStub: """A stub for coverage.cmdline.CoverageScript, used by CmdMainTest.""" - def command_line(self, argv): + def command_line(self, argv: List[str]) -> int: """Stub for command_line, the arg determines what it will do.""" if argv[0] == 'hello': print("Hello, world!") @@ -1065,33 +1084,33 @@ class CmdMainTest(CoverageTest): raise AssertionError(f"Bad CoverageScriptStub: {argv!r}") return 0 - def setUp(self): + def setUp(self) -> None: super().setUp() old_CoverageScript = coverage.cmdline.CoverageScript - coverage.cmdline.CoverageScript = self.CoverageScriptStub + coverage.cmdline.CoverageScript = self.CoverageScriptStub # type: ignore self.addCleanup(setattr, coverage.cmdline, 'CoverageScript', old_CoverageScript) - def test_normal(self): + def test_normal(self) -> None: ret = coverage.cmdline.main(['hello']) assert ret == 0 assert self.stdout() == "Hello, world!\n" - def test_raise(self): + def test_raise(self) -> None: ret = coverage.cmdline.main(['raise']) assert ret == 1 out, err = self.stdouterr() assert out == "" print(err) - err = err.splitlines(keepends=True) - assert err[0] == 'Traceback (most recent call last):\n' - assert ' raise Exception("oh noes!")\n' in err - assert err[-1] == 'Exception: oh noes!\n' + err_parts = err.splitlines(keepends=True) + assert err_parts[0] == 'Traceback (most recent call last):\n' + assert ' raise Exception("oh noes!")\n' in err_parts + assert err_parts[-1] == 'Exception: oh noes!\n' - def test_internalraise(self): + def test_internalraise(self) -> None: with pytest.raises(ValueError, match="coverage is broken"): coverage.cmdline.main(['internalraise']) - def test_exit(self): + def test_exit(self) -> None: ret = coverage.cmdline.main(['exit']) assert ret == 23 @@ -1099,7 +1118,14 @@ class CmdMainTest(CoverageTest): class CoverageReportingFake: """A fake Coverage.coverage test double for FailUnderTest methods.""" # pylint: disable=missing-function-docstring - def __init__(self, report_result, html_result=0, xml_result=0, json_report=0, lcov_result=0): + def __init__( + self, + report_result: float, + html_result: float=0, + xml_result: float=0, + json_report: float=0, + lcov_result: float=0, + ) -> None: self.config = CoverageConfig() self.report_result = report_result self.html_result = html_result @@ -1107,28 +1133,28 @@ class CoverageReportingFake: self.json_result = json_report self.lcov_result = lcov_result - def set_option(self, optname, optvalue): + def set_option(self, optname: str, optvalue: TConfigValue) -> None: self.config.set_option(optname, optvalue) - def get_option(self, optname): + def get_option(self, optname: str) -> TConfigValue: return self.config.get_option(optname) - def load(self): + def load(self) -> None: pass - def report(self, *args_unused, **kwargs_unused): + def report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.report_result - def html_report(self, *args_unused, **kwargs_unused): + def html_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.html_result - def xml_report(self, *args_unused, **kwargs_unused): + def xml_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.xml_result - def json_report(self, *args_unused, **kwargs_unused): + def json_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.json_result - def lcov_report(self, *args_unused, **kwargs_unused): + def lcov_report(self, *args_unused: Any, **kwargs_unused: Any) -> float: return self.lcov_result @@ -1161,7 +1187,13 @@ class FailUnderTest(CoverageTest): # Command-line overrides configuration. ((20, 30, 40, 50, 60), 19, "report --fail-under=21", 2), ]) - def test_fail_under(self, results, fail_under, cmd, ret): + def test_fail_under( + self, + results: Tuple[float, float, float, float, float], + fail_under: Optional[float], + cmd: str, + ret: int, + ) -> None: cov = CoverageReportingFake(*results) if fail_under is not None: cov.set_option("report:fail_under", fail_under) @@ -1175,7 +1207,7 @@ class FailUnderTest(CoverageTest): (20.12345, "report --fail-under=20.1235 --precision=5", 2, "Coverage failure: total of 20.12345 is less than fail-under=20.12350\n"), ]) - def test_fail_under_with_precision(self, result, cmd, ret, msg): + def test_fail_under_with_precision(self, result: float, cmd: str, ret: int, msg: str) -> None: cov = CoverageReportingFake(report_result=result) with mock.patch("coverage.cmdline.Coverage", lambda *a,**kw: cov): self.command_line(cmd, ret) diff --git a/tox.ini b/tox.ini index 10bdf6d3..882b38f9 100644 --- a/tox.ini +++ b/tox.ini @@ -96,20 +96,17 @@ deps = setenv = {[testenv]setenv} C__B=coverage/__init__.py coverage/__main__.py coverage/annotate.py coverage/bytecode.py - C_CE=coverage/config.py coverage/context.py coverage/control.py coverage/data.py coverage/disposition.py coverage/env.py coverage/exceptions.py + C_CC=coverage/cmdline.py coverage/config.py coverage/context.py coverage/control.py + C_DE=coverage/data.py coverage/disposition.py coverage/env.py coverage/exceptions.py C_FN=coverage/files.py coverage/inorout.py coverage/jsonreport.py coverage/lcovreport.py coverage/multiproc.py coverage/numbits.py C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py C_QZ=coverage/report.py coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py coverage/version.py - T_AN=tests/test_api.py tests/goldtest.py tests/helpers.py tests/test_html.py - TYPEABLE={env:C__B} {env:C_CE} {env:C_FN} {env:C_OP} {env:C_QZ} {env:T_AN} + T_AN=tests/test_api.py tests/test_cmdline.py tests/goldtest.py tests/helpers.py tests/test_html.py + TYPEABLE={env:C__B} {env:C_CC} {env:C_DE} {env:C_FN} {env:C_OP} {env:C_QZ} {env:T_AN} commands = # PYVERSIONS mypy --python-version=3.7 {env:TYPEABLE} - mypy --python-version=3.8 {env:TYPEABLE} - mypy --python-version=3.9 {env:TYPEABLE} - mypy --python-version=3.10 {env:TYPEABLE} - mypy --python-version=3.11 {env:TYPEABLE} mypy --python-version=3.12 {env:TYPEABLE} [gh-actions] -- cgit v1.2.1