diff --git a/pyproject.toml b/pyproject.toml index 25fdd0a88..bc2e99fcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,9 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "globus-sdk==4.6.0", - "click>=8.1.4,<8.4", + # NOTE: `click` does not use semver; minor releases are expected to contain + # minor breaking changes + "click>=8.4,<8.5", "jmespath==1.1.0", "packaging>=17.0", "typing_extensions>=4.0;python_version<'3.11'", @@ -61,7 +63,7 @@ test = [ ] test-mindeps = [ {include-group = "test"}, - "click==8.1.8", + "click==8.4.0", "requests==2.32.4", "pyjwt==2.0.0", "cryptography==3.3.1", diff --git a/src/globus_cli/_click_compat.py b/src/globus_cli/_click_compat.py deleted file mode 100644 index 562adfefb..000000000 --- a/src/globus_cli/_click_compat.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -A compatibility module for handling click v8.2.0+ and 8.1.x API differences. -""" - -import functools -import importlib.metadata -import typing as t - -import click - -C = t.TypeVar("C", bound=t.Callable[..., t.Any]) - -CLICK_VERSION = importlib.metadata.version("click") - -OLDER_CLICK_API = CLICK_VERSION.startswith("8.1.") -NEWER_CLICK_API = not OLDER_CLICK_API - - -def shim_get_metavar(f: C) -> C: - """ - Make a ParamType.get_metavar function compatible with both the 8.1.x and - the 8.2.0+ APIs. - - Under 8.2.0, `ctx: click.Context` is passed, while older versions do not. - Therefore, do nothing on 8.2.0+ and pass `ctx=None' if the older click - version is in use. - - NOTE: we pass `ctx=None` which violates the declared types (but works at - runtime) because when running under older click versions, there may not be - a current click context. - """ - if OLDER_CLICK_API: - - @functools.wraps(f) - def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: - return f(*args, **kwargs, ctx=None) - - return wrapper # type: ignore[return-value] - - return f - - -def shim_get_missing_message(f: C) -> C: - """ - Shim `get_missing_message` in a similar way to `get_metavar` above. - """ - if OLDER_CLICK_API: - - @functools.wraps(f) - def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: - return f(*args, **kwargs, ctx=click.get_current_context()) - - return wrapper # type: ignore[return-value] - - return f diff --git a/src/globus_cli/commands/api.py b/src/globus_cli/commands/api.py index 23157bd9a..3231f665a 100644 --- a/src/globus_cli/commands/api.py +++ b/src/globus_cli/commands/api.py @@ -10,7 +10,6 @@ from globus_sdk.scopes import ScopeParser from globus_cli import termio, version -from globus_cli._click_compat import shim_get_metavar from globus_cli.login_manager import LoginManager, is_client_login from globus_cli.login_manager.scopes import CLI_SCOPE_REQUIREMENTS from globus_cli.parsing import command, endpoint_id_arg, group, mutex_option_group @@ -20,8 +19,7 @@ C = t.TypeVar("C", bound=AnyCommand) -class QueryParamType(click.ParamType): - @shim_get_metavar +class QueryParamType(click.ParamType[tuple[str, str] | None]): def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key=Value" @@ -33,21 +31,17 @@ def get_type_annotation(self, param: click.Parameter) -> type: def convert( self, - value: str | None, + value: str, param: click.Parameter | None, ctx: click.Context | None, ) -> tuple[str, str] | None: - value = super().convert(value, param, ctx) - if value is None: - return None if "=" not in value: self.fail("invalid query param", param=param, ctx=ctx) left, right = value.split("=", 1) return (left, right) -class HeaderParamType(click.ParamType): - @shim_get_metavar +class HeaderParamType(click.ParamType[tuple[str, str] | None]): def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key:Value" @@ -59,13 +53,10 @@ def get_type_annotation(self, param: click.Parameter) -> type: def convert( self, - value: str | None, + value: str, param: click.Parameter | None, ctx: click.Context | None, ) -> tuple[str, str] | None: - value = super().convert(value, param, ctx) - if value is None: - return None if ":" not in value: self.fail("invalid header param", param=param, ctx=ctx) left, right = value.split(":", 1) diff --git a/src/globus_cli/commands/collection/list.py b/src/globus_cli/commands/collection/list.py index eed9d57f3..8425acf90 100644 --- a/src/globus_cli/commands/collection/list.py +++ b/src/globus_cli/commands/collection/list.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys import typing as t import uuid @@ -12,22 +11,8 @@ from globus_cli.termio import Field, display, formatters from globus_cli.utils import PagingWrapper -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias -# until our minimum click version is 8.2.0+ , we need to handle the fact that -# click.Choice became a generic in 8.2.0 -# we cannot leave it without a defined type parameter, as we have -# "no-any-generics" set for mypy -if t.TYPE_CHECKING: - ChoiceType: TypeAlias = click.Choice[str] -else: - ChoiceType = click.Choice - - -class ChoiceSlugified(ChoiceType): +class ChoiceSlugified(click.Choice[str]): """ Allow either hyphens or underscores, e.g. both 'mapped-collections' or 'mapped_collections' @@ -37,10 +22,8 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.cast(type, t.Literal[tuple(self._slugify(c) for c in self.choices)]) def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + self, value: str, param: click.Parameter | None, ctx: click.Context | None ) -> t.Any: - if value is None: - return None return self._slugify(super().convert(value.replace("_", "-"), param, ctx)) def _slugify(self, value: str) -> str: diff --git a/src/globus_cli/commands/endpoint/set_subscription_id.py b/src/globus_cli/commands/endpoint/set_subscription_id.py index 973136826..7d7392ce6 100644 --- a/src/globus_cli/commands/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/endpoint/set_subscription_id.py @@ -9,7 +9,7 @@ from globus_cli.termio import display -class SubscriptionIdType(click.ParamType): +class SubscriptionIdType(click.ParamType[str]): def convert( self, value: str, param: click.Parameter | None, ctx: click.Context | None ) -> str: diff --git a/src/globus_cli/commands/flows/_common.py b/src/globus_cli/commands/flows/_common.py index 846951246..fec38c7e8 100644 --- a/src/globus_cli/commands/flows/_common.py +++ b/src/globus_cli/commands/flows/_common.py @@ -125,7 +125,9 @@ ) -class SubscriptionIdType(click.ParamType): +class SubscriptionIdType( + click.ParamType[uuid.UUID | t.Literal["DEFAULT"] | globus_sdk.MissingType] +): name = "SUBSCRIPTION_ID" def __init__(self, *, omittable: bool = False) -> None: diff --git a/src/globus_cli/commands/flows/start.py b/src/globus_cli/commands/flows/start.py index 50ca7fa7b..da74f5ad0 100644 --- a/src/globus_cli/commands/flows/start.py +++ b/src/globus_cli/commands/flows/start.py @@ -8,7 +8,6 @@ import click import globus_sdk -from globus_cli._click_compat import shim_get_metavar from globus_cli.commands.flows._common import FlowScopeInjector from globus_cli.commands.flows._fields import flow_run_format_fields from globus_cli.login_manager import LoginManager @@ -43,7 +42,6 @@ class ActivityNotificationPolicyType(JSONStringOrFile): choices = ("INACTIVE", "FAILED", "SUCCEEDED") - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return f"[{{{','.join(self.choices)}}}|JSON_FILE|JSON]" diff --git a/src/globus_cli/commands/gcp/set_subscription_id.py b/src/globus_cli/commands/gcp/set_subscription_id.py index 2740e9c1a..a15698072 100644 --- a/src/globus_cli/commands/gcp/set_subscription_id.py +++ b/src/globus_cli/commands/gcp/set_subscription_id.py @@ -11,7 +11,7 @@ from globus_cli.termio import display -class GCPSubscriptionIdType(click.ParamType): +class GCPSubscriptionIdType(click.ParamType[uuid.UUID | ExplicitNullType]): def convert( self, value: str, param: click.Parameter | None, ctx: click.Context | None ) -> uuid.UUID | ExplicitNullType: diff --git a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py index d2252e6ce..6a0869b4a 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -11,7 +11,9 @@ from globus_cli.termio import display -class GCSSubscriptionIdType(click.ParamType): +class GCSSubscriptionIdType( + click.ParamType[uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType] +): def convert( self, value: str, param: click.Parameter | None, ctx: click.Context | None ) -> uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType: diff --git a/src/globus_cli/commands/gcs/endpoint/update.py b/src/globus_cli/commands/gcs/endpoint/update.py index 0365ac329..c7bd1a9db 100644 --- a/src/globus_cli/commands/gcs/endpoint/update.py +++ b/src/globus_cli/commands/gcs/endpoint/update.py @@ -7,7 +7,6 @@ import click import globus_sdk -from globus_cli._click_compat import shim_get_metavar from globus_cli.commands.gcs.endpoint._common import GCS_ENDPOINT_FIELDS from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType from globus_cli.login_manager import LoginManager @@ -19,8 +18,7 @@ F = t.TypeVar("F", bound=AnyCallable) -class SubscriptionIdType(click.ParamType): - @shim_get_metavar +class SubscriptionIdType(click.ParamType[str | ExplicitNullType]): def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "[|DEFAULT|null]" diff --git a/src/globus_cli/commands/group/_common.py b/src/globus_cli/commands/group/_common.py index 2d5245fe9..6a657eb09 100644 --- a/src/globus_cli/commands/group/_common.py +++ b/src/globus_cli/commands/group/_common.py @@ -78,7 +78,7 @@ def group_id_arg(f: C) -> C: return click.argument("GROUP_ID", type=click.UUID)(f) -class GroupSubscriptionVerifiedIdType(click.ParamType): +class GroupSubscriptionVerifiedIdType(click.ParamType[uuid.UUID | ExplicitNullType]): name = "TEXT" def convert( diff --git a/src/globus_cli/commands/login.py b/src/globus_cli/commands/login.py index c9f9d0e17..1f2ff081a 100644 --- a/src/globus_cli/commands/login.py +++ b/src/globus_cli/commands/login.py @@ -13,7 +13,6 @@ ) from globus_sdk.services.flows import SpecificFlowClient -from globus_cli._click_compat import shim_get_metavar from globus_cli.login_manager import LoginManager, is_client_login from globus_cli.parsing import command, no_local_server_option from globus_cli.termio import verbosity @@ -50,10 +49,9 @@ """ -class GCSEndpointType(click.ParamType): +class GCSEndpointType(click.ParamType[uuid.UUID | tuple[uuid.UUID, uuid.UUID]]): name = "GCS Server" - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "[:]" @@ -88,10 +86,9 @@ def convert( return endpoint_id if not collection_id else (endpoint_id, collection_id) -class TimerResourceType(click.ParamType): +class TimerResourceType(click.ParamType[tuple[t.Literal["flow"], uuid.UUID]]): name = "TIMER_RESOURCE" - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "flow:" diff --git a/src/globus_cli/parsing/commands.py b/src/globus_cli/parsing/commands.py index 510e56fac..a78dadc2c 100644 --- a/src/globus_cli/parsing/commands.py +++ b/src/globus_cli/parsing/commands.py @@ -16,7 +16,6 @@ import click -from globus_cli._click_compat import OLDER_CLICK_API from globus_cli.exception_handling import custom_except_hook from globus_cli.termio import env_interactive @@ -176,23 +175,6 @@ def _lazy_load(self, ctx: click.Context, cmd_name: str) -> click.Command: ) return cmd_object - # only redefine `invoke()` if we're on click<8.2.0 - # on newer versions, the behavior is improved upstream - if OLDER_CLICK_API: - - 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 - # no_args_is_help , since that treats options (without a subcommand) as - # being arguments and blows up with a "Missing command" failure - # for reference to the original version (as of 2017-02-26): - # https://github.com/pallets/click/blob/02ea9ee7e864581258b4902d6e6c1264b0226b9f/click/core.py#L1039-L1052 - if self.no_args_is_help and not ctx.protected_args: - click.echo(ctx.get_help()) - ctx.exit() - return super().invoke(ctx) - class TopLevelGroup(GlobusCommandGroup): """ diff --git a/src/globus_cli/parsing/param_types/delimited.py b/src/globus_cli/parsing/param_types/delimited.py index bf66799c1..8a0098c3e 100644 --- a/src/globus_cli/parsing/param_types/delimited.py +++ b/src/globus_cli/parsing/param_types/delimited.py @@ -5,17 +5,11 @@ import click import globus_sdk -from globus_cli._click_compat import ( - OLDER_CLICK_API, - shim_get_metavar, - shim_get_missing_message, -) - if t.TYPE_CHECKING: from click.shell_completion import CompletionItem -class CommaDelimitedList(click.ParamType): +class CommaDelimitedList(click.ParamType[list[str] | globus_sdk.MissingType]): def __init__( self, *, @@ -28,7 +22,6 @@ def __init__( self.convert_values = convert_values self.choices = list(choices) if choices is not None else None - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: if self.choices is not None: return "{" + ",".join(self.choices) + "}" @@ -75,7 +68,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return list[str] -class ColonDelimitedChoiceTuple(click.ParamType): +class ColonDelimitedChoiceTuple(click.ParamType[tuple[str, ...]]): """ A colon-delimited choice type which wraps the existing click.Choice type. @@ -102,25 +95,15 @@ def __init__( self.unpacked_choices = self._unpack_choices() - def to_info_dict(self) -> dict[str, t.Any]: + def to_info_dict(self) -> click.types.ParamTypeInfoDict: return self.inner_choice_param.to_info_dict() - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str | None: - if OLDER_CLICK_API: - # type checking on newer click versions will flag this, but incorrectly so - return self.inner_choice_param.get_metavar(param) # type: ignore[call-arg] return self.inner_choice_param.get_metavar(param, ctx) - @shim_get_missing_message def get_missing_message( self, param: click.Parameter, ctx: click.Context | None ) -> str: - if OLDER_CLICK_API: - # type checking on newer click versions will flag this, but incorrectly so - return self.inner_choice_param.get_missing_message( - param=param # type: ignore[call-arg] - ) return self.inner_choice_param.get_missing_message(param=param, ctx=ctx) def shell_complete( diff --git a/src/globus_cli/parsing/param_types/endpoint_plus_path.py b/src/globus_cli/parsing/param_types/endpoint_plus_path.py index a9b05a832..902a2dcb1 100644 --- a/src/globus_cli/parsing/param_types/endpoint_plus_path.py +++ b/src/globus_cli/parsing/param_types/endpoint_plus_path.py @@ -5,10 +5,8 @@ import click -from globus_cli._click_compat import shim_get_metavar - -class EndpointPlusPath(click.ParamType): +class EndpointPlusPath(click.ParamType[tuple[uuid.UUID, str | None]]): """ Custom type for ":" Supports path being required and path being optional. @@ -33,7 +31,6 @@ def get_type_annotation(self, param: click.Parameter) -> type: else: return t.Tuple[uuid.UUID, t.Union[str, None]] # type: ignore[return-value] - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: """ Default metavar for this instance of the type. @@ -67,7 +64,7 @@ def convert( # split the value on the first colon, leave the rest intact splitval = value.split(":", 1) # first element is the endpoint_id - endpoint_id = click.UUID(splitval[0]) + endpoint_id: uuid.UUID = click.UUID.convert(splitval[0], param, ctx) # get the second element, defaulting to `None` if there was no colon in # the original value diff --git a/src/globus_cli/parsing/param_types/guest_activity_notify_param.py b/src/globus_cli/parsing/param_types/guest_activity_notify_param.py index 8f8217e47..a88b30ae0 100644 --- a/src/globus_cli/parsing/param_types/guest_activity_notify_param.py +++ b/src/globus_cli/parsing/param_types/guest_activity_notify_param.py @@ -3,11 +3,12 @@ import click from click.shell_completion import CompletionItem -from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import ExplicitNullType -class GCSManagerGuestActivityNotificationParamType(click.ParamType): +class GCSManagerGuestActivityNotificationParamType( + click.ParamType[dict[str, list[str]] | ExplicitNullType] +): """ For the GCS Manager API: @@ -39,7 +40,6 @@ class GCSManagerGuestActivityNotificationParamType(click.ParamType): VALID_NOTIFICATION_VALUES = VALID_TRANSFER_USES | VALID_STATUSES - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "{all,succeeded,failed,source,destination}" diff --git a/src/globus_cli/parsing/param_types/identity_type.py b/src/globus_cli/parsing/param_types/identity_type.py index 922e14426..6df28e883 100644 --- a/src/globus_cli/parsing/param_types/identity_type.py +++ b/src/globus_cli/parsing/param_types/identity_type.py @@ -6,8 +6,6 @@ import click -from globus_cli._click_compat import shim_get_metavar - ParsedIdentity = namedtuple("ParsedIdentity", ["value", "idtype"]) @@ -35,7 +33,7 @@ def _b32decode(v: str) -> str: raise _B32DecodeError("decode and load as UUID failed") -class IdentityType(click.ParamType): +class IdentityType(click.ParamType[ParsedIdentity]): """ Parameter type for handling identities. By default, just allows usernames or identity IDs. With options, it can be set to allow domain names as an "identity" @@ -86,7 +84,6 @@ def convert( self.fail(f"'{value}' does not appear to be a valid identity", param=param) - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return self.metavar diff --git a/src/globus_cli/parsing/param_types/json_strorfile.py b/src/globus_cli/parsing/param_types/json_strorfile.py index 0352d3c04..395fca1c3 100644 --- a/src/globus_cli/parsing/param_types/json_strorfile.py +++ b/src/globus_cli/parsing/param_types/json_strorfile.py @@ -8,7 +8,6 @@ import click -from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType from globus_cli.types import JsonValue @@ -26,7 +25,7 @@ class ParsedJSONData: data: JsonValue -class JSONStringOrFile(click.ParamType): +class JSONStringOrFile(click.ParamType[ExplicitNullType | ParsedJSONData]): """ Parse an input which could be a filename or could be a JSON blob being supplied on the commandline. @@ -58,7 +57,6 @@ def __init__( self.null = null super().__init__(*args, **kwargs) - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "[JSON_FILE|JSON|file:JSON_FILE]" diff --git a/src/globus_cli/parsing/param_types/location.py b/src/globus_cli/parsing/param_types/location.py index e51348099..3fdec2ef9 100644 --- a/src/globus_cli/parsing/param_types/location.py +++ b/src/globus_cli/parsing/param_types/location.py @@ -6,7 +6,7 @@ import click -class LocationType(click.ParamType): +class LocationType(click.ParamType[str]): """ Validates that given location string is two comma separated floats """ diff --git a/src/globus_cli/parsing/param_types/notify_param.py b/src/globus_cli/parsing/param_types/notify_param.py index e9168c86b..1195fe514 100644 --- a/src/globus_cli/parsing/param_types/notify_param.py +++ b/src/globus_cli/parsing/param_types/notify_param.py @@ -3,8 +3,6 @@ import click from click.shell_completion import CompletionItem -from globus_cli._click_compat import shim_get_metavar - def _empty_dict_callback( ctx: click.Context, param: click.Parameter, value: dict[str, bool] | None @@ -14,10 +12,9 @@ def _empty_dict_callback( return value -class NotificationParamType(click.ParamType): +class NotificationParamType(click.ParamType[dict[str, bool]]): STANDARD_CALLBACK = _empty_dict_callback - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "{on,off,succeeded,failed,inactive}" diff --git a/src/globus_cli/parsing/param_types/nullable.py b/src/globus_cli/parsing/param_types/nullable.py index 63a7303dc..2b58c311f 100644 --- a/src/globus_cli/parsing/param_types/nullable.py +++ b/src/globus_cli/parsing/param_types/nullable.py @@ -4,17 +4,15 @@ import click -from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType -class StringOrNull(click.ParamType): +class StringOrNull(click.ParamType[str | ExplicitNullType]): """ Very similar to a basic string type, but one in which the empty string will be converted into an EXPLICIT_NULL """ - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "TEXT" @@ -33,7 +31,6 @@ class UrlOrNull(StringOrNull): http or https URL. """ - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "TEXT" @@ -53,13 +50,12 @@ def convert( return value -class IntOrNull(click.ParamType): +class IntOrNull(click.ParamType[int | ExplicitNullType]): """ Very similar to a basic int type, but one in which the empty string will be converted into an EXPLICIT_NULL """ - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "[INT|null]" diff --git a/src/globus_cli/parsing/param_types/omittable.py b/src/globus_cli/parsing/param_types/omittable.py index 557269984..c466dedc7 100644 --- a/src/globus_cli/parsing/param_types/omittable.py +++ b/src/globus_cli/parsing/param_types/omittable.py @@ -7,14 +7,8 @@ import click.types import globus_sdk -from globus_cli._click_compat import ( - OLDER_CLICK_API, - shim_get_metavar, - shim_get_missing_message, -) - -class OmittableInt(click.ParamType): +class OmittableInt(click.ParamType[int | globus_sdk.MissingType]): name = "integer" def convert( @@ -22,13 +16,13 @@ def convert( ) -> int | globus_sdk.MissingType: if value is globus_sdk.MISSING: return globus_sdk.MISSING - return click.INT.convert(value, param, ctx) # type: ignore[no-any-return] + return click.INT.convert(value, param, ctx) def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[int, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableString(click.ParamType): +class OmittableString(click.ParamType[str | globus_sdk.MissingType]): name = "text" def convert( @@ -36,13 +30,13 @@ def convert( ) -> str | globus_sdk.MissingType: if value is globus_sdk.MISSING: return globus_sdk.MISSING - return click.STRING.convert(value, param, ctx) # type: ignore[no-any-return] + return click.STRING.convert(value, param, ctx) def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[str, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableUUID(click.ParamType): +class OmittableUUID(click.ParamType[uuid.UUID | globus_sdk.MissingType]): name = "uuid" def convert( @@ -50,32 +44,24 @@ def convert( ) -> uuid.UUID | globus_sdk.MissingType: if value is globus_sdk.MISSING: return globus_sdk.MISSING - return click.UUID.convert(value, param, ctx) # type: ignore[no-any-return] + return click.UUID.convert(value, param, ctx) def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[uuid.UUID, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableChoice(click.ParamType): +class OmittableChoice(click.ParamType[str | globus_sdk.MissingType]): name = "choice" def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: self._inner_choice = click.Choice(choices, case_sensitive=case_sensitive) - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str | None: - if OLDER_CLICK_API: - return self._inner_choice.get_metavar(param) # type: ignore[call-arg] return self._inner_choice.get_metavar(param, ctx) - @shim_get_missing_message def get_missing_message( self, param: click.Parameter, ctx: click.Context | None ) -> str | None: - if OLDER_CLICK_API: - return self._inner_choice.get_missing_message( # type: ignore[call-arg] - param - ) return self._inner_choice.get_missing_message(param, ctx) def convert( @@ -92,7 +78,17 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[literal, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableDateTime(click.DateTime): +# The converted type of a ParamType is signified by its type parameter, but we want to +# inherit the runtime behavior of DateTime, which defines this as `datetime.datetime`. +# To make type checking accurate but inherit behavior at runtime, define the base class +# differently at runtime vs type-checking-time. +if t.TYPE_CHECKING: + _OmittableDateTimeBase = click.ParamType[datetime.datetime | globus_sdk.MissingType] +else: + _OmittableDateTimeBase = click.DateTime + + +class OmittableDateTime(_OmittableDateTimeBase): name = "datetime" def convert( @@ -100,7 +96,7 @@ def convert( ) -> datetime.datetime | globus_sdk.MissingType: if value is globus_sdk.MISSING: return globus_sdk.MISSING - return super().convert(value, param, ctx) # type: ignore[no-any-return] + return super().convert(value, param, ctx) def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[ # type: ignore[return-value] diff --git a/src/globus_cli/parsing/param_types/task_path.py b/src/globus_cli/parsing/param_types/task_path.py index 268397404..a771956c1 100644 --- a/src/globus_cli/parsing/param_types/task_path.py +++ b/src/globus_cli/parsing/param_types/task_path.py @@ -59,7 +59,7 @@ def _pathjoin(a: str, b: str) -> str: return a + "/" + b -class TaskPath(click.ParamType): +class TaskPath(click.ParamType["TaskPath"]): def __init__( self, base_dir: str | None = None, diff --git a/src/globus_cli/parsing/param_types/timedelta.py b/src/globus_cli/parsing/param_types/timedelta.py index 6244e89af..381859286 100644 --- a/src/globus_cli/parsing/param_types/timedelta.py +++ b/src/globus_cli/parsing/param_types/timedelta.py @@ -25,7 +25,7 @@ ) -class TimedeltaType(click.ParamType): +class TimedeltaType(click.ParamType[datetime.timedelta | int]): """ Parse a number of seconds, minutes, hours, days, and weeks from a string into a timedelta object diff --git a/tests/conftest.py b/tests/conftest.py index c615bbbc3..43959dd08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,6 @@ from ruamel.yaml import YAML import globus_cli -from globus_cli._click_compat import NEWER_CLICK_API from globus_cli.login_manager.scopes import CURRENT_SCOPE_CONTRACT_VERSION yaml = YAML() @@ -232,10 +231,7 @@ def test_file_dir(): @pytest.fixture def cli_runner(): - if NEWER_CLICK_API: - return CliRunner() - else: - return CliRunner(mix_stderr=False) + return CliRunner() @pytest.fixture