Skip to content

Commit 8a2b489

Browse files
authored
ParamType and other typing improvements (#3371)
2 parents 30a7f7c + ded5b69 commit 8a2b489

9 files changed

Lines changed: 196 additions & 109 deletions

File tree

CHANGES.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
.. currentmodule:: click
22

3+
Version 8.4.0
4+
-------------
5+
6+
Unreleased
7+
8+
- :class:`ParamType` typing improvements. :pr:`3371`
9+
10+
- :class:`ParamType` is now a generic abstract base class,
11+
parameterized by its converted value type.
12+
- :meth:`~ParamType.convert` return types are narrowed on all
13+
concrete types (``str`` for :class:`STRING`, ``int`` for
14+
:class:`INT`, etc.).
15+
- :meth:`~ParamType.to_info_dict` returns specific
16+
:class:`~typing.TypedDict` subclasses instead of
17+
``dict[str, Any]``.
18+
- :class:`CompositeParamType` and the number-range base are now
19+
generic with abstract methods.
20+
321
Version 8.3.3
422
-------------
523

docs/parameter-types.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ The resulting value from an option will always be one of the originally passed c
7070
regardless of `case_sensitive`.
7171
```
7272

73+
```{versionchanged} 8.4.0
74+
{class}`Choice` is now generic. Parameterize it with the choice value type
75+
({class}`!Choice[HashType]` for an enum, {class}`!Choice[str]` for plain
76+
strings) to enable type-checked consumers.
77+
```
78+
7379
(ranges)=
7480

7581
### Int and Float Ranges
@@ -153,16 +159,21 @@ To implement a custom type, you need to subclass the {class}`ParamType` class. F
153159
function that fails with a `ValueError` is also supported, though discouraged. Override the {meth}`~ParamType.convert`
154160
method to convert the value from a string to the correct type.
155161

162+
{class}`ParamType` is generic in the converted value type: parameterize it with
163+
the type returned by `convert` so that consumers (and type checkers) can rely
164+
on the narrowed return type.
165+
156166
The following code implements an integer type that accepts hex and octal numbers in addition to normal integers, and
157167
converts them into regular integers.
158168

159169
```python
160170
import click
161171

162-
class BasedIntParamType(click.ParamType):
172+
173+
class BasedIntParamType(click.ParamType[int]):
163174
name = "integer"
164175

165-
def convert(self, value, param, ctx):
176+
def convert(self, value, param, ctx) -> int:
166177
if isinstance(value, int):
167178
return value
168179

@@ -175,6 +186,7 @@ class BasedIntParamType(click.ParamType):
175186
except ValueError:
176187
self.fail(f"{value!r} is not a valid integer", param, ctx)
177188

189+
178190
BASED_INT = BasedIntParamType()
179191
```
180192

@@ -184,3 +196,10 @@ conversion fails. The `param` and `ctx` arguments may be `None` in some cases su
184196
Values from user input or the command line will be strings, but default values and Python arguments may already be the
185197
correct type. The custom type should check at the top if the value is already valid and pass it through to support those
186198
cases.
199+
200+
```{versionchanged} 8.4.0
201+
{class}`ParamType` is now a generic abstract base class. Parameterize it with
202+
the converted value type ({class}`!ParamType[int]` for an integer-returning
203+
type) so that {meth}`~ParamType.convert` and downstream consumers carry the
204+
narrowed type.
205+
```

docs/shell-completion.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ indicate special handling for paths, and `help` for shells that support showing
120120
In this example, the type will suggest environment variables that start with the incomplete value.
121121

122122
```python
123-
class EnvVarType(ParamType):
123+
class EnvVarType(ParamType[str]):
124124
name = "envvar"
125125

126126
def shell_complete(self, ctx, param, incomplete):

docs/support-multiple-versions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def add_ctx_arg(f: F) -> F:
5555
Here's an example ``ParamType`` subclass which uses this:
5656

5757
```python
58-
class CommaDelimitedString(click.ParamType):
58+
class CommaDelimitedString(click.ParamType[str]):
5959
@add_ctx_arg
6060
def get_metavar(self, param: click.Parameter, ctx: click.Context | None) -> str:
6161
return "TEXT,TEXT,..."

examples/validation/validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ def validate_count(ctx, param, value):
99
return value
1010

1111

12-
class URL(click.ParamType):
12+
class URL(click.ParamType[urlparse.ParseResult]):
1313
name = "url"
1414

15-
def convert(self, value, param, ctx):
15+
def convert(self, value, param, ctx) -> urlparse.ParseResult:
1616
if not isinstance(value, tuple):
1717
value = urlparse.urlparse(value)
1818
if value.scheme not in ("http", "https"):

src/click/core.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,7 +2149,7 @@ class Parameter:
21492149
def __init__(
21502150
self,
21512151
param_decls: cabc.Sequence[str] | None = None,
2152-
type: types.ParamType | t.Any | None = None,
2152+
type: types.ParamType[t.Any] | t.Any | None = None,
21532153
required: bool = False,
21542154
# XXX The default historically embed two concepts:
21552155
# - the declaration of a Parameter object carrying the default (handy to
@@ -2181,7 +2181,7 @@ def __init__(
21812181
self.name, self.opts, self.secondary_opts = self._parse_decls(
21822182
param_decls or (), expose_value
21832183
)
2184-
self.type: types.ParamType = types.convert_type(type, default)
2184+
self.type: types.ParamType[t.Any] = types.convert_type(type, default)
21852185

21862186
# Default nargs to what the type tells us if we have that
21872187
# information available.
@@ -2648,7 +2648,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
26482648
"""Return a list of completions for the incomplete value. If a
26492649
``shell_complete`` function was given during init, it is used.
26502650
Otherwise, the :attr:`type`
2651-
:meth:`~click.types.ParamType.shell_complete` function is used.
2651+
:meth:`~click.types.ParamType[t.Any].shell_complete` function is used.
26522652
26532653
:param ctx: Invocation context for this command.
26542654
:param incomplete: Value being completed. May be empty.
@@ -2749,7 +2749,7 @@ def __init__(
27492749
multiple: bool = False,
27502750
count: bool = False,
27512751
allow_from_autoenv: bool = True,
2752-
type: types.ParamType | t.Any | None = None,
2752+
type: types.ParamType[t.Any] | t.Any | None = None,
27532753
help: str | None = None,
27542754
hidden: bool = False,
27552755
show_choices: bool = True,
@@ -2825,7 +2825,7 @@ def __init__(
28252825
if type is None:
28262826
# A flag without a flag_value is a boolean flag.
28272827
if flag_value is UNSET:
2828-
self.type: types.ParamType = types.BoolParamType()
2828+
self.type: types.ParamType[t.Any] = types.BoolParamType()
28292829
# If the flag value is a boolean, use BoolParamType.
28302830
elif isinstance(flag_value, bool):
28312831
self.type = types.BoolParamType()

src/click/termui.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _build_prompt(
6363
show_default: bool | str = False,
6464
default: t.Any | None = None,
6565
show_choices: bool = True,
66-
type: ParamType | None = None,
66+
type: ParamType[t.Any] | None = None,
6767
) -> str:
6868
prompt = text
6969
if type is not None and show_choices and isinstance(type, Choice):
@@ -87,7 +87,7 @@ def prompt(
8787
default: t.Any | None = None,
8888
hide_input: bool = False,
8989
confirmation_prompt: bool | str = False,
90-
type: ParamType | t.Any | None = None,
90+
type: ParamType[t.Any] | t.Any | None = None,
9191
value_proc: t.Callable[[str], t.Any] | None = None,
9292
prompt_suffix: str = ": ",
9393
show_default: bool | str = True,

0 commit comments

Comments
 (0)