Skip to content

Commit b02fdf9

Browse files
Parameter typing improvements
Co-authored-by: Kevin Deldycke <kevin@deldycke.com>
1 parent 25edc1e commit b02fdf9

3 files changed

Lines changed: 43 additions & 35 deletions

File tree

CHANGES.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ Unreleased
1717
``dict[str, Any]``.
1818
- :class:`CompositeParamType` and the number-range base are now
1919
generic with abstract methods.
20+
- :class:`Parameter` typing improvements. :pr:`2805`
21+
22+
- :class:`Parameter` is now an abstract base class, making explicit
23+
that it cannot be instantiated directly.
24+
- :attr:`Parameter.name` is now ``str`` instead of ``str | None``.
25+
When ``expose_value=False``, the name is set to ``""`` instead
26+
of ``None``.
27+
- The ``ctx`` parameter of :meth:`Parameter.get_error_hint` is now
28+
typed as ``Context | None``, matching the runtime behavior.
2029
- Split string values from ``default_map`` for parameters with ``nargs > 1``
2130
or :class:`Tuple` type, matching environment variable behavior.
2231
:issue:`2745` :pr:`3364`

src/click/core.py

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import os
88
import sys
99
import typing as t
10+
from abc import ABC
11+
from abc import abstractmethod
1012
from collections import abc
1113
from collections import Counter
1214
from contextlib import AbstractContextManager
@@ -838,9 +840,7 @@ def invoke(
838840
# https://github.com/pallets/click/pull/3068
839841
if default_value is UNSET:
840842
default_value = None
841-
kwargs[param.name] = param.type_cast_value( # type: ignore
842-
ctx, default_value
843-
)
843+
kwargs[param.name] = param.type_cast_value(ctx, default_value)
844844

845845
# Track all kwargs as params, so that forward() will pass
846846
# them on in subsequent calls.
@@ -1320,7 +1320,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
13201320
or param.hidden
13211321
or (
13221322
not param.multiple
1323-
and ctx.get_parameter_source(param.name) # type: ignore
1323+
and ctx.get_parameter_source(param.name)
13241324
is ParameterSource.COMMANDLINE
13251325
)
13261326
):
@@ -2053,7 +2053,7 @@ def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]:
20532053
return iter(value)
20542054

20552055

2056-
class Parameter:
2056+
class Parameter(ABC):
20572057
r"""A parameter to a command comes in two versions: they are either
20582058
:class:`Option`\s or :class:`Argument`\s. Other subclasses are currently
20592059
not supported by design as some of the internals for parsing are
@@ -2175,7 +2175,7 @@ def __init__(
21752175
| None = None,
21762176
deprecated: bool | str = False,
21772177
) -> None:
2178-
self.name: str | None
2178+
self.name: str
21792179
self.opts: list[str]
21802180
self.secondary_opts: list[str]
21812181
self.name, self.opts, self.secondary_opts = self._parse_decls(
@@ -2248,17 +2248,17 @@ def to_info_dict(self) -> dict[str, t.Any]:
22482248
def __repr__(self) -> str:
22492249
return f"<{self.__class__.__name__} {self.name}>"
22502250

2251+
@abstractmethod
22512252
def _parse_decls(
22522253
self, decls: cabc.Sequence[str], expose_value: bool
2253-
) -> tuple[str | None, list[str], list[str]]:
2254-
raise NotImplementedError()
2254+
) -> tuple[str, list[str], list[str]]: ...
22552255

22562256
@property
22572257
def human_readable_name(self) -> str:
22582258
"""Returns the human readable name of this parameter. This is the
22592259
same as the name for options, but the metavar for arguments.
22602260
"""
2261-
return self.name # type: ignore
2261+
return self.name
22622262

22632263
def make_metavar(self, ctx: Context) -> str:
22642264
if self.metavar is not None:
@@ -2307,19 +2307,18 @@ def get_default(
23072307
.. versionchanged:: 8.0
23082308
Added the ``call`` parameter.
23092309
"""
2310-
name = self.name
2311-
value = ctx.lookup_default(name, call=False) if name is not None else None
2310+
value = ctx.lookup_default(self.name, call=False)
23122311

2313-
if value is None and not ctx._default_map_has(name):
2312+
if value is None and not ctx._default_map_has(self.name):
23142313
value = self.default
23152314

23162315
if call and callable(value):
23172316
value = value()
23182317

23192318
return value
23202319

2321-
def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
2322-
raise NotImplementedError()
2320+
@abstractmethod
2321+
def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: ...
23232322

23242323
def consume_value(
23252324
self, ctx: Context, opts: cabc.Mapping[str, t.Any]
@@ -2335,7 +2334,7 @@ def consume_value(
23352334
:meta private:
23362335
"""
23372336
# Collect from the parse the value passed by the user to the CLI.
2338-
value = opts.get(self.name, UNSET) # type: ignore
2337+
value = opts.get(self.name, UNSET)
23392338
# If the value is set, it means it was sourced from the command line by the
23402339
# parser, otherwise it left unset by default.
23412340
source = (
@@ -2351,7 +2350,7 @@ def consume_value(
23512350
source = ParameterSource.ENVIRONMENT
23522351

23532352
if value is UNSET:
2354-
default_map_value = ctx.lookup_default(self.name) # type: ignore[arg-type]
2353+
default_map_value = ctx.lookup_default(self.name)
23552354
if default_map_value is not None or ctx._default_map_has(self.name):
23562355
value = default_map_value
23572356
source = ParameterSource.DEFAULT_MAP
@@ -2587,7 +2586,7 @@ def handle_parse_result(
25872586
with augment_usage_errors(ctx, param=self):
25882587
value, source = self.consume_value(ctx, opts)
25892588

2590-
ctx.set_parameter_source(self.name, source) # type: ignore
2589+
ctx.set_parameter_source(self.name, source)
25912590

25922591
# Display a deprecation warning if necessary.
25932592
if (
@@ -2627,24 +2626,22 @@ def handle_parse_result(
26272626
# the same name to override each other.
26282627
and (self.name not in ctx.params or ctx.params[self.name] is UNSET)
26292628
):
2630-
# Click is logically enforcing that the name is None if the parameter is
2631-
# not to be exposed. We still assert it here to please the type checker.
2632-
assert self.name is not None, (
2633-
f"{self!r} parameter's name should not be None when exposing value."
2634-
)
26352629
ctx.params[self.name] = value
26362630

26372631
return value, args
26382632

26392633
def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
2640-
pass
2634+
return None
26412635

26422636
def get_usage_pieces(self, ctx: Context) -> list[str]:
26432637
return []
26442638

2645-
def get_error_hint(self, ctx: Context) -> str:
2639+
def get_error_hint(self, ctx: Context | None) -> str:
26462640
"""Get a stringified version of the param for use in error messages to
26472641
indicate which param caused the error.
2642+
2643+
.. versionchanged:: 8.4.0
2644+
``ctx`` can be ``None``.
26482645
"""
26492646
hint_list = self.opts or [self.human_readable_name]
26502647
return " / ".join(f"'{x}'" for x in hint_list)
@@ -2970,15 +2967,15 @@ def get_default(
29702967

29712968
return value
29722969

2973-
def get_error_hint(self, ctx: Context) -> str:
2970+
def get_error_hint(self, ctx: Context | None) -> str:
29742971
result = super().get_error_hint(ctx)
29752972
if self.show_envvar and self.envvar is not None:
29762973
result += f" (env var: '{self.envvar}')"
29772974
return result
29782975

29792976
def _parse_decls(
29802977
self, decls: cabc.Sequence[str], expose_value: bool
2981-
) -> tuple[str | None, list[str], list[str]]:
2978+
) -> tuple[str, list[str], list[str]]:
29822979
opts = []
29832980
secondary_opts = []
29842981
name = None
@@ -3017,7 +3014,7 @@ def _parse_decls(
30173014

30183015
if name is None:
30193016
if not expose_value:
3020-
return None, opts, secondary_opts
3017+
return "", opts, secondary_opts
30213018
raise TypeError(
30223019
f"Could not determine name for option with declarations {decls!r}"
30233020
)
@@ -3433,14 +3430,14 @@ def __init__(
34333430
def human_readable_name(self) -> str:
34343431
if self.metavar is not None:
34353432
return self.metavar
3436-
return self.name.upper() # type: ignore
3433+
return self.name.upper()
34373434

34383435
def make_metavar(self, ctx: Context) -> str:
34393436
if self.metavar is not None:
34403437
return self.metavar
34413438
var = self.type.get_metavar(param=self, ctx=ctx)
34423439
if not var:
3443-
var = self.name.upper() # type: ignore
3440+
var = self.name.upper()
34443441
if self.deprecated:
34453442
var += "!"
34463443
if not self.required:
@@ -3451,10 +3448,10 @@ def make_metavar(self, ctx: Context) -> str:
34513448

34523449
def _parse_decls(
34533450
self, decls: cabc.Sequence[str], expose_value: bool
3454-
) -> tuple[str | None, list[str], list[str]]:
3451+
) -> tuple[str, list[str], list[str]]:
34553452
if not decls:
34563453
if not expose_value:
3457-
return None, [], []
3454+
return "", [], []
34583455
raise TypeError("Argument is marked as exposed, but does not have a name.")
34593456
if len(decls) == 1:
34603457
name = arg = decls[0]
@@ -3469,8 +3466,10 @@ def _parse_decls(
34693466
def get_usage_pieces(self, ctx: Context) -> list[str]:
34703467
return [self.make_metavar(ctx)]
34713468

3472-
def get_error_hint(self, ctx: Context) -> str:
3473-
return f"'{self.make_metavar(ctx)}'"
3469+
def get_error_hint(self, ctx: Context | None) -> str:
3470+
if ctx is not None:
3471+
return f"'{self.make_metavar(ctx)}'"
3472+
return f"'{self.human_readable_name}'"
34743473

34753474
def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
34763475
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)

src/click/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def format_message(self) -> str:
129129
if self.param_hint is not None:
130130
param_hint = self.param_hint
131131
elif self.param is not None:
132-
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
132+
param_hint = self.param.get_error_hint(self.ctx)
133133
else:
134134
return _("Invalid value: {message}").format(message=self.message)
135135

@@ -165,7 +165,7 @@ def format_message(self) -> str:
165165
if self.param_hint is not None:
166166
param_hint: cabc.Sequence[str] | str | None = self.param_hint
167167
elif self.param is not None:
168-
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
168+
param_hint = self.param.get_error_hint(self.ctx)
169169
else:
170170
param_hint = None
171171

0 commit comments

Comments
 (0)