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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Unreleased
behavior for multiple parameters using the same name. :issue:`3071` :pr:`3079`
- Fix rendering when ``prompt`` and ``confirm`` parameter ``prompt_suffix`` is
empty. :issue:`3019` :pr:`3021`
- When ``Sentinel.UNSET`` is found during parsing, it will skip calls to
``type_cast_value``. :issue:`3069` :pr:`3090`

Version 8.3.0
--------------
Expand Down
36 changes: 27 additions & 9 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,7 @@ def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any:
"""Convert and validate a value against the parameter's
:attr:`type`, :attr:`multiple`, and :attr:`nargs`.
"""
if value in (None, UNSET):
if value is None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this as it returns type_cast_value to its original code before my #3030 PR.

if self.multiple or self.nargs == -1:
return ()
else:
Expand Down Expand Up @@ -2421,7 +2421,16 @@ def process_value(self, ctx: Context, value: t.Any) -> t.Any:

:meta private:
"""
value = self.type_cast_value(ctx, value)
# shelter `type_cast_value` from ever seeing an `UNSET` value by handling the
# cases in which `UNSET` gets special treatment explicitly at this layer
#
# Refs:
# https://github.com/pallets/click/issues/3069
if value is UNSET:
if self.multiple or self.nargs == -1:
value = ()
else:
value = self.type_cast_value(ctx, value)

if self.required and self.value_is_missing(value):
raise MissingParameter(ctx=ctx, param=self)
Expand Down Expand Up @@ -3256,13 +3265,22 @@ def consume_value(

return value, source

def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any:
if self.is_flag and not self.required:
if value is UNSET:
if self.is_bool_flag:
# If the flag is a boolean flag, we return False if it is not set.
value = False
return super().type_cast_value(ctx, value)
def process_value(self, ctx: Context, value: t.Any) -> t.Any:
# process_value has to be overridden on Options in order to capture
# `value == UNSET` cases before `type_cast_value()` gets called.
#
# Refs:
# https://github.com/pallets/click/issues/3069
if self.is_flag and not self.required and self.is_bool_flag and value is UNSET:
value = False

if self.callback is not None:
value = self.callback(ctx, self, value)

return value

# in the normal case, rely on Parameter.process_value
return super().process_value(ctx, value)


class Argument(Parameter):
Expand Down
37 changes: 37 additions & 0 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,40 @@ def cli(one, two):

with pytest.warns(UserWarning):
runner.invoke(cli, [])


@pytest.mark.parametrize(
("argument_kwargs", "pass_argv"),
(
# there is a large potential parameter space to explore here
# this is just a very small sample of it
({}, ["myvalue"]),
({"nargs": -1}, []),
({"nargs": -1}, ["myvalue"]),
({"default": None}, ["myvalue"]),
({"required": False}, []),
({"required": False}, ["myvalue"]),
),
)
def test_argument_custom_class_can_override_type_cast_value_and_never_sees_unset(
runner, argument_kwargs, pass_argv
):
"""
Test that overriding type_cast_value is supported

In particular, the argument is never passed an UNSET sentinel value.
"""

class CustomArgument(click.Argument):
def type_cast_value(self, ctx, value):
assert value is not UNSET
return value

@click.command()
@click.argument("myarg", **argument_kwargs, cls=CustomArgument)
def cmd(myarg):
click.echo("ok")

result = runner.invoke(cmd, pass_argv)
assert not result.exception
assert result.exit_code == 0
36 changes: 36 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,42 @@ def cmd(testoption):
assert "you wont see me" not in result.output


@pytest.mark.parametrize(
("param_decl", "option_kwargs", "pass_argv"),
(
# there is a large potential parameter space to explore here
# this is just a very small sample of it
("--opt", {}, []),
("--opt", {"multiple": True}, []),
("--opt", {"is_flag": True}, []),
("--opt/--no-opt", {"is_flag": True, "default": None}, []),
("--req", {"is_flag": True, "required": True}, ["--req"]),
),
)
def test_option_custom_class_can_override_type_cast_value_and_never_sees_unset(
runner, param_decl, option_kwargs, pass_argv
):
"""
Test that overriding type_cast_value is supported

In particular, the option is never passed an UNSET sentinel value.
"""

class CustomOption(click.Option):
def type_cast_value(self, ctx, value):
assert value is not UNSET
return value

@click.command()
@click.option("myparam", param_decl, **option_kwargs, cls=CustomOption)
def cmd(myparam):
click.echo("ok")

result = runner.invoke(cmd, pass_argv)
assert not result.exception
assert result.exit_code == 0


def test_option_custom_class_reusable(runner):
"""Ensure we can reuse a custom class option. See Issue #926"""

Expand Down