diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index c78dc60cc4..b000e14ee1 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -74,6 +74,37 @@ 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`: + +{* docs_src/parameter_types/enum/tutorial007_an.py hl[18] *} + +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: + +{* docs_src/parameter_types/enum/tutorial005_an.py hl[8:11,18] *} + +```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: @@ -112,6 +143,29 @@ Buying groceries: Eggs, Bacon +You can also combine `enum_by_name=True` with a list of enums: + +{* docs_src/parameter_types/enum/tutorial006_an.py hl[19] *} + +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 +``` + +
+ + ### Literal choices You can also use `Literal` to represent a set of possible predefined choices, without having to use an `Enum`: 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..0ddcdd4d74 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py @@ -0,0 +1,32 @@ +from enum import Enum + +import typer + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..12327332b0 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -0,0 +1,35 @@ +from enum import Enum +from typing import Annotated + +import typer + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..21306a5744 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002.py @@ -0,0 +1,28 @@ +from enum import Enum + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..98a1ea26d1 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py @@ -0,0 +1,36 @@ +from enum import Enum +from typing import Annotated + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..d815ee2297 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py @@ -0,0 +1,32 @@ +from enum import Enum + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..e978fd3d6c --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py @@ -0,0 +1,36 @@ +from enum import Enum +from typing import Annotated + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() diff --git a/docs_src/parameter_types/enum/tutorial005.py b/docs_src/parameter_types/enum/tutorial005.py new file mode 100644 index 0000000000..fc53a87a30 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial005.py @@ -0,0 +1,22 @@ +import enum + +import typer + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +app = typer.Typer() + + +@app.command() +def main(access: Access = typer.Option("private", enum_by_name=True)): + typer.echo(f"Access level: {access.name} ({access.value})") + + +if __name__ == "__main__": + app() 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..96aa2c7b4d --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial005_an.py @@ -0,0 +1,23 @@ +import enum +from typing import Annotated + +import typer + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +app = typer.Typer() + + +@app.command() +def main(access: Annotated[Access, typer.Option(enum_by_name=True)] = "private"): + typer.echo(f"Access level: {access.name} ({access.value})") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial006.py b/docs_src/parameter_types/enum/tutorial006.py new file mode 100644 index 0000000000..aa4edd78e9 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006.py @@ -0,0 +1,21 @@ +from enum import Enum + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() 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..0d633f792e --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006_an.py @@ -0,0 +1,24 @@ +from enum import Enum +from typing import Annotated + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +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__": + app() diff --git a/docs_src/parameter_types/enum/tutorial007.py b/docs_src/parameter_types/enum/tutorial007.py new file mode 100644 index 0000000000..a4cc486c6e --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial007.py @@ -0,0 +1,22 @@ +import enum +import logging + +import typer + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +app = typer.Typer() + + +@app.command() +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__": + app() diff --git a/docs_src/parameter_types/enum/tutorial007_an.py b/docs_src/parameter_types/enum/tutorial007_an.py new file mode 100644 index 0000000000..eaf9b3da36 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial007_an.py @@ -0,0 +1,23 @@ +import enum +import logging +from typing import Annotated + +import typer + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +app = typer.Typer() + + +@app.command() +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__": + app() diff --git a/pyproject.toml b/pyproject.toml index 5897e1aa8a..87ad99f0b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,6 +225,7 @@ ignore = [ "docs_src/options_autocompletion/tutorial008_an_py39.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an_py39.py" = ["B006"] "docs_src/parameter_types/enum/tutorial003_an_py39.py" = ["B006"] +"docs_src/parameter_types/enum/tutorial006_an.py" = ["B006"] # Loop control variable `value` not used within loop body "docs_src/progressbar/tutorial001_py39.py" = ["B007"] "docs_src/progressbar/tutorial003_py39.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..cc57aac480 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py @@ -0,0 +1,50 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import tutorial003 as mod + +runner = CliRunner() +app = mod.app + + +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.output + + +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..6ff743ea2f --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,52 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import ( + tutorial003_an as mod, +) + +runner = CliRunner() +app = mod.app + + +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.output + + +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..1407974161 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002 as mod + +runner = CliRunner() +app = mod.app + + +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..bef118c1ab --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002_an as mod + +runner = CliRunner() +app = mod.app + + +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..fa1a1de195 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003 as mod + +runner = CliRunner() +app = mod.app + + +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..d008030297 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003_an as mod + +runner = CliRunner() +app = mod.app + + +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_tutorial005.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py new file mode 100644 index 0000000000..6aaf8521bd --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py @@ -0,0 +1,29 @@ +import subprocess + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial005 as mod + +runner = CliRunner() +app = mod.app + + +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..d77070dc37 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py @@ -0,0 +1,29 @@ +import subprocess + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial005_an as mod + +runner = CliRunner() +app = mod.app + + +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..bf95a14e0f --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py @@ -0,0 +1,44 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006 as mod + +runner = CliRunner() +app = mod.app + + +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..fd9b2404e7 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py @@ -0,0 +1,44 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006_an as mod + +runner = CliRunner() +app = mod.app + + +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_tutorial007.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial007.py new file mode 100644 index 0000000000..d0b2834ee1 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial007.py @@ -0,0 +1,29 @@ +import subprocess + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial007 as mod + +runner = CliRunner() +app = mod.app + + +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_tutorial007_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial007_an.py new file mode 100644 index 0000000000..62ecc82e9d --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial007_an.py @@ -0,0 +1,29 @@ +import subprocess + +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial007_an as mod + +runner = CliRunner() +app = mod.app + + +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/typer/main.py b/typer/main.py index e8c6b9e429..bd0c7d8bc5 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 @@ -646,6 +651,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[[Optional[Sequence[Any]]], Optional[list[Any]]]: @@ -659,8 +676,9 @@ def internal_convertor(value: Optional[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, ...]], @@ -795,15 +813,16 @@ def get_click_type( atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, Enum): + if parameter_info.enum_by_name: + choices = [item.name for item in annotation] + else: + choices = [item.value for item in annotation] # The custom TyperChoice is only needed for Click < 8.2.0, to parse the # command line values matching them to the enum values. Click 8.2.0 added # support for enum values but reading enum names. # Passing here the list of enum values (instead of just the enum) accounts for # Click < 8.2.0. - return TyperChoice( - [item.value for item in annotation], - case_sensitive=parameter_info.case_sensitive, - ) + return TyperChoice(choices, case_sensitive=parameter_info.case_sensitive) elif is_literal_type(annotation): return click.Choice( literal_values(annotation), @@ -884,13 +903,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: is_flag = True diff --git a/typer/models.py b/typer/models.py index 78d1a5354d..7e975fa4b9 100644 --- a/typer/models.py +++ b/typer/models.py @@ -191,6 +191,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, @@ -243,6 +244,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 @@ -310,6 +312,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, @@ -356,6 +359,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -430,6 +434,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, @@ -476,6 +481,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 2a03c03e71..d0315814c6 100644 --- a/typer/params.py +++ b/typer/params.py @@ -48,6 +48,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, @@ -114,6 +115,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, @@ -179,6 +181,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, @@ -234,6 +237,7 @@ def Option( show_envvar=show_envvar, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -291,6 +295,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, @@ -348,6 +353,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, @@ -404,6 +410,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, @@ -453,6 +460,7 @@ def Argument( hidden=hidden, # Choice case_sensitive=case_sensitive, + enum_by_name=enum_by_name, # Numbers min=min, max=max,