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
13 changes: 13 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ Unreleased
keyword, is deprecated and raises `TypeError` in Click 9.0. {pr}`3866`
- An {class}`Option` name written as a Python identifier is deprecated when it
is not already lower-cased. {pr}`3866`
- Add {attr}`Parameter.spec`, which returns how the user spells the parameter on
the command line: `--times` for an option declared as `-t`, `--times`, and
`FILENAME` for an argument declared as `filename`. {pr}`3877`
- Move {meth}`get_help_spec` from {class}`Option` to {class}`Parameter`, and give
{class}`Argument` an override that returns its metavar. {pr}`3877`
- Error messages and deprecation warnings name an option by its spec instead of
its internal name, so they read `The option '--my-option' is deprecated.` where
they read `The option 'my_option' is deprecated.` before. {pr}`3877`
- {attr}`Parameter.human_readable_name` is deprecated and will be removed in
Click 9.0. Use {attr}`Parameter.spec` instead. It returned the internal name
for options, so its value changes from `my_option` to `--my-option`. Click no
longer reads it, and a subclass that overrides it gets a warning when the class
is defined. {pr}`3877`

## Version 8.5.1

Expand Down
46 changes: 34 additions & 12 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,47 @@ On each principle type you can specify {ref}`parameter-types`. Specifying these

## Parameter Names

Parameters (options and arguments) have a name that will be used as
the Python argument name when calling the decorated function with
values.
Three distinct strings designate a parameter, and each one has its own audience:

In the example, the argument's name is `filename`. The name must match the python arg name. To provide a different name for use in help text, see {ref}`doc-meta-variables`.
The option's names are `-t` and `--times`. More names are available for options and are covered in {ref}`options`.
- `decls` are the declarations you pass to the decorator. Click parses them to derive every other string in this section.
- `spec` is what the user reads in `--help` and types on the command line. Read it from {attr}`Parameter.spec`, or from {meth}`Parameter.get_help_spec` for the longer form the help page lays out in its left column.
- `name` is what Click uses internally to identify the parameter. It is used as a Python argument and is passed to the decorated function. Read it from {attr}`Parameter.name`.

Click also keeps the parsed declarations on the parameter. {attr}`Parameter.opts` holds every spelling an option answers to, and {attr}`Parameter.secondary_opts` the ones that set a boolean flag to false.

```{eval-rst}
.. click:example::
@click.command()
@click.argument('filename')
@click.option('-t', '--times', type=int)
def multi_echo(filename, times):
"""Print value filename multiple times."""
for x in range(times):
click.echo(filename)
@click.argument('recipe')
@click.option('-g', '--gluten-free/--no-gluten-free', default=False)
def bake(recipe, gluten_free):
"""Bake the RECIPE, with or without gluten."""
note = 'no gluten' if gluten_free else 'with gluten'
click.echo(f'{recipe}: {note}')
.. click:run::
invoke(bake, ['--gluten-free', 'brioche'], prog_name='bake')
.. click:run::
invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo')
invoke(bake, ['--help'], prog_name='bake')
```

The example holds one argument and one option. The table lists all five strings for each of them:

| | `@click.argument('recipe')` | `@click.option('-g', '--gluten-free/--no-gluten-free')` |
|---|---|---|
| `decls` | `('recipe',)` | `('-g', '--gluten-free/--no-gluten-free')` |
| `spec` | `RECIPE` | `--gluten-free` |
| `name` | `recipe` | `gluten_free` |
| `opts` | `['recipe']` | `['-g', '--gluten-free']` |
| `secondary_opts` | `[]` | `['--no-gluten-free']` |

Click derives an option's `name` from the declaration with the longest prefix, so it picks `--gluten-free` over `-g`. It then lowercases that declaration and replaces its dashes with underscores, which gives `gluten_free`. The name must match the Python argument name of the decorated function. More declarations are available for options and are covered in {ref}`options`.

An argument has a single declaration. It becomes the only entry in `opts`, and `name` is that declaration lowercased, with dashes replaced by underscores. The `spec` is the name in upper case. `secondary_opts` stays empty, since the parser binds an argument by name and never matches a spelling. To choose the spec an argument shows in help text, see {ref}`doc-meta-variables`.

The help page above shows each `spec` where the user reads it: `RECIPE` in the usage line, and `-g, --gluten-free / --no-gluten-free` in the left column of the options list, which is the longer form {meth}`Parameter.get_help_spec` returns.
3 changes: 2 additions & 1 deletion docs/upgrade-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ For each deprecation, provide a brief explanation, and direct users to new funct
- A parameter name that is not a valid Python identifier, or that is a [Python keyword](https://docs.python.org/3/reference/lexical_analysis.html#keywords), is deprecated and raises `TypeError` in 9.0. [`str.isidentifier()`](https://docs.python.org/3/library/stdtypes.html#str.isidentifier) decides the first case, so `click.argument("0foo")` fails it. It accepts a keyword, so `click.option("--from")` names a parameter `from`, which no callback can declare. [Soft keywords](https://docs.python.org/3/reference/lexical_analysis.html#soft-keywords) such as `match` and `type` are contextual and unaffected, and `--True` and `--None` lower case out of the keyword set. To migrate, pass an explicit name (`click.option("--from", "source")`, `click.option("--0-file", "zero_file")`). An argument takes one declaration and has no explicit-name channel, so rename it and pass `metavar` to keep its old display (`click.argument("zero_file", metavar="0-FILE")`). See [#3827](https://github.com/pallets/click/pull/3827) and [#3866](https://github.com/pallets/click/pull/3866).
- An option name written as a Python identifier is deprecated when it is not already lower cased: `click.option("--x", "Foo_Bar")` names `foo_bar` rather than `Foo_Bar`. To migrate, spell the name the way the callback declares it. See [#3827](https://github.com/pallets/click/pull/3827).
- `CliRunner.isolated_filesystem()` is deprecated and will be removed in Click 9.0. The helper predates Python 3 and modern pytest, and it relies on `os.chdir`, which mutates process-global state and is therefore not thread-safe. Replace it with a temporary directory (`tempfile.TemporaryDirectory`, or pytest's `tmp_path` fixture) and pass absolute paths to the command instead of relying on the current working directory. To run commands in parallel, use process-based isolation (such as `pytest-xdist`) rather than threads, since `CliRunner.invoke()` also redirects the process-global standard streams and other interpreter-wide state. See [#3700](https://github.com/pallets/click/issues/3700), [#3501](https://github.com/pallets/click/issues/3501) and the [testing guide](testing.md#running-tests-in-parallel).
- `Parameter.human_readable_name` is deprecated and will be removed in Click 9.0. Use `Parameter.spec` instead, which returns how the user spells the parameter on the command line. For an argument the two agree, since both return the metavar. For an option the old property returned the internal `name`, so the value changes from `my_option` to `--my-option`. Click no longer reads the property, so an override in a subclass has no effect: override `Parameter.spec` instead. Defining a subclass that still overrides `human_readable_name` emits a `DeprecationWarning`. See the [parameter names](parameters.md#parameter-names) section for the three strings that designate a parameter.

### Removals with prior deprecation

Expand All @@ -29,7 +30,7 @@ The changes were not able to be deprecated prior to removal. Explain clearly why

### Changes

- TBD
- Error messages and deprecation warnings name an option by its spec rather than its internal name, so `The option 'my_option' is deprecated.` now reads `The option '--my-option' is deprecated.` Tests asserting on the old wording need updating.

### Fixes

Expand Down
133 changes: 121 additions & 12 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,13 @@ def _echo_aborted() -> None:
echo(_("Aborted!"), file=sys.stderr)


def _outside_click_stacklevel() -> int:
def _outside_click_stacklevel(skip: cabc.Container[str] = ()) -> int:
"""Depth of the first stack frame outside Click.
:param skip: Other module names to walk past. ``abc`` is one: it sits
between ``__init_subclass__`` and the ``class`` statement that the
warning must point at.
.. versionadded:: 8.6.0
"""
frame: FrameType | None = sys._getframe(1)
Expand All @@ -116,7 +120,7 @@ def _outside_click_stacklevel() -> int:
while frame is not None:
module = frame.f_globals.get("__name__", "")

if module != "click" and not module.startswith("click."):
if module != "click" and not module.startswith("click.") and module not in skip:
return level

frame = frame.f_back
Expand Down Expand Up @@ -2242,6 +2246,17 @@ class Parameter(ABC):
Some settings are supported by both options and arguments.
Three distinct strings designate a parameter, each with its own audience:
``decls``
What the developer passes to the constructor as ``param_decls``.
``spec``
What the user reads in ``--help`` and types on the command line. See
:attr:`spec` and :meth:`get_help_spec`.
``name``
What Click uses internally to identify the parameter and to pass its
value to the command callback. See :attr:`name`.
:param param_decls: the parameter declarations for this option or
argument. This is a list of flags or argument
names.
Expand Down Expand Up @@ -2334,8 +2349,29 @@ class Parameter(ABC):
param_type_name = "parameter"

name: str
"""What Click uses internally to identify the parameter, and the Python
argument name it passes to the command callback. Click derives it from the
declarations, lower cased and with dashes replaced by underscores. An
:class:`Option` takes the declaration with the longest prefix, so
``"-t", "--times"`` names ``times``, while an :class:`Argument` takes its
single declaration.
"""
opts: list[str]
"""Every command-line spelling that designates this parameter.
For an :class:`Option` these are the prefixed declarations the parser
matches, in declaration order: ``["-t", "--times"]``. :attr:`spec` is the
canonical one.
For an :class:`Argument` this holds the single declaration the developer
wrote, ``["filename"]``. The parser never reads it: it binds the value by
:attr:`name`. The string the user reads is :attr:`spec`, ``FILENAME``.
"""
secondary_opts: list[str]
"""The spellings that set a boolean :class:`Option` to ``False``, taken from
the right side of a ``--color/--no-color`` declaration. Always empty for an
:class:`Argument`.
"""
# `Parameter.type` is annotated in `__init__` to avoid confusing mypy
required: bool
callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None
Expand All @@ -2353,6 +2389,20 @@ class Parameter(ABC):
deprecated: bool | str
help: str | None

def __init_subclass__(cls, **kwargs: t.Any) -> None:
super().__init_subclass__(**kwargs)

if "human_readable_name" in cls.__dict__:
import warnings

warnings.warn(
f"{cls.__name__} overrides 'human_readable_name'. Click no"
" longer reads that property. Override 'spec' instead. Click"
" will remove 'human_readable_name' in 9.0.",
DeprecationWarning,
stacklevel=_outside_click_stacklevel(skip=("abc",)),
)

def __init__(
self,
param_decls: cabc.Sequence[str] | None = None,
Expand Down Expand Up @@ -2432,7 +2482,7 @@ def __init__(

if required and deprecated:
raise ValueError(
f"The {self.param_type_name} '{self.human_readable_name}' "
f"The {self.param_type_name} '{self.spec}' "
"is deprecated and still required. A deprecated "
f"{self.param_type_name} cannot be required."
)
Expand Down Expand Up @@ -2516,12 +2566,56 @@ def _check_name_is_usable(self, name: str, decls: cabc.Sequence[str]) -> None:
stacklevel=_outside_click_stacklevel(),
)

@property
def spec(self) -> str:
"""Returns how the user spells this parameter on the command line.
This is the declaration with the longest prefix, so an option declared
as ``"-t", "--times"`` returns ``--times``. Click derives :attr:`name`
from that same declaration, unless the developer passed an explicit
name. A parameter without declarations returns its :attr:`name`.
.. versionadded:: 8.6.0
"""
if not self.opts:
return self.name

return max(self.opts, key=lambda opt: len(_split_opt(opt)[0]))

@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.
"""Returns how the user spells this parameter on the command line.
.. deprecated:: 8.6.0
Will be removed in Click 9.0. Use :attr:`spec` instead. For an
option this returned the internal :attr:`name`, so the value
changes from ``times`` to ``--times``. Click no longer reads this
property, so an override in a subclass has no effect.
"""
return self.name
import warnings

warnings.warn(
"'human_readable_name' is deprecated and will be removed in Click"
" 9.0. Use 'spec' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.spec

def get_help_spec(self, ctx: Context) -> str:
"""Returns the left column of this parameter's help record.
This implementation returns :attr:`spec`. :class:`Option` returns every
spelling, plus the metavar when the option takes a value.
:class:`Argument` returns its metavar, which already carries the markers
for an optional, repeated or deprecated value.
Unlike ``get_help_record()``, the spec is produced even when the
parameter is left out of the help page, like a hidden option.
.. versionadded:: 8.6.0
"""
return self.spec

def make_metavar(self, ctx: Context) -> str:
if self.metavar is not None:
Expand Down Expand Up @@ -2873,7 +2967,7 @@ def handle_parse_result(
"{extra_message}"
).format(
param_type=self.param_type_name,
name=self.human_readable_name,
name=self.spec,
extra_message=_format_deprecated_suffix(self.deprecated),
)
echo(style(message, fg="red"), err=True)
Expand Down Expand Up @@ -2933,7 +3027,7 @@ def get_error_hint(self, ctx: Context | None) -> str:
.. versionchanged:: 8.4.0
``ctx`` can be ``None``.
"""
hint_list = self.opts or [self.human_readable_name]
hint_list = self.opts or [self.spec]
return " / ".join(f"'{x}'" for x in hint_list)

def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]:
Expand Down Expand Up @@ -3835,7 +3929,14 @@ def __init__(
super().__init__(param_decls, required=required, help=help, **attrs)

@property
def human_readable_name(self) -> str:
def spec(self) -> str:
"""Returns the argument's metavar, which is how the user reads it in
``--help``. An argument has no flag to spell, so this is the
:attr:`metavar` if one was given, and the uppercased :attr:`name`
otherwise.
.. versionadded:: 8.6.0
"""
if self.metavar is not None:
return self.metavar
return self.name.upper()
Expand Down Expand Up @@ -3883,6 +3984,14 @@ def _parse_decls(
def get_usage_pieces(self, ctx: Context) -> list[str]:
return [self.make_metavar(ctx)]

def get_help_spec(self, ctx: Context) -> str:
"""Returns the left column of the argument's help record: its metavar,
like ``FILENAME`` or ``[FILENAME]``.
.. versionadded:: 8.6.0
"""
return self.make_metavar(ctx)

def get_help_record(self, ctx: Context) -> tuple[str, str]:
"""Returns the argument's help row: its metavar and its help text.
Expand All @@ -3894,12 +4003,12 @@ def get_help_record(self, ctx: Context) -> tuple[str, str]:
Always returns a tuple. It used to return ``None`` when ``help`` was
not set.
"""
return self.make_metavar(ctx), self.help or ""
return self.get_help_spec(ctx), self.help or ""

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}'"
return f"'{self.get_help_spec(ctx)}'"
return f"'{self.spec}'"

def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,36 @@ def cli(f):
assert "(DEPRECATED)" in result.output


@pytest.mark.parametrize(
("kwargs", "expected_spec", "expected_help_spec"),
[
({}, "FILENAME", "FILENAME"),
({"required": False}, "FILENAME", "[FILENAME]"),
({"nargs": -1}, "FILENAME", "[FILENAME]..."),
({"metavar": "<file>"}, "<file>", "<file>"),
],
)
def test_spec_and_help_spec(kwargs, expected_spec, expected_help_spec):
"""The spec names the argument, while the help spec decorates it with the
optionality and repetition markers shown on the help page.
"""
arg = click.Argument(["filename"], **kwargs)
ctx = click.Context(click.Command("cli"))
assert arg.spec == expected_spec
assert arg.get_help_spec(ctx) == expected_help_spec


@pytest.mark.parametrize("help_text", [None, "path to the file"])
def test_help_spec_matches_help_record(help_text):
"""The help spec is the left column of the argument's help record, whether
or not the argument documents itself.
"""
arg = click.Argument(["filename"], help=help_text)
ctx = click.Context(click.Command("cli"))
assert arg.get_help_spec(ctx) == "FILENAME"
assert arg.get_help_record(ctx)[0] == arg.get_help_spec(ctx)


@pytest.mark.parametrize(
("deprecated", "expected"),
[(True, "(DEPRECATED)"), ("USE B INSTEAD", "(DEPRECATED: USE B INSTEAD)")],
Expand Down
Loading
Loading