Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten mypy: globus_cli.parsing #922

Merged
merged 12 commits into from
Dec 20, 2023
27 changes: 0 additions & 27 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,6 @@ warn_no_return = true
no_implicit_optional = true
disallow_untyped_defs = true

[mypy-globus_cli.parsing.command_state]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.commands]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.one_use_option]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.param_types.endpoint_plus_path]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.shared_options]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.shared_options.endpoint_create_and_update]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.shared_options.id_args]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.shared_options.transfer_task_options]
disallow_untyped_defs = false

[mypy-globus_cli.parsing.shell_completion]
disallow_untyped_defs = false

[mypy-globus_cli.services.gcs]
disallow_untyped_defs = false

Expand Down
2 changes: 1 addition & 1 deletion src/globus_cli/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
""",
)
@task_submission_options
@delete_and_rm_options
@delete_and_rm_options()
@local_user_option
@click.argument("endpoint_plus_path", type=ENDPOINT_PLUS_OPTPATH)
@LoginManager.requires_login("transfer")
Expand Down
6 changes: 3 additions & 3 deletions src/globus_cli/commands/timer/create/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ def resolve_optional_local_time(
"destination", metavar="DEST_ENDPOINT_ID[:DEST_PATH]", type=ENDPOINT_PLUS_OPTPATH
)
@transfer_batch_option
@sync_level_option
@sync_level_option()
@transfer_recursive_option
@encrypt_data_option
@encrypt_data_option()
@verify_checksum_option
@preserve_timestamp_option
@preserve_timestamp_option()
@skip_source_errors_option
@fail_on_quota_errors_option
@task_notify_option
Expand Down
24 changes: 13 additions & 11 deletions src/globus_cli/parsing/command_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
F = t.TypeVar("F", bound=t.Union[t.Callable, click.Command])


def _setup_logging(level="DEBUG"):
def _setup_logging(level: str = "DEBUG") -> None:
conf = {
"version": 1,
"formatters": {
Expand Down Expand Up @@ -68,7 +68,7 @@ def is_verbose(self) -> bool:


def format_option(f: F) -> F:
def callback(ctx, param, value):
def callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
if not value:
return

Expand All @@ -80,7 +80,9 @@ def callback(ctx, param, value):

state.output_format = value.lower()

def jmespath_callback(ctx, param, value):
def jmespath_callback(
ctx: click.Context, param: click.Parameter, value: t.Any
) -> None:
if value is None:
return

Expand Down Expand Up @@ -116,7 +118,7 @@ def jmespath_callback(ctx, param, value):


def debug_option(f: F) -> F:
def callback(ctx, param, value):
def callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
if not value or ctx.resilient_parsing:
# turn off warnings altogether
_warnings.simplefilter("ignore")
Expand All @@ -138,7 +140,7 @@ def callback(ctx, param, value):


def verbose_option(f: F) -> F:
def callback(ctx, param, value):
def callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
# set state verbosity value from option
state = ctx.ensure_object(CommandState)
state.verbosity = value
Expand Down Expand Up @@ -186,7 +188,7 @@ def callback(ctx, param, value):
def map_http_status_option(f: F) -> F:
exit_stat_set = [0, 1] + list(range(50, 100))

def per_val_callback(ctx, value):
def per_val_callback(ctx: click.Context, value: str | None) -> None:
if value is None:
return None
state = ctx.ensure_object(CommandState)
Expand All @@ -197,12 +199,12 @@ def per_val_callback(ctx, value):
# iterate over those pairs, splitting them on `=` signs
for http_stat, exit_stat in (pair.split("=") for pair in pairs):
# "parse" as ints
http_stat, exit_stat = int(http_stat), int(exit_stat)
http_stat_int, exit_stat_int = int(http_stat), int(exit_stat)
# force into the desired range
if exit_stat not in exit_stat_set:
if exit_stat_int not in exit_stat_set:
raise ValueError()
# map the status
state.http_status_map[http_stat] = exit_stat
state.http_status_map[http_stat_int] = exit_stat_int
# two conditions can cause ValueError: split didn't give right number
# of args, or results weren't int()-able
except ValueError:
Expand All @@ -212,7 +214,7 @@ def per_val_callback(ctx, value):
"0,1,50-99"
)

def callback(ctx, param, value):
def callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
"""
Wrap the per-value callback -- multiple=True means that the value is
always a tuple of given vals.
Expand All @@ -233,7 +235,7 @@ def callback(ctx, param, value):


def show_server_timing_option(f: F) -> F:
def callback(ctx, param, value):
def callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
if not value:
return
state = ctx.ensure_object(CommandState)
Expand Down
66 changes: 29 additions & 37 deletions src/globus_cli/parsing/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import importlib
import logging
import sys
import typing as t
from shutil import get_terminal_size

import click
Expand All @@ -20,6 +21,8 @@
from .shared_options import common_options
from .shell_completion import print_completer_option

