8000 `Choice` prefers `Enum.name` even for `StrEnum` where it should use the value · Issue #2911 · pallets/click · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Choice prefers Enum.name even for StrEnum where it should use the value #2911

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

Closed
jwodder opened this issue May 13, 2025 · 10 comments
Closed
Labels

Comments

@jwodder
Copy link
Contributor
jwodder commented May 13, 2025

Consider the following code:

from enum import Enum
import click


class Color(str, Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

    def __str__(self) -> str:
        return self.value


@click.command()
@click.option("-C", "--color", type=click.Choice(list(Color)), show_default=True)
def main(color: Color) -> None:
    print(repr(color))


if __name__ == "__main__":
    main()

With Click 8.1.8, running this script with -C red works as expected, but with Click 8.2.0, it instead fails with:

Usage: enum-choice01a.py [OPTIONS]
Try 'enum-choice01a.py --help' for help.

Error: Invalid value for '-C' / '--color': 'red' is not one of 'RED', 'GREEN', 'BLUE'.

This was observed with Python 3.13.3 on macOS 14.7.5.

@qdegraaf
Copy link

Could this be the culprit: https://github.com/pallets/click/pull/2796/files#diff-dcb534e6a7591b92836537d4655ddbd2f18e3b293c3420144c30a9ca08f65c4eR298 ?

And if so, is this behaviour intentional? If not, I'd be happy to open a PR with an additional test for Enum's that are also str's and tweak the line to return the string value in that additional case. I am new to the repo though so could be missing context.

@davidism davidism changed the title Choice in Click 8.2.0 stringifies enums by name, even when they're str subclasses Choice stringifies enums by name, even when they're str subclasses May 13, 2025
@qdegraaf
Copy link
qdegraaf commented May 13, 2025

I've replicated the example as a test and see now that it is think it might be by design. case_sensitive: bool = True by default. @jwodder setting case_sensitive to False in click.Choice makes your example above work for me.

I guess the follow-up question is whether this default is desirable. I could see quite a few

class SomeStrEnum(StrEnum)
    FOO = "foo"
    BAR = "bar"

class SomeOtherStrEnum(str, Enum)
    BLA = "bla"
    ETC = "etc"

that are currently being used in click.Choice that all implicitly used the str values of the Enum values.

We experienced a similar, but not exactly the same, issue for StrEnum default values in the typer.Option wrapper around click.Choice where something like:

@dataclass
class CLIOptions
    foo_preference: Annotated[
        SomeStrEnum,
        typer.Option( "-f", "--foo-preference"),
    ] = SomeStrEnum.FOO

broke as SomeStrEnum.FOO was now not one of "foo"|"bar". I could see case insensitive defaults occurring in similar scenarios there.

EDIT: Looping in @tiangolo in case reports of scenarios above come in for Typer as well and the click connection isn't immediately clear

@jwodder
Copy link
Contributor Author
jwodder commented May 13, 2025

@qdegraaf

  • What makes you think this is by design?
  • What about enums where the value isn't just the lowercase version of the name? (e.g., MULTI_VALUE = "multi-value") case_sensitive won't help with those, and normalize_choice() is only available in Click 8.2.0.

@qdegraaf
Copy link

Just guessing based on the default value and the way the function is written. Apologies, I should not have stated it so definitively. I am not a developer on click so it's a question for the maintainers or the developer of the feature. Maybe @AndreasBackx can shed some light?

@AndreasBackx
Copy link
Collaborator

@qdegraaf #2796 added support for Enum in general, StrEnum (or well class StrEnum(str, Enum)) is slightly different. Enum.name is used in the implementation because Enum can be anything. Then token normalisation is applied. Let me rename the issue to reflect this.

A good solution would perhaps be to get the starting value in Choice.normalize_choice to be like the following:

    def normalize_choice(self, choice: ParamTypeValue, ctx: Context | None) -> str:
        """
        Normalize a choice value, used to map a passed string to a choice.
        Each choice must have a unique normalized value.
        By default uses :meth:`Context.token_normalize_func` and if not case
        sensitive, convert it to a casefolded value.
        .. versionadded:: 8.2.0
        """
-		normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice)
+		match choice:
+			case str(choice):
+				normed_value = choice
+			case Enum(choice):
+				normed_value = choice.name
+			case _:
+				normed_value = str(choice)

        if ctx is not None and ctx.token_normalize_func is not None:
            normed_value = ctx.token_normalize_func(normed_value)

        if not self.case_sensitive:
            normed_value = normed_value.casefold()

        return normed_value

@AndreasBackx AndreasBackx changed the title Choice stringifies enums by name, even when they're str subclasses Choice prefers Enum.name even for StrEnum where it should use the value May 14, 2025
@qdegraaf
Copy link

Thanks for clarifying! Because the tests used a class MyEnum(str, enum.Enum), I thought maybe it was supposed to work out of the box for that specific scenario as well.

In the case of a class that is both str and Enum do we want to allow both .name and str()? The solution direction provided above will make the current test_choice_argument_enum fail at the following assert.

assert "Usage: cli [OPTIONS] {foo-value|bar-value|baz-value}\nTry 'cli --help' for help.\n\nError: Invalid value for '{foo-value|bar-value|baz-value}': 'foo' is not one of 'foo-value', 'bar-value', 'baz-value'.\n" == 'foo-value\n'

Which makes sense of course, but could undermine the original intention.

@kieranyyu
Copy link
Contributor

Working on this (PyCon 2025)

@davidism
Copy link
Member
davidism commented May 19, 2025

I don't like the idea of using value for string enums but name for others. That's inconsistent and will result in further bug reports down the road. name is the part of an enum that will always be a string, that's what we can use.

@jwodder
Copy link
Contributor Author
jwodder commented May 19, 2025

@davidism What about just using str(choice) where isinstance(choice, str)? That gives you the same result for StrEnums and other str-likes.

@davidism
Copy link
Member

That will still result in unexpected inconsistencies in enums. I'm marking this as a documentation fix rather than something we'll change.

If custom behavior is needed, then a custom type or callback can be written to do exactly what that application needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants
0