Open codethief opened 3 years ago
Thanks for the code example! I tried it, but I get an error. Thoughts?
min.py:
import typer
from enum import Enum
from typing import Dict
class A:
pass
a = A()
networks: Dict[str, A] = {"A": a}
NetworkEnum = Enum(
"NetworkEnum",
names=[(name, network) for name, network in networks.items()],
module=__name__,
)
app = typer.Typer()
@app.command()
def main(network: NetworkEnum):
the_network = networks[network.value]
print(the_network)
if __name__ == "__main__":
app()
❯ python min.py --help
Traceback (most recent call last):
File "min.py", line 33, in <module>
app()
File "/Users/user/anaconda3/lib/python3.8/site-packages/typer/main.py", line 214, in __call__
return get_command(self)(*args, **kwargs)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1128, in __call__
return self.main(*args, **kwargs)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1052, in main
with self.make_context(prog_name, args, **extra) as ctx:
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 914, in make_context
self.parse_args(ctx, args)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1370, in parse_args
value, args = param.handle_parse_result(ctx, opts, args)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 2347, in handle_parse_result
value = self.process_value(ctx, value)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 2309, in process_value
value = self.callback(ctx, self, value)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1270, in show_help
echo(ctx.get_help(), color=ctx.color)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 693, in get_help
return self.command.get_help(self)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1295, in get_help
self.format_help(ctx, formatter)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1324, in format_help
self.format_usage(ctx, formatter)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1239, in format_usage
pieces = self.collect_usage_pieces(ctx)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 1249, in collect_usage_pieces
rv.extend(param.get_usage_pieces(ctx))
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/core.py", line 2947, in get_usage_pieces
return [self.make_metavar()]
File "/Users/user/anaconda3/lib/python3.8/site-packages/typer/core.py", line 185, in make_metavar
type_var = self.type.get_metavar(self)
File "/Users/user/anaconda3/lib/python3.8/site-packages/click/types.py", line 248, in get_metavar
choices_str = "|".join(self.choices)
TypeError: sequence item 0: expected str instance, A found
Here's a workaround:
import typer
from enum import Enum
from typing import Dict
class A:
pass
a = A()
networks: Dict[str, A] = {"A": a}
NetworkEnum = Enum(
"NetworkEnum",
names=[(name, name) for name in networks.keys()],
module=__name__,
)
app = typer.Typer()
@app.command()
def main(network: NetworkEnum):
the_network = networks[network.value]
print(the_network)
if __name__ == "__main__":
app()
which results in
❯ python min.py --help
Usage: min.py [OPTIONS] NETWORK:{A}
Arguments:
NETWORK:{A} [required]
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.
Right, my apologies, this must have been a typo!
The point is basically that, while the existing option to map a string (= user input) to an enum member is nice, it doesn't cover the other classic kind of mapping: dictionaries, i.e. mapping (string) dictionary keys to dictionary values. This is particularly useful when the dictionary is dynamic and cannot be rewritten as an enum (at least not easily – this is what I meant to illustrate with the code example you fixed).
I feel like this is an extension of the more basic functionality which is the option to specify choices from a list. Of course you can generate an enum but I feel like this is conflicting with the whole point of typer which is to make defining CLIs clean and simple. Similar to the examples provided, we could have:
import typing
import typer
value_options = ['hello', 'world']
def main(value: typing.Annotated[str, typer.Argument(choices=value_options)]):
print(value)
if __name__ == "__main__":
typer.run(main)
instead of the current solution
import enum
import typer
value_options = ['hello', 'world']
ValueEnum = enum.Enum('ValueEnum', {
f'value_{index}': value
for index, value in enumerate(value_options)
})
def main(value: ValueEnum):
print(value.value)
if __name__ == "__main__":
typer.run(main)
I think a big benefit here is that you can correctly type-annotate the value. Related again to this would be support for typing.Literal
which I guess could just be syntactic sugar around the first option, although looks like there was an issue (#76) for that which was closed for some reason...:
import typing
import typer
def main(value: typing.Literal['hello', 'world']):
print(value.value)
if __name__ == "__main__":
typer.run(main)
I feel like this is an extension of the more basic functionality which is the option to specify choices from a list. Of course you can generate an enum but I feel like this is conflicting with the whole point of typer which is to make defining CLIs clean and simple. Similar to the examples provided, we could have: ...
Imho, the point of using enum
is friendly for static analysis (including IDE autocompletion) and thus less error-prune. Your codes don't benefit from that. But this does,
import enum
import typer
class ValueEnum(enum.Enum):
HELLO = "hello"
WORLD = "world"
def main(value: ValueEnum) -> None:
print(value.value)
# What if I have typo here like `ValueEnum.HELLOW`?
# It will be caught either by Python itself or static analysis tools.
if value is ValueEnum.HELLO: ...
if __name__ == "__main__":
typer.run(main)
@jfcherng That's why I think supporting typing.Literal
is particularly useful but that seems to have met several roadblocks. My main point around the use of enums is that there are cases where you will have to construct the enum procedurally rather than define it as a literal. One example might be if the valid choices are defined in a file, or depend on the environment in some way. That use case doesn't work with static typing. When you're in that situation, I think the best you can really do is have the fundamental type annotation, e.g. str
, and also have an option to tell typer what the choices are for CLI validation and help text. It's fairly common to find Python APIs that use string values like enums. Certainly for retro-fitting typer to an existing function, it would be a useful option to have rather than having to change your API just to get typer to do what you want.
@jfcherng I agree with @inkychris. Static analysis is not possible here, anyway, because the whole point is to generate the choices dynamically:
This is particularly useful when the dictionary is dynamic and cannot be rewritten as an enum (at least not easily [let alone statically] […])
Is there any updated guidance for doing this now?
I have a (dynamic) dictionary mapping strings (names) to more complicated objects. I want to define a Typer argument (or option) that allows the user to choose one of those objects by providing the corresponding name.
The solution I would like
Pseudo-code:
Basically, I would like to tell Typer to use the dictionary's string keys as the possible values the user can choose from on the command line and then map them to the corresponding NeuralNetwork object when invoking
main()
.Alternatives I've considered
Since Typer currently doesn't support Union types (let alone dynamically created ones), the only alternative with proper type checking and auto-completion that I've found is the following:
While this works, it requires boilerplate code and is much less readable.