C = t.TypeVar("C", bound=t.Union[click.Command, t.Callable[..., t.Any]])

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -60,7 +63,7 @@ def example_command(*, foo_bar: list[tuple[Literal["foo", "bar"], Any]]):
you will need to manually activate the endpoint. See 'globus endpoint activate'
for more details."""

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
self.adoc_output = kwargs.pop("adoc_output", None)
self.adoc_examples = kwargs.pop("adoc_examples", None)
self.globus_disable_opts = kwargs.pop("globus_disable_opts", [])
Expand All @@ -84,9 +87,12 @@ def __init__(self, *args, **kwargs):
pass
super().__init__(*args, **kwargs)

def invoke(self, ctx):
def invoke(self, ctx: click.Context) -> t.Any:
log.debug("command invoke start")
try:
# this isn't forcing interactive mode, it's just checking to see that
# the GLOBUS_CLI_INTERACTIVE value is *valid*
env_interactive(raising=True)
return super().invoke(ctx)
finally:
log.debug("command invoke exit")
Expand Down Expand Up @@ -125,12 +131,6 @@ def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
raise


class GlobusCommandEnvChecks(GlobusCommand):
def invoke(self, ctx):
env_interactive(raising=True)
return super().invoke(ctx)


class GlobusCommandGroup(click.Group):
"""
This is a click.Group with any customizations which we deem necessary
Expand All @@ -144,10 +144,10 @@ class GlobusCommandGroup(click.Group):

def __init__(
self,
*args,
*args: t.Any,
lazy_subcommands: dict[str, tuple[str, str]] | None = None,
**kwargs,
):
**kwargs: t.Any,
) -> None:
super().__init__(*args, **kwargs)
# lazy_subcommands is a map of the form:
#
Expand Down Expand Up @@ -186,7 +186,7 @@ def _lazy_load(self, ctx: click.Context, cmd_name: str) -> click.Command:
)
return cmd_object

def invoke(self, ctx):
def invoke(self, ctx: click.Context) -> t.Any:
# if no subcommand was given (but, potentially, flags were passed),
# ctx.protected_args will be empty
# improves upon the built-in detection given on click.Group by
Expand All @@ -208,53 +208,45 @@ class TopLevelGroup(GlobusCommandGroup):
passes them to a custom error handler.
"""

def invoke(self, ctx):
def invoke(self, ctx: click.Context) -> t.Any:
try:
return super().invoke(ctx)
except Exception:
custom_except_hook(sys.exc_info())
# mypy thinks that exc_info could be (None, None, None), but... nope. False.
custom_except_hook(sys.exc_info()) # type: ignore[arg-type]


def main_group(**kwargs):
def decorator(f):
f = click.group("globus", cls=TopLevelGroup, **kwargs)(f)
f = common_options(f)
f = print_completer_option(f)
return f
def main_group(**kwargs: t.Any) -> t.Callable[[C], TopLevelGroup]:
def decorator(f: C) -> TopLevelGroup:
grp = click.group("globus", cls=TopLevelGroup, **kwargs)(f)
grp = common_options()(grp)
grp = print_completer_option(grp)
return grp

return decorator


def command(*args, **kwargs):
def command(*args: t.Any, **kwargs: t.Any) -> t.Callable[[C], GlobusCommand]:
"""
A helper for decorating commands a-la `click.command`, but pulling the help string
from `<function>.__doc__` by default.

Also allows the use of custom arguments, which are stored on the command, as in
"adoc_examples".

`skip_env_checks` is used to disable environment variable validation prior to
running a command, but is ignored when a specific `cls` argument is passed.
"""
disable_opts = kwargs.pop("disable_options", [])

def _inner_decorator(func):
if "cls" not in kwargs:
if kwargs.get("skip_env_checks", False) is True:
kwargs["cls"] = GlobusCommand
else:
kwargs["cls"] = GlobusCommandEnvChecks

def _inner_decorator(func: C) -> GlobusCommand:
kwargs["globus_disable_opts"] = disable_opts

return common_options(disable_options=disable_opts)(
click.command(*args, **kwargs)(func)
click.command(*args, cls=GlobusCommand, **kwargs)(func)
)

return _inner_decorator


def group(*args, **kwargs):
def group(*args: t.Any, **kwargs: t.Any) -> t.Callable[[C], GlobusCommandGroup]:
"""
Wrapper over click.group which sets GlobusCommandGroup as the Class

Expand All @@ -266,9 +258,9 @@ def group(*args, **kwargs):
"""
disable_opts = kwargs.pop("disable_options", [])

def inner_decorator(f):
f = click.group(*args, cls=GlobusCommandGroup, **kwargs)(f)
f = common_options(disable_options=disable_opts)(f)
return f
def inner_decorator(f: C) -> GlobusCommandGroup:
grp = click.group(*args, cls=GlobusCommandGroup, **kwargs)(f)
grp = common_options(disable_options=disable_opts)(grp)
return grp

return inner_decorator
16 changes: 12 additions & 4 deletions src/globus_cli/parsing/param_types/endpoint_plus_path.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations

import typing as t
import uuid

import click
Expand All @@ -13,7 +16,7 @@ class EndpointPlusPath(click.ParamType):

name = "endpoint plus path"

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
# path requirement defaults to True, but can be tweaked by a kwarg
self.path_required = kwargs.pop("path_required", True)

Expand All @@ -25,14 +28,14 @@ def get_type_annotation(self, param: click.Parameter) -> type:
else:
return tuple[uuid.UUID, str | None]

def get_metavar(self, param):
def get_metavar(self, param: click.Parameter | None) -> str:
"""
Default metavar for this instance of the type.
"""
return self.metavar

@property
def metavar(self):
def metavar(self) -> str:
"""
Metavar as a property, so that we can make it different if `path_required`
"""
Expand All @@ -41,7 +44,12 @@ def metavar(self):
else:
return "ENDPOINT_ID[:PATH]"

def convert(self, value, param, ctx):
def convert(
self,
value: tuple[uuid.UUID, str | None] | str,
param: click.Parameter | None,
ctx: click.Context | None,
) -> tuple[uuid.UUID, str | None]:
"""
ParamType.convert() is the actual processing method that takes a
provided parameter and parses it.
Expand Down
Loading