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
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Unreleased
``dict[str, Any]``.
- :class:`CompositeParamType` and the number-range base are now
generic with abstract methods.
- :class:`Parameter` typing improvements. :pr:`2805`

- :class:`Parameter` is now an abstract base class, making explicit
that it cannot be instantiated directly.
- :attr:`Parameter.name` is now ``str`` instead of ``str | None``.
When ``expose_value=False``, the name is set to ``""`` instead
of ``None``.
- The ``ctx`` parameter of :meth:`Parameter.get_error_hint` is now
typed as ``Context | None``, matching the runtime behavior.
- Split string values from ``default_map`` for parameters with ``nargs > 1``
or :class:`Tuple` type, matching environment variable behavior.
:issue:`2745` :pr:`3364`
Expand Down
75 changes: 35 additions & 40 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import os
import sys
import typing as t
from abc import ABC
from abc import abstractmethod
from collections import abc
from collections import Counter
from contextlib import AbstractContextManager
Expand Down Expand Up @@ -838,9 +840,7 @@ def invoke(
# https://github.com/pallets/click/pull/3068
if default_value is UNSET:
default_value = None
kwargs[param.name] = param.type_cast_value( # type: ignore
ctx, default_value
)
kwargs[param.name] = param.type_cast_value(ctx, default_value)

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


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

@abstractmethod
def _parse_decls(
self, decls: cabc.Sequence[str], expose_value: bool
) -> tuple[str | None, list[str], list[str]]:
raise NotImplementedError()
) -> tuple[str, list[str], list[str]]: ...

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

def make_metavar(self, ctx: Context) -> str:
if self.metavar is not None:
Expand Down Expand Up @@ -2307,19 +2307,18 @@ def get_default(
.. versionchanged:: 8.0
Added the ``call`` parameter.
"""
name = self.name
value = ctx.lookup_default(name, call=False) if name is not None else None
value = ctx.lookup_default(self.name, call=False)

if value is None and not ctx._default_map_has(name):
if value is None and not ctx._default_map_has(self.name):
value = self.default

if call and callable(value):
value = value()

return value

def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
raise NotImplementedError()
@abstractmethod
def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: ...

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

if value is UNSET:
default_map_value = ctx.lookup_default(self.name) # type: ignore[arg-type]
default_map_value = ctx.lookup_default(self.name)
if default_map_value is not None or ctx._default_map_has(self.name):
value = default_map_value
source = ParameterSource.DEFAULT_MAP
Expand Down Expand Up @@ -2587,7 +2586,7 @@ def handle_parse_result(
with augment_usage_errors(ctx, param=self):
value, source = self.consume_value(ctx, opts)

ctx.set_parameter_source(self.name, source) # type: ignore
ctx.set_parameter_source(self.name, source)

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

return value, args

def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
pass
Comment thread
kdeldycke marked this conversation as resolved.
return None

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

def get_error_hint(self, ctx: Context) -> str:
def get_error_hint(self, ctx: Context | None) -> str:
"""Get a stringified version of the param for use in error messages to
indicate which param caused the error.

.. versionchanged:: 8.4.0
``ctx`` can be ``None``.
"""
hint_list = self.opts or [self.human_readable_name]
return " / ".join(f"'{x}'" for x in hint_list)
Expand Down Expand Up @@ -2776,7 +2773,7 @@ def __init__(
)

if prompt is True:
if self.name is None:
if not self.name:
raise TypeError("'name' is required with 'prompt=True'.")

prompt_text: str | None = self.name.replace("_", " ").capitalize()
Expand Down Expand Up @@ -2970,15 +2967,15 @@ def get_default(

return value

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

def _parse_decls(
self, decls: cabc.Sequence[str], expose_value: bool
) -> tuple[str | None, list[str], list[str]]:
) -> tuple[str, list[str], list[str]]:
opts = []
secondary_opts = []
name = None
Expand Down Expand Up @@ -3017,7 +3014,7 @@ def _parse_decls(

if name is None:
if not expose_value:
return None, opts, secondary_opts
return "", opts, secondary_opts
raise TypeError(
f"Could not determine name for option with declarations {decls!r}"
)
Expand Down Expand Up @@ -3125,7 +3122,7 @@ def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra:
if (
self.allow_from_autoenv
and ctx.auto_envvar_prefix is not None
and self.name is not None
and self.name
):
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"

Expand Down Expand Up @@ -3264,11 +3261,7 @@ def resolve_envvar_value(self, ctx: Context) -> str | None:
if rv is not None:
return rv

if (
self.allow_from_autoenv
and ctx.auto_envvar_prefix is not None
and self.name is not None
):
if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None and self.name:
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
rv = os.environ.get(envvar)

Expand Down Expand Up @@ -3433,14 +3426,14 @@ def __init__(
def human_readable_name(self) -> str:
if self.metavar is not None:
return self.metavar
return self.name.upper() # type: ignore
return self.name.upper()

def make_metavar(self, ctx: Context) -> str:
if self.metavar is not None:
return self.metavar
var = self.type.get_metavar(param=self, ctx=ctx)
if not var:
var = self.name.upper() # type: ignore
var = self.name.upper()
if self.deprecated:
var += "!"
if not self.required:
Expand All @@ -3451,10 +3444,10 @@ def make_metavar(self, ctx: Context) -> str:

def _parse_decls(
self, decls: cabc.Sequence[str], expose_value: bool
) -> tuple[str | None, list[str], list[str]]:
) -> tuple[str, list[str], list[str]]:
if not decls:
if not expose_value:
return None, [], []
return "", [], []
raise TypeError("Argument is marked as exposed, but does not have a name.")
if len(decls) == 1:
name = arg = decls[0]
Expand All @@ -3469,8 +3462,10 @@ def _parse_decls(
def get_usage_pieces(self, ctx: Context) -> list[str]:
return [self.make_metavar(ctx)]

def get_error_hint(self, ctx: Context) -> str:
return f"'{self.make_metavar(ctx)}'"
def get_error_hint(self, ctx: Context | None) -> str:
if ctx is not None:
return f"'{self.make_metavar(ctx)}'"
return f"'{self.human_readable_name}'"

def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
Expand Down
4 changes: 2 additions & 2 deletions src/click/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def format_message(self) -> str:
if self.param_hint is not None:
param_hint = self.param_hint
elif self.param is not None:
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
param_hint = self.param.get_error_hint(self.ctx)
else:
return _("Invalid value: {message}").format(message=self.message)

Expand Down Expand Up @@ -165,7 +165,7 @@ def format_message(self) -> str:
if self.param_hint is not None:
param_hint: cabc.Sequence[str] | str | None = self.param_hint
elif self.param is not None:
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
param_hint = self.param.get_error_hint(self.ctx)
else:
param_hint = None

Expand Down
2 changes: 0 additions & 2 deletions src/click/shell_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,6 @@ def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
if not isinstance(param, Argument):
return False

assert param.name is not None
# Will be None if expose_value is False.
value = ctx.params.get(param.name)
return (
param.nargs == -1
Expand Down