Skip to content
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

Must use typer.Option to supply default argument with Enums created with functional API #389

Open
7 tasks done
Torvaney opened this issue May 3, 2022 · 6 comments · May be fixed by #398
Open
7 tasks done

Must use typer.Option to supply default argument with Enums created with functional API #389

Torvaney opened this issue May 3, 2022 · 6 comments · May be fixed by #398
Labels
investigate question Question or problem

Comments

@Torvaney
Copy link

Torvaney commented May 3, 2022

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to Typer but to Click.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

import enum
import typer

Choice = enum.Enum('Choice', {k: k for k in ['first', 'second']})

def main(option: Choice = Choice.first):
    pass

typer.run(main)

# Usage: typer_reprex.py [OPTIONS]
# Try 'typer_reprex.py --help' for help.
# 
# Error: Invalid value for '--option': <Choice.first: 'first'> is not one of 'first', 'second'.

Description

When an enum is created via the functional API (as above), it cannot be used as a choice constraint, because it won't also subclass str.

In the documentation, choices are created by subclassing both str and enum.Enum. For example:

class Choice(str, Enum):
    first = 'first'
    second = 'second'

However, as far as I know, this isn't possible when using the functional API to create enums.

This can be worked around by supplying typer.Option with the default argument instead:

def main(option: Choice = typer.Option('first')):
    pass

While the workaround is very simple and non-obstructive, it's not clear from the docs that this is the case.

Operating System

macOS

Operating System Details

No response

Typer Version

0.4.1

Python Version

3.7.2

Additional Context

No response

@Torvaney Torvaney added the question Question or problem label May 3, 2022
@joaonc
Copy link

joaonc commented May 16, 2022

Funny I just bumped into this today. Would love to see Typer fully handle Enums created with the Functional API, but I think this might be a Python limitation.

Maybe the fix/improvement is to make it known that in order to use Enums defined with the functional API:

  • Need to be an enum that extends both str and Enum (note: looks like StrEnum will be added in Python 3.11 and there's also the StrEnum package here.
  • The default value needs to be a string, and not the Enum (ex. 'first' instead of Choice.first)

If this is brought up in the documentation with an example, I'm thinking folks should understand how it works and be ok with the limitations.

@joaonc
Copy link

joaonc commented May 17, 2022

Here's a good example that worked for me: having as an input parameter, the log level:

LogLevel = StrEnum('LogLevel', {k: k for k in logging._levelToName.values()})  # noqa

@app.command()
def main(
        log_level: LogLevel = typer.Option('INFO'),
)
    # `str(...)` is not needed, but adding for clarity and avoid linters complaining
    logging.basicConfig(level=str(log_level))

When doing --help all the values for logging will appear, including if you added new ones with logging.addLevelName(...).

@Torvaney
Copy link
Author

If this is brought up in the documentation with an example, I'm thinking folks should understand how it works and be ok with the limitations.

I agree - this isn't a major issue, but it would be helpful to have it documented

@Torvaney
Copy link
Author

I have created a quick PR with some additional documentation on Enums (#398) - it would be good to get your thoughts on it, if possible, @joaonc 😄

@mrjk
Copy link

mrjk commented Nov 4, 2022

Not sure if I'm not off topic, but: what about this example with arguments instead of option ?

#!/usr/bin/env python3

from enum import Enum
import typer
NeuralNetwork = Enum("NeuralNetwork", {k: k for k in ["simple", "conv", "lstm"]})


def main(network: NeuralNetwork = typer.Argument("simple", case_sensitive=False)):
    typer.echo(f"Training neural network of type: {network.value}")

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

When I run the help usage, this is what it shows:

$ ./test.py --help
                                                                             
 Usage: test.py [OPTIONS] [NETWORK]:[simple|conv|lstm]                       
                                                                             
╭─ Arguments ───────────────────────────────────────────────────────────────╮
│   network      [NETWORK]:[simple|conv|lstm]  [default: simple]            │
╰───────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────╮
│ --install-completion          Install completion for the current shell.   │
│ --show-completion             Show completion for the current shell, to   │
│                               copy it or customize the installation.      │
│ --help                        Show this message and exit.                 │
╰───────────────────────────────────────────────────────────────────────────╯

The help usage for the argument is a bit weird, and it does not do it for options.

How do you fix the help usage for Arguments (not Options) ?
Also, as default value, if you want to use the Enum, you need to use it this way:

def main(network: NeuralNetwork = typer.Argument(NeuralNetwork.simple.value, case_sensitive=False)):

Because if used without the value:

def main(network: NeuralNetwork = typer.Argument(NeuralNetwork.simple, case_sensitive=False)):

This give you the following help message:

$ ./test.py --help
                                                                                               
 Usage: test.py [OPTIONS] [NETWORK]:[simple|conv|lstm]                                         
                                                                                               
╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────╮
│   network      [NETWORK]:[simple|conv|lstm]  [default: NeuralNetwork.simple]                │
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
...

I think those behaviors should be documented as well, but I'm not sure to fully understand the rational behind this behavior.

@Garrett-R
Copy link

Garrett-R commented Jul 4, 2023

The default value needs to be a string, and not the Enum ... If this is brought up in the documentation with an example, I'm thinking folks should understand how it works and be ok with the limitations.

Hmm I might be misunderstanding, sorry if I'm missing something, but this idea doesn't seem to work:

#!/usr/bin/env python3
from enum import Enum
from typing import Annotated

import typer
from typer import Option


class Color(Enum):
    RED = 'red'
    GREEN = 'green'

def main(color: Annotated[Color, Option(help='the color')] = 'red') -> None:
    print(color)


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

But that's a Mypy violation.

Passing typing.Option(.) as the default arg also doesn't work (this time Mypy is happy, but Typer is not):

#!/usr/bin/env python3
from enum import Enum
from typing import Annotated

import typer
from typer import Option


class Color(Enum):
    RED = 'red'
    GREEN = 'green'

def main(color: Annotated[Color, Option(help='the color')] = Option(Color.RED)) -> None:
    print(color)


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

Running this gives:

MixedAnnotatedAndDefaultStyleError: Cannot specify `Option` in `Annotated` and default value together for 'color'

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

Successfully merging a pull request may close this issue.

5 participants