Skip to content

Commit 5bd448d

Browse files
Parameter typing improvements
Co-authored-by: Kevin Deldycke <kevin@deldycke.com>
1 parent 8bd8b4a commit 5bd448d

4 files changed

Lines changed: 50 additions & 35 deletions

File tree

CHANGES.rst

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

3+
Version 8.4.0
4+
-------------
5+
6+
Unreleased
7+
8+
- :class:`Parameter` typing improvements. :pr:`2805`
9+
10+
- :class:`Parameter` is now an abstract base class, making explicit
11+
that it cannot be instantiated directly.
12+
- :attr:`Parameter.name` is now ``str`` instead of ``str | None``.
13+
When ``expose_value=False``, the name is set to ``""`` instead
14+
of ``None``.
15+
- The ``ctx`` parameter of :meth:`Parameter.get_error_hint` is now
16+
typed as ``Context | None``, matching the runtime behavior.
17+
318
Version 8.3.3
419
-------------
520

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
@@ -2582,7 +2581,7 @@ def handle_parse_result(
25822581
with augment_usage_errors(ctx, param=self):
25832582
value, source = self.consume_value(ctx, opts)
25842583

2585-
ctx.set_parameter_source(self.name, source) # type: ignore
2584+
ctx.set_parameter_source(self.name, source)
25862585

25872586
# Display a deprecation warning if necessary.
25882587
if (
@@ -2622,24 +2621,22 @@ def handle_parse_result(
26222621
# the same name to override each other.
26232622
and (self.name not in ctx.params or ctx.params[self.name] is UNSET)
26242623
):
2625-
# Click is logically enforcing that the name is None if the parameter is
2626-
# not to be exposed. We still assert it here to please the type checker.
2627-
assert self.name is not None, (
2628-
f"{self!r} parameter's name should not be None when exposing value."
2629-
)
26302624
ctx.params[self.name] = value
26312625

26322626
return value, args
26332627

26342628
def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
2635-
pass
2629+
return None
26362630

26372631
def get_usage_pieces(self, ctx: Context) -> list[str]:
26382632
return []
26392633

2640-
def get_error_hint(self, ctx: Context) -> str:
2634+
def get_error_hint(self, ctx: Context | None) -> str:
26412635
"""Get a stringified version of the param for use in error messages to
26422636
indicate which param caused the error.
2637+
2638+
.. versionchanged:: 8.4.0
2639+
``ctx`` can be ``None``.
26432640
"""
26442641
hint_list = self.opts or [self.human_readable_name]
26452642
return " / ".join(f"'{x}'" for x in hint_list)
@@ -2946,15 +2943,15 @@ def get_default(
29462943

29472944
return value
29482945

2949-
def get_error_hint(self, ctx: Context) -> str:
2946+
def get_error_hint(self, ctx: Context | None) -> str:
29502947
result = super().get_error_hint(ctx)
29512948
if self.show_envvar and self.envvar is not None:
29522949
result += f" (env var: '{self.envvar}')"
29532950
return result
29542951

29552952
def _parse_decls(
29562953
self, decls: cabc.Sequence[str], expose_value: bool
2957-
) -> tuple[str | None, list[str], list[str]]:
2954+
) -> tuple[str, list[str], list[str]]:
29582955
opts = []
29592956
secondary_opts = []
29602957
name = None
@@ -2993,7 +2990,7 @@ def _parse_decls(
29932990

29942991
if name is None:
29952992
if not expose_value:
2996-
return None, opts, secondary_opts
2993+
return "", opts, secondary_opts
29972994
raise TypeError(
29982995
f"Could not determine name for option with declarations {decls!r}"
29992996
)
@@ -3409,14 +3406,14 @@ def __init__(
34093406
def human_readable_name(self) -> str:
34103407
if self.metavar is not None:
34113408
return self.metavar
3412-
return self.name.upper() # type: ignore
3409+
return self.name.upper()
34133410

34143411
def make_metavar(self, ctx: Context) -> str:
34153412
if self.metavar is not None:
34163413
return self.metavar
34173414
var = self.type.get_metavar(param=self, ctx=ctx)
34183415
if not var:
3419-
var = self.name.upper() # type: ignore
3416+
var = self.name.upper()
34203417
if self.deprecated:
34213418
var += "!"
34223419
if not self.required:
@@ -3427,10 +3424,10 @@ def make_metavar(self, ctx: Context) -> str:
34273424

34283425
def _parse_decls(
34293426
self, decls: cabc.Sequence[str], expose_value: bool
3430-
) -> tuple[str | None, list[str], list[str]]:
3427+
) -> tuple[str, list[str], list[str]]:
34313428
if not decls:
34323429
if not expose_value:
3433-
return None, [], []
3430+
return "", [], []
34343431
raise TypeError("Argument is marked as exposed, but does not have a name.")
34353432
if len(decls) == 1:
34363433
name = arg = decls[0]
@@ -3445,8 +3442,10 @@ def _parse_decls(
34453442
def get_usage_pieces(self, ctx: Context) -> list[str]:
34463443
return [self.make_metavar(ctx)]
34473444

3448-
def get_error_hint(self, ctx: Context) -> str:
3449-
return f"'{self.make_metavar(ctx)}'"
3445+
def get_error_hint(self, ctx: Context | None) -> str:
3446+
if ctx is not None:
3447+
return f"'{self.make_metavar(ctx)}'"
3448+
return f"'{self.human_readable_name}'"
34503449

34513450
def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
34523451
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
@@ -125,7 +125,7 @@ def format_message(self) -> str:
125125
if self.param_hint is not None:
126126
param_hint = self.param_hint
127127
elif self.param is not None:
128-
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
128+
param_hint = self.param.get_error_hint(self.ctx)
129129
else:
130130
return _("Invalid value: {message}").format(message=self.message)
131131

@@ -161,7 +161,7 @@ def format_message(self) -> str:
161161
if self.param_hint is not None:
162162
param_hint: cabc.Sequence[str] | str | None = self.param_hint
163163
elif self.param is not None:
164-
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
164+
param_hint = self.param.get_error_hint(self.ctx)
165165
else:
166166
param_hint = None
167167

tests/test_imports.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def tracking_import(module, locals=None, globals=None, fromlist=None,
2727

2828
ALLOWED_IMPORTS = {
2929
"__future__",
30+
"abc",
3031
"codecs",
3132
"collections",
3233
"collections.abc",

0 commit comments

Comments
 (0)