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
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ Unreleased
allows the user to search for future output of the generator when
using less and then aborting the program using ctrl-c.

- ``deprecated: bool | str`` can now be used on options and arguments. This
previously was only available for ``Command``. The message can now also be
customised by using a ``str`` instead of a ``bool``. :issue:`2263` :pr:`2271`

- ``Command.deprecated`` formatting in ``--help`` changed from
``(Deprecated) help`` to ``help (DEPRECATED)``.
- Parameters cannot be required nor prompted or an error is raised.
- A warning will be printed when something deprecated is used.


Version 8.1.8
-------------

Expand Down
100 changes: 91 additions & 9 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,12 +856,15 @@ class Command:
If enabled this will add ``--help`` as argument
if no arguments are passed
:param hidden: hide this command from help outputs.

:param deprecated: issues a message indicating that
the command is deprecated.
:param deprecated: If ``True`` or non-empty string, issues a message
indicating that the command is deprecated and highlights
its deprecation in --help. The message can be customized
by using a string as the value.

.. versionchanged:: 8.2
This is the base class for all commands, not ``BaseCommand``.
``deprecated`` can be set to a string as well to customize the
deprecation message.

.. versionchanged:: 8.1
``help``, ``epilog``, and ``short_help`` are stored unprocessed,
Expand Down Expand Up @@ -905,7 +908,7 @@ def __init__(
add_help_option: bool = True,
no_args_is_help: bool = False,
hidden: bool = False,
deprecated: bool = False,
deprecated: bool | str = False,
) -> None:
#: the name the command thinks it has. Upon registering a command
#: on a :class:`Group` the group will default the command name
Expand Down Expand Up @@ -1059,7 +1062,14 @@ def get_short_help_str(self, limit: int = 45) -> str:
text = ""

if self.deprecated:
text = _("(Deprecated) {text}").format(text=text)
deprecated_message = (
f"(DEPRECATED: {self.deprecated})"
if isinstance(self.deprecated, str)
else "(DEPRECATED)"
)
text = _("{text} {deprecated_message}").format(
text=text, deprecated_message=deprecated_message
)

return text.strip()

Expand Down Expand Up @@ -1089,7 +1099,14 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
text = ""

if self.deprecated:
text = _("(Deprecated) {text}").format(text=text)
deprecated_message = (
f"(DEPRECATED: {self.deprecated})"
if isinstance(self.deprecated, str)
else "(DEPRECATED)"
)
text = _("{text} {deprecated_message}").format(
text=text, deprecated_message=deprecated_message
)

if text:
formatter.write_paragraph()
Expand Down Expand Up @@ -1183,9 +1200,13 @@ def invoke(self, ctx: Context) -> t.Any:
in the right way.
"""
if self.deprecated:
extra_message = (
f" {self.deprecated}" if isinstance(self.deprecated, str) else ""
)
message = _(
"DeprecationWarning: The command {name!r} is deprecated."
).format(name=self.name)
"{extra_message}"
).format(name=self.name, extra_message=extra_message)
echo(style(message, fg="red"), err=True)

if self.callback is not None:
Expand Down Expand Up @@ -1988,6 +2009,18 @@ class Parameter:
given. Takes ``ctx, param, incomplete`` and must return a list
of :class:`~click.shell_completion.CompletionItem` or a list of
strings.
:param deprecated: If ``True`` or non-empty string, issues a message
indicating that the argument is deprecated and highlights
its deprecation in --help. The message can be customized
by using a string as the value. A deprecated parameter
cannot be required, a ValueError will be raised otherwise.

.. versionchanged:: 8.2.0
Introduction of ``deprecated``.

.. versionchanged:: 8.2
Adding duplicate parameter names to a :class:`~click.core.Command` will
result in a ``UserWarning`` being shown.

.. versionchanged:: 8.2
Adding duplicate parameter names to a :class:`~click.core.Command` will
Expand Down Expand Up @@ -2044,6 +2077,7 @@ def __init__(
[Context, Parameter, str], list[CompletionItem] | list[str]
]
| None = None,
deprecated: bool | str = False,
) -> None:
self.name: str | None
self.opts: list[str]
Expand Down Expand Up @@ -2071,6 +2105,7 @@ def __init__(
self.metavar = metavar
self.envvar = envvar
self._custom_shell_complete = shell_complete
self.deprecated = deprecated

if __debug__:
if self.type.is_composite and nargs != self.type.arity:
Expand Down Expand Up @@ -2113,6 +2148,13 @@ def __init__(
f"'default' {subject} must match nargs={nargs}."
)

if required and deprecated:
raise ValueError(
f"The {self.param_type_name} '{self.human_readable_name}' "
"is deprecated and still required. A deprecated "
f"{self.param_type_name} cannot be required."
)

def to_info_dict(self) -> dict[str, t.Any]:
"""Gather information that could be useful for a tool generating
user-facing documentation.
Expand Down Expand Up @@ -2332,6 +2374,29 @@ def handle_parse_result(
) -> tuple[t.Any, list[str]]:
with augment_usage_errors(ctx, param=self):
value, source = self.consume_value(ctx, opts)

if (
self.deprecated
and value is not None
and source
not in (
ParameterSource.DEFAULT,
ParameterSource.DEFAULT_MAP,
)
):
extra_message = (
f" {self.deprecated}" if isinstance(self.deprecated, str) else ""
)
message = _(
"DeprecationWarning: The {param_type} {name!r} is deprecated."
"{extra_message}"
).format(
param_type=self.param_type_name,
name=self.human_readable_name,
extra_message=extra_message,
)
echo(style(message, fg="red"), err=True)

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

try:
Expand Down Expand Up @@ -2402,7 +2467,8 @@ class Option(Parameter):
Normally, environment variables are not shown.
:param prompt: If set to ``True`` or a non empty string then the
user will be prompted for input. If set to ``True`` the prompt
will be the option name capitalized.
will be the option name capitalized. A deprecated option cannot be
prompted.
:param confirmation_prompt: Prompt a second time to confirm the
value if it was prompted for. Can be set to a string instead of
``True`` to customize the message.
Expand Down Expand Up @@ -2469,13 +2535,16 @@ def __init__(
hidden: bool = False,
show_choices: bool = True,
show_envvar: bool = False,
deprecated: bool | str = False,
**attrs: t.Any,
) -> None:
if help:
help = inspect.cleandoc(help)

default_is_missing = "default" not in attrs
super().__init__(param_decls, type=type, multiple=multiple, **attrs)
super().__init__(
param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs
)

if prompt is True:
if self.name is None:
Expand All @@ -2487,6 +2556,14 @@ def __init__(
else:
prompt_text = prompt

if deprecated:
deprecated_message = (
f"(DEPRECATED: {deprecated})"
if isinstance(deprecated, str)
else "(DEPRECATED)"
)
help = help + deprecated_message if help is not None else deprecated_message

self.prompt = prompt_text
self.confirmation_prompt = confirmation_prompt
self.prompt_required = prompt_required
Expand Down Expand Up @@ -2548,6 +2625,9 @@ def __init__(
self.show_envvar = show_envvar

if __debug__:
if deprecated and prompt:
raise ValueError("`deprecated` options cannot use `prompt`.")

if self.nargs == -1:
raise TypeError("nargs=-1 is not supported for options.")

Expand Down Expand Up @@ -2983,6 +3063,8 @@ def make_metavar(self) -> str:
var = self.type.get_metavar(self)
if not var:
var = self.name.upper() # type: ignore
if self.deprecated:
var += "!"
if not self.required:
var = f"[{var}]"
if self.nargs != 1:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,44 @@ def cli(f):
assert result.output == "test\n"


def test_deprecated_usage(runner):
@click.command()
@click.argument("f", required=False, deprecated=True)
def cli(f):
click.echo(f)

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "[F!]" in result.output


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_warning(runner, deprecated):
@click.command()
@click.argument(
"my-argument", required=False, deprecated=deprecated, default="default argument"
)
def cli(my_argument: str):
click.echo(f"{my_argument}")

# defaults should not give a deprecated warning
result = runner.invoke(cli, [])
assert result.exit_code == 0, result.output
assert "is deprecated" not in result.output

result = runner.invoke(cli, ["hello"])
assert result.exit_code == 0, result.output
assert "argument 'MY_ARGUMENT' is deprecated" in result.output

if isinstance(deprecated, str):
assert deprecated in result.output


def test_deprecated_required(runner):
with pytest.raises(ValueError, match="is deprecated and still required"):
click.Argument(["a"], required=True, deprecated=True)


def test_eat_options(runner):
@click.command()
@click.option("-f")
Expand Down
18 changes: 13 additions & 5 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,23 +318,31 @@ def cli(verbose, args):


@pytest.mark.parametrize("doc", ["CLI HELP", None])
def test_deprecated_in_help_messages(runner, doc):
@click.command(deprecated=True, help=doc)
@pytest.mark.parametrize("deprecated", [True, "USE OTHER COMMAND INSTEAD"])
def test_deprecated_in_help_messages(runner, doc, deprecated):
@click.command(deprecated=deprecated, help=doc)
def cli():
pass

result = runner.invoke(cli, ["--help"])
assert "(Deprecated)" in result.output
assert "(DEPRECATED" in result.output

if isinstance(deprecated, str):
assert deprecated in result.output

def test_deprecated_in_invocation(runner):
@click.command(deprecated=True)

@pytest.mark.parametrize("deprecated", [True, "USE OTHER COMMAND INSTEAD"])
def test_deprecated_in_invocation(runner, deprecated):
@click.command(deprecated=deprecated)
def deprecated_cmd():
pass

result = runner.invoke(deprecated_cmd)
assert "DeprecationWarning:" in result.output

if isinstance(deprecated, str):
assert deprecated in result.output


def test_command_parse_args_collects_option_prefixes():
@click.command()
Expand Down
46 changes: 46 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,52 @@ def test_invalid_option(runner):
assert "'--foo'" in message


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_usage(runner, deprecated):
@click.command()
@click.option("--foo", default="bar", deprecated=deprecated)
def cmd(foo):
click.echo(foo)

result = runner.invoke(cmd, ["--help"])
assert "(DEPRECATED" in result.output

if isinstance(deprecated, str):
assert deprecated in result.output


@pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"])
def test_deprecated_warning(runner, deprecated):
@click.command()
@click.option(
"--my-option", required=False, deprecated=deprecated, default="default option"
)
def cli(my_option: str):
click.echo(f"{my_option}")

# defaults should not give a deprecated warning
result = runner.invoke(cli, [])
assert result.exit_code == 0, result.output
assert "is deprecated" not in result.output

result = runner.invoke(cli, ["--my-option", "hello"])
assert result.exit_code == 0, result.output
assert "option 'my_option' is deprecated" in result.output

if isinstance(deprecated, str):
assert deprecated in result.output


def test_deprecated_required(runner):
with pytest.raises(ValueError, match="is deprecated and still required"):
click.Option(["--a"], required=True, deprecated=True)


def test_deprecated_prompt(runner):
with pytest.raises(ValueError, match="`deprecated` options cannot use `prompt`"):
click.Option(["--a"], prompt=True, deprecated=True)


def test_invalid_nargs(runner):
with pytest.raises(TypeError, match="nargs=-1"):

Expand Down