Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
.. currentmodule:: click

Version 8.4.0
-------------

Unreleased

- :class:`ParamType` typing improvements. :pr:`3371`

- :class:`ParamType` is now a generic abstract base class,
parameterized by its converted value type.
- :meth:`~ParamType.convert` return types are narrowed on all
concrete types (``str`` for :class:`STRING`, ``int`` for
:class:`INT`, etc.).
- :meth:`~ParamType.to_info_dict` returns specific
:class:`~typing.TypedDict` subclasses instead of
``dict[str, Any]``.
- :class:`CompositeParamType` and the number-range base are now
generic with abstract methods.

Version 8.3.3
-------------

Expand Down
23 changes: 21 additions & 2 deletions docs/parameter-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ The resulting value from an option will always be one of the originally passed c
regardless of `case_sensitive`.
```

```{versionchanged} 8.4.0
{class}`Choice` is now generic. Parameterize it with the choice value type
({class}`!Choice[HashType]` for an enum, {class}`!Choice[str]` for plain
strings) to enable type-checked consumers.
```

(ranges)=

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

{class}`ParamType` is generic in the converted value type: parameterize it with
the type returned by `convert` so that consumers (and type checkers) can rely
on the narrowed return type.

The following code implements an integer type that accepts hex and octal numbers in addition to normal integers, and
converts them into regular integers.

```python
import click

class BasedIntParamType(click.ParamType):

class BasedIntParamType(click.ParamType[int]):
name = "integer"

def convert(self, value, param, ctx):
def convert(self, value, param, ctx) -> int:
if isinstance(value, int):
return value

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


BASED_INT = BasedIntParamType()
```

Expand All @@ -184,3 +196,10 @@ conversion fails. The `param` and `ctx` arguments may be `None` in some cases su
Values from user input or the command line will be strings, but default values and Python arguments may already be the
correct type. The custom type should check at the top if the value is already valid and pass it through to support those
cases.

```{versionchanged} 8.4.0
{class}`ParamType` is now a generic abstract base class. Parameterize it with
the converted value type ({class}`!ParamType[int]` for an integer-returning
type) so that {meth}`~ParamType.convert` and downstream consumers carry the
narrowed type.
```
2 changes: 1 addition & 1 deletion docs/shell-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ indicate special handling for paths, and `help` for shells that support showing
In this example, the type will suggest environment variables that start with the incomplete value.

```python
class EnvVarType(ParamType):
class EnvVarType(ParamType[str]):
name = "envvar"

def shell_complete(self, ctx, param, incomplete):
Expand Down
2 changes: 1 addition & 1 deletion docs/support-multiple-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def add_ctx_arg(f: F) -> F:
Here's an example ``ParamType`` subclass which uses this:

```python
class CommaDelimitedString(click.ParamType):
class CommaDelimitedString(click.ParamType[str]):
@add_ctx_arg
def get_metavar(self, param: click.Parameter, ctx: click.Context | None) -> str:
return "TEXT,TEXT,..."
Expand Down
4 changes: 2 additions & 2 deletions examples/validation/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ def validate_count(ctx, param, value):
return value


class URL(click.ParamType):
class URL(click.ParamType[urlparse.ParseResult]):
name = "url"

def convert(self, value, param, ctx):
def convert(self, value, param, ctx) -> urlparse.ParseResult:
if not isinstance(value, tuple):
value = urlparse.urlparse(value)
if value.scheme not in ("http", "https"):
Expand Down
10 changes: 5 additions & 5 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2149,7 +2149,7 @@ class Parameter:
def __init__(
self,
param_decls: cabc.Sequence[str] | None = None,
type: types.ParamType | t.Any | None = None,
Comment thread
kdeldycke marked this conversation as resolved.
type: types.ParamType[t.Any] | t.Any | None = None,
required: bool = False,
# XXX The default historically embed two concepts:
# - the declaration of a Parameter object carrying the default (handy to
Expand Down Expand Up @@ -2181,7 +2181,7 @@ def __init__(
self.name, self.opts, self.secondary_opts = self._parse_decls(
param_decls or (), expose_value
)
self.type: types.ParamType = types.convert_type(type, default)
self.type: types.ParamType[t.Any] = types.convert_type(type, default)

# Default nargs to what the type tells us if we have that
# information available.
Expand Down Expand Up @@ -2648,7 +2648,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
"""Return a list of completions for the incomplete value. If a
``shell_complete`` function was given during init, it is used.
Otherwise, the :attr:`type`
:meth:`~click.types.ParamType.shell_complete` function is used.
:meth:`~click.types.ParamType[t.Any].shell_complete` function is used.

:param ctx: Invocation context for this command.
:param incomplete: Value being completed. May be empty.
Expand Down Expand Up @@ -2749,7 +2749,7 @@ def __init__(
multiple: bool = False,
count: bool = False,
allow_from_autoenv: bool = True,
type: types.ParamType | t.Any | None = None,
type: types.ParamType[t.Any] | t.Any | None = None,
help: str | None = None,
hidden: bool = False,
show_choices: bool = True,
Expand Down Expand Up @@ -2825,7 +2825,7 @@ def __init__(
if type is None:
# A flag without a flag_value is a boolean flag.
if flag_value is UNSET:
self.type: types.ParamType = types.BoolParamType()
self.type: types.ParamType[t.Any] = types.BoolParamType()
# If the flag value is a boolean, use BoolParamType.
elif isinstance(flag_value, bool):
self.type = types.BoolParamType()
Expand Down
4 changes: 2 additions & 2 deletions src/click/termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _build_prompt(
show_default: bool | str = False,
default: t.Any | None = None,
show_choices: bool = True,
type: ParamType | None = None,
type: ParamType[t.Any] | None = None,
) -> str:
prompt = text
if type is not None and show_choices and isinstance(type, Choice):
Expand All @@ -87,7 +87,7 @@ def prompt(
default: t.Any | None = None,
hide_input: bool = False,
confirmation_prompt: bool | str = False,
type: ParamType | t.Any | None = None,
type: ParamType[t.Any] | t.Any | None = None,
value_proc: t.Callable[[str], t.Any] | None = None,
prompt_suffix: str = ": ",
show_default: bool | str = True,
Expand Down
Loading