diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 288eee5a92..7064070cba 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -96,6 +96,77 @@ Training neural network of type: lstm +### Using Enum names instead of values + +Sometimes you want to accept `Enum` names from the command line and convert +that into `Enum` values in the command handler. You can enable this by setting +`enum_by_name=True`: + +//// tab | Python 3.7+ + +```Python hl_lines="14" +{!> ../docs_src/parameter_types/enum/tutorial004_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="13" +{!> ../docs_src/parameter_types/enum/tutorial004.py!} +``` + +//// + +And then the names of the `Enum` will be used instead of values: + +
+ +```console +$ python main.py --log-level debug + +Log level set to DEBUG +``` + +
+ +This can be particularly useful if the enum values are not strings: + +//// tab | Python 3.7+ + +```Python hl_lines="8-11 14" +{!> ../docs_src/parameter_types/enum/tutorial005_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-10 13" +{!../docs_src/parameter_types/enum/tutorial005.py!} +``` + +//// + +```console +$ python main.py --access protected + +Access level: protected (2) +``` + + ### List of Enum values A *CLI parameter* can also take a list of `Enum` values: @@ -153,3 +224,45 @@ Buying groceries: Eggs, Bacon ``` + +You can also combine `enum_by_name=True` with a list of enums: + +//// tab | Python 3.7+ + +```Python hl_lines="15" +{!> ../docs_src/parameter_types/enum/tutorial006_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="13" +{!> ../docs_src/parameter_types/enum/tutorial006.py!} +``` + +//// + +This works exactly the same, but you're using the enum names instead of values: + +
+ +```console +// Try it with a single value +$ python main.py --groceries "f1" + +Buying groceries: Eggs + +// Try it with multiple values +$ python main.py --groceries "f1" --groceries "f2" + +Buying groceries: Eggs, Bacon +``` + +
diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py new file mode 100644 index 0000000000..bbfd575643 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py @@ -0,0 +1,29 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +def main( + names: Tuple[str, str, str, SuperHero] = typer.Argument( + ("Harry", "Hermione", "Ron", "hero3"), + enum_by_name=True, + case_sensitive=False, + help="Select 4 characters to play with", + ), +): + for name in names: + if isinstance(name, Enum): + print(f"Hello {name.value}") + else: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py new file mode 100644 index 0000000000..801ee8c6ce --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -0,0 +1,32 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +def main( + names: Annotated[ + Tuple[str, str, str, SuperHero], + typer.Argument( + enum_by_name=True, + help="Select 4 characters to play with", + case_sensitive=False, + ), + ] = ("Harry", "Hermione", "Ron", "hero3"), +): + for name in names: + if isinstance(name, Enum): + print(f"Hello {name.value}") + else: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial002.py b/docs_src/multiple_values/options_with_multiple_values/tutorial002.py new file mode 100644 index 0000000000..bf3a2bf556 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(user: Tuple[str, int, bool, Food] = typer.Option((None, None, None, Food.f1))): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py new file mode 100644 index 0000000000..c28f832681 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py @@ -0,0 +1,33 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main( + user: Annotated[Tuple[str, int, bool, Food], typer.Option()] = ( + None, + None, + None, + Food.f1, + ), +): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py new file mode 100644 index 0000000000..94715e1a13 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py @@ -0,0 +1,29 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main( + user: Tuple[str, int, bool, Food] = typer.Option( + (None, None, None, "f1"), enum_by_name=True + ), +): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py new file mode 100644 index 0000000000..534825977f --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py @@ -0,0 +1,33 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main( + user: Annotated[Tuple[str, int, bool, Food], typer.Option(enum_by_name=True)] = ( + None, + None, + None, + "f1", + ), +): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py new file mode 100644 index 0000000000..d2ecd0c16d --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -0,0 +1,18 @@ +import enum +import logging + +import typer + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +def main(log_level: LogLevel = typer.Option("warning", enum_by_name=True)): + typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial004_an.py b/docs_src/parameter_types/enum/tutorial004_an.py new file mode 100644 index 0000000000..ca4a15e416 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004_an.py @@ -0,0 +1,19 @@ +import enum +import logging + +import typer +from typing_extensions import Annotated + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +def main(log_level: Annotated[LogLevel, typer.Option(enum_by_name=True)] = "warning"): + typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial005.py b/docs_src/parameter_types/enum/tutorial005.py new file mode 100644 index 0000000000..2804d3960f --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial005.py @@ -0,0 +1,18 @@ +import enum + +import typer + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +def main(access: Access = typer.Option("private", enum_by_name=True)): + typer.echo(f"Access level: {access.name} ({access.value})") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial005_an.py b/docs_src/parameter_types/enum/tutorial005_an.py new file mode 100644 index 0000000000..8a0d2d4bac --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial005_an.py @@ -0,0 +1,19 @@ +import enum + +import typer +from typing_extensions import Annotated + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +def main(access: Annotated[Access, typer.Option(enum_by_name=True)] = "private"): + typer.echo(f"Access level: {access.name} ({access.value})") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial006.py b/docs_src/parameter_types/enum/tutorial006.py new file mode 100644 index 0000000000..6c8a238dd1 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006.py @@ -0,0 +1,18 @@ +from enum import Enum +from typing import List + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(groceries: List[Food] = typer.Option(["f1", "f3"], enum_by_name=True)): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial006_an.py b/docs_src/parameter_types/enum/tutorial006_an.py new file mode 100644 index 0000000000..bbe605428d --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006_an.py @@ -0,0 +1,21 @@ +from enum import Enum +from typing import List + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main( + groceries: Annotated[List[Food], typer.Option(enum_by_name=True)] = ["f1", "f3"], +): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/pyproject.toml b/pyproject.toml index ce9d61afa3..e936bb6d22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ ignore = [ "docs_src/options_autocompletion/tutorial008_an.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an.py" = ["B006"] "docs_src/parameter_types/enum/tutorial003_an.py" = ["B006"] +"docs_src/parameter_types/enum/tutorial006_an.py" = ["B006"] # Loop control variable `value` not used within loop body "docs_src/progressbar/tutorial001.py" = ["B007"] "docs_src/progressbar/tutorial003.py" = ["B007"] diff --git a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py new file mode 100644 index 0000000000..7e53566854 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py @@ -0,0 +1,52 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values 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 "[OPTIONS] [NAMES]..." in result.output + assert "Arguments" in result.output + assert "[default: Harry, Hermione, Ron, hero3]" in result.output + + +def test_defaults(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Harry" in result.output + assert "Hello Hermione" in result.output + assert "Hello Ron" in result.output + assert "Hello Wonder woman" in result.output + + +def test_invalid_args(): + result = runner.invoke(app, ["Draco", "Hagrid"]) + assert result.exit_code != 0 + assert "Argument 'names' takes 4 values" in result.stdout + + +def test_valid_args(): + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "hero1"]) + assert result.exit_code == 0 + assert "Hello Draco" in result.stdout + assert "Hello Hagrid" in result.stdout + assert "Hello Dobby" in result.stdout + assert "Hello Superman" in result.stdout + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py new file mode 100644 index 0000000000..6e5f8c2d09 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,54 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import ( + tutorial003_an 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 "[OPTIONS] [NAMES]..." in result.output + assert "Arguments" in result.output + assert "[default: Harry, Hermione, Ron, hero3]" in result.output + + +def test_defaults(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Harry" in result.output + assert "Hello Hermione" in result.output + assert "Hello Ron" in result.output + assert "Hello Wonder woman" in result.output + + +def test_invalid_args(): + result = runner.invoke(app, ["Draco", "Hagrid"]) + assert result.exit_code != 0 + assert "Argument 'names' takes 4 values" in result.stdout + + +def test_valid_args(): + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "HERO1"]) + assert result.exit_code == 0 + assert "Hello Draco" in result.stdout + assert "Hello Hagrid" in result.stdout + assert "Hello Dobby" in result.stdout + assert "Hello Superman" in result.stdout + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py new file mode 100644 index 0000000000..1533509a77 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "Eggs"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "Bacon"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py new file mode 100644 index 0000000000..e38c33329e --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002_an as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "Eggs"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "Bacon"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py new file mode 100644 index 0000000000..75f457c806 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "f1"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "f2"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py new file mode 100644 index 0000000000..1f8da4a329 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003_an as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "f1"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "f2"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py new file mode 100644 index 0000000000..0eab8e9be9 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_enum_names_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Log level set to: WARNING" in result.output + + +def test_enum_names(): + result = runner.invoke(app, ["--log-level", "debug"]) + assert result.exit_code == 0 + assert "Log level set to: DEBUG" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py new file mode 100644 index 0000000000..d183ad3cb9 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_enum_names_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Log level set to: WARNING" in result.output + + +def test_enum_names(): + result = runner.invoke(app, ["--log-level", "debug"]) + assert result.exit_code == 0 + assert "Log level set to: DEBUG" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py new file mode 100644 index 0000000000..db63a8dc48 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial005 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_int_enum_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Access level: private (1)" in result.output + + +def test_int_enum(): + result = runner.invoke(app, ["--access", "open"]) + assert result.exit_code == 0 + assert "Access level: open (4)" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py new file mode 100644 index 0000000000..7e7ffcd02c --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial005_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_int_enum_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Access level: private (1)" in result.output + + +def test_int_enum(): + result = runner.invoke(app, ["--access", "open"]) + assert result.exit_code == 0 + assert "Access level: open (4)" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py new file mode 100644 index 0000000000..6afefbae02 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006 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 "--groceries" in result.output + assert "[f1|f2|f3]" in result.output + assert "default: f1, f3" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Cheese" in result.output + + +def test_call_single_arg(): + result = runner.invoke(app, ["--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Bacon" in result.output + + +def test_call_multiple_arg(): + result = runner.invoke(app, ["--groceries", "f1", "--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Bacon" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py new file mode 100644 index 0000000000..695a817863 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006_an 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 "--groceries" in result.output + assert "[f1|f2|f3]" in result.output + assert "default: f1, f3" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Cheese" in result.output + + +def test_call_single_arg(): + result = runner.invoke(app, ["--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Bacon" in result.output + + +def test_call_multiple_arg(): + result = runner.invoke(app, ["--groceries", "f1", "--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Bacon" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/main.py b/typer/main.py index a621bda6ad..462f6d65bf 100644 --- a/typer/main.py +++ b/typer/main.py @@ -616,12 +616,17 @@ def get_command_from_info( return command -def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]: +def determine_type_convertor( + type_: Any, enum_by_name: bool +) -> Optional[Callable[[Any], Any]]: convertor: Optional[Callable[[Any], Any]] = None if lenient_issubclass(type_, Path): convertor = param_path_convertor if lenient_issubclass(type_, Enum): - convertor = generate_enum_convertor(type_) + if enum_by_name: + convertor = generate_enum_name_convertor(type_) + else: + convertor = generate_enum_convertor(type_) return convertor @@ -644,6 +649,18 @@ def convertor(value: Any) -> Any: return convertor +def generate_enum_name_convertor(enum: Type[Enum]) -> Callable[..., Any]: + val_map = {str(item.name): item for item in enum} + + def convertor(value: Any) -> Any: + if value is not None: + val = str(value) + if val in val_map: + return val_map[val] + + return convertor + + def generate_list_convertor( convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any] ) -> Callable[[Sequence[Any]], Optional[List[Any]]]: @@ -657,8 +674,9 @@ def internal_convertor(value: Sequence[Any]) -> Optional[List[Any]]: def generate_tuple_convertor( types: Sequence[Any], + enum_by_name: bool, ) -> Callable[[Optional[Tuple[Any, ...]]], Optional[Tuple[Any, ...]]]: - convertors = [determine_type_convertor(type_) for type_ in types] + convertors = [determine_type_convertor(type_, enum_by_name) for type_ in types] def internal_convertor( param_args: Optional[Tuple[Any, ...]], @@ -793,10 +811,11 @@ def get_click_type( atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, Enum): - return click.Choice( - [item.value for item in annotation], - case_sensitive=parameter_info.case_sensitive, - ) + if parameter_info.enum_by_name: + choices = [item.name for item in annotation] + else: + choices = [item.value for item in annotation] + return click.Choice(choices, case_sensitive=parameter_info.case_sensitive) raise RuntimeError(f"Type not yet supported: {annotation}") # pragma: no cover @@ -872,13 +891,14 @@ def get_click_param( parameter_type = get_click_type( annotation=main_type, parameter_info=parameter_info ) - convertor = determine_type_convertor(main_type) + enum_by_name = parameter_info.enum_by_name + convertor = determine_type_convertor(main_type, enum_by_name) if is_list: convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) if is_tuple: - convertor = generate_tuple_convertor(get_args(main_type)) + convertor = generate_tuple_convertor(get_args(main_type), enum_by_name) if isinstance(parameter_info, OptionInfo): if main_type is bool and parameter_info.is_flag is not False: is_flag = True diff --git a/typer/models.py b/typer/models.py index 9bbe2a36d2..16b37ff962 100644 --- a/typer/models.py +++ b/typer/models.py @@ -192,6 +192,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -244,6 +245,7 @@ def __init__( self.hidden = hidden # Choice self.case_sensitive = case_sensitive + self.enum_by_name = enum_by_name # Numbers self.min = min self.max = max @@ -308,6 +310,7 @@ def __init__( show_envvar: bool = True, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -354,6 +357,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -419,6 +423,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -465,6 +470,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max, diff --git a/typer/params.py b/typer/params.py index 2fd025c90d..846172710c 100644 --- a/typer/params.py +++ b/typer/params.py @@ -45,6 +45,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -108,6 +109,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -170,6 +172,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -225,6 +228,7 @@ def Option( show_envvar=show_envvar, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -280,6 +284,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -335,6 +340,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -389,6 +395,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -438,6 +445,7 @@ def Argument( hidden=hidden, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max,