fastapi / typer

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

[FEATURE] Use enum.name as parameter, and enum.value as value received by the function #151

Open sapristi opened 4 years ago

sapristi commented 4 years ago

Is your feature request related to a problem

I would like to be able to unleash the power of Enum ! The first case I stumbled onto is that of logging, e.g have the following Enum usable in typer:

class LogLevel(Enum):
    debug = 10
    info = 20
    warning = 30

Instead I have to use this one, and then convert the string to the right value

class LogLevel(Enum):
    debug = "debug"
    info = "info"
    warning = "warning"

The solution you would like

I would like typer to interpret the value.name as arguments, and provide value.value to the called function, so that the following program would work as intended:

from enum import Enum
import typer
import logging

logger = logging.getLogger(__name__)

class LogLevel(Enum):
    debug = logging.DEBUG
    info = logging.INFO
    warning = logging.WARNING

def main(
        level: LogLevel,
):
    logger.setLevel(level)
    ...

if __name__ == "__main__":
    typer.run(main)
jwindridge commented 4 years ago

I think if this was configurable as an option, this would be great - on the other hand, we have a use-case where we have an Enum set up as

class Preset(str, Enum):
  PRESET_FOOBAR = "foo:bar"
  PRESET_BAZQUX = "baz:qux"

which then means we can do run --preset foo:bar in the CLI but reference Preset.PRESET_FOOBAR in code (which this PR / issue would break)

Do you think there's some way to nicely make the behaviour configurable?

crmne commented 4 years ago

@sapristi +1

sapristi commented 4 years ago

One way to make this feature configurable as an option would be to create a dummy NamedEnum class:

class NamedEnum(Enum):
    pass

And to add the behaviour I described only to instances of NamedEnum.

senpos commented 3 years ago

+1 to this feature!

I often have enums with numerical values, which are not convenient to use and see in the help message. But enum names are ALWAYS understandable.

I guess, this will also fix the problem when using IntEnum, as now it crashes because it can't join ints because it expects strings. :)

beanaroo commented 3 years ago

The documentation for Enums - Choices is also slightly confusing in this aspect as the examples use the same words for both enum.name and enum.value:

class NeuralNetwork(str, Enum):
    simple = "simple"
    conv = "conv"
    lstm = "lstm"
sapristi commented 1 year ago

Ok I finally found a way to have this without changing typer:

class MyEnum(Enum):
    choice1 = {"k": "v"}
    choice2 = 2

    def __init__(self, val):
        self.val = val

    @property
    def value(self):
        return self.name

With these re-definition of enum methods, one can use an Enum in typer, and obtained objects have the custom values in the val attribute:

@app.command()
def test(value: ME):
    print("HELLO", value.val)

Edit: not perfect though , as default values need to be passed as x.name:


@app.command()
def test(value: ME = ME.choice1.name):
    print("HELLO", value.val)
multiplemonomials commented 9 months ago

@HaiyiMei's workaround from here seems to actually work quite well for this.

import typer
import click

import enum
from typing import Annotated

class Color(enum.IntEnum):
    RED = 0xFF0000
    GREEN = 0x00FF00
    BLUE = 0x0000FF

ColorArgument = typer.Argument(help="Color to set",
                               click_type=click.Choice(Color._member_names_, case_sensitive=False),
                               show_default=False)

def main(color_str: Annotated[str, ColorArgument] = Color.BLUE.name):
    color = Color[color_str] # This is guaranteed to work because click validates the names
    print(f"Set color to 0x{color.value:06x}")

if __name__ == "__main__":
    typer.run(main)

This produces a nice help message too:

 Usage: scratch.py [OPTIONS] [COLOR_STR]:[RED|GREEN|BLUE]                      

┌─ Arguments ─────────────────────────────────────────────────────────────────┐
│   color_str      [COLOR_STR]:[RED|GREEN|BLUE]  Color to set                 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─ Options ───────────────────────────────────────────────────────────────────┐
│ --help          Show this message and exit.                                 │
└─────────────────────────────────────────────────────────────────────────────┘
msvensson222 commented 7 months ago

@HaiyiMei's workaround from here seems to actually work quite well for this.

import typer
import click

import enum
from typing import Annotated

class Color(enum.IntEnum):
    RED = 0xFF0000
    GREEN = 0x00FF00
    BLUE = 0x0000FF

ColorArgument = typer.Argument(help="Color to set",
                               click_type=click.Choice(Color._member_names_, case_sensitive=False),
                               show_default=False)

def main(color_str: Annotated[str, ColorArgument] = Color.BLUE.name):
    color = Color[color_str] # This is guaranteed to work because click validates the names
    print(f"Set color to 0x{color.value:06x}")

if __name__ == "__main__":
    typer.run(main)

This produces a nice help message too:

 Usage: scratch.py [OPTIONS] [COLOR_STR]:[RED|GREEN|BLUE]                      

┌─ Arguments ─────────────────────────────────────────────────────────────────┐
│   color_str      [COLOR_STR]:[RED|GREEN|BLUE]  Color to set                 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─ Options ───────────────────────────────────────────────────────────────────┐
│ --help          Show this message and exit.                                 │
└─────────────────────────────────────────────────────────────────────────────┘

Nicely done @multiplemonomials! Any ideas around how one could do something similar for multiple colors? Ie. a list of colors as input arguments?