fastapi / typer

Typer, build great CLIs. Easy to code. Based on Python type hints.
https://typer.tiangolo.com/
MIT License
15.37k stars 655 forks source link

Using typer.prompt with choices from Enum doesn't display choices #472

Open StFS opened 1 year ago

StFS commented 1 year ago

First Check

Commit to Help

Example Code

import typer
from enum import Enum

class ChoiceEnum(Enum):
    option_one = "opt1"
    option_two = "opt2"
    option_three = "opt3"

app = typer.Typer()

@app.command()
def subcommand(argument_option: ChoiceEnum = typer.Option(ChoiceEnum.option_two.value,
                                                          prompt="argument option",
                                                          show_choices=True)):
    prompt_option: ChoiceEnum = typer.prompt("prompt option",
                                             ChoiceEnum.option_three.value,
                                             show_choices=True)
    print(f"Argument choice: {argument_option}")
    print(f"Prompt choice: {prompt_option}")

if __name__ == "__main__":
    app()

# The result of running the above script results in the following output:
#
# > argument option (opt1, opt2, opt3) [opt2]: 
# > prompt option [opt3]: 
# > Argument choice: ChoiceEnum.option_two
# > Prompt choice: opt3

Description

Using the typer.prompt function isn't displaying the choices of an enum the same way that an Option does.

Am I doing something wrong here? Is there some other way I should be doing the prompt in order for it to display the choices as is done with the argument choices?

Also, why are the return values different (the prompt gives me the string "opt3", while the argument returns the enum value itself).

Operating System

macOS

Operating System Details

No response

Typer Version

0.6.1

Python Version

Python 3.10.8

Additional Context

No response

StFS commented 1 year ago

Managed to figure out that I needed to add type=ChoiceEnum to the prompt call to get the return value to be of the same type as the argument. Still doesn't fix showing the choices.

StFS commented 1 year ago

Managed to find a workaround for this by not using an enum and instead use the click.Choice type:

    click_choice = click.Choice(['opt1a', 'opt2a', 'opt3a'])
    click_prompt_option: click.Choice = typer.prompt("click prompt option",
                                                     "opt1a",
                                                     show_choices=True,
                                                     type=click_choice)

Still think that the typer behavior needs to be looked at to make the behavior a bit more predictable. Would think that label should be updated to bug.

MShekow commented 1 year ago

I solved the issue by creating a new Choice class for (Int)Enums:

class IntEnumChoice(click.Choice):
    def __init__(self, enum_type: EnumType, case_sensitive: bool = True) -> None:
        choices = [f"{value.value}: {value.name}" for value in enum_type]
        super().__init__(choices, case_sensitive)
        self._enum_type = enum_type

    def convert(self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]) -> t.Any:
        try:
            return self._enum_type(int(value))
        except ValueError:
            choices_str = ", ".join(map(repr, self.choices))
            self.fail(
                ngettext(
                    "{value!r} is not {choice}.",
                    "{value!r} is not one of {choices}.",
                    len(self.choices),
                ).format(value=value, choice=choices_str, choices=choices_str),
                param,
                ctx,
            )

You can easily adapt it for string Enums (just remove the type conversion to int in the return self._enum_type(int(value)) line).

alanwilter commented 2 months ago

I'm looking for a typer way to replace:

parser.add_argument('move', choices=['rock', 'paper', 'scissors'])

It would be great to have a similar feature in the core of typer.