From 09d56ba971e68e22c5a752c6abe08033f6f401d2 Mon Sep 17 00:00:00 2001 From: Dan Walters Date: Thu, 8 Oct 2020 15:03:09 -0500 Subject: [PATCH] Support for typing.Literal as an alternative to Enum in Python 3.8+. --- docs/tutorial/parameter-types/enum.md | 39 +++++++++++++ docs_src/parameter_types/enum/tutorial003.py | 11 ++++ .../test_enum/test_tutorial003.py | 56 +++++++++++++++++++ typer/main.py | 7 +++ 4 files changed, 113 insertions(+) create mode 100644 docs_src/parameter_types/enum/tutorial003.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 6af834bf54..561e889d0d 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -66,3 +66,42 @@ Training neural network of type: lstm ``` + +### Literal choices + +With Python 3.8+, you can also use `typing.Literal` to represent a set of possible predefined choices, without having to use an `Enum`: + +```Python hl_lines="13" +{!../docs_src/parameter_types/enum/tutorial003.py!} +``` + +
+ +```console +$ python main.py --help + +// Notice the predefined values [simple|conv|lstm] +Usage: main.py [OPTIONS] + +Options: + --network [simple|conv|lstm] [default: simple] + --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. + +// Try it +$ python main.py --network conv + +Training neural network of type: conv + +// Invalid value +$ python main.py --network capsule + +Usage: main.py [OPTIONS] +Try "main.py --help" for help. + +Error: Invalid value for '--network': invalid choice: capsule. (choose from simple, conv, lstm) +``` + +
+ diff --git a/docs_src/parameter_types/enum/tutorial003.py b/docs_src/parameter_types/enum/tutorial003.py new file mode 100644 index 0000000000..6271eac338 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial003.py @@ -0,0 +1,11 @@ +from typing import Literal + +import typer + + +def main(network: Literal["simple", "conv", "lstm"] = "simple"): + typer.echo(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py new file mode 100644 index 0000000000..700fb469c7 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py @@ -0,0 +1,56 @@ +import subprocess +import sys + +import pytest +import typer +from typer.testing import CliRunner + +pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8") + + +runner = CliRunner() + + +@pytest.fixture(scope="module") +def mod(): + from docs_src.parameter_types.enum import tutorial003 as mod + + return mod + + +@pytest.fixture(scope="module") +def app(mod): + app = typer.Typer() + app.command()(mod.main) + return app + + +def test_help(app): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--network [simple|conv|lstm]" in result.output + + +def test_main(app): + result = runner.invoke(app, ["--network", "conv"]) + assert result.exit_code == 0 + assert "Training neural network of type: conv" in result.output + + +def test_invalid(app): + result = runner.invoke(app, ["--network", "capsule"]) + assert result.exit_code != 0 + assert ( + "Error: Invalid value for '--network': invalid choice: capsule. (choose from simple, conv, lstm)" + in result.output + ) + + +def test_script(mod): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/main.py b/typer/main.py index 02d9a5d7fe..e7f48d2a0d 100644 --- a/typer/main.py +++ b/typer/main.py @@ -1,4 +1,6 @@ import inspect +import sys +import typing from datetime import datetime from enum import Enum from functools import update_wrapper @@ -584,6 +586,11 @@ def get_click_type( [item.value for item in annotation], case_sensitive=parameter_info.case_sensitive, ) + elif sys.version_info >= (3, 8) and typing.get_origin(annotation) == typing.Literal: + return click.Choice( + tuple(typing.get_args(annotation)), + case_sensitive=parameter_info.case_sensitive, + ) raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover