diff --git a/docs/tutorial/parameter-types/choices.md b/docs/tutorial/parameter-types/choices.md new file mode 100644 index 0000000000..f7dfe2b74b --- /dev/null +++ b/docs/tutorial/parameter-types/choices.md @@ -0,0 +1,155 @@ +To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python +`enum.Enum` or the +standard type hint `typing.Literal` + +# Enum + +```Python hl_lines="1 6 7 8 9 12 13" +{!../docs_src/parameter_types/choices/tutorial001.py!} +``` + +!!! tip + Notice that the function parameter `network` will be an `Enum`, not a `str`. + + To get the `str` value in your function's code use `network.value`. + +Check it: + +
+ +```console +$ python main.py --help + +// Notice the predefined values [simple|conv|lstm] +Usage: main.py [OPTIONS] + +Options: + --network [simple|conv|lstm] [default: simple] + --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) +``` + +
+ +### Case insensitive Enum choices + +You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter: + +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../docs_src/parameter_types/choices/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="13" + {!> ../docs_src/parameter_types/choices/tutorial002.py!} + ``` + +And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix: + +
+ +```console +// Notice the upper case CONV +$ python main.py --network CONV + +Training neural network of type: conv + +// A mix also works +$ python main.py --network LsTm + +Training neural network of type: lstm +``` + +
+ + +## Literal + +```Python hl_lines="2 4 7" +{!../docs_src/parameter_types/choices/tutorial003.py!} +``` + +Check it: + +
+ +```console +$ python main.py --help + +// Notice the predefined values [simple|conv|lstm] +Usage: main.py [OPTIONS] + +Options: + --network [simple|conv|lstm] [default: simple] + --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) +``` + +
+ +### Case insensitive Literal choices + +You can make an `Literal` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter: + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../docs_src/parameter_types/choices/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../docs_src/parameter_types/choices/tutorial004.py!} + ``` + +And then the values of the `Literal` will be checked no matter if lower case, upper case, or a mix: + +
+ +```console +// Notice the upper case CONV +$ python main.py --network CONV + +Training neural network of type: conv + +// A mix also works +$ python main.py --network LsTm + +Training neural network of type: lstm +``` + +
diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md deleted file mode 100644 index 767c329b27..0000000000 --- a/docs/tutorial/parameter-types/enum.md +++ /dev/null @@ -1,77 +0,0 @@ -To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python `enum.Enum`: - -```Python hl_lines="1 6 7 8 9 12 13" -{!../docs_src/parameter_types/enum/tutorial001.py!} -``` - -!!! tip - Notice that the function parameter `network` will be an `Enum`, not a `str`. - - To get the `str` value in your function's code use `network.value`. - -Check it: - -
- -```console -$ python main.py --help - -// Notice the predefined values [simple|conv|lstm] -Usage: main.py [OPTIONS] - -Options: - --network [simple|conv|lstm] [default: simple] - --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) -``` - -
- -### Case insensitive Enum choices - -You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter: - -=== "Python 3.6+" - - ```Python hl_lines="15" - {!> ../docs_src/parameter_types/enum/tutorial002_an.py!} - ``` - -=== "Python 3.6+ non-Annotated" - - !!! tip - Prefer to use the `Annotated` version if possible. - - ```Python hl_lines="13" - {!> ../docs_src/parameter_types/enum/tutorial002.py!} - ``` - -And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix: - -
- -```console -// Notice the upper case CONV -$ python main.py --network CONV - -Training neural network of type: conv - -// A mix also works -$ python main.py --network LsTm - -Training neural network of type: lstm -``` - -
diff --git a/docs_src/parameter_types/enum/__init__.py b/docs_src/parameter_types/choices/__init__.py similarity index 100% rename from docs_src/parameter_types/enum/__init__.py rename to docs_src/parameter_types/choices/__init__.py diff --git a/docs_src/parameter_types/enum/tutorial001.py b/docs_src/parameter_types/choices/tutorial001.py similarity index 100% rename from docs_src/parameter_types/enum/tutorial001.py rename to docs_src/parameter_types/choices/tutorial001.py diff --git a/docs_src/parameter_types/enum/tutorial002.py b/docs_src/parameter_types/choices/tutorial002.py similarity index 100% rename from docs_src/parameter_types/enum/tutorial002.py rename to docs_src/parameter_types/choices/tutorial002.py diff --git a/docs_src/parameter_types/enum/tutorial002_an.py b/docs_src/parameter_types/choices/tutorial002_an.py similarity index 100% rename from docs_src/parameter_types/enum/tutorial002_an.py rename to docs_src/parameter_types/choices/tutorial002_an.py diff --git a/docs_src/parameter_types/choices/tutorial003.py b/docs_src/parameter_types/choices/tutorial003.py new file mode 100644 index 0000000000..1532756554 --- /dev/null +++ b/docs_src/parameter_types/choices/tutorial003.py @@ -0,0 +1,12 @@ +import typer +from typing_extensions import Literal + +NeuralNetworkType = Literal["simple", "conv", "lstm"] + + +def main(network: NeuralNetworkType = "simple"): + print(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/choices/tutorial004.py b/docs_src/parameter_types/choices/tutorial004.py new file mode 100644 index 0000000000..cb8628e623 --- /dev/null +++ b/docs_src/parameter_types/choices/tutorial004.py @@ -0,0 +1,12 @@ +import typer +from typing_extensions import Literal + +NeuralNetworkType = Literal["simple", "conv", "lstm"] + + +def main(network: NeuralNetworkType = typer.Option("simple", case_sensitive=False)): + print(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/choices/tutorial004_an.py b/docs_src/parameter_types/choices/tutorial004_an.py new file mode 100644 index 0000000000..9d4fe56fc2 --- /dev/null +++ b/docs_src/parameter_types/choices/tutorial004_an.py @@ -0,0 +1,14 @@ +import typer +from typing_extensions import Annotated, Literal + +NeuralNetworkType = Literal["simple", "conv", "lstm"] + + +def main( + network: Annotated[NeuralNetworkType, typer.Option(case_sensitive=False)] = "simple" +): + print(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/mkdocs.yml b/mkdocs.yml index 8022a19589..a3cf819af8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,7 +57,7 @@ nav: - Boolean CLI Options: tutorial/parameter-types/bool.md - UUID: tutorial/parameter-types/uuid.md - DateTime: tutorial/parameter-types/datetime.md - - Enum - Choices: tutorial/parameter-types/enum.md + - Choices: tutorial/parameter-types/choices.md - Path: tutorial/parameter-types/path.md - File: tutorial/parameter-types/file.md - Custom Types: tutorial/parameter-types/custom-types.md diff --git a/tests/test_tutorial/test_parameter_types/test_enum/__init__.py b/tests/test_tutorial/test_parameter_types/test_choices/__init__.py similarity index 100% rename from tests/test_tutorial/test_parameter_types/test_enum/__init__.py rename to tests/test_tutorial/test_parameter_types/test_choices/__init__.py diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py rename to tests/test_tutorial/test_parameter_types/test_choices/test_tutorial001.py index 584b71a0c7..5b91ff4b8c 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial001.py @@ -4,7 +4,7 @@ import typer from typer.testing import CliRunner -from docs_src.parameter_types.enum import tutorial001 as mod +from docs_src.parameter_types.choices import tutorial001 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002.py similarity index 92% rename from tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002.py rename to tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002.py index 293a1760bf..7eba5bd215 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002.py @@ -4,7 +4,7 @@ import typer from typer.testing import CliRunner -from docs_src.parameter_types.enum import tutorial002 as mod +from docs_src.parameter_types.choices import tutorial002 as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002_an.py similarity index 91% rename from tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py rename to tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002_an.py index c60013daa9..51f9b411f3 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial002_an.py @@ -4,7 +4,7 @@ import typer from typer.testing import CliRunner -from docs_src.parameter_types.enum import tutorial002_an as mod +from docs_src.parameter_types.choices import tutorial002_an as mod runner = CliRunner() diff --git a/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial003.py new file mode 100644 index 0000000000..bb55383a7c --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial003.py @@ -0,0 +1,50 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.choices import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--network" in result.output + assert "[simple|conv|lstm]" in result.output + + +def test_main(): + result = runner.invoke(app, ["--network", "conv"]) + assert result.exit_code == 0 + assert "Training neural network of type: conv" in result.output + + +def test_invalid(): + result = runner.invoke(app, ["--network", "capsule"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Invalid value for '--network': 'capsule' is not one of" in result.output + or "Invalid value for '--network': invalid choice: capsule. (choose from" + in result.output + ) + assert "simple" in result.output + assert "conv" in result.output + assert "lstm" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004.py new file mode 100644 index 0000000000..5fe0d7cefb --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004.py @@ -0,0 +1,34 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.choices import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_upper(): + result = runner.invoke(app, ["--network", "CONV"]) + assert result.exit_code == 0 + assert "Training neural network of type: conv" in result.output + + +def test_mix(): + result = runner.invoke(app, ["--network", "LsTm"]) + assert result.exit_code == 0 + assert "Training neural network of type: lstm" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004_an.py new file mode 100644 index 0000000000..aded51a8ea --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_choices/test_tutorial004_an.py @@ -0,0 +1,34 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.choices import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_upper(): + result = runner.invoke(app, ["--network", "CONV"]) + assert result.exit_code == 0 + assert "Training neural network of type: conv" in result.output + + +def test_mix(): + result = runner.invoke(app, ["--network", "LsTm"]) + assert result.exit_code == 0 + assert "Training neural network of type: lstm" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "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 9de5f5960d..944c2575d1 100644 --- a/typer/main.py +++ b/typer/main.py @@ -13,6 +13,7 @@ import click +from ._typing import is_literal_type, literal_values from .completion import get_completion_inspect_parameters from .core import MarkupMode, TyperArgument, TyperCommand, TyperGroup, TyperOption from .models import ( @@ -776,6 +777,13 @@ def get_click_type( [item.value for item in annotation], case_sensitive=parameter_info.case_sensitive, ) + + if is_literal_type(annotation): + return click.Choice( + literal_values(annotation), + case_sensitive=parameter_info.case_sensitive, + ) + raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover