Skip to content

Commit

Permalink
Introduce Parameter.deprecated + Command.deprecated message customiza…
Browse files Browse the repository at this point in the history
…tion (pallets#2271)

Co-authored-by: Andreas Backx <andreas@backx.org>
  • Loading branch information
peacock0803sz and AndreasBackx authored Nov 29, 2024
1 parent fde47b4 commit 5961d31
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 14 deletions.
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

0 comments on commit 5961d31

Please sign in to comment.