From c236c88bd0f1acf7d1156abe20f426e3978928ad Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 21 May 2026 10:40:38 -0500 Subject: [PATCH 1/7] Support click v8.4 8.4 introduces relatively few changes, but does make `ParamType` a Generic parametrized over the return type of `convert()`. Unfortunately, we also support Python 3.9 , which means we must support older click versions. As such, `ParamType` is not even subscriptable on that version. To work around this, we use `TYPE_CHECKING` guards to split definitions between a generic and non-generic variant. Only one line of runtime code is changed: A superfluous call to `super().convert()` was flagged by `mypy` for having the wrong type and is here removed. --- pyproject.toml | 2 +- src/globus_cli/commands/api.py | 13 +++++--- .../commands/endpoint/set_subscription_id.py | 8 ++++- src/globus_cli/commands/flows/_common.py | 10 +++++- .../commands/gcp/set_subscription_id.py | 8 ++++- .../gcs/endpoint/set_subscription_id.py | 9 +++++- .../commands/gcs/endpoint/update.py | 7 ++++- src/globus_cli/commands/group/_common.py | 7 ++++- src/globus_cli/commands/login.py | 12 +++++-- .../parsing/param_types/delimited.py | 12 +++++-- .../parsing/param_types/endpoint_plus_path.py | 11 +++++-- .../guest_activity_notify_param.py | 9 +++++- .../parsing/param_types/identity_type.py | 8 ++++- .../parsing/param_types/json_strorfile.py | 6 +++- .../parsing/param_types/location.py | 7 ++++- .../parsing/param_types/notify_param.py | 9 +++++- .../parsing/param_types/nullable.py | 12 +++++-- .../parsing/param_types/omittable.py | 31 +++++++++++++------ .../parsing/param_types/task_path.py | 9 +++++- .../parsing/param_types/timedelta.py | 8 ++++- 20 files changed, 161 insertions(+), 37 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25fdd0a88..d7b3dede2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "globus-sdk==4.6.0", - "click>=8.1.4,<8.4", + "click>=8.1.4,<8.5", "jmespath==1.1.0", "packaging>=17.0", "typing_extensions>=4.0;python_version<'3.11'", diff --git a/src/globus_cli/commands/api.py b/src/globus_cli/commands/api.py index 23157bd9a..a45bd8a69 100644 --- a/src/globus_cli/commands/api.py +++ b/src/globus_cli/commands/api.py @@ -17,10 +17,17 @@ from globus_cli.termio import display from globus_cli.types import AnyCommand, ServiceNameLiteral +if t.TYPE_CHECKING: + _QueryParamTypeBase = click.ParamType[tuple[str, str] | None] + _HeaderParamTypeBase = click.ParamType[tuple[str, str] | None] +else: + _QueryParamTypeBase = click.ParamType + _HeaderParamTypeBase = click.ParamType + C = t.TypeVar("C", bound=AnyCommand) -class QueryParamType(click.ParamType): +class QueryParamType(_QueryParamTypeBase): @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key=Value" @@ -37,7 +44,6 @@ def convert( 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: @@ -46,7 +52,7 @@ def convert( return (left, right) -class HeaderParamType(click.ParamType): +class HeaderParamType(_HeaderParamTypeBase): @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key:Value" @@ -63,7 +69,6 @@ def convert( 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: diff --git a/src/globus_cli/commands/endpoint/set_subscription_id.py b/src/globus_cli/commands/endpoint/set_subscription_id.py index 973136826..ab5ed02fd 100644 --- a/src/globus_cli/commands/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/endpoint/set_subscription_id.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing as t import uuid import click @@ -8,8 +9,13 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display +if t.TYPE_CHECKING: + _SubscriptionIdTypeBase = click.ParamType[str] +else: + _SubscriptionIdTypeBase = click.ParamType -class SubscriptionIdType(click.ParamType): + +class SubscriptionIdType(_SubscriptionIdTypeBase): 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..67c756fdf 100644 --- a/src/globus_cli/commands/flows/_common.py +++ b/src/globus_cli/commands/flows/_common.py @@ -14,6 +14,14 @@ from globus_cli.parsing import OMITTABLE_STRING, JSONStringOrFile from globus_cli.utils import CLIAuthRequirementsError +if t.TYPE_CHECKING: + _SubscriptionIdTypeBase = click.ParamType[ + uuid.UUID | t.Literal["DEFAULT"] | globus_sdk.MissingType + ] +else: + _SubscriptionIdTypeBase = click.ParamType + + _input_schema_helptext = """ The JSON input schema that governs the parameters used to start the flow. @@ -125,7 +133,7 @@ ) -class SubscriptionIdType(click.ParamType): +class SubscriptionIdType(_SubscriptionIdTypeBase): name = "SUBSCRIPTION_ID" def __init__(self, *, omittable: bool = False) -> None: diff --git a/src/globus_cli/commands/gcp/set_subscription_id.py b/src/globus_cli/commands/gcp/set_subscription_id.py index 2740e9c1a..c03455bf5 100644 --- a/src/globus_cli/commands/gcp/set_subscription_id.py +++ b/src/globus_cli/commands/gcp/set_subscription_id.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing as t import uuid import click @@ -10,8 +11,13 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display +if t.TYPE_CHECKING: + _SubscriptionIdTypeBase = click.ParamType["uuid.UUID | ExplicitNullType"] +else: + _SubscriptionIdTypeBase = click.ParamType -class GCPSubscriptionIdType(click.ParamType): + +class GCPSubscriptionIdType(_SubscriptionIdTypeBase): 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..2a746baf8 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -10,8 +10,15 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display +if t.TYPE_CHECKING: + _SubscriptionIdTypeBase = click.ParamType[ + uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType + ] +else: + _SubscriptionIdTypeBase = click.ParamType -class GCSSubscriptionIdType(click.ParamType): + +class GCSSubscriptionIdType(_SubscriptionIdTypeBase): 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..c427d9929 100644 --- a/src/globus_cli/commands/gcs/endpoint/update.py +++ b/src/globus_cli/commands/gcs/endpoint/update.py @@ -16,10 +16,15 @@ from globus_cli.termio import display from globus_cli.types import AnyCallable +if t.TYPE_CHECKING: + _SubscriptionIdTypeBase = click.ParamType["str | ExplicitNullType"] +else: + _SubscriptionIdTypeBase = click.ParamType + F = t.TypeVar("F", bound=AnyCallable) -class SubscriptionIdType(click.ParamType): +class SubscriptionIdType(_SubscriptionIdTypeBase): @shim_get_metavar 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..761a0d690 100644 --- a/src/globus_cli/commands/group/_common.py +++ b/src/globus_cli/commands/group/_common.py @@ -9,6 +9,11 @@ from globus_cli.termio import Field, formatters from globus_cli.types import AnyCommand +if t.TYPE_CHECKING: + _SubscriptionVerifiedTypeBase = click.ParamType["uuid.UUID | ExplicitNullType"] +else: + _SubscriptionVerifiedTypeBase = click.ParamType + C = t.TypeVar("C", bound=AnyCommand) # cannot do this because it causes immediate imports and ruins the lazy import @@ -78,7 +83,7 @@ def group_id_arg(f: C) -> C: return click.argument("GROUP_ID", type=click.UUID)(f) -class GroupSubscriptionVerifiedIdType(click.ParamType): +class GroupSubscriptionVerifiedIdType(_SubscriptionVerifiedTypeBase): name = "TEXT" def convert( diff --git a/src/globus_cli/commands/login.py b/src/globus_cli/commands/login.py index c9f9d0e17..18588a223 100644 --- a/src/globus_cli/commands/login.py +++ b/src/globus_cli/commands/login.py @@ -18,6 +18,14 @@ from globus_cli.parsing import command, no_local_server_option from globus_cli.termio import verbosity +if t.TYPE_CHECKING: + _GCSEndpointTypeBase = click.ParamType[uuid.UUID | tuple[uuid.UUID, uuid.UUID]] + _TimerResourceTypeBase = click.ParamType[tuple[t.Literal["flow"], uuid.UUID]] +else: + _GCSEndpointTypeBase = click.ParamType + _TimerResourceTypeBase = click.ParamType + + _SHARED_EPILOG = """\ You can check your primary identity with @@ -50,7 +58,7 @@ """ -class GCSEndpointType(click.ParamType): +class GCSEndpointType(_GCSEndpointTypeBase): name = "GCS Server" @shim_get_metavar @@ -88,7 +96,7 @@ def convert( return endpoint_id if not collection_id else (endpoint_id, collection_id) -class TimerResourceType(click.ParamType): +class TimerResourceType(_TimerResourceTypeBase): name = "TIMER_RESOURCE" @shim_get_metavar diff --git a/src/globus_cli/parsing/param_types/delimited.py b/src/globus_cli/parsing/param_types/delimited.py index bf66799c1..954dc4ce9 100644 --- a/src/globus_cli/parsing/param_types/delimited.py +++ b/src/globus_cli/parsing/param_types/delimited.py @@ -14,8 +14,14 @@ if t.TYPE_CHECKING: from click.shell_completion import CompletionItem + _CommaDelimitedListBase = click.ParamType[list[str] | globus_sdk.MissingType] + _ColonDelimitedChoiceBase = click.ParamType[tuple[str, ...]] +else: + _CommaDelimitedListBase = click.ParamType + _ColonDelimitedChoiceBase = click.ParamType -class CommaDelimitedList(click.ParamType): + +class CommaDelimitedList(_CommaDelimitedListBase): def __init__( self, *, @@ -75,7 +81,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return list[str] -class ColonDelimitedChoiceTuple(click.ParamType): +class ColonDelimitedChoiceTuple(_ColonDelimitedChoiceBase): """ A colon-delimited choice type which wraps the existing click.Choice type. @@ -102,7 +108,7 @@ 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 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..25a4bbe50 100644 --- a/src/globus_cli/parsing/param_types/endpoint_plus_path.py +++ b/src/globus_cli/parsing/param_types/endpoint_plus_path.py @@ -7,8 +7,13 @@ from globus_cli._click_compat import shim_get_metavar +if t.TYPE_CHECKING: + _EndpointPlusPathBase = click.ParamType[tuple[uuid.UUID, str | None]] +else: + _EndpointPlusPathBase = click.ParamType -class EndpointPlusPath(click.ParamType): + +class EndpointPlusPath(_EndpointPlusPathBase): """ Custom type for ":" Supports path being required and path being optional. @@ -67,7 +72,9 @@ 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]) + # NOTE: for some reason, mypy thinks `click.UUID()` can return `None`, but that + # does not match its annotation + endpoint_id: uuid.UUID = click.UUID(splitval[0]) # type: ignore[assignment] # 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..b975ace0c 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 @@ -1,13 +1,20 @@ from __future__ import annotations +import typing as t + import click from click.shell_completion import CompletionItem from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import ExplicitNullType +if t.TYPE_CHECKING: + _ActivityNotificationBase = click.ParamType[dict[str, list[str]] | ExplicitNullType] +else: + _ActivityNotificationBase = click.ParamType + -class GCSManagerGuestActivityNotificationParamType(click.ParamType): +class GCSManagerGuestActivityNotificationParamType(_ActivityNotificationBase): """ For the GCS Manager API: diff --git a/src/globus_cli/parsing/param_types/identity_type.py b/src/globus_cli/parsing/param_types/identity_type.py index 922e14426..e78195f62 100644 --- a/src/globus_cli/parsing/param_types/identity_type.py +++ b/src/globus_cli/parsing/param_types/identity_type.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import typing as t import uuid from collections import namedtuple @@ -10,6 +11,11 @@ ParsedIdentity = namedtuple("ParsedIdentity", ["value", "idtype"]) +if t.TYPE_CHECKING: + _IdentityTypeBase = click.ParamType[ParsedIdentity] +else: + _IdentityTypeBase = click.ParamType + class _B32DecodeError(ValueError): """custom exception type""" @@ -35,7 +41,7 @@ def _b32decode(v: str) -> str: raise _B32DecodeError("decode and load as UUID failed") -class IdentityType(click.ParamType): +class IdentityType(_IdentityTypeBase): """ 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" diff --git a/src/globus_cli/parsing/param_types/json_strorfile.py b/src/globus_cli/parsing/param_types/json_strorfile.py index 0352d3c04..d6451ad9b 100644 --- a/src/globus_cli/parsing/param_types/json_strorfile.py +++ b/src/globus_cli/parsing/param_types/json_strorfile.py @@ -15,6 +15,10 @@ if t.TYPE_CHECKING: from click.shell_completion import CompletionItem + _JsonStrOrFileBase = click.ParamType["ExplicitNullType | ParsedJSONData"] +else: + _JsonStrOrFileBase = click.ParamType + @dataclasses.dataclass class ParsedJSONData: @@ -26,7 +30,7 @@ class ParsedJSONData: data: JsonValue -class JSONStringOrFile(click.ParamType): +class JSONStringOrFile(_JsonStrOrFileBase): """ Parse an input which could be a filename or could be a JSON blob being supplied on the commandline. diff --git a/src/globus_cli/parsing/param_types/location.py b/src/globus_cli/parsing/param_types/location.py index e51348099..be5f7916b 100644 --- a/src/globus_cli/parsing/param_types/location.py +++ b/src/globus_cli/parsing/param_types/location.py @@ -5,8 +5,13 @@ import click +if t.TYPE_CHECKING: + _LocTypeBase = click.ParamType[str] +else: + _LocTypeBase = click.ParamType -class LocationType(click.ParamType): + +class LocationType(_LocTypeBase): """ 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..529e47681 100644 --- a/src/globus_cli/parsing/param_types/notify_param.py +++ b/src/globus_cli/parsing/param_types/notify_param.py @@ -1,10 +1,17 @@ from __future__ import annotations +import typing as t + import click from click.shell_completion import CompletionItem from globus_cli._click_compat import shim_get_metavar +if t.TYPE_CHECKING: + _NotificationTypeBase = click.ParamType[dict[str, bool]] +else: + _NotificationTypeBase = click.ParamType + def _empty_dict_callback( ctx: click.Context, param: click.Parameter, value: dict[str, bool] | None @@ -14,7 +21,7 @@ def _empty_dict_callback( return value -class NotificationParamType(click.ParamType): +class NotificationParamType(_NotificationTypeBase): STANDARD_CALLBACK = _empty_dict_callback @shim_get_metavar diff --git a/src/globus_cli/parsing/param_types/nullable.py b/src/globus_cli/parsing/param_types/nullable.py index 63a7303dc..6ba5f2daf 100644 --- a/src/globus_cli/parsing/param_types/nullable.py +++ b/src/globus_cli/parsing/param_types/nullable.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing as t from urllib.parse import urlparse import click @@ -7,8 +8,15 @@ from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType +if t.TYPE_CHECKING: + _NullableStrBase = click.ParamType[str | ExplicitNullType] + _NullableIntBase = click.ParamType[int | ExplicitNullType] +else: + _NullableStrBase = click.ParamType + _NullableIntBase = click.ParamType -class StringOrNull(click.ParamType): + +class StringOrNull(_NullableStrBase): """ Very similar to a basic string type, but one in which the empty string will be converted into an EXPLICIT_NULL @@ -53,7 +61,7 @@ def convert( return value -class IntOrNull(click.ParamType): +class IntOrNull(_NullableIntBase): """ Very similar to a basic int type, but one in which the empty string will be converted into an EXPLICIT_NULL diff --git a/src/globus_cli/parsing/param_types/omittable.py b/src/globus_cli/parsing/param_types/omittable.py index 557269984..ac0bb75ed 100644 --- a/src/globus_cli/parsing/param_types/omittable.py +++ b/src/globus_cli/parsing/param_types/omittable.py @@ -13,8 +13,19 @@ shim_get_missing_message, ) - -class OmittableInt(click.ParamType): +if t.TYPE_CHECKING: + _OmittableIntBase = click.ParamType[int | globus_sdk.MissingType] + _OmittableStrBase = click.ParamType[str | globus_sdk.MissingType] + _OmittableUUIDBase = click.ParamType[uuid.UUID | globus_sdk.MissingType] + _OmittableDateTimeBase = click.ParamType[datetime.datetime | globus_sdk.MissingType] +else: + _OmittableIntBase = click.ParamType + _OmittableStrBase = click.ParamType + _OmittableUUIDBase = click.ParamType + _OmittableDateTimeBase = click.DateTime + + +class OmittableInt(_OmittableIntBase): name = "integer" def convert( @@ -22,13 +33,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(_OmittableStrBase): name = "text" def convert( @@ -36,13 +47,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(_OmittableUUIDBase): name = "uuid" def convert( @@ -50,13 +61,13 @@ 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(_OmittableStrBase): name = "choice" def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: @@ -92,7 +103,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[literal, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableDateTime(click.DateTime): +class OmittableDateTime(_OmittableDateTimeBase): name = "datetime" def convert( @@ -100,7 +111,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..f30cd47c5 100644 --- a/src/globus_cli/parsing/param_types/task_path.py +++ b/src/globus_cli/parsing/param_types/task_path.py @@ -1,7 +1,14 @@ from __future__ import annotations +import typing as t + import click +if t.TYPE_CHECKING: + _TaskPathBase = click.ParamType["TaskPath"] +else: + _TaskPathBase = click.ParamType + def _normpath(path: str) -> str: """ @@ -59,7 +66,7 @@ def _pathjoin(a: str, b: str) -> str: return a + "/" + b -class TaskPath(click.ParamType): +class TaskPath(_TaskPathBase): 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..3aa46d72c 100644 --- a/src/globus_cli/parsing/param_types/timedelta.py +++ b/src/globus_cli/parsing/param_types/timedelta.py @@ -2,9 +2,15 @@ import datetime import re +import typing as t import click +if t.TYPE_CHECKING: + _TimedeltaBase = click.ParamType[datetime.timedelta | int] +else: + _TimedeltaBase = click.ParamType + _timedelta_regex = re.compile( r""" ^ @@ -25,7 +31,7 @@ ) -class TimedeltaType(click.ParamType): +class TimedeltaType(_TimedeltaBase): """ Parse a number of seconds, minutes, hours, days, and weeks from a string into a timedelta object From a534e2c8177c27252e7565492a882850294b455d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 15:11:26 -0500 Subject: [PATCH 2/7] Update lower bound on 'click' to v8.4 --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d7b3dede2..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.5", + # 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", From 2d7f11db2913effa149deb051b062bef9af852a7 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 15:46:37 -0500 Subject: [PATCH 3/7] Simplify ParamType inheritance for click 8.4+ Now that we have removed support for click 8.3 and lower, `ParamType` is always generic and can have its type parameter specified directly. In one very specific case -- omittable datetimes -- this is specially handled with an explanatory comment. --- src/globus_cli/commands/api.py | 11 ++----- .../commands/endpoint/set_subscription_id.py | 8 +---- src/globus_cli/commands/flows/_common.py | 12 ++------ .../commands/gcp/set_subscription_id.py | 8 +---- .../gcs/endpoint/set_subscription_id.py | 11 ++----- .../commands/gcs/endpoint/update.py | 7 +---- src/globus_cli/commands/group/_common.py | 7 +---- src/globus_cli/commands/login.py | 12 ++------ .../parsing/param_types/delimited.py | 10 ++----- .../parsing/param_types/endpoint_plus_path.py | 7 +---- .../guest_activity_notify_param.py | 11 ++----- .../parsing/param_types/identity_type.py | 8 +---- .../parsing/param_types/json_strorfile.py | 6 +--- .../parsing/param_types/location.py | 7 +---- .../parsing/param_types/notify_param.py | 9 +----- .../parsing/param_types/nullable.py | 12 ++------ .../parsing/param_types/omittable.py | 29 +++++++++---------- .../parsing/param_types/task_path.py | 9 +----- .../parsing/param_types/timedelta.py | 8 +---- 19 files changed, 42 insertions(+), 150 deletions(-) diff --git a/src/globus_cli/commands/api.py b/src/globus_cli/commands/api.py index a45bd8a69..f5e476461 100644 --- a/src/globus_cli/commands/api.py +++ b/src/globus_cli/commands/api.py @@ -17,17 +17,10 @@ from globus_cli.termio import display from globus_cli.types import AnyCommand, ServiceNameLiteral -if t.TYPE_CHECKING: - _QueryParamTypeBase = click.ParamType[tuple[str, str] | None] - _HeaderParamTypeBase = click.ParamType[tuple[str, str] | None] -else: - _QueryParamTypeBase = click.ParamType - _HeaderParamTypeBase = click.ParamType - C = t.TypeVar("C", bound=AnyCommand) -class QueryParamType(_QueryParamTypeBase): +class QueryParamType(click.ParamType[tuple[str, str] | None]): @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key=Value" @@ -52,7 +45,7 @@ def convert( return (left, right) -class HeaderParamType(_HeaderParamTypeBase): +class HeaderParamType(click.ParamType[tuple[str, str] | None]): @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key:Value" diff --git a/src/globus_cli/commands/endpoint/set_subscription_id.py b/src/globus_cli/commands/endpoint/set_subscription_id.py index ab5ed02fd..7d7392ce6 100644 --- a/src/globus_cli/commands/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/endpoint/set_subscription_id.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing as t import uuid import click @@ -9,13 +8,8 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display -if t.TYPE_CHECKING: - _SubscriptionIdTypeBase = click.ParamType[str] -else: - _SubscriptionIdTypeBase = click.ParamType - -class SubscriptionIdType(_SubscriptionIdTypeBase): +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 67c756fdf..fec38c7e8 100644 --- a/src/globus_cli/commands/flows/_common.py +++ b/src/globus_cli/commands/flows/_common.py @@ -14,14 +14,6 @@ from globus_cli.parsing import OMITTABLE_STRING, JSONStringOrFile from globus_cli.utils import CLIAuthRequirementsError -if t.TYPE_CHECKING: - _SubscriptionIdTypeBase = click.ParamType[ - uuid.UUID | t.Literal["DEFAULT"] | globus_sdk.MissingType - ] -else: - _SubscriptionIdTypeBase = click.ParamType - - _input_schema_helptext = """ The JSON input schema that governs the parameters used to start the flow. @@ -133,7 +125,9 @@ ) -class SubscriptionIdType(_SubscriptionIdTypeBase): +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/gcp/set_subscription_id.py b/src/globus_cli/commands/gcp/set_subscription_id.py index c03455bf5..a15698072 100644 --- a/src/globus_cli/commands/gcp/set_subscription_id.py +++ b/src/globus_cli/commands/gcp/set_subscription_id.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing as t import uuid import click @@ -11,13 +10,8 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display -if t.TYPE_CHECKING: - _SubscriptionIdTypeBase = click.ParamType["uuid.UUID | ExplicitNullType"] -else: - _SubscriptionIdTypeBase = click.ParamType - -class GCPSubscriptionIdType(_SubscriptionIdTypeBase): +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 2a746baf8..6a0869b4a 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -10,15 +10,10 @@ from globus_cli.parsing import command, endpoint_id_arg from globus_cli.termio import display -if t.TYPE_CHECKING: - _SubscriptionIdTypeBase = click.ParamType[ - uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType - ] -else: - _SubscriptionIdTypeBase = click.ParamType - -class GCSSubscriptionIdType(_SubscriptionIdTypeBase): +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 c427d9929..1841dfdcc 100644 --- a/src/globus_cli/commands/gcs/endpoint/update.py +++ b/src/globus_cli/commands/gcs/endpoint/update.py @@ -16,15 +16,10 @@ from globus_cli.termio import display from globus_cli.types import AnyCallable -if t.TYPE_CHECKING: - _SubscriptionIdTypeBase = click.ParamType["str | ExplicitNullType"] -else: - _SubscriptionIdTypeBase = click.ParamType - F = t.TypeVar("F", bound=AnyCallable) -class SubscriptionIdType(_SubscriptionIdTypeBase): +class SubscriptionIdType(click.ParamType[str | ExplicitNullType]): @shim_get_metavar 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 761a0d690..6a657eb09 100644 --- a/src/globus_cli/commands/group/_common.py +++ b/src/globus_cli/commands/group/_common.py @@ -9,11 +9,6 @@ from globus_cli.termio import Field, formatters from globus_cli.types import AnyCommand -if t.TYPE_CHECKING: - _SubscriptionVerifiedTypeBase = click.ParamType["uuid.UUID | ExplicitNullType"] -else: - _SubscriptionVerifiedTypeBase = click.ParamType - C = t.TypeVar("C", bound=AnyCommand) # cannot do this because it causes immediate imports and ruins the lazy import @@ -83,7 +78,7 @@ def group_id_arg(f: C) -> C: return click.argument("GROUP_ID", type=click.UUID)(f) -class GroupSubscriptionVerifiedIdType(_SubscriptionVerifiedTypeBase): +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 18588a223..9a9ba8a0c 100644 --- a/src/globus_cli/commands/login.py +++ b/src/globus_cli/commands/login.py @@ -18,14 +18,6 @@ from globus_cli.parsing import command, no_local_server_option from globus_cli.termio import verbosity -if t.TYPE_CHECKING: - _GCSEndpointTypeBase = click.ParamType[uuid.UUID | tuple[uuid.UUID, uuid.UUID]] - _TimerResourceTypeBase = click.ParamType[tuple[t.Literal["flow"], uuid.UUID]] -else: - _GCSEndpointTypeBase = click.ParamType - _TimerResourceTypeBase = click.ParamType - - _SHARED_EPILOG = """\ You can check your primary identity with @@ -58,7 +50,7 @@ """ -class GCSEndpointType(_GCSEndpointTypeBase): +class GCSEndpointType(click.ParamType[uuid.UUID | tuple[uuid.UUID, uuid.UUID]]): name = "GCS Server" @shim_get_metavar @@ -96,7 +88,7 @@ def convert( return endpoint_id if not collection_id else (endpoint_id, collection_id) -class TimerResourceType(_TimerResourceTypeBase): +class TimerResourceType(click.ParamType[tuple[t.Literal["flow"], uuid.UUID]]): name = "TIMER_RESOURCE" @shim_get_metavar diff --git a/src/globus_cli/parsing/param_types/delimited.py b/src/globus_cli/parsing/param_types/delimited.py index 954dc4ce9..bf1d348f7 100644 --- a/src/globus_cli/parsing/param_types/delimited.py +++ b/src/globus_cli/parsing/param_types/delimited.py @@ -14,14 +14,8 @@ if t.TYPE_CHECKING: from click.shell_completion import CompletionItem - _CommaDelimitedListBase = click.ParamType[list[str] | globus_sdk.MissingType] - _ColonDelimitedChoiceBase = click.ParamType[tuple[str, ...]] -else: - _CommaDelimitedListBase = click.ParamType - _ColonDelimitedChoiceBase = click.ParamType - -class CommaDelimitedList(_CommaDelimitedListBase): +class CommaDelimitedList(click.ParamType[list[str] | globus_sdk.MissingType]): def __init__( self, *, @@ -81,7 +75,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return list[str] -class ColonDelimitedChoiceTuple(_ColonDelimitedChoiceBase): +class ColonDelimitedChoiceTuple(click.ParamType[tuple[str, ...]]): """ A colon-delimited choice type which wraps the existing click.Choice type. 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 25a4bbe50..836cfa55f 100644 --- a/src/globus_cli/parsing/param_types/endpoint_plus_path.py +++ b/src/globus_cli/parsing/param_types/endpoint_plus_path.py @@ -7,13 +7,8 @@ from globus_cli._click_compat import shim_get_metavar -if t.TYPE_CHECKING: - _EndpointPlusPathBase = click.ParamType[tuple[uuid.UUID, str | None]] -else: - _EndpointPlusPathBase = click.ParamType - -class EndpointPlusPath(_EndpointPlusPathBase): +class EndpointPlusPath(click.ParamType[tuple[uuid.UUID, str | None]]): """ Custom type for ":" Supports path being required and path being optional. 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 b975ace0c..feb17c2a1 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 @@ -1,20 +1,15 @@ from __future__ import annotations -import typing as t - import click from click.shell_completion import CompletionItem from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import ExplicitNullType -if t.TYPE_CHECKING: - _ActivityNotificationBase = click.ParamType[dict[str, list[str]] | ExplicitNullType] -else: - _ActivityNotificationBase = click.ParamType - -class GCSManagerGuestActivityNotificationParamType(_ActivityNotificationBase): +class GCSManagerGuestActivityNotificationParamType( + click.ParamType[dict[str, list[str]] | ExplicitNullType] +): """ For the GCS Manager API: diff --git a/src/globus_cli/parsing/param_types/identity_type.py b/src/globus_cli/parsing/param_types/identity_type.py index e78195f62..989e212e8 100644 --- a/src/globus_cli/parsing/param_types/identity_type.py +++ b/src/globus_cli/parsing/param_types/identity_type.py @@ -1,7 +1,6 @@ from __future__ import annotations import base64 -import typing as t import uuid from collections import namedtuple @@ -11,11 +10,6 @@ ParsedIdentity = namedtuple("ParsedIdentity", ["value", "idtype"]) -if t.TYPE_CHECKING: - _IdentityTypeBase = click.ParamType[ParsedIdentity] -else: - _IdentityTypeBase = click.ParamType - class _B32DecodeError(ValueError): """custom exception type""" @@ -41,7 +35,7 @@ def _b32decode(v: str) -> str: raise _B32DecodeError("decode and load as UUID failed") -class IdentityType(_IdentityTypeBase): +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" diff --git a/src/globus_cli/parsing/param_types/json_strorfile.py b/src/globus_cli/parsing/param_types/json_strorfile.py index d6451ad9b..1177a5452 100644 --- a/src/globus_cli/parsing/param_types/json_strorfile.py +++ b/src/globus_cli/parsing/param_types/json_strorfile.py @@ -15,10 +15,6 @@ if t.TYPE_CHECKING: from click.shell_completion import CompletionItem - _JsonStrOrFileBase = click.ParamType["ExplicitNullType | ParsedJSONData"] -else: - _JsonStrOrFileBase = click.ParamType - @dataclasses.dataclass class ParsedJSONData: @@ -30,7 +26,7 @@ class ParsedJSONData: data: JsonValue -class JSONStringOrFile(_JsonStrOrFileBase): +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. diff --git a/src/globus_cli/parsing/param_types/location.py b/src/globus_cli/parsing/param_types/location.py index be5f7916b..3fdec2ef9 100644 --- a/src/globus_cli/parsing/param_types/location.py +++ b/src/globus_cli/parsing/param_types/location.py @@ -5,13 +5,8 @@ import click -if t.TYPE_CHECKING: - _LocTypeBase = click.ParamType[str] -else: - _LocTypeBase = click.ParamType - -class LocationType(_LocTypeBase): +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 529e47681..7a56b02a2 100644 --- a/src/globus_cli/parsing/param_types/notify_param.py +++ b/src/globus_cli/parsing/param_types/notify_param.py @@ -1,17 +1,10 @@ from __future__ import annotations -import typing as t - import click from click.shell_completion import CompletionItem from globus_cli._click_compat import shim_get_metavar -if t.TYPE_CHECKING: - _NotificationTypeBase = click.ParamType[dict[str, bool]] -else: - _NotificationTypeBase = click.ParamType - def _empty_dict_callback( ctx: click.Context, param: click.Parameter, value: dict[str, bool] | None @@ -21,7 +14,7 @@ def _empty_dict_callback( return value -class NotificationParamType(_NotificationTypeBase): +class NotificationParamType(click.ParamType[dict[str, bool]]): STANDARD_CALLBACK = _empty_dict_callback @shim_get_metavar diff --git a/src/globus_cli/parsing/param_types/nullable.py b/src/globus_cli/parsing/param_types/nullable.py index 6ba5f2daf..17970c268 100644 --- a/src/globus_cli/parsing/param_types/nullable.py +++ b/src/globus_cli/parsing/param_types/nullable.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing as t from urllib.parse import urlparse import click @@ -8,15 +7,8 @@ from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType -if t.TYPE_CHECKING: - _NullableStrBase = click.ParamType[str | ExplicitNullType] - _NullableIntBase = click.ParamType[int | ExplicitNullType] -else: - _NullableStrBase = click.ParamType - _NullableIntBase = click.ParamType - -class StringOrNull(_NullableStrBase): +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 @@ -61,7 +53,7 @@ def convert( return value -class IntOrNull(_NullableIntBase): +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 diff --git a/src/globus_cli/parsing/param_types/omittable.py b/src/globus_cli/parsing/param_types/omittable.py index ac0bb75ed..18e3b7f02 100644 --- a/src/globus_cli/parsing/param_types/omittable.py +++ b/src/globus_cli/parsing/param_types/omittable.py @@ -13,19 +13,8 @@ shim_get_missing_message, ) -if t.TYPE_CHECKING: - _OmittableIntBase = click.ParamType[int | globus_sdk.MissingType] - _OmittableStrBase = click.ParamType[str | globus_sdk.MissingType] - _OmittableUUIDBase = click.ParamType[uuid.UUID | globus_sdk.MissingType] - _OmittableDateTimeBase = click.ParamType[datetime.datetime | globus_sdk.MissingType] -else: - _OmittableIntBase = click.ParamType - _OmittableStrBase = click.ParamType - _OmittableUUIDBase = click.ParamType - _OmittableDateTimeBase = click.DateTime - -class OmittableInt(_OmittableIntBase): +class OmittableInt(click.ParamType[int | globus_sdk.MissingType]): name = "integer" def convert( @@ -39,7 +28,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[int, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableString(_OmittableStrBase): +class OmittableString(click.ParamType[str | globus_sdk.MissingType]): name = "text" def convert( @@ -53,7 +42,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[str, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableUUID(_OmittableUUIDBase): +class OmittableUUID(click.ParamType[uuid.UUID | globus_sdk.MissingType]): name = "uuid" def convert( @@ -67,7 +56,7 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[uuid.UUID, globus_sdk.MissingType] # type: ignore[return-value] -class OmittableChoice(_OmittableStrBase): +class OmittableChoice(click.ParamType[str | globus_sdk.MissingType]): name = "choice" def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: @@ -103,6 +92,16 @@ def get_type_annotation(self, param: click.Parameter) -> type: return t.Union[literal, globus_sdk.MissingType] # type: ignore[return-value] +# 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" diff --git a/src/globus_cli/parsing/param_types/task_path.py b/src/globus_cli/parsing/param_types/task_path.py index f30cd47c5..a771956c1 100644 --- a/src/globus_cli/parsing/param_types/task_path.py +++ b/src/globus_cli/parsing/param_types/task_path.py @@ -1,14 +1,7 @@ from __future__ import annotations -import typing as t - import click -if t.TYPE_CHECKING: - _TaskPathBase = click.ParamType["TaskPath"] -else: - _TaskPathBase = click.ParamType - def _normpath(path: str) -> str: """ @@ -66,7 +59,7 @@ def _pathjoin(a: str, b: str) -> str: return a + "/" + b -class TaskPath(_TaskPathBase): +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 3aa46d72c..381859286 100644 --- a/src/globus_cli/parsing/param_types/timedelta.py +++ b/src/globus_cli/parsing/param_types/timedelta.py @@ -2,15 +2,9 @@ import datetime import re -import typing as t import click -if t.TYPE_CHECKING: - _TimedeltaBase = click.ParamType[datetime.timedelta | int] -else: - _TimedeltaBase = click.ParamType - _timedelta_regex = re.compile( r""" ^ @@ -31,7 +25,7 @@ ) -class TimedeltaType(_TimedeltaBase): +class TimedeltaType(click.ParamType[datetime.timedelta | int]): """ Parse a number of seconds, minutes, hours, days, and weeks from a string into a timedelta object From f8120c11acbb072ff42362d915d4081cff86b167 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 15:52:46 -0500 Subject: [PATCH 4/7] Remove the '_click_compat' module and all usage This module is no longer needed now that our minimum version of click is 8.4 . Remove it and update all usages (which were structured to make this removal simple). --- src/globus_cli/_click_compat.py | 55 ------------------- src/globus_cli/commands/api.py | 3 - src/globus_cli/commands/flows/start.py | 2 - .../commands/gcs/endpoint/update.py | 2 - src/globus_cli/commands/login.py | 3 - src/globus_cli/parsing/commands.py | 18 ------ .../parsing/param_types/delimited.py | 17 ------ .../parsing/param_types/endpoint_plus_path.py | 3 - .../guest_activity_notify_param.py | 2 - .../parsing/param_types/identity_type.py | 3 - .../parsing/param_types/json_strorfile.py | 2 - .../parsing/param_types/notify_param.py | 3 - .../parsing/param_types/nullable.py | 4 -- .../parsing/param_types/omittable.py | 14 ----- tests/conftest.py | 6 +- 15 files changed, 1 insertion(+), 136 deletions(-) delete mode 100644 src/globus_cli/_click_compat.py 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 f5e476461..c9886106a 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 @@ -21,7 +20,6 @@ class QueryParamType(click.ParamType[tuple[str, str] | None]): - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key=Value" @@ -46,7 +44,6 @@ def convert( class HeaderParamType(click.ParamType[tuple[str, str] | None]): - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "Key:Value" 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/gcs/endpoint/update.py b/src/globus_cli/commands/gcs/endpoint/update.py index 1841dfdcc..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 @@ -20,7 +19,6 @@ class SubscriptionIdType(click.ParamType[str | ExplicitNullType]): - @shim_get_metavar def get_metavar(self, param: click.Parameter, ctx: click.Context) -> str: return "[|DEFAULT|null]" diff --git a/src/globus_cli/commands/login.py b/src/globus_cli/commands/login.py index 9a9ba8a0c..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 @@ -53,7 +52,6 @@ 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 "[:]" @@ -91,7 +89,6 @@ def convert( 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 bf1d348f7..8a0098c3e 100644 --- a/src/globus_cli/parsing/param_types/delimited.py +++ b/src/globus_cli/parsing/param_types/delimited.py @@ -5,12 +5,6 @@ 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 @@ -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) + "}" @@ -105,22 +98,12 @@ def __init__( 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 836cfa55f..276603945 100644 --- a/src/globus_cli/parsing/param_types/endpoint_plus_path.py +++ b/src/globus_cli/parsing/param_types/endpoint_plus_path.py @@ -5,8 +5,6 @@ import click -from globus_cli._click_compat import shim_get_metavar - class EndpointPlusPath(click.ParamType[tuple[uuid.UUID, str | None]]): """ @@ -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. 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 feb17c2a1..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,7 +3,6 @@ import click from click.shell_completion import CompletionItem -from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import ExplicitNullType @@ -41,7 +40,6 @@ class GCSManagerGuestActivityNotificationParamType( 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 989e212e8..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"]) @@ -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 1177a5452..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 @@ -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/notify_param.py b/src/globus_cli/parsing/param_types/notify_param.py index 7a56b02a2..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 @@ -17,7 +15,6 @@ def _empty_dict_callback( 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 17970c268..2b58c311f 100644 --- a/src/globus_cli/parsing/param_types/nullable.py +++ b/src/globus_cli/parsing/param_types/nullable.py @@ -4,7 +4,6 @@ import click -from globus_cli._click_compat import shim_get_metavar from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType @@ -14,7 +13,6 @@ class StringOrNull(click.ParamType[str | ExplicitNullType]): 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" @@ -59,7 +56,6 @@ class IntOrNull(click.ParamType[int | ExplicitNullType]): 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 18e3b7f02..c466dedc7 100644 --- a/src/globus_cli/parsing/param_types/omittable.py +++ b/src/globus_cli/parsing/param_types/omittable.py @@ -7,12 +7,6 @@ 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[int | globus_sdk.MissingType]): name = "integer" @@ -62,20 +56,12 @@ class OmittableChoice(click.ParamType[str | globus_sdk.MissingType]): 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( 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 From 1986d42964e15a9c5fdaf2d635c74bbe12eb3440 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 15:55:29 -0500 Subject: [PATCH 5/7] Remove version-dispatched `click` handling Searching for other `click`-version-sensitive code, outside of what is handled by `_click_compat`, we find one usage site, updated here. --- src/globus_cli/commands/collection/list.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/globus_cli/commands/collection/list.py b/src/globus_cli/commands/collection/list.py index eed9d57f3..d425620c2 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' From 9cfff0d39ba305501a7019f1cfbe982c279eb19c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 16:00:45 -0500 Subject: [PATCH 6/7] Fix any 'convert()' definitions which check `None` Part of the documented signature of `convert()` is that `None` will never be passed to it. In spite of this, several `convert()` definitions in the CLI had inherited this handling from very old versions of `click`. Update to match current style. (NB: callbacks can still receive a `None`, which is used for various parameter definitions.) --- src/globus_cli/commands/api.py | 8 ++------ src/globus_cli/commands/collection/list.py | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/globus_cli/commands/api.py b/src/globus_cli/commands/api.py index c9886106a..3231f665a 100644 --- a/src/globus_cli/commands/api.py +++ b/src/globus_cli/commands/api.py @@ -31,12 +31,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: - if value is None: - return None if "=" not in value: self.fail("invalid query param", param=param, ctx=ctx) left, right = value.split("=", 1) @@ -55,12 +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: - 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 d425620c2..8425acf90 100644 --- a/src/globus_cli/commands/collection/list.py +++ b/src/globus_cli/commands/collection/list.py @@ -22,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: From 3191f10674ece13bae623ab6901b045638d80b9f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 27 May 2026 16:07:37 -0500 Subject: [PATCH 7/7] Refine `click.UUID` usage `__call__` takes `str | None` and returns `UUID | None`. `convert()` takes `str` and returns `UUID`. Switching to use `convert()` has a positive effect. --- src/globus_cli/parsing/param_types/endpoint_plus_path.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 276603945..902a2dcb1 100644 --- a/src/globus_cli/parsing/param_types/endpoint_plus_path.py +++ b/src/globus_cli/parsing/param_types/endpoint_plus_path.py @@ -64,9 +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 - # NOTE: for some reason, mypy thinks `click.UUID()` can return `None`, but that - # does not match its annotation - endpoint_id: uuid.UUID = click.UUID(splitval[0]) # type: ignore[assignment] + 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