Skip to content

Commit

Permalink
mypy: add cmdline.py and test_cmdline.py
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Jan 1, 2023
1 parent 09f9188 commit a3f3841
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 115 deletions.
84 changes: 53 additions & 31 deletions coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import textwrap
import traceback

from typing import cast, Any, List, NoReturn, Optional, Tuple

import coverage
from coverage import Coverage
from coverage import env
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -278,19 +281,19 @@ class OptionParserError(Exception):
"""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
Expand All @@ -299,7 +302,7 @@ def error(self, msg):
class GlobalOptionParser(CoverageOptionParser):
"""Command-line parser for coverage.py global option arguments."""

def __init__(self):
def __init__(self) -> None:
super().__init__()

self.add_options([
Expand All @@ -311,14 +314,19 @@ def __init__(self):
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:
Expand All @@ -327,18 +335,18 @@ def __init__(self, action, options, defaults=None, usage=None, description=None)
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"<CmdOptionParser:{self.cmd}>")

__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()

Expand Down Expand Up @@ -540,7 +548,11 @@ def get_prog_name(self):
}


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

Expand Down Expand Up @@ -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))
Expand All @@ -587,11 +600,11 @@ def show_help(error=None, topic=None, parser=None):
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.
Expand All @@ -606,6 +619,7 @@ def command_line(self, argv):

# 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()
Expand All @@ -619,6 +633,7 @@ def command_line(self, argv):
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):
Expand Down Expand Up @@ -740,8 +755,8 @@ def command_line(self, argv):
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),
Expand All @@ -753,7 +768,12 @@ def command_line(self, argv):

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.
Expand All @@ -770,9 +790,9 @@ def do_help(self, options, args, parser):
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:
Expand All @@ -786,15 +806,15 @@ def do_help(self, options, args, parser):

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:
if options.module:
# 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"}:
Expand Down Expand Up @@ -845,7 +865,7 @@ def do_run(self, options, args):

return OK

def do_debug(self, args):
def do_debug(self, args: List[str]) -> int:
"""Implementation of 'coverage debug'."""

if not args:
Expand Down Expand Up @@ -878,7 +898,7 @@ def do_debug(self, args):
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
Expand All @@ -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 = []
Expand Down Expand Up @@ -938,7 +958,7 @@ def unglob_args(args):
}


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.
Expand Down Expand Up @@ -976,7 +996,9 @@ def main(argv=None):
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:
Expand Down
4 changes: 2 additions & 2 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def current(cls) -> Optional[Coverage]:

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,
Expand Down Expand Up @@ -219,7 +219,7 @@ def __init__( # pylint: disable=too-many-arguments
# 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.
Expand Down
2 changes: 1 addition & 1 deletion coverage/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Protocol: # pylint: disable=missing-class-docstring
## 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]

Expand Down
4 changes: 2 additions & 2 deletions tests/coveragetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def assert_recent_datetime(self, dt, seconds=10, msg=None):
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
Expand Down Expand Up @@ -467,7 +467,7 @@ def setUp(self):
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.
Expand Down
Loading

0 comments on commit a3f3841

Please sign in to comment.