typer
typer copied to clipboard
Must use typer.Option to supply default argument with Enums created with functional API
First Check
- [X] I added a very descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Typer documentation, with the integrated search.
- [X] I already searched in Google "How to X in Typer" and didn't find any information.
- [X] I already read and followed all the tutorial in the docs and didn't find an answer.
- [X] I already checked if it is not related to Typer but to Click.
Commit to Help
- [X] 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
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
andEnum
(note: looks likeStrEnum
will be added in Python 3.11 and there's also theStrEnum
package here. - The default value needs to be a string, and not the Enum (ex.
'first'
instead ofChoice.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.
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(...)
.
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
I have created a quick PR with some additional documentation on Enums (https://github.com/tiangolo/typer/pull/398) - it would be good to get your thoughts on it, if possible, @joaonc 😄
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.
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'