Require an automatic parameter name to be a Python identifier - #3827
Require an automatic parameter name to be a Python identifier#3827kdeldycke wants to merge 1 commit into
Conversation
|
Both of these seem like bugs. I feel like the automatic names should always be normalized and pass |
254d9e4 to
49a5f5a
Compare
OK cool, that was also my feeling. We don't want special treatments of the |
49a5f5a to
ad8601c
Compare
Argument name to be a Python identifier
@davidism Done. I did not pushed the normalization too much. So there is one bothering difference between |
Argument name to be a Python identifierArgument name to be a Python identifier
05c4439 to
3d4142f
Compare
Argument name to be a Python identifierArgument and Option automatic naming
|
I just pushed the alignment and normalization of naming a bit more and updated the differences in the body of that PR. |
3d4142f to
b4606f9
Compare
Argument and Option automatic namingb4606f9 to
ff79a3c
Compare
|
OK these were my final edits. You can review this PR. |
ff79a3c to
6f9943e
Compare
| ```{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 `_`. | ||
| ``` | ||
|
|
There was a problem hiding this comment.
This is probably too much detail. I doubt anyone's writing these, but linking to isidentifier should already be enough.
| (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). | ||
| ``` |
There was a problem hiding this comment.
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.
| - `expose_value=False` no longer excuses that check on either kind. Pass an | ||
| explicit name to {class}`Option`, or rename an {class}`Argument` and pass | ||
| `metavar` to keep its display. {pr}`3827` | ||
| - Neither kind builds a parameter without a declaration. `click.argument()` and |
There was a problem hiding this comment.
Is this saying that Parameter.name can no longer be None? Do we need to update the annotation? Would be great if so.
|
This needs to be targeted at 9.0 and listed high up in the changelog, the name normalization is a breaking change. Along with the keyword check I mention below, this could be significantly disruptive. We should probably issue a deprecation warning and accept names that are currently accepted, before applying the new rules. It might be good for the Overall I think the docs are a little too verbose about this. Giving a few examples and linking to You bring up a good point about keywords. I think the intention of this code was clear: to only accept names that can be used in signatures rather than |
|
|
||
| raise TypeError( | ||
| _( | ||
| "Could not determine name for {param_type} with declarations {decls!r}" |
There was a problem hiding this comment.
"Could not determine a valid Python identifier for param_type decls."
This PR changes the behavior of automatic naming:
0-file,foo.barandfoo barare now refused.expose_value=Falseno longer bypasses that check, on both arguments and options.click.argument(expose_value=False)andclick.option(expose_value=False)used to produce a""parameter.What does not change:
-becomes_and the result is lower cased.click.option("--0-file").click.option("--in-file", "Input_File")still names its parameterInput_File.How to migrate:
click.option("--0-file", "zero_file").metavarto keep the old display:click.argument("zero_file", metavar="0-FILE").This PR also lockdown all other edge-cases and canonical naming behavior.
It is in the same vein as my previous PR at #3808 , and is also based on a collection of edge-cases I accumulated over the years. It also covers platform differences between Windows and Unix-like (see: #2483).
Note: this PR started as simple test coverage expansion but ended up implementing sanitizing fixes.