-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Require an automatic parameter name to be a Python identifier #3827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stable
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,80 @@ recognized, otherwise {data}`STRING` is used. If no default value is | |
| provided, the type is assumed to be {data}`STRING`. See | ||
| {ref}`type-inference` for the types that are recognized. | ||
|
|
||
| (argument-names)= | ||
|
|
||
| ## Argument Names | ||
|
|
||
| The single declaration is not used as the name verbatim. Every `-` is replaced | ||
| with `_` and the result is lower cased, so `click.argument("input-file")` names | ||
| its parameter `input_file`. That is the same transform options apply, and it is | ||
| likewise not reversible. | ||
|
|
||
| ```{eval-rst} | ||
| .. list-table:: Examples | ||
| :widths: 15 15 | ||
| :header-rows: 1 | ||
|
|
||
| * - Decorator Arguments | ||
| - Inferred Argument Name | ||
| * - ``"foo-bar"`` | ||
| - foo_bar | ||
| * - ``"x"`` | ||
| - x | ||
| * - ``"CamelCase"`` | ||
| - camelcase | ||
| * - ``"Foo_Bar"`` | ||
| - foo_bar | ||
| * - ``"café"`` | ||
| - café | ||
| * - ``"ΟΔΟΣ"`` | ||
| - οδος | ||
| * - ``"\N{KELVIN SIGN}"`` | ||
| - k | ||
| * - ``"foo-٣"`` | ||
| - foo_٣ | ||
| * - ``"0-file"`` | ||
| - :exc:`TypeError` | ||
| * - ``"٣foo"`` | ||
| - :exc:`TypeError` | ||
| * - ``"foo.bar"`` | ||
| - :exc:`TypeError` | ||
| * - ``"foo\N{NON-BREAKING HYPHEN}bar"`` | ||
| - :exc:`TypeError` | ||
| * - ``"a\N{ZERO WIDTH SPACE}b"`` | ||
| - :exc:`TypeError` | ||
| * - ``""`` | ||
| - :exc:`TypeError` | ||
| * - ``"foo", "bar"`` | ||
| - :exc:`TypeError` | ||
| ``` | ||
|
|
||
| The name must satisfy {meth}`str.isidentifier`. Options apply the same check, | ||
| and the {ref}`caution about reserved keywords <keyword-names>` applies here | ||
| too. | ||
|
|
||
| `expose_value=False` is no exception. The name is also the key the parser stores | ||
| the value under, so an argument that gave it up would share that key with the | ||
| next one. Rename the declaration and pass `metavar` to keep the old display: | ||
| `click.argument("zero_file", expose_value=False, metavar="0-FILE")`. | ||
|
|
||
| (unicode-names)= | ||
|
|
||
| ```{caution} | ||
| Only the ASCII hyphen is replaced, so a separator that merely looks like one is | ||
| refused, as is any character that renders nothing. | ||
|
|
||
| The identifier set itself moves with the Unicode table Python ships: the | ||
| zero-width joiner (`U+200D`) entered it in Python 3.13, so a declaration holding | ||
| one is refused up to Python 3.12 and names a parameter from 3.13 on. Prefer a | ||
| declaration that is already a lower-case identifier with `-` for `_`. | ||
| ``` | ||
|
|
||
|
Comment on lines
+112
to
+121
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably too much detail. I doubt anyone's writing these, but linking to |
||
| One difference from {ref}`option names <options>` remains. An option takes | ||
| several declarations, so one that is already an identifier is read as an | ||
| explicit name and kept as written. An argument takes exactly one, which serves | ||
| as both the name and the metavar, so it is always transformed. | ||
|
|
||
| ```{admonition} Note on Required Arguments | ||
| :class: note | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,10 +68,6 @@ follows: | |
| declared is chosen. | ||
| 3. Otherwise, the first positional argument prefixed with `-` is chosen. | ||
|
|
||
| To get the argument name, the chosen positional argument is converted to lower | ||
| case, a leading `-` or `--` is removed if found, and any remaining `-` | ||
| characters are replaced with `_`. | ||
|
|
||
| ```{eval-rst} | ||
| .. list-table:: Examples | ||
| :widths: 15 15 | ||
|
|
@@ -87,12 +83,60 @@ characters are replaced with `_`. | |
| - dest | ||
| * - ``"--CamelCase"`` | ||
| - camelcase | ||
| * - ``"-f", "--filename", "Dest"`` | ||
| - Dest | ||
| * - ``"-f", "-fb"`` | ||
| - f | ||
| * - ``"--f", "--foo-bar"`` | ||
| - f | ||
| * - ``"---f"`` | ||
| - _f | ||
| * - ``"--0-file"`` | ||
| - :exc:`TypeError` | ||
| * - ``"--foo.bar"`` | ||
| - :exc:`TypeError` | ||
| ``` | ||
|
|
||
| The name must satisfy {meth}`str.isidentifier`. {ref}`Arguments | ||
| <argument-names>` derive their name the same way and apply the same check, | ||
| including the {ref}`caution about Unicode declarations <unicode-names>`. | ||
|
|
||
| (keyword-names)= | ||
|
|
||
| ```{caution} | ||
| A [reserved keyword](https://docs.python.org/3/reference/lexical_analysis.html#keywords) | ||
| satisfies that check, so Click accepts one: `click.option("--from")` names its | ||
| parameter `from`. Three things follow, and Click reports none of them. | ||
|
|
||
| - The callback cannot declare it. `def cmd(from)` is a {exc}`SyntaxError`, so | ||
| the command has to take `**kwargs` instead. | ||
| - That `**kwargs` then covers every other parameter too, and Python stops | ||
| checking the callback signature. An option you rename or drop used to raise | ||
| `TypeError: got an unexpected keyword argument`, and is now absorbed in | ||
| silence. | ||
| - {meth}`Context.invoke` cannot name it either. Write | ||
| `ctx.invoke(other, **{"from": value})`, because `ctx.invoke(other, from=value)` | ||
| is a {exc}`SyntaxError`. {meth}`Context.forward` is unaffected, since it | ||
| unpacks {attr}`Context.params`. | ||
|
|
||
| Pass an explicit name instead: `click.option("--from", "source")`. An argument | ||
| takes one declaration and has no explicit-name channel, so rename the | ||
| declaration there. Soft keywords (`match`, `case`, `type`, `_`) are contextual | ||
| and name a parameter fine, and `--True`, `--False` and `--None` lower case out | ||
| of the keyword set. | ||
| ``` | ||
|
|
||
| `expose_value=False` is no exception. The name is also the parser dest the value | ||
| is stored under, so two options that gave it up would share that dest and each | ||
| read the other's value. Pass an explicit name instead: | ||
| `click.option("--0-file", "zero_file", expose_value=False)`. | ||
|
|
||
| ```{caution} | ||
| Transformation from option name to argument name is not reversible. And is many-to-one: several option names can map to the same argument name. | ||
|
|
||
| For example, `--foo-bar`, `--Foo-Bar` and `--FOO-BAR` all map to `foo_bar`. | ||
|
|
||
| This is allowed so that options can deliberately form a [feature switch group](#feature-switch-group). | ||
| ``` | ||
|
Comment on lines
+104
to
140
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also seems like too much detail. The example at the end about capitalization can be added to the table at the top if it's not there. |
||
|
|
||
| ## Basic Example | ||
|
|
@@ -509,6 +553,8 @@ literally. | |
| ¹: `default=True` is substituted with `flag_value`. | ||
| ``` | ||
|
|
||
| (feature-switch-group)= | ||
|
|
||
| #### Feature switch groups (multiple flags sharing one variable) | ||
|
|
||
| Several `flag_value` options can target the same parameter name to form a | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2353,9 +2353,7 @@ def __init__( | |
| | None = None, | ||
| deprecated: bool | str = False, | ||
| ) -> None: | ||
| self.name, self.opts, self.secondary_opts = self._parse_decls( | ||
| param_decls or (), expose_value | ||
| ) | ||
| self.name, self.opts, self.secondary_opts = self._parse_decls(param_decls or ()) | ||
| self.type: types.ParamType[t.Any] = types.convert_type(type, default) | ||
|
|
||
| # Default nargs to what the type tells us if we have that | ||
|
|
@@ -2436,9 +2434,47 @@ def __repr__(self) -> str: | |
|
|
||
| @abstractmethod | ||
| def _parse_decls( | ||
| self, decls: cabc.Sequence[str], expose_value: bool | ||
| self, decls: cabc.Sequence[str] | ||
| ) -> tuple[str, list[str], list[str]]: ... | ||
|
|
||
| @staticmethod | ||
| def _name_from_spec(spec: str) -> str: | ||
| """Derive a parameter name from a single declaration. | ||
|
|
||
| The declaration is lower-cased and every ``-`` becomes a ``_``, so | ||
| ``--input-file``, ``--Input-File`` and ``INPUT_FILE`` all name | ||
| ``input_file``. An option passes the declaration with its prefix | ||
| already stripped; an argument passes its sole declaration whole. | ||
|
|
||
| The transform is many-to-one, and so cannot be reversed: the name does | ||
| not tell you which declaration produced it. | ||
| """ | ||
| return spec.replace("-", "_").lower() | ||
|
|
||
| def _resolve_name(self, name: str | None, decls: cabc.Sequence[str]) -> str: | ||
| """Settle the name derived from ``decls``, or refuse it. | ||
|
|
||
| A parameter's value reaches the command callback as a keyword | ||
| argument. The name must satisfy :meth:`str.isidentifier`. A keyword | ||
| such as ``from`` passes, and can then only be received by a | ||
| ``**kwargs`` callback. Every kind of parameter is held to this. | ||
|
|
||
| ``expose_value=False`` is no exception. The name is also the key the | ||
| parser stores the value under, so two parameters that gave it up would | ||
| share that key and each read the other's value. | ||
|
|
||
| :raises TypeError: when no name was derived, or the one derived is not | ||
| an identifier. | ||
| """ | ||
| if name is not None and name.isidentifier(): | ||
| return name | ||
|
|
||
| raise TypeError( | ||
| _( | ||
| "Could not determine name for {param_type} with declarations {decls!r}" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Could not determine a valid Python identifier for param_type decls." |
||
| ).format(param_type=self.param_type_name, decls=decls) | ||
| ) | ||
|
|
||
| @property | ||
| def human_readable_name(self) -> str: | ||
| """Returns the human readable name of this parameter. This is the | ||
|
|
@@ -2929,6 +2965,11 @@ class Option(Parameter): | |
| :param hidden: hide this option from help outputs. | ||
| :param attrs: Other command arguments described in :class:`Parameter`. | ||
|
|
||
| .. versionchanged:: 8.5.1 | ||
| ``expose_value=False`` no longer excuses a declaration that names no | ||
| Python identifier. Pass an explicit name, such as | ||
| ``click.option("--0-file", "zero_file", expose_value=False)``. | ||
|
|
||
| .. versionchanged:: 8.4.0 | ||
| Non-basic ``flag_value`` types (not ``str``, ``int``, ``float``, or | ||
| ``bool``) are passed through unchanged instead of being stringified. | ||
|
|
@@ -3007,9 +3048,6 @@ def __init__( | |
| # Phase 1: prompt-related attributes. ``_infer_flag_kind`` reads ``self.prompt`` | ||
| # and ``self.prompt_required`` so this must run first. | ||
| if prompt is True: | ||
| if not self.name: | ||
| raise TypeError("'name' is required with 'prompt=True'.") | ||
|
|
||
| prompt_text = self.name.replace("_", " ").capitalize() | ||
| elif prompt is False: | ||
| prompt_text = None | ||
|
|
@@ -3261,7 +3299,7 @@ def get_error_hint(self, ctx: Context | None) -> str: | |
| return result | ||
|
|
||
| def _parse_decls( | ||
| self, decls: cabc.Sequence[str], expose_value: bool | ||
| self, decls: cabc.Sequence[str] | ||
| ) -> tuple[str, list[str], list[str]]: | ||
| opts = [] | ||
| secondary_opts = [] | ||
|
|
@@ -3297,18 +3335,9 @@ def _parse_decls( | |
|
|
||
| if name is None and possible_names: | ||
| possible_names.sort(key=lambda x: -len(x[0])) # group long options first | ||
| name = possible_names[0][1].replace("-", "_").lower() | ||
| if not name.isidentifier(): | ||
| name = None | ||
| name = self._name_from_spec(possible_names[0][1]) | ||
|
|
||
| if name is None: | ||
| if not expose_value: | ||
| return "", opts, secondary_opts | ||
| raise TypeError( | ||
| _( | ||
| "Could not determine name for option with declarations {decls!r}" | ||
| ).format(decls=decls) | ||
| ) | ||
| name = self._resolve_name(name, decls) | ||
|
|
||
| if not opts and not secondary_opts: | ||
| raise TypeError( | ||
|
|
@@ -3708,6 +3737,13 @@ class Argument(Parameter): | |
|
|
||
| :param help: the help string. | ||
|
|
||
| .. versionchanged:: 8.5.1 | ||
| Exactly one declaration is required, and it must name a Python | ||
| identifier once it is lower-cased and every ``-`` is replaced with | ||
| ``_``. ``expose_value=False`` is no exception. This aligns with | ||
| option's behavior. Pass ``metavar`` to render a display the | ||
| declaration can no longer carry. | ||
|
|
||
| .. versionchanged:: 8.5.0 | ||
| Added the ``help`` parameter. | ||
| """ | ||
|
|
@@ -3778,22 +3814,18 @@ def make_metavar(self, ctx: Context) -> str: | |
| return var | ||
|
|
||
| def _parse_decls( | ||
| self, decls: cabc.Sequence[str], expose_value: bool | ||
| self, decls: cabc.Sequence[str] | ||
| ) -> tuple[str, list[str], list[str]]: | ||
| if not decls: | ||
| if not expose_value: | ||
| return "", [], [] | ||
| raise TypeError("Argument is marked as exposed, but does not have a name.") | ||
| if len(decls) == 1: | ||
| name = arg = decls[0] | ||
| name = name.replace("-", "_").lower() | ||
| else: | ||
| if len(decls) != 1: | ||
| raise TypeError( | ||
| _( | ||
| "Arguments take exactly one parameter declaration, got" | ||
| " {length}: {decls}." | ||
| ).format(length=len(decls), decls=decls) | ||
| ) | ||
|
|
||
| arg = decls[0] | ||
| name = self._resolve_name(self._name_from_spec(arg), decls) | ||
| return name, [arg], [] | ||
|
|
||
| def get_usage_pieces(self, ctx: Context) -> list[str]: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this saying that
Parameter.namecan no longer beNone? Do we need to update the annotation? Would be great if so.