From bea8ed3fcd1cdbe58467d6741a53e6fbd9e1e332 Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 18:16:18 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20PEP-593=20`An?= =?UTF-8?q?notated`=20for=20specifying=20options=20and=20arguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements #184 --- .coveragerc | 3 + docs_src/arguments/default/tutorial001_an.py | 10 + docs_src/arguments/default/tutorial002_an.py | 16 + docs_src/arguments/envvar/tutorial001_an.py | 10 + docs_src/arguments/envvar/tutorial002_an.py | 12 + docs_src/arguments/envvar/tutorial003_an.py | 14 + docs_src/arguments/help/tutorial001_an.py | 10 + docs_src/arguments/help/tutorial002_an.py | 13 + docs_src/arguments/help/tutorial003_an.py | 13 + docs_src/arguments/help/tutorial004_an.py | 17 + docs_src/arguments/help/tutorial005_an.py | 17 + docs_src/arguments/help/tutorial006_an.py | 10 + docs_src/arguments/help/tutorial007_an.py | 22 + docs_src/arguments/help/tutorial008_an.py | 13 + docs_src/arguments/optional/tutorial001_an.py | 10 + docs_src/arguments/optional/tutorial002_an.py | 15 + docs_src/commands/help/tutorial001_an.py | 67 ++ docs_src/commands/help/tutorial004_an.py | 37 ++ docs_src/commands/help/tutorial005_an.py | 37 ++ docs_src/commands/help/tutorial007_an.py | 46 ++ docs_src/commands/options/tutorial001_an.py | 43 ++ .../tutorial002_an.py | 17 + .../multiple_options/tutorial001_an.py | 16 + .../multiple_options/tutorial002_an.py | 12 + .../tutorial001_an.py | 18 + docs_src/options/callback/tutorial001_an.py | 16 + docs_src/options/callback/tutorial003_an.py | 19 + docs_src/options/callback/tutorial004_an.py | 19 + docs_src/options/help/tutorial001_an.py | 22 + docs_src/options/help/tutorial002_an.py | 33 + docs_src/options/help/tutorial003_an.py | 10 + docs_src/options/name/tutorial001_an.py | 10 + docs_src/options/name/tutorial002_an.py | 10 + docs_src/options/name/tutorial003_an.py | 10 + docs_src/options/name/tutorial004_an.py | 10 + docs_src/options/name/tutorial005_an.py | 16 + docs_src/options/prompt/tutorial001_an.py | 10 + docs_src/options/prompt/tutorial002_an.py | 13 + docs_src/options/prompt/tutorial003_an.py | 12 + docs_src/options/version/tutorial003_an.py | 32 + .../options_autocompletion/tutorial002_an.py | 22 + .../options_autocompletion/tutorial003_an.py | 28 + .../options_autocompletion/tutorial004_an.py | 33 + .../options_autocompletion/tutorial007_an.py | 35 + .../options_autocompletion/tutorial008_an.py | 38 ++ .../options_autocompletion/tutorial009_an.py | 39 ++ .../parameter_types/bool/tutorial001_an.py | 13 + .../parameter_types/bool/tutorial002_an.py | 17 + .../parameter_types/bool/tutorial003_an.py | 13 + .../parameter_types/bool/tutorial004_an.py | 13 + .../datetime/tutorial002_an.py | 19 + .../parameter_types/enum/tutorial002_an.py | 22 + .../parameter_types/file/tutorial001_an.py | 11 + .../parameter_types/file/tutorial002_an.py | 11 + .../parameter_types/file/tutorial003_an.py | 14 + .../parameter_types/file/tutorial004_an.py | 18 + .../parameter_types/file/tutorial005_an.py | 11 + .../parameter_types/number/tutorial001_an.py | 16 + .../parameter_types/number/tutorial002_an.py | 16 + .../parameter_types/number/tutorial003_an.py | 10 + .../parameter_types/path/tutorial001_an.py | 22 + .../parameter_types/path/tutorial002_an.py | 25 + pyproject.toml | 15 +- tests/test_ambiguous_params.py | 230 +++++++ tests/test_annotated.py | 59 ++ .../test_default/test_tutorial001_an.py | 42 ++ .../test_default/test_tutorial002_an.py | 44 ++ .../test_envvar/test_tutorial001_an.py | 62 ++ .../test_envvar/test_tutorial002_an.py | 49 ++ .../test_envvar/test_tutorial003_an.py | 49 ++ .../test_help/test_tutorial001_an.py | 52 ++ .../test_help/test_tutorial002_an.py | 39 ++ .../test_help/test_tutorial003_an.py | 39 ++ .../test_help/test_tutorial004_an.py | 39 ++ .../test_help/test_tutorial005_an.py | 37 ++ .../test_help/test_tutorial006_an.py | 37 ++ .../test_help/test_tutorial007_an.py | 37 ++ .../test_help/test_tutorial008_an.py | 50 ++ .../test_optional/test_tutorial001_an.py | 51 ++ .../test_optional/test_tutorial002_an.py | 40 ++ .../test_help/test_tutorial001_an.py | 126 ++++ .../test_help/test_tutorial004_an.py | 60 ++ .../test_help/test_tutorial005_an.py | 61 ++ .../test_help/test_tutorial007_an.py | 56 ++ .../test_options/test_tutorial001_an.py | 97 +++ .../test_tutorial002_an.py | 58 ++ .../test_tutorial001_an.py | 44 ++ .../test_tutorial002_an.py | 39 ++ .../test_tutorial001_an.py | 53 ++ .../test_callback/test_tutorial001_an.py | 34 + .../test_callback/test_tutorial003_an.py | 53 ++ .../test_callback/test_tutorial004_an.py | 53 ++ .../test_help/test_tutorial001_an.py | 49 ++ .../test_help/test_tutorial002_an.py | 45 ++ .../test_help/test_tutorial003_an.py | 36 + .../test_name/test_tutorial001_an.py | 36 + .../test_name/test_tutorial002_an.py | 43 ++ .../test_name/test_tutorial003_an.py | 37 ++ .../test_name/test_tutorial004_an.py | 43 ++ .../test_name/test_tutorial005_an.py | 55 ++ .../test_prompt/test_tutorial001_an.py | 43 ++ .../test_prompt/test_tutorial002_an.py | 43 ++ .../test_prompt/test_tutorial003_an.py | 57 ++ .../test_version/test_tutorial003_an.py | 58 ++ .../test_tutorial002_an.py | 43 ++ .../test_tutorial003_an.py | 43 ++ .../test_tutorial004_an.py | 43 ++ .../test_tutorial007_an.py | 44 ++ .../test_tutorial008_an.py | 46 ++ .../test_tutorial009_an.py | 46 ++ .../test_bool/test_tutorial001_an.py | 52 ++ .../test_bool/test_tutorial002_an.py | 71 ++ .../test_bool/test_tutorial003_an.py | 43 ++ .../test_bool/test_tutorial004_an.py | 47 ++ .../test_datetime/test_tutorial002_an.py | 34 + .../test_enum/test_tutorial002_an.py | 34 + .../test_file/test_tutorial001_an.py | 33 + .../test_file/test_tutorial002_an.py | 35 + .../test_file/test_tutorial003_an.py | 32 + .../test_file/test_tutorial004_an.py | 36 + .../test_file/test_tutorial005_an.py | 38 ++ .../test_number/test_tutorial001_an.py | 99 +++ .../test_number/test_tutorial002_an.py | 42 ++ .../test_number/test_tutorial003_an.py | 58 ++ .../test_path/test_tutorial001_an.py | 55 ++ .../test_path/test_tutorial002_an.py | 48 ++ typer/_typing.py | 627 ++++++++++++++++++ typer/models.py | 6 + typer/params.py | 8 +- typer/utils.py | 178 ++++- 130 files changed, 5263 insertions(+), 14 deletions(-) create mode 100644 docs_src/arguments/default/tutorial001_an.py create mode 100644 docs_src/arguments/default/tutorial002_an.py create mode 100644 docs_src/arguments/envvar/tutorial001_an.py create mode 100644 docs_src/arguments/envvar/tutorial002_an.py create mode 100644 docs_src/arguments/envvar/tutorial003_an.py create mode 100644 docs_src/arguments/help/tutorial001_an.py create mode 100644 docs_src/arguments/help/tutorial002_an.py create mode 100644 docs_src/arguments/help/tutorial003_an.py create mode 100644 docs_src/arguments/help/tutorial004_an.py create mode 100644 docs_src/arguments/help/tutorial005_an.py create mode 100644 docs_src/arguments/help/tutorial006_an.py create mode 100644 docs_src/arguments/help/tutorial007_an.py create mode 100644 docs_src/arguments/help/tutorial008_an.py create mode 100644 docs_src/arguments/optional/tutorial001_an.py create mode 100644 docs_src/arguments/optional/tutorial002_an.py create mode 100644 docs_src/commands/help/tutorial001_an.py create mode 100644 docs_src/commands/help/tutorial004_an.py create mode 100644 docs_src/commands/help/tutorial005_an.py create mode 100644 docs_src/commands/help/tutorial007_an.py create mode 100644 docs_src/commands/options/tutorial001_an.py create mode 100644 docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an.py create mode 100644 docs_src/multiple_values/multiple_options/tutorial001_an.py create mode 100644 docs_src/multiple_values/multiple_options/tutorial002_an.py create mode 100644 docs_src/multiple_values/options_with_multiple_values/tutorial001_an.py create mode 100644 docs_src/options/callback/tutorial001_an.py create mode 100644 docs_src/options/callback/tutorial003_an.py create mode 100644 docs_src/options/callback/tutorial004_an.py create mode 100644 docs_src/options/help/tutorial001_an.py create mode 100644 docs_src/options/help/tutorial002_an.py create mode 100644 docs_src/options/help/tutorial003_an.py create mode 100644 docs_src/options/name/tutorial001_an.py create mode 100644 docs_src/options/name/tutorial002_an.py create mode 100644 docs_src/options/name/tutorial003_an.py create mode 100644 docs_src/options/name/tutorial004_an.py create mode 100644 docs_src/options/name/tutorial005_an.py create mode 100644 docs_src/options/prompt/tutorial001_an.py create mode 100644 docs_src/options/prompt/tutorial002_an.py create mode 100644 docs_src/options/prompt/tutorial003_an.py create mode 100644 docs_src/options/version/tutorial003_an.py create mode 100644 docs_src/options_autocompletion/tutorial002_an.py create mode 100644 docs_src/options_autocompletion/tutorial003_an.py create mode 100644 docs_src/options_autocompletion/tutorial004_an.py create mode 100644 docs_src/options_autocompletion/tutorial007_an.py create mode 100644 docs_src/options_autocompletion/tutorial008_an.py create mode 100644 docs_src/options_autocompletion/tutorial009_an.py create mode 100644 docs_src/parameter_types/bool/tutorial001_an.py create mode 100644 docs_src/parameter_types/bool/tutorial002_an.py create mode 100644 docs_src/parameter_types/bool/tutorial003_an.py create mode 100644 docs_src/parameter_types/bool/tutorial004_an.py create mode 100644 docs_src/parameter_types/datetime/tutorial002_an.py create mode 100644 docs_src/parameter_types/enum/tutorial002_an.py create mode 100644 docs_src/parameter_types/file/tutorial001_an.py create mode 100644 docs_src/parameter_types/file/tutorial002_an.py create mode 100644 docs_src/parameter_types/file/tutorial003_an.py create mode 100644 docs_src/parameter_types/file/tutorial004_an.py create mode 100644 docs_src/parameter_types/file/tutorial005_an.py create mode 100644 docs_src/parameter_types/number/tutorial001_an.py create mode 100644 docs_src/parameter_types/number/tutorial002_an.py create mode 100644 docs_src/parameter_types/number/tutorial003_an.py create mode 100644 docs_src/parameter_types/path/tutorial001_an.py create mode 100644 docs_src/parameter_types/path/tutorial002_an.py create mode 100644 tests/test_ambiguous_params.py create mode 100644 tests/test_annotated.py create mode 100644 tests/test_tutorial/test_arguments/test_default/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_arguments/test_default/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_arguments/test_envvar/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_arguments/test_envvar/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_arguments/test_envvar/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial007_an.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial008_an.py create mode 100644 tests/test_tutorial/test_arguments/test_optional/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_arguments/test_optional/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_commands/test_help/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_commands/test_help/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_commands/test_help/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py create mode 100644 tests/test_tutorial/test_commands/test_options/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_options/test_help/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_options/test_help/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_options/test_help/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options/test_name/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_options/test_name/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_options/test_name/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options/test_name/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_options/test_name/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_options/test_prompt/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_options/test_prompt/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_options/test_prompt/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options/test_version/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py create mode 100644 tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_bool/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_bool/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_bool/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_file/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_file/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_file/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_file/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_file/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_number/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_number/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_path/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py create mode 100644 typer/_typing.py diff --git a/.coveragerc b/.coveragerc index 54df9e71c1..cd101bb7e9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,5 +5,8 @@ source = tests docs_src +omit = + typer/_typing.py + parallel = True context = '${CONTEXT}' diff --git a/docs_src/arguments/default/tutorial001_an.py b/docs_src/arguments/default/tutorial001_an.py new file mode 100644 index 0000000000..61e46749f2 --- /dev/null +++ b/docs_src/arguments/default/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument()] = "Wade Wilson"): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/default/tutorial002_an.py b/docs_src/arguments/default/tutorial002_an.py new file mode 100644 index 0000000000..64e0369d2c --- /dev/null +++ b/docs_src/arguments/default/tutorial002_an.py @@ -0,0 +1,16 @@ +import random + +import typer +from typing_extensions import Annotated + + +def get_name(): + return random.choice(["Deadpool", "Rick", "Morty", "Hiro"]) + + +def main(name: Annotated[str, typer.Argument(default_factory=get_name)]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/envvar/tutorial001_an.py b/docs_src/arguments/envvar/tutorial001_an.py new file mode 100644 index 0000000000..d6c57921ee --- /dev/null +++ b/docs_src/arguments/envvar/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(envvar="AWESOME_NAME")] = "World"): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/envvar/tutorial002_an.py b/docs_src/arguments/envvar/tutorial002_an.py new file mode 100644 index 0000000000..f5373e134a --- /dev/null +++ b/docs_src/arguments/envvar/tutorial002_an.py @@ -0,0 +1,12 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[str, typer.Argument(envvar=["AWESOME_NAME", "GOD_NAME"])] = "World" +): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/envvar/tutorial003_an.py b/docs_src/arguments/envvar/tutorial003_an.py new file mode 100644 index 0000000000..8cb29195a9 --- /dev/null +++ b/docs_src/arguments/envvar/tutorial003_an.py @@ -0,0 +1,14 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[ + str, typer.Argument(envvar="AWESOME_NAME", show_envvar=False) + ] = "World" +): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial001_an.py b/docs_src/arguments/help/tutorial001_an.py new file mode 100644 index 0000000000..95ac95c147 --- /dev/null +++ b/docs_src/arguments/help/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial002_an.py b/docs_src/arguments/help/tutorial002_an.py new file mode 100644 index 0000000000..b3557e6561 --- /dev/null +++ b/docs_src/arguments/help/tutorial002_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]): + """ + Say hi to NAME very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial003_an.py b/docs_src/arguments/help/tutorial003_an.py new file mode 100644 index 0000000000..cee1cdbfe3 --- /dev/null +++ b/docs_src/arguments/help/tutorial003_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(help="Who to greet")] = "World"): + """ + Say hi to NAME very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial004_an.py b/docs_src/arguments/help/tutorial004_an.py new file mode 100644 index 0000000000..7338d06754 --- /dev/null +++ b/docs_src/arguments/help/tutorial004_an.py @@ -0,0 +1,17 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[ + str, typer.Argument(help="Who to greet", show_default=False) + ] = "World" +): + """ + Say hi to NAME very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial005_an.py b/docs_src/arguments/help/tutorial005_an.py new file mode 100644 index 0000000000..394f54e38f --- /dev/null +++ b/docs_src/arguments/help/tutorial005_an.py @@ -0,0 +1,17 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[ + str, + typer.Argument( + help="Who to greet", show_default="Deadpoolio the amazing's name" + ), + ] = "Wade Wilson" +): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial006_an.py b/docs_src/arguments/help/tutorial006_an.py new file mode 100644 index 0000000000..4d745278b4 --- /dev/null +++ b/docs_src/arguments/help/tutorial006_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(metavar="✨username✨")] = "World"): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial007_an.py b/docs_src/arguments/help/tutorial007_an.py new file mode 100644 index 0000000000..cf07e2b45d --- /dev/null +++ b/docs_src/arguments/help/tutorial007_an.py @@ -0,0 +1,22 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[str, typer.Argument(help="Who to greet")], + lastname: Annotated[ + str, typer.Argument(help="The last name", rich_help_panel="Secondary Arguments") + ] = "", + age: Annotated[ + str, + typer.Argument(help="The user's age", rich_help_panel="Secondary Arguments"), + ] = "", +): + """ + Say hi to NAME very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/help/tutorial008_an.py b/docs_src/arguments/help/tutorial008_an.py new file mode 100644 index 0000000000..aaa357f6bf --- /dev/null +++ b/docs_src/arguments/help/tutorial008_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument(hidden=True)] = "World"): + """ + Say hi to NAME very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/optional/tutorial001_an.py b/docs_src/arguments/optional/tutorial001_an.py new file mode 100644 index 0000000000..d2b49f42b5 --- /dev/null +++ b/docs_src/arguments/optional/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[str, typer.Argument()]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/optional/tutorial002_an.py b/docs_src/arguments/optional/tutorial002_an.py new file mode 100644 index 0000000000..299a0bb6ea --- /dev/null +++ b/docs_src/arguments/optional/tutorial002_an.py @@ -0,0 +1,15 @@ +from typing import Optional + +import typer +from typing_extensions import Annotated + + +def main(name: Annotated[Optional[str], typer.Argument()] = None): + if name is None: + print("Hello World!") + else: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/commands/help/tutorial001_an.py b/docs_src/commands/help/tutorial001_an.py new file mode 100644 index 0000000000..a7736da05c --- /dev/null +++ b/docs_src/commands/help/tutorial001_an.py @@ -0,0 +1,67 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer(help="Awesome CLI user manager.") + + +@app.command() +def create(username: str): + """ + Create a new user with USERNAME. + """ + print(f"Creating user: {username}") + + +@app.command() +def delete( + username: str, + force: Annotated[ + bool, + typer.Option( + prompt="Are you sure you want to delete the user?", + help="Force deletion without confirmation.", + ), + ], +): + """ + Delete a user with USERNAME. + + If --force is not used, will ask for confirmation. + """ + if force: + print(f"Deleting user: {username}") + else: + print("Operation cancelled") + + +@app.command() +def delete_all( + force: Annotated[ + bool, + typer.Option( + prompt="Are you sure you want to delete ALL users?", + help="Force deletion without confirmation.", + ), + ] +): + """ + Delete ALL users in the database. + + If --force is not used, will ask for confirmation. + """ + if force: + print("Deleting all users") + else: + print("Operation cancelled") + + +@app.command() +def init(): + """ + Initialize the users database. + """ + print("Initializing user database") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial004_an.py b/docs_src/commands/help/tutorial004_an.py new file mode 100644 index 0000000000..d4bad589eb --- /dev/null +++ b/docs_src/commands/help/tutorial004_an.py @@ -0,0 +1,37 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: Annotated[ + str, typer.Argument(help="The username to be [green]created[/green]") + ] +): + """ + [bold green]Create[/bold green] a new [italic]shinny[/italic] user. :sparkles: + + This requires a [underline]username[/underline]. + """ + print(f"Creating user: {username}") + + +@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].") +def delete( + username: Annotated[ + str, typer.Argument(help="The username to be [red]deleted[/red]") + ], + force: Annotated[ + bool, typer.Option(help="Force the [bold red]deletion[/bold red] :boom:") + ] = False, +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial005_an.py b/docs_src/commands/help/tutorial005_an.py new file mode 100644 index 0000000000..208d97e680 --- /dev/null +++ b/docs_src/commands/help/tutorial005_an.py @@ -0,0 +1,37 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer(rich_markup_mode="markdown") + + +@app.command() +def create( + username: Annotated[str, typer.Argument(help="The username to be **created**")] +): + """ + **Create** a new *shinny* user. :sparkles: + + * Create a username + + * Show that the username is created + + --- + + Learn more at the [Typer docs website](https://typer.tiangolo.com) + """ + print(f"Creating user: {username}") + + +@app.command(help="**Delete** a user with *USERNAME*.") +def delete( + username: Annotated[str, typer.Argument(help="The username to be **deleted**")], + force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False, +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial007_an.py b/docs_src/commands/help/tutorial007_an.py new file mode 100644 index 0000000000..cd1dcc39f6 --- /dev/null +++ b/docs_src/commands/help/tutorial007_an.py @@ -0,0 +1,46 @@ +from typing import Union + +import typer +from typing_extensions import Annotated + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: Annotated[str, typer.Argument(help="The username to create")], + lastname: Annotated[ + str, + typer.Argument( + help="The last name of the new user", rich_help_panel="Secondary Arguments" + ), + ] = "", + force: Annotated[bool, typer.Option(help="Force the creation of the user")] = False, + age: Annotated[ + Union[int, None], + typer.Option(help="The age of the new user", rich_help_panel="Additional Data"), + ] = None, + favorite_color: Annotated[ + Union[str, None], + typer.Option( + help="The favorite color of the new user", + rich_help_panel="Additional Data", + ), + ] = None, +): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +@app.command(rich_help_panel="Utils and Configs") +def config(configuration: str): + """ + [blue]Configure[/blue] the system. :wrench: + """ + print(f"Configuring the system with: {configuration}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/options/tutorial001_an.py b/docs_src/commands/options/tutorial001_an.py new file mode 100644 index 0000000000..bafc4543ca --- /dev/null +++ b/docs_src/commands/options/tutorial001_an.py @@ -0,0 +1,43 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer() + + +@app.command() +def create(username: str): + print(f"Creating user: {username}") + + +@app.command() +def delete( + username: str, + force: Annotated[ + bool, typer.Option(prompt="Are you sure you want to delete the user?") + ], +): + if force: + print(f"Deleting user: {username}") + else: + print("Operation cancelled") + + +@app.command() +def delete_all( + force: Annotated[ + bool, typer.Option(prompt="Are you sure you want to delete ALL users?") + ] +): + if force: + print("Deleting all users") + else: + print("Operation cancelled") + + +@app.command() +def init(): + print("Initializing user database") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an.py new file mode 100644 index 0000000000..7ef3af7e51 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an.py @@ -0,0 +1,17 @@ +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +def main( + names: Annotated[ + Tuple[str, str, str], typer.Argument(help="Select 3 characters to play with") + ] = ("Harry", "Hermione", "Ron") +): + for name in names: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/multiple_options/tutorial001_an.py b/docs_src/multiple_values/multiple_options/tutorial001_an.py new file mode 100644 index 0000000000..68ad2519ea --- /dev/null +++ b/docs_src/multiple_values/multiple_options/tutorial001_an.py @@ -0,0 +1,16 @@ +from typing import List, Optional + +import typer +from typing_extensions import Annotated + + +def main(user: Annotated[Optional[List[str]], typer.Option()] = None): + if not user: + print("No provided users") + raise typer.Abort() + for u in user: + print(f"Processing user: {u}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/multiple_options/tutorial002_an.py b/docs_src/multiple_values/multiple_options/tutorial002_an.py new file mode 100644 index 0000000000..5b872add6d --- /dev/null +++ b/docs_src/multiple_values/multiple_options/tutorial002_an.py @@ -0,0 +1,12 @@ +from typing import List + +import typer +from typing_extensions import Annotated + + +def main(number: Annotated[List[float], typer.Option()] = []): + print(f"The sum is {sum(number)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial001_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial001_an.py new file mode 100644 index 0000000000..f6480e73ef --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial001_an.py @@ -0,0 +1,18 @@ +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +def main(user: Annotated[Tuple[str, int, bool], typer.Option()] = (None, None, None)): + username, coins, is_wizard = 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!") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/callback/tutorial001_an.py b/docs_src/options/callback/tutorial001_an.py new file mode 100644 index 0000000000..9f57221dc8 --- /dev/null +++ b/docs_src/options/callback/tutorial001_an.py @@ -0,0 +1,16 @@ +import typer +from typing_extensions import Annotated + + +def name_callback(value: str): + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +def main(name: Annotated[str, typer.Option(callback=name_callback)]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/callback/tutorial003_an.py b/docs_src/options/callback/tutorial003_an.py new file mode 100644 index 0000000000..18a374d65d --- /dev/null +++ b/docs_src/options/callback/tutorial003_an.py @@ -0,0 +1,19 @@ +import typer +from typing_extensions import Annotated + + +def name_callback(ctx: typer.Context, value: str): + if ctx.resilient_parsing: + return + print("Validating name") + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +def main(name: Annotated[str, typer.Option(callback=name_callback)]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/callback/tutorial004_an.py b/docs_src/options/callback/tutorial004_an.py new file mode 100644 index 0000000000..d8412ed784 --- /dev/null +++ b/docs_src/options/callback/tutorial004_an.py @@ -0,0 +1,19 @@ +import typer +from typing_extensions import Annotated + + +def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if ctx.resilient_parsing: + return + print(f"Validating param: {param.name}") + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +def main(name: Annotated[str, typer.Option(callback=name_callback)]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/help/tutorial001_an.py b/docs_src/options/help/tutorial001_an.py new file mode 100644 index 0000000000..61613caa1f --- /dev/null +++ b/docs_src/options/help/tutorial001_an.py @@ -0,0 +1,22 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: str, + lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "", + formal: Annotated[bool, typer.Option(help="Say hi formally.")] = False, +): + """ + Say hi to NAME, optionally with a --lastname. + + If --formal is used, say hi very formally. + """ + if formal: + print(f"Good day Ms. {name} {lastname}.") + else: + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/help/tutorial002_an.py b/docs_src/options/help/tutorial002_an.py new file mode 100644 index 0000000000..1faefce33b --- /dev/null +++ b/docs_src/options/help/tutorial002_an.py @@ -0,0 +1,33 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: str, + lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "", + formal: Annotated[ + bool, + typer.Option( + help="Say hi formally.", rich_help_panel="Customization and Utils" + ), + ] = False, + debug: Annotated[ + bool, + typer.Option( + help="Enable debugging.", rich_help_panel="Customization and Utils" + ), + ] = False, +): + """ + Say hi to NAME, optionally with a --lastname. + + If --formal is used, say hi very formally. + """ + if formal: + print(f"Good day Ms. {name} {lastname}.") + else: + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/help/tutorial003_an.py b/docs_src/options/help/tutorial003_an.py new file mode 100644 index 0000000000..272446617f --- /dev/null +++ b/docs_src/options/help/tutorial003_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(fullname: Annotated[str, typer.Option(show_default=False)] = "Wade Wilson"): + print(f"Hello {fullname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/name/tutorial001_an.py b/docs_src/options/name/tutorial001_an.py new file mode 100644 index 0000000000..9a6e4564d0 --- /dev/null +++ b/docs_src/options/name/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(user_name: Annotated[str, typer.Option("--name")]): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/name/tutorial002_an.py b/docs_src/options/name/tutorial002_an.py new file mode 100644 index 0000000000..42362cde03 --- /dev/null +++ b/docs_src/options/name/tutorial002_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(user_name: Annotated[str, typer.Option("--name", "-n")]): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/name/tutorial003_an.py b/docs_src/options/name/tutorial003_an.py new file mode 100644 index 0000000000..1bc1956c5f --- /dev/null +++ b/docs_src/options/name/tutorial003_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(user_name: Annotated[str, typer.Option("-n")]): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/name/tutorial004_an.py b/docs_src/options/name/tutorial004_an.py new file mode 100644 index 0000000000..42da7464b1 --- /dev/null +++ b/docs_src/options/name/tutorial004_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(user_name: Annotated[str, typer.Option("--user-name", "-n")]): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/name/tutorial005_an.py b/docs_src/options/name/tutorial005_an.py new file mode 100644 index 0000000000..02fec5e442 --- /dev/null +++ b/docs_src/options/name/tutorial005_an.py @@ -0,0 +1,16 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: Annotated[str, typer.Option("--name", "-n")], + formal: Annotated[bool, typer.Option("--formal", "-f")] = False, +): + if formal: + print(f"Good day Ms. {name}.") + else: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/prompt/tutorial001_an.py b/docs_src/options/prompt/tutorial001_an.py new file mode 100644 index 0000000000..5c34b494f0 --- /dev/null +++ b/docs_src/options/prompt/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: str, lastname: Annotated[str, typer.Option(prompt=True)]): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/prompt/tutorial002_an.py b/docs_src/options/prompt/tutorial002_an.py new file mode 100644 index 0000000000..fb0eef45e0 --- /dev/null +++ b/docs_src/options/prompt/tutorial002_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: str, + lastname: Annotated[str, typer.Option(prompt="Please tell me your last name")], +): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/prompt/tutorial003_an.py b/docs_src/options/prompt/tutorial003_an.py new file mode 100644 index 0000000000..63e9097894 --- /dev/null +++ b/docs_src/options/prompt/tutorial003_an.py @@ -0,0 +1,12 @@ +import typer +from typing_extensions import Annotated + + +def main( + project_name: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)] +): + print(f"Deleting project {project_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/version/tutorial003_an.py b/docs_src/options/version/tutorial003_an.py new file mode 100644 index 0000000000..e13bddc05f --- /dev/null +++ b/docs_src/options/version/tutorial003_an.py @@ -0,0 +1,32 @@ +from typing import Optional + +import typer +from typing_extensions import Annotated + +__version__ = "0.1.0" + + +def version_callback(value: bool): + if value: + print(f"Awesome CLI Version: {__version__}") + raise typer.Exit() + + +def name_callback(name: str): + if name != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return name + + +def main( + name: Annotated[str, typer.Option(callback=name_callback)], + version: Annotated[ + Optional[bool], + typer.Option("--version", callback=version_callback, is_eager=True), + ] = None, +): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options_autocompletion/tutorial002_an.py b/docs_src/options_autocompletion/tutorial002_an.py new file mode 100644 index 0000000000..a2df928a0f --- /dev/null +++ b/docs_src/options_autocompletion/tutorial002_an.py @@ -0,0 +1,22 @@ +import typer +from typing_extensions import Annotated + + +def complete_name(): + return ["Camila", "Carlos", "Sebastian"] + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial003_an.py b/docs_src/options_autocompletion/tutorial003_an.py new file mode 100644 index 0000000000..ed874388af --- /dev/null +++ b/docs_src/options_autocompletion/tutorial003_an.py @@ -0,0 +1,28 @@ +import typer +from typing_extensions import Annotated + +valid_names = ["Camila", "Carlos", "Sebastian"] + + +def complete_name(incomplete: str): + completion = [] + for name in valid_names: + if name.startswith(incomplete): + completion.append(name) + return completion + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial004_an.py b/docs_src/options_autocompletion/tutorial004_an.py new file mode 100644 index 0000000000..673f8a4d34 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial004_an.py @@ -0,0 +1,33 @@ +import typer +from typing_extensions import Annotated + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + + +def complete_name(incomplete: str): + completion = [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete): + completion_item = (name, help_text) + completion.append(completion_item) + return completion + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial007_an.py b/docs_src/options_autocompletion/tutorial007_an.py new file mode 100644 index 0000000000..de6c8054e4 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial007_an.py @@ -0,0 +1,35 @@ +from typing import List + +import typer +from typing_extensions import Annotated + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + + +def complete_name(ctx: typer.Context, incomplete: str): + names = ctx.params.get("name") or [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete) and name not in names: + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + List[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial008_an.py b/docs_src/options_autocompletion/tutorial008_an.py new file mode 100644 index 0000000000..9dcb0df77e --- /dev/null +++ b/docs_src/options_autocompletion/tutorial008_an.py @@ -0,0 +1,38 @@ +from typing import List + +import typer +from rich.console import Console +from typing_extensions import Annotated + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + +err_console = Console(stderr=True) + + +def complete_name(args: List[str], incomplete: str): + err_console.print(f"{args}") + for name, help_text in valid_completion_items: + if name.startswith(incomplete): + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + List[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial009_an.py b/docs_src/options_autocompletion/tutorial009_an.py new file mode 100644 index 0000000000..c5b825eaf0 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial009_an.py @@ -0,0 +1,39 @@ +from typing import List + +import typer +from rich.console import Console +from typing_extensions import Annotated + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + +err_console = Console(stderr=True) + + +def complete_name(ctx: typer.Context, args: List[str], incomplete: str): + err_console.print(f"{args}") + names = ctx.params.get("name") or [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete) and name not in names: + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + List[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/bool/tutorial001_an.py b/docs_src/parameter_types/bool/tutorial001_an.py new file mode 100644 index 0000000000..ea6f3d5c3e --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial001_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(force: Annotated[bool, typer.Option("--force")] = False): + if force: + print("Forcing operation") + else: + print("Not forcing") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/bool/tutorial002_an.py b/docs_src/parameter_types/bool/tutorial002_an.py new file mode 100644 index 0000000000..ba4cf41eb2 --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial002_an.py @@ -0,0 +1,17 @@ +from typing import Optional + +import typer +from typing_extensions import Annotated + + +def main(accept: Annotated[Optional[bool], typer.Option("--accept/--reject")] = None): + if accept is None: + print("I don't know what you want yet") + elif accept: + print("Accepting!") + else: + print("Rejecting!") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/bool/tutorial003_an.py b/docs_src/parameter_types/bool/tutorial003_an.py new file mode 100644 index 0000000000..0687db1adb --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial003_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False): + if force: + print("Forcing operation") + else: + print("Not forcing") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/bool/tutorial004_an.py b/docs_src/parameter_types/bool/tutorial004_an.py new file mode 100644 index 0000000000..1cb42fcc86 --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial004_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True): + if in_prod: + print("Running in production") + else: + print("Running demo") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/datetime/tutorial002_an.py b/docs_src/parameter_types/datetime/tutorial002_an.py new file mode 100644 index 0000000000..6897432df1 --- /dev/null +++ b/docs_src/parameter_types/datetime/tutorial002_an.py @@ -0,0 +1,19 @@ +from datetime import datetime + +import typer +from typing_extensions import Annotated + + +def main( + launch_date: Annotated[ + datetime, + typer.Argument( + formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"] + ), + ] +): + print(f"Launch will be at: {launch_date}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial002_an.py b/docs_src/parameter_types/enum/tutorial002_an.py new file mode 100644 index 0000000000..77c1057570 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial002_an.py @@ -0,0 +1,22 @@ +from enum import Enum + +import typer +from typing_extensions import Annotated + + +class NeuralNetwork(str, Enum): + simple = "simple" + conv = "conv" + lstm = "lstm" + + +def main( + network: Annotated[ + NeuralNetwork, typer.Option(case_sensitive=False) + ] = NeuralNetwork.simple +): + print(f"Training neural network of type: {network.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/file/tutorial001_an.py b/docs_src/parameter_types/file/tutorial001_an.py new file mode 100644 index 0000000000..e31037af6f --- /dev/null +++ b/docs_src/parameter_types/file/tutorial001_an.py @@ -0,0 +1,11 @@ +import typer +from typing_extensions import Annotated + + +def main(config: Annotated[typer.FileText, typer.Option()]): + for line in config: + print(f"Config line: {line}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/file/tutorial002_an.py b/docs_src/parameter_types/file/tutorial002_an.py new file mode 100644 index 0000000000..4ac474cee2 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial002_an.py @@ -0,0 +1,11 @@ +import typer +from typing_extensions import Annotated + + +def main(config: Annotated[typer.FileTextWrite, typer.Option()]): + config.write("Some config written by the app") + print("Config written") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/file/tutorial003_an.py b/docs_src/parameter_types/file/tutorial003_an.py new file mode 100644 index 0000000000..9df65aa4f2 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial003_an.py @@ -0,0 +1,14 @@ +import typer +from typing_extensions import Annotated + + +def main(file: Annotated[typer.FileBinaryRead, typer.Option()]): + processed_total = 0 + for bytes_chunk in file: + # Process the bytes in bytes_chunk + processed_total += len(bytes_chunk) + print(f"Processed bytes total: {processed_total}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/file/tutorial004_an.py b/docs_src/parameter_types/file/tutorial004_an.py new file mode 100644 index 0000000000..66221769f0 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial004_an.py @@ -0,0 +1,18 @@ +import typer +from typing_extensions import Annotated + + +def main(file: Annotated[typer.FileBinaryWrite, typer.Option()]): + first_line_str = "some settings\n" + # You cannot write str directly to a binary file, you have to encode it to get bytes + first_line_bytes = first_line_str.encode("utf-8") + # Then you can write the bytes + file.write(first_line_bytes) + # This is already bytes, it starts with b" + second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o" + file.write(second_line) + print("Binary file written") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/file/tutorial005_an.py b/docs_src/parameter_types/file/tutorial005_an.py new file mode 100644 index 0000000000..ca1c03f590 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial005_an.py @@ -0,0 +1,11 @@ +import typer +from typing_extensions import Annotated + + +def main(config: Annotated[typer.FileText, typer.Option(mode="a")]): + config.write("This is a single line\n") + print("Config line written") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/number/tutorial001_an.py b/docs_src/parameter_types/number/tutorial001_an.py new file mode 100644 index 0000000000..b2c417eca7 --- /dev/null +++ b/docs_src/parameter_types/number/tutorial001_an.py @@ -0,0 +1,16 @@ +import typer +from typing_extensions import Annotated + + +def main( + id: Annotated[int, typer.Argument(min=0, max=1000)], + age: Annotated[int, typer.Option(min=18)] = 20, + score: Annotated[float, typer.Option(max=100)] = 0, +): + print(f"ID is {id}") + print(f"--age is {age}") + print(f"--score is {score}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/number/tutorial002_an.py b/docs_src/parameter_types/number/tutorial002_an.py new file mode 100644 index 0000000000..78f6e0b165 --- /dev/null +++ b/docs_src/parameter_types/number/tutorial002_an.py @@ -0,0 +1,16 @@ +import typer +from typing_extensions import Annotated + + +def main( + id: Annotated[int, typer.Argument(min=0, max=1000)], + rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0, + score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0, +): + print(f"ID is {id}") + print(f"--rank is {rank}") + print(f"--score is {score}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/number/tutorial003_an.py b/docs_src/parameter_types/number/tutorial003_an.py new file mode 100644 index 0000000000..82d37a5dbe --- /dev/null +++ b/docs_src/parameter_types/number/tutorial003_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0): + print(f"Verbose level is {verbose}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/path/tutorial001_an.py b/docs_src/parameter_types/path/tutorial001_an.py new file mode 100644 index 0000000000..0706110164 --- /dev/null +++ b/docs_src/parameter_types/path/tutorial001_an.py @@ -0,0 +1,22 @@ +from pathlib import Path +from typing import Optional + +import typer +from typing_extensions import Annotated + + +def main(config: Annotated[Optional[Path], typer.Option()] = None): + if config is None: + print("No config file") + raise typer.Abort() + if config.is_file(): + text = config.read_text() + print(f"Config file contents: {text}") + elif config.is_dir(): + print("Config is a directory, will use all its config files") + elif not config.exists(): + print("The config doesn't exist") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/path/tutorial002_an.py b/docs_src/parameter_types/path/tutorial002_an.py new file mode 100644 index 0000000000..052d3106dd --- /dev/null +++ b/docs_src/parameter_types/path/tutorial002_an.py @@ -0,0 +1,25 @@ +from pathlib import Path + +import typer +from typing_extensions import Annotated + + +def main( + config: Annotated[ + Path, + typer.Option( + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + resolve_path=True, + ), + ] +): + text = config.read_text() + print(f"Config file contents: {text}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/pyproject.toml b/pyproject.toml index acaf8f895d..cfbca17620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,8 @@ classifiers = [ "License :: OSI Approved :: MIT License" ] requires = [ - "click >= 7.1.1, <9.0.0" + "click >= 7.1.1, <9.0.0", + "typing-extensions >= 3.7.4.3", ] description-file = "README.md" requires-python = ">=3.6" @@ -45,18 +46,18 @@ test = [ "pytest-cov >=2.10.0,<5.0.0", "coverage >=6.2,<7.0", "pytest-xdist >=1.32.0,<4.0.0", - "pytest-sugar >=0.9.4,<0.10.0", + # "pytest-sugar >=0.8.4,<0.10.0", "mypy ==0.910", "black >=22.3.0,<23.0.0", "isort >=5.0.6,<6.0.0", "rich >=10.11.0,<13.0.0", ] doc = [ - "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=8.1.4,<9.0.0", - "mdx-include >=1.4.1,<2.0.0", - "pillow >=9.3.0,<10.0.0", - "cairosvg >=2.5.2,<3.0.0", + # "mkdocs >=1.1.2,<2.0.0", + # "mkdocs-material >=8.1.4,<9.0.0", + # "mdx-include >=1.4.1,<2.0.0", + # "pillow >=9.3.0,<10.0.0", + # "cairosvg >=2.5.2,<3.0.0", ] dev = [ "autoflake >=1.3.1,<2.0.0", diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py new file mode 100644 index 0000000000..1b4dd4e18b --- /dev/null +++ b/tests/test_ambiguous_params.py @@ -0,0 +1,230 @@ +import pytest +import typer +from typer.testing import CliRunner +from typer.utils import ( + AnnotatedParamWithDefaultValueError, + DefaultFactoryAndDefaultValueError, + MixedAnnotatedAndDefaultStyleError, + MultipleTyperAnnotationsError, + _split_annotation_from_typer_annotations, +) +from typing_extensions import Annotated + +runner = CliRunner() + + +def test_split_annotations_from_typer_annotations_simple(): + # Simple sanity check that this utility works. If this isn't working on a given + # python version, then no other tests for Annotated will work. + given = Annotated[str, typer.Argument()] + base, typer_annotations = _split_annotation_from_typer_annotations(given) + assert base is str + # No equality check on the param types. Checking the length is sufficient. + assert len(typer_annotations) == 1 + + +def test_forbid_default_value_in_annotated_argument(): + app = typer.Typer() + + # This test case only works with `typer.Argument`. `typer.Option` uses positionals + # for param_decls too. + @app.command() + def cmd(my_param: Annotated[str, typer.Argument("foo")]): + ... + + with pytest.raises(AnnotatedParamWithDefaultValueError) as excinfo: + runner.invoke(app) + + assert vars(excinfo.value) == dict( + param_type=typer.models.ArgumentInfo, + argument_name="my_param", + ) + + +def test_allow_options_to_have_names(): + app = typer.Typer() + + @app.command() + def cmd(my_param: Annotated[str, typer.Option("--some-opt")]): + print(my_param) + + result = runner.invoke(app, ["--some-opt", "hello"]) + assert result.exit_code == 0, result.output + assert "hello" in result.output + + +@pytest.mark.parametrize( + ["param", "param_info_type"], + [ + (typer.Argument, typer.models.ArgumentInfo), + (typer.Option, typer.models.OptionInfo), + ], +) +def test_forbid_annotated_param_and_default_param(param, param_info_type): + app = typer.Typer() + + @app.command() + def cmd(my_param: Annotated[str, param()] = param("foo")): + ... + + with pytest.raises(MixedAnnotatedAndDefaultStyleError) as excinfo: + runner.invoke(app) + + assert vars(excinfo.value) == dict( + argument_name="my_param", + annotated_param_type=param_info_type, + default_param_type=param_info_type, + ) + + +def test_forbid_multiple_typer_params_in_annotated(): + app = typer.Typer() + + @app.command() + def cmd(my_param: Annotated[str, typer.Argument(), typer.Argument()]): + ... + + with pytest.raises(MultipleTyperAnnotationsError) as excinfo: + runner.invoke(app) + + assert vars(excinfo.value) == dict(argument_name="my_param") + + +def test_allow_multiple_non_typer_params_in_annotated(): + app = typer.Typer() + + @app.command() + def cmd(my_param: Annotated[str, "someval", typer.Argument(), 4] = "hello"): + print(my_param) + + result = runner.invoke(app) + # Should behave like normal + assert result.exit_code == 0, result.output + assert "hello" in result.output + + +@pytest.mark.parametrize( + ["param", "param_info_type"], + [ + (typer.Argument, typer.models.ArgumentInfo), + (typer.Option, typer.models.OptionInfo), + ], +) +def test_forbid_default_factory_and_default_value_in_annotated(param, param_info_type): + def make_string(): + return "foo" + + app = typer.Typer() + + @app.command() + def cmd(my_param: Annotated[str, param(default_factory=make_string)] = "hello"): + ... + + with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo: + runner.invoke(app) + + assert vars(excinfo.value) == dict( + argument_name="my_param", + param_type=param_info_type, + ) + + +@pytest.mark.parametrize( + "param", + [ + typer.Argument, + typer.Option, + ], +) +def test_allow_default_factory_with_default_param(param): + def make_string(): + return "foo" + + app = typer.Typer() + + @app.command() + def cmd(my_param: str = param(default_factory=make_string)): + print(my_param) + + result = runner.invoke(app) + assert result.exit_code == 0, result.output + assert "foo" in result.output + + +@pytest.mark.parametrize( + ["param", "param_info_type"], + [ + (typer.Argument, typer.models.ArgumentInfo), + (typer.Option, typer.models.OptionInfo), + ], +) +def test_forbid_default_and_default_factory_with_default_param(param, param_info_type): + def make_string(): + return "foo" + + app = typer.Typer() + + @app.command() + def cmd(my_param: str = param("hi", default_factory=make_string)): + ... + + with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo: + runner.invoke(app) + + assert vars(excinfo.value) == dict( + argument_name="my_param", + param_type=param_info_type, + ) + + +@pytest.mark.parametrize( + ["error", "message"], + [ + ( + AnnotatedParamWithDefaultValueError( + argument_name="my_argument", + param_type=typer.models.ArgumentInfo, + ), + "`Argument` default value cannot be set in `Annotated` for 'my_argument'. Set the default value with `=` instead.", + ), + ( + MixedAnnotatedAndDefaultStyleError( + argument_name="my_argument", + annotated_param_type=typer.models.OptionInfo, + default_param_type=typer.models.ArgumentInfo, + ), + "Cannot specify `Option` in `Annotated` and `Argument` as a default value together for 'my_argument'", + ), + ( + MixedAnnotatedAndDefaultStyleError( + argument_name="my_argument", + annotated_param_type=typer.models.OptionInfo, + default_param_type=typer.models.OptionInfo, + ), + "Cannot specify `Option` in `Annotated` and default value together for 'my_argument'", + ), + ( + MixedAnnotatedAndDefaultStyleError( + argument_name="my_argument", + annotated_param_type=typer.models.ArgumentInfo, + default_param_type=typer.models.ArgumentInfo, + ), + "Cannot specify `Argument` in `Annotated` and default value together for 'my_argument'", + ), + ( + MultipleTyperAnnotationsError( + argument_name="my_argument", + ), + "Cannot specify multiple `Annotated` Typer arguments for 'my_argument'", + ), + ( + DefaultFactoryAndDefaultValueError( + argument_name="my_argument", + param_type=typer.models.OptionInfo, + ), + "Cannot specify `default_factory` and a default value together for `Option`", + ), + ], +) +def test_error_rendering(error, message): + assert str(error) == message diff --git a/tests/test_annotated.py b/tests/test_annotated.py new file mode 100644 index 0000000000..6436ad668e --- /dev/null +++ b/tests/test_annotated.py @@ -0,0 +1,59 @@ +import typer +from typer.testing import CliRunner +from typing_extensions import Annotated + +runner = CliRunner() + + +def test_annotated_argument_with_default(): + app = typer.Typer() + + @app.command() + def cmd(val: Annotated[int, typer.Argument()] = 0): + print(f"hello {val}") + + result = runner.invoke(app) + assert result.exit_code == 0, result.output + assert "hello 0" in result.output + + result = runner.invoke(app, ["42"]) + assert result.exit_code == 0, result.output + assert "hello 42" in result.output + + +def test_annotated_argument_with_default_factory(): + app = typer.Typer() + + def make_string(): + return "I made it" + + @app.command() + def cmd(val: Annotated[str, typer.Argument(default_factory=make_string)]): + print(val) + + result = runner.invoke(app) + assert result.exit_code == 0, result.output + assert "I made it" in result.output + + result = runner.invoke(app, ["overridden"]) + assert result.exit_code == 0, result.output + assert "overridden" in result.output + + +def test_annotated_option_with_argname_doesnt_mutate_multiple_calls(): + app = typer.Typer() + + @app.command() + def cmd(force: Annotated[bool, typer.Option("--force")] = False): + if force: + print("Forcing operation") + else: + print("Not forcing") + + result = runner.invoke(app) + assert result.exit_code == 0, result.output + assert "Not forcing" in result.output + + result = runner.invoke(app, ["--force"]) + assert result.exit_code == 0, result.output + assert "Forcing operation" in result.output diff --git a/tests/test_tutorial/test_arguments/test_default/test_tutorial001_an.py b/tests/test_tutorial/test_arguments/test_default/test_tutorial001_an.py new file mode 100644 index 0000000000..b1d5655366 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_default/test_tutorial001_an.py @@ -0,0 +1,42 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.default import tutorial001_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] [NAME]" in result.output + assert "Arguments" in result.output + assert "[default: Wade Wilson]" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0, result.output + assert "Hello Wade Wilson" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_default/test_tutorial002_an.py b/tests/test_tutorial/test_arguments/test_default/test_tutorial002_an.py new file mode 100644 index 0000000000..4bf1332d9c --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_default/test_tutorial002_an.py @@ -0,0 +1,44 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.default import tutorial002_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] [NAME]" in result.output + assert "Arguments" in result.output + assert "[default: (dynamic)]" in result.output + + +def test_call_no_arg(): + greetings = ["Hello Deadpool", "Hello Rick", "Hello Morty", "Hello Hiro"] + for i in range(3): + result = runner.invoke(app) + assert result.exit_code == 0 + assert any(greet in result.output for greet in greetings) + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_envvar/test_tutorial001_an.py b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial001_an.py new file mode 100644 index 0000000000..90a5df9f96 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial001_an.py @@ -0,0 +1,62 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.arguments.envvar import tutorial001_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] [NAME]" in result.output + assert "Arguments" in result.output + assert "env var: AWESOME_NAME" in result.output + assert "default: World" in result.output + + +def test_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] [NAME]" in result.output + assert "Arguments" in result.output + assert "env var: AWESOME_NAME" in result.output + assert "default: World" in result.output + typer.core.rich = rich + + +def test_call_arg(): + result = runner.invoke(app, ["Wednesday"]) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var(): + result = runner.invoke(app, env={"AWESOME_NAME": "Wednesday"}) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var_arg(): + result = runner.invoke(app, ["Czernobog"], env={"AWESOME_NAME": "Wednesday"}) + assert result.exit_code == 0 + assert "Hello Mr. Czernobog" 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_arguments/test_envvar/test_tutorial002_an.py b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial002_an.py new file mode 100644 index 0000000000..a62d4e0df2 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial002_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.envvar import tutorial002_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] [NAME]" in result.output + assert "Arguments" in result.output + assert "env var: AWESOME_NAME, GOD_NAME" in result.output + assert "default: World" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Wednesday"]) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var1(): + result = runner.invoke(app, env={"AWESOME_NAME": "Wednesday"}) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var2(): + result = runner.invoke(app, env={"GOD_NAME": "Anubis"}) + assert result.exit_code == 0 + assert "Hello Mr. Anubis" 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_arguments/test_envvar/test_tutorial003_an.py b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial003_an.py new file mode 100644 index 0000000000..c1cc2bc8a8 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_envvar/test_tutorial003_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.envvar 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] [NAME]" in result.output + assert "Arguments" in result.output + assert "env var: AWESOME_NAME" not in result.output + assert "default: World" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Wednesday"]) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var(): + result = runner.invoke(app, env={"AWESOME_NAME": "Wednesday"}) + assert result.exit_code == 0 + assert "Hello Mr. Wednesday" in result.output + + +def test_call_env_var_arg(): + result = runner.invoke(app, ["Czernobog"], env={"AWESOME_NAME": "Wednesday"}) + assert result.exit_code == 0 + assert "Hello Mr. Czernobog" 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_arguments/test_help/test_tutorial001_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial001_an.py new file mode 100644 index 0000000000..7ca0bf7ce6 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial001_an.py @@ -0,0 +1,52 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial001_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] NAME" in result.output + assert "Arguments" in result.output + assert "NAME" in result.output + assert "The name of the user to greet" in result.output + assert "[required]" in result.output + + +def test_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] NAME" in result.output + assert "Arguments" in result.output + assert "NAME" in result.output + assert "The name of the user to greet" in result.output + assert "[required]" in result.output + typer.core.rich = rich + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial002_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial002_an.py new file mode 100644 index 0000000000..5473708509 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial002_an.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial002_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] NAME" in result.output + assert "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" in result.output + assert "NAME" in result.output + assert "The name of the user to greet" in result.output + assert "[required]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial003_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial003_an.py new file mode 100644 index 0000000000..7be39e0b95 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial003_an.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help 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] [NAME]" in result.output + assert "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" in result.output + assert "NAME" in result.output + assert "Who to greet" in result.output + assert "[default: World]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial004_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial004_an.py new file mode 100644 index 0000000000..0c87e811b5 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial004_an.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial004_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] [NAME]" in result.output + assert "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" in result.output + assert "NAME" in result.output + assert "Who to greet" in result.output + assert "[default: World]" not in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial005_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial005_an.py new file mode 100644 index 0000000000..908e8f1d1d --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial005_an.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial005_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] [NAME]" in result.output + assert "Arguments" in result.output + assert "Who to greet" in result.output + assert "[default: (Deadpoolio the amazing's name)]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial006_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py new file mode 100644 index 0000000000..64a985d26d --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help 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 "[OPTIONS] ✨username✨" in result.output + assert "Arguments" in result.output + assert "✨username✨" in result.output + assert "[default: World]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial007_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial007_an.py new file mode 100644 index 0000000000..fae243df06 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial007_an.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial007_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 "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" in result.output + assert "Secondary Arguments" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_help/test_tutorial008_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial008_an.py new file mode 100644 index 0000000000..66316d2b11 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial008_an.py @@ -0,0 +1,50 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial008_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] [NAME]" in result.output + assert "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" not in result.output + assert "[default: World]" not in result.output + + +def test_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] [NAME]" in result.output + assert "Say hi to NAME very gently, like Dirk." in result.output + assert "Arguments" not in result.output + assert "[default: World]" not in result.output + typer.core.rich = rich + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_optional/test_tutorial001_an.py b/tests/test_tutorial/test_arguments/test_optional/test_tutorial001_an.py new file mode 100644 index 0000000000..d1ad8ebde4 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_optional/test_tutorial001_an.py @@ -0,0 +1,51 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.arguments.optional import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "Missing argument 'NAME'." in result.output + + +def test_call_no_arg_standalone(): + # Mainly for coverage + result = runner.invoke(app, standalone_mode=False) + assert result.exit_code != 0 + + +def test_call_no_arg_no_rich(): + # Mainly for coverage + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app) + assert result.exit_code != 0 + assert "Error: Missing argument 'NAME'" in result.stdout + typer.core.rich = rich + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_arguments/test_optional/test_tutorial002_an.py b/tests/test_tutorial/test_arguments/test_optional/test_tutorial002_an.py new file mode 100644 index 0000000000..5a0a768976 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_optional/test_tutorial002_an.py @@ -0,0 +1,40 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.optional import tutorial002_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] [NAME]" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello World!" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_commands/test_help/test_tutorial001_an.py b/tests/test_tutorial/test_commands/test_help/test_tutorial001_an.py new file mode 100644 index 0000000000..8be85698d0 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial001_an.py @@ -0,0 +1,126 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.help import tutorial001_an as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Awesome CLI user manager." in result.output + assert "create" in result.output + assert "Create a new user with USERNAME." in result.output + assert "delete" in result.output + assert "Delete a user with USERNAME." in result.output + assert "delete-all" in result.output + assert "Delete ALL users in the database." in result.output + assert "init" in result.output + assert "Initialize the users database." in result.output + + +def test_help_create(): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + assert "create [OPTIONS] USERNAME" in result.output + assert "Create a new user with USERNAME." in result.output + + +def test_help_delete(): + result = runner.invoke(app, ["delete", "--help"]) + assert result.exit_code == 0 + assert "delete [OPTIONS] USERNAME" in result.output + assert "Delete a user with USERNAME." in result.output + assert "--force" in result.output + assert "--no-force" in result.output + assert "Force deletion without confirmation." in result.output + + +def test_help_delete_all(): + result = runner.invoke(app, ["delete-all", "--help"]) + assert result.exit_code == 0 + assert "delete-all [OPTIONS]" in result.output + assert "Delete ALL users in the database." in result.output + assert "If --force is not used, will ask for confirmation." in result.output + assert "[required]" in result.output + assert "--force" in result.output + assert "--no-force" in result.output + assert "Force deletion without confirmation." in result.output + + +def test_help_init(): + result = runner.invoke(app, ["init", "--help"]) + assert result.exit_code == 0 + assert "init [OPTIONS]" in result.output + assert "Initialize the users database." in result.output + + +def test_create(): + result = runner.invoke(app, ["create", "Camila"]) + assert result.exit_code == 0 + assert "Creating user: Camila" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete", "Camila"], input="y\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete the user? [y/n]:" in result.output + or "Are you sure you want to delete the user? [y/N]:" in result.output + ) + assert "Deleting user: Camila" in result.output + + +def test_no_delete(): + result = runner.invoke(app, ["delete", "Camila"], input="n\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete the user? [y/n]:" in result.output + or "Are you sure you want to delete the user? [y/N]:" in result.output + ) + assert "Operation cancelled" in result.output + + +def test_delete_all(): + result = runner.invoke(app, ["delete-all"], input="y\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete ALL users? [y/n]:" in result.output + or "Are you sure you want to delete ALL users? [y/N]:" in result.output + ) + assert "Deleting all users" in result.output + + +def test_no_delete_all(): + result = runner.invoke(app, ["delete-all"], input="n\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete ALL users? [y/n]:" in result.output + or "Are you sure you want to delete ALL users? [y/N]:" in result.output + ) + assert "Operation cancelled" in result.output + + +def test_init(): + result = runner.invoke(app, ["init"]) + assert result.exit_code == 0 + assert "Initializing user database" 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_commands/test_help/test_tutorial004_an.py b/tests/test_tutorial/test_commands/test_help/test_tutorial004_an.py new file mode 100644 index 0000000000..a7481667f4 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial004_an.py @@ -0,0 +1,60 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.help import tutorial004_an as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + assert "Create a new shinny user. ✨" in result.output + assert "delete" in result.output + assert "Delete a user with USERNAME." in result.output + assert "Some internal utility function to create." not in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_help_create(): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + assert "Create a new shinny user. ✨" in result.output + assert "The username to be created" in result.output + assert "Some internal utility function to create." not in result.output + + +def test_help_delete(): + result = runner.invoke(app, ["delete", "--help"]) + assert result.exit_code == 0 + assert "Delete a user with USERNAME." in result.output + assert "The username to be deleted" in result.output + assert "Force the deletion 💥" in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_create(): + result = runner.invoke(app, ["create", "Camila"]) + assert result.exit_code == 0 + assert "Creating user: Camila" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete", "Camila"]) + assert result.exit_code == 0 + assert "Deleting user: Camila" 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_commands/test_help/test_tutorial005_an.py b/tests/test_tutorial/test_commands/test_help/test_tutorial005_an.py new file mode 100644 index 0000000000..91af901bb6 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial005_an.py @@ -0,0 +1,61 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.help import tutorial005_an as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + assert "Create a new shinny user. ✨" in result.output + assert "delete" in result.output + assert "Delete a user with USERNAME." in result.output + assert "Some internal utility function to create." not in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_help_create(): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + assert "Create a new shinny user. ✨" in result.output + assert "The username to be created" in result.output + assert "Learn more at the Typer docs website" in result.output + assert "Some internal utility function to create." not in result.output + + +def test_help_delete(): + result = runner.invoke(app, ["delete", "--help"]) + assert result.exit_code == 0 + assert "Delete a user with USERNAME." in result.output + assert "The username to be deleted" in result.output + assert "Force the deletion 💥" in result.output + assert "Some internal utility function to delete." not in result.output + + +def test_create(): + result = runner.invoke(app, ["create", "Camila"]) + assert result.exit_code == 0 + assert "Creating user: Camila" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete", "Camila"]) + assert result.exit_code == 0 + assert "Deleting user: Camila" 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_commands/test_help/test_tutorial007_an.py b/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py new file mode 100644 index 0000000000..98c748a36a --- /dev/null +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py @@ -0,0 +1,56 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.help import tutorial007_an as mod + +app = mod.app + +runner = CliRunner() + + +def test_main_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + assert "Create a new user. ✨" in result.output + assert "Utils and Configs" in result.output + assert "config" in result.output + assert "Configure the system. 🔧" in result.output + + +def test_create_help(): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + assert "username" in result.output + assert "The username to create" in result.output + assert "Secondary Arguments" in result.output + assert "lastname" in result.output + assert "The last name of the new user" in result.output + assert "--force" in result.output + assert "--no-force" in result.output + assert "Force the creation of the user" in result.output + assert "Additional Data" in result.output + assert "--age" in result.output + assert "The age of the new user" in result.output + assert "--favorite-color" in result.output + assert "The favorite color of the new user" in result.output + + +def test_call(): + # Mainly for coverage + result = runner.invoke(app, ["create", "Morty"]) + assert result.exit_code == 0 + result = runner.invoke(app, ["config", "Morty"]) + assert result.exit_code == 0 + + +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_commands/test_options/test_tutorial001_an.py b/tests/test_tutorial/test_commands/test_options/test_tutorial001_an.py new file mode 100644 index 0000000000..b65fe03f44 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_options/test_tutorial001_an.py @@ -0,0 +1,97 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.commands.options import tutorial001_an as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Commands" in result.output + assert "create" in result.output + assert "delete" in result.output + assert "delete-all" in result.output + assert "init" in result.output + + +def test_create(): + result = runner.invoke(app, ["create", "Camila"]) + assert result.exit_code == 0 + assert "Creating user: Camila" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete", "Camila"], input="y\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete the user? [y/n]:" in result.output + or "Are you sure you want to delete the user? [y/N]:" in result.output + ) + assert "Deleting user: Camila" in result.output + + +def test_no_delete(): + result = runner.invoke(app, ["delete", "Camila"], input="n\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete the user? [y/n]:" in result.output + or "Are you sure you want to delete the user? [y/N]:" in result.output + ) + assert "Operation cancelled" in result.output + + +def test_delete_all(): + result = runner.invoke(app, ["delete-all"], input="y\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete ALL users? [y/n]:" in result.output + or "Are you sure you want to delete ALL users? [y/N]:" in result.output + ) + assert "Deleting all users" in result.output + + +def test_no_delete_all(): + result = runner.invoke(app, ["delete-all"], input="n\n") + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete ALL users? [y/n]:" in result.output + or "Are you sure you want to delete ALL users? [y/N]:" in result.output + ) + assert "Operation cancelled" in result.output + + +def test_delete_all_force(): + result = runner.invoke(app, ["delete-all", "--force"]) + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + assert ( + "Are you sure you want to delete ALL users? [y/n]:" not in result.output + or "Are you sure you want to delete ALL users? [y/N]:" not in result.output + ) + assert "Deleting all users" in result.output + + +def test_init(): + result = runner.invoke(app, ["init"]) + assert result.exit_code == 0 + assert "Initializing user database" 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_multiple_values/test_arguments_with_multiple_values/test_tutorial002_an.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial002_an.py new file mode 100644 index 0000000000..d99a38e651 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial002_an.py @@ -0,0 +1,58 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import ( + tutorial002_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]" 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 + + +def test_invalid_args(): + result = runner.invoke(app, ["Draco", "Hagrid"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Argument 'names' takes 3 values" in result.stdout + or "argument names takes 3 values" in result.stdout + ) + + +def test_valid_args(): + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby"]) + assert result.exit_code == 0 + assert "Hello Draco" in result.stdout + assert "Hello Hagrid" in result.stdout + assert "Hello Dobby" in result.stdout + + +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_multiple_values/test_multiple_options/test_tutorial001_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial001_an.py new file mode 100644 index 0000000000..0009fd2f05 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial001_an.py @@ -0,0 +1,44 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.multiple_options import tutorial001_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 provided users" in result.output + assert "Aborted" in result.output + + +def test_1_user(): + result = runner.invoke(app, ["--user", "Camila"]) + assert result.exit_code == 0 + assert "Processing user: Camila" in result.output + + +def test_3_user(): + result = runner.invoke( + app, ["--user", "Camila", "--user", "Rick", "--user", "Morty"] + ) + assert result.exit_code == 0 + assert "Processing user: Camila" in result.output + assert "Processing user: Rick" in result.output + assert "Processing user: Morty" 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_multiple_values/test_multiple_options/test_tutorial002_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial002_an.py new file mode 100644 index 0000000000..92fb291cdb --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options/test_tutorial002_an.py @@ -0,0 +1,39 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.multiple_options 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 "The sum is 0" in result.output + + +def test_1_number(): + result = runner.invoke(app, ["--number", "2"]) + assert result.exit_code == 0 + assert "The sum is 2.0" in result.output + + +def test_2_number(): + result = runner.invoke(app, ["--number", "2", "--number", "3", "--number", "4.5"]) + assert result.exit_code == 0 + assert "The sum is 9.5" 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_multiple_values/test_options_with_multiple_values/test_tutorial001_an.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial001_an.py new file mode 100644 index 0000000000..c83b7b6bb2 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial001_an.py @@ -0,0 +1,53 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial001_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"]) + 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 + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no"]) + 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 + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Option '--user' requires 3 arguments" in result.output + or "--user option requires 3 arguments" 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_options/test_callback/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py new file mode 100644 index 0000000000..7d5cda20cc --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial001_an.py @@ -0,0 +1,34 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.callback import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Only Camila is allowed" 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_options/test_callback/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py new file mode 100644 index 0000000000..7bb3754816 --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py @@ -0,0 +1,53 @@ +import os +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.callback import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Validating name" in result.output + assert "Hello Camila" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Only Camila is allowed" 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 + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL003_AN.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial003_an.py --", + "COMP_CWORD": "1", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "--name" in result.stdout diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py new file mode 100644 index 0000000000..34810f8eef --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py @@ -0,0 +1,53 @@ +import os +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.callback import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Validating param: name" in result.output + assert "Hello Camila" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Only Camila is allowed" 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 + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL004_AN.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial004_an.py --", + "COMP_CWORD": "1", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "--name" in result.stdout diff --git a/tests/test_tutorial/test_options/test_help/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_help/test_tutorial001_an.py new file mode 100644 index 0000000000..99d3279aac --- /dev/null +++ b/tests/test_tutorial/test_options/test_help/test_tutorial001_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.help import tutorial001_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 "Say hi to NAME, optionally with a --lastname." in result.output + assert "If --formal is used, say hi very formally." in result.output + assert "Last name of person to greet." in result.output + assert "Say hi formally." in result.output + + +def test_1(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_formal(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez", "--formal"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila Gutiérrez." 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_options/test_help/test_tutorial002_an.py b/tests/test_tutorial/test_options/test_help/test_tutorial002_an.py new file mode 100644 index 0000000000..7d61a41dd8 --- /dev/null +++ b/tests/test_tutorial/test_options/test_help/test_tutorial002_an.py @@ -0,0 +1,45 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.help import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_call(): + result = runner.invoke(app, ["World"]) + assert result.exit_code == 0 + assert "Hello World" in result.output + + +def test_formal(): + result = runner.invoke(app, ["World", "--formal"]) + assert result.exit_code == 0 + assert "Good day Ms. World" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname" in result.output + assert "Customization and Utils" in result.output + assert "--formal" in result.output + assert "--no-formal" in result.output + assert "--debug" in result.output + assert "--no-debug" 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_options/test_help/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_help/test_tutorial003_an.py new file mode 100644 index 0000000000..88e71ab473 --- /dev/null +++ b/tests/test_tutorial/test_options/test_help/test_tutorial003_an.py @@ -0,0 +1,36 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.help import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_call(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Wade Wilson" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--fullname" in result.output + assert "TEXT" in result.output + assert "[default: Wade Wilson]" not 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_options/test_name/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py new file mode 100644 index 0000000000..87dfbc308b --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py @@ -0,0 +1,36 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.name import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--name" in result.output + assert "TEXT" in result.output + assert "--user-name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options/test_name/test_tutorial002_an.py b/tests/test_tutorial/test_options/test_name/test_tutorial002_an.py new file mode 100644 index 0000000000..b77c3bcc29 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial002_an.py @@ -0,0 +1,43 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.name import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n" in result.output + assert "--name" in result.output + assert "TEXT" in result.output + assert "--user-name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_long(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options/test_name/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_name/test_tutorial003_an.py new file mode 100644 index 0000000000..cf097e2388 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial003_an.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.name import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n" in result.output + assert "TEXT" in result.output + assert "--user-name" not in result.output + assert "--name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options/test_name/test_tutorial004_an.py b/tests/test_tutorial/test_options/test_name/test_tutorial004_an.py new file mode 100644 index 0000000000..087b436d55 --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial004_an.py @@ -0,0 +1,43 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.name import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n" in result.output + assert "--user-name" in result.output + assert "TEXT" in result.output + assert "--name" not in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_long(): + result = runner.invoke(app, ["--user-name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options/test_name/test_tutorial005_an.py b/tests/test_tutorial/test_options/test_name/test_tutorial005_an.py new file mode 100644 index 0000000000..5ca123f0bd --- /dev/null +++ b/tests/test_tutorial/test_options/test_name/test_tutorial005_an.py @@ -0,0 +1,55 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.name import tutorial005_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "-n" in result.output + assert "--name" in result.output + assert "TEXT" in result.output + assert "-f" in result.output + assert "--formal" in result.output + + +def test_call(): + result = runner.invoke(app, ["-n", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_call_formal(): + result = runner.invoke(app, ["-n", "Camila", "-f"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila." in result.output + + +def test_call_formal_condensed(): + result = runner.invoke(app, ["-fn", "Camila"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila." in result.output + + +def test_call_condensed_wrong_order(): + result = runner.invoke(app, ["-nf", "Camila"]) + assert result.exit_code != 0 + + +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_options/test_prompt/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial001_an.py new file mode 100644 index 0000000000..eb2333c9f6 --- /dev/null +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial001_an.py @@ -0,0 +1,43 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.prompt import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_option_lastname_prompt(): + result = runner.invoke(app, ["Camila"], input="Gutiérrez") + assert result.exit_code == 0 + assert "Lastname: " in result.output + assert "Hello Camila Gutiérrez" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname" in result.output + assert "TEXT" in result.output + assert "[required]" 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_options/test_prompt/test_tutorial002_an.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial002_an.py new file mode 100644 index 0000000000..5d81c6d8d1 --- /dev/null +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial002_an.py @@ -0,0 +1,43 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.prompt import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_option_lastname_prompt(): + result = runner.invoke(app, ["Camila"], input="Gutiérrez") + assert result.exit_code == 0 + assert "Please tell me your last name: " in result.output + assert "Hello Camila Gutiérrez" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname" in result.output + assert "TEXT" in result.output + assert "[required]" 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_options/test_prompt/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_prompt/test_tutorial003_an.py new file mode 100644 index 0000000000..4588b31b06 --- /dev/null +++ b/tests/test_tutorial/test_options/test_prompt/test_tutorial003_an.py @@ -0,0 +1,57 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.prompt import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_prompt(): + result = runner.invoke(app, input="Old Project\nOld Project\n") + assert result.exit_code == 0 + assert "Deleting project Old Project" in result.output + + +def test_prompt_not_equal(): + result = runner.invoke( + app, input="Old Project\nNew Spice\nOld Project\nOld Project\n" + ) + assert result.exit_code == 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Error: The two entered values do not match" in result.output + or "Error: the two entered values do not match" in result.output + ) + assert "Deleting project Old Project" in result.output + + +def test_option(): + result = runner.invoke(app, ["--project-name", "Old Project"]) + assert result.exit_code == 0 + assert "Deleting project Old Project" in result.output + assert "Project name: " not in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--project-name" in result.output + assert "TEXT" in result.output + assert "[required]" 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_options/test_version/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py new file mode 100644 index 0000000000..75b024af52 --- /dev/null +++ b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py @@ -0,0 +1,58 @@ +import os +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.version import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Rick", "--version"]) + assert result.exit_code == 0 + assert "Awesome CLI Version: 0.1.0" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Only Camila is allowed" in result.output + + +def test_3(): + result = runner.invoke(app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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 + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL003_AN.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial003_an.py --name Rick --v", + "COMP_CWORD": "3", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "--version" in result.stdout diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py new file mode 100644 index 0000000000..e23af20fb3 --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py @@ -0,0 +1,43 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial002_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL002_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial002_an.py --name ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "Camila" in result.stdout + assert "Carlos" in result.stdout + assert "Sebastian" in result.stdout + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options_autocompletion/test_tutorial003_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py new file mode 100644 index 0000000000..b8e04cb8e3 --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py @@ -0,0 +1,43 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial003_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL003_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial003_an.py --name Seb", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "Camila" not in result.stdout + assert "Carlos" not in result.stdout + assert "Sebastian" in result.stdout + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options_autocompletion/test_tutorial004_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py new file mode 100644 index 0000000000..cb951c75a4 --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py @@ -0,0 +1,43 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial004_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL004_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial004_an_aux.py --name ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert '"Camila":"The reader of books."' in result.stdout + assert '"Carlos":"The writer of scripts."' in result.stdout + assert '"Sebastian":"The type hints guy."' in result.stdout + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" 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_options_autocompletion/test_tutorial007_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py new file mode 100644 index 0000000000..a015f6af9e --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py @@ -0,0 +1,44 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial007_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL007_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial007_an.py --name Sebastian --name ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert '"Camila":"The reader of books."' in result.stdout + assert '"Carlos":"The writer of scripts."' in result.stdout + assert '"Sebastian":"The type hints guy."' not in result.stdout + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila", "--name", "Sebastian"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + assert "Hello Sebastian" 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_options_autocompletion/test_tutorial008_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py new file mode 100644 index 0000000000..adacd66e8d --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py @@ -0,0 +1,46 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial008_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL008_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial008_an.py --name ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert '"Camila":"The reader of books."' in result.stdout + assert '"Carlos":"The writer of scripts."' in result.stdout + assert '"Sebastian":"The type hints guy."' in result.stdout + # TODO: when deprecating Click 7, remove second option + assert "[]" in result.stderr or "['--name']" in result.stderr + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila", "--name", "Sebastian"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + assert "Hello Sebastian" 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_options_autocompletion/test_tutorial009_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py new file mode 100644 index 0000000000..8ac91a0aaa --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py @@ -0,0 +1,46 @@ +import os +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.options_autocompletion import tutorial009_an as mod + +runner = CliRunner() + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL009_AN.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial009_an.py --name Sebastian --name ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert '"Camila":"The reader of books."' in result.stdout + assert '"Carlos":"The writer of scripts."' in result.stdout + assert '"Sebastian":"The type hints guy."' not in result.stdout + # TODO: when deprecating Click 7, remove second option + assert "[]" in result.stderr or "['--name', 'Sebastian', '--name']" in result.stderr + + +def test_1(): + result = runner.invoke(mod.app, ["--name", "Camila", "--name", "Sebastian"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + assert "Hello Sebastian" 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_bool/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001_an.py new file mode 100644 index 0000000000..52cd1cd290 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001_an.py @@ -0,0 +1,52 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.bool import tutorial001_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 "--force" in result.output + assert "--no-force" not in result.output + + +def test_no_force(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Not forcing" in result.output + + +def test_force(): + result = runner.invoke(app, ["--force"]) + assert result.exit_code == 0 + assert "Forcing operation" in result.output + + +def test_invalid_no_force(): + result = runner.invoke(app, ["--no-force"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "No such option: --no-force" in result.output + or "no such option: --no-force" 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_bool/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial002_an.py new file mode 100644 index 0000000000..d13e645438 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial002_an.py @@ -0,0 +1,71 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.parameter_types.bool import tutorial002_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 "--accept" in result.output + assert "--reject" in result.output + assert "--no-accept" not in result.output + + +def test_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--accept" in result.output + assert "--reject" in result.output + assert "--no-accept" not in result.output + typer.core.rich = rich + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "I don't know what you want yet" in result.output + + +def test_accept(): + result = runner.invoke(app, ["--accept"]) + assert result.exit_code == 0 + assert "Accepting!" in result.output + + +def test_reject(): + result = runner.invoke(app, ["--reject"]) + assert result.exit_code == 0 + assert "Rejecting!" in result.output + + +def test_invalid_no_accept(): + result = runner.invoke(app, ["--no-accept"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "No such option: --no-accept" in result.output + or "no such option: --no-accept" 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_bool/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial003_an.py new file mode 100644 index 0000000000..c1f09c411b --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial003_an.py @@ -0,0 +1,43 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.bool 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 "-f" in result.output + assert "--force" in result.output + assert "-F" in result.output + assert "--no-force" in result.output + + +def test_force(): + result = runner.invoke(app, ["-f"]) + assert result.exit_code == 0 + assert "Forcing operation" in result.output + + +def test_no_force(): + result = runner.invoke(app, ["-F"]) + assert result.exit_code == 0 + assert "Not forcing" 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_bool/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial004_an.py new file mode 100644 index 0000000000..b4d1edeacd --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial004_an.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.bool import tutorial004_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 "-d" in result.output + assert "--demo" in result.output + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Running in production" in result.output + + +def test_demo(): + result = runner.invoke(app, ["--demo"]) + assert result.exit_code == 0 + assert "Running demo" in result.output + + +def test_short_demo(): + result = runner.invoke(app, ["-d"]) + assert result.exit_code == 0 + assert "Running demo" 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_datetime/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial002_an.py new file mode 100644 index 0000000000..ebae67a0ef --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial002_an.py @@ -0,0 +1,34 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.datetime import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app, ["1969-10-29"]) + assert result.exit_code == 0 + assert "Launch will be at: 1969-10-29 00:00:00" in result.output + + +def test_usa_weird_date_format(): + result = runner.invoke(app, ["10/29/1969"]) + assert result.exit_code == 0 + assert "Launch will be at: 1969-10-29 00:00:00" 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_enum/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py new file mode 100644 index 0000000000..c60013daa9 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial002_an.py @@ -0,0 +1,34 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial002_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/tests/test_tutorial/test_parameter_types/test_file/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial001_an.py new file mode 100644 index 0000000000..78b4893072 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial001_an.py @@ -0,0 +1,33 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.file import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(tmpdir): + config_file = Path(tmpdir) / "config.txt" + config_file.write_text("some settings\nsome more settings") + result = runner.invoke(app, ["--config", f"{config_file}"]) + config_file.unlink() + assert result.exit_code == 0 + assert "Config line: some settings" in result.output + assert "Config line: some more settings" 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_file/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial002_an.py new file mode 100644 index 0000000000..8f4550fbb7 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial002_an.py @@ -0,0 +1,35 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.file import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(tmpdir): + config_file = Path(tmpdir) / "config.txt" + if config_file.exists(): # pragma no cover + config_file.unlink() + result = runner.invoke(app, ["--config", f"{config_file}"]) + text = config_file.read_text() + config_file.unlink() + assert result.exit_code == 0 + assert "Config written" in result.output + assert "Some config written by the app" in text + + +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_file/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial003_an.py new file mode 100644 index 0000000000..5dc083133b --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial003_an.py @@ -0,0 +1,32 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.file import tutorial003_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(tmpdir): + binary_file = Path(tmpdir) / "config.txt" + binary_file.write_bytes(b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o") + result = runner.invoke(app, ["--file", f"{binary_file}"]) + binary_file.unlink() + assert result.exit_code == 0 + assert "Processed bytes total:" 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_file/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial004_an.py new file mode 100644 index 0000000000..2808609a95 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial004_an.py @@ -0,0 +1,36 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.file import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(tmpdir): + binary_file = Path(tmpdir) / "config.txt" + if binary_file.exists(): # pragma no cover + binary_file.unlink() + result = runner.invoke(app, ["--file", f"{binary_file}"]) + text = binary_file.read_text() + binary_file.unlink() + assert result.exit_code == 0 + assert "Binary file written" in result.output + assert "some settings" in text + assert "la cigüeña trae al niño" in text + + +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_file/test_tutorial005_an.py b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial005_an.py new file mode 100644 index 0000000000..1fff5875e8 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_file/test_tutorial005_an.py @@ -0,0 +1,38 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.file import tutorial005_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_main(tmpdir): + config_file = Path(tmpdir) / "config.txt" + if config_file.exists(): # pragma no cover + config_file.unlink() + config_file.write_text("") + result = runner.invoke(app, ["--config", f"{config_file}"]) + result = runner.invoke(app, ["--config", f"{config_file}"]) + result = runner.invoke(app, ["--config", f"{config_file}"]) + text = config_file.read_text() + config_file.unlink() + assert result.exit_code == 0 + assert "Config line written" + assert "This is a single line\nThis is a single line\nThis is a single line" in text + + +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_number/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py new file mode 100644 index 0000000000..c77048b460 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001_an.py @@ -0,0 +1,99 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.parameter_types.number import tutorial001_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 "--age" in result.output + assert "INTEGER RANGE" in result.output + assert "--score" in result.output + assert "FLOAT RANGE" in result.output + + +def test_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--age" in result.output + assert "INTEGER RANGE" in result.output + assert "--score" in result.output + assert "FLOAT RANGE" in result.output + typer.core.rich = rich + + +def test_params(): + result = runner.invoke(app, ["5", "--age", "20", "--score", "90"]) + assert result.exit_code == 0 + assert "ID is 5" in result.output + assert "--age is 20" in result.output + assert "--score is 90.0" in result.output + + +def test_invalid_id(): + result = runner.invoke(app, ["1002"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + assert ( + ( + "Invalid value for 'ID': 1002 is not in the range 0<=x<=1000." + in result.output + ) + or "Invalid value for 'ID': 1002 is not in the valid range of 0 to 1000." + in result.output + ) + + +def test_invalid_age(): + result = runner.invoke(app, ["5", "--age", "15"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Invalid value for '--age': 15 is not in the range x>=18" in result.output + or "Invalid value for '--age': 15 is smaller than the minimum valid value 18." + in result.output + ) + + +def test_invalid_score(): + result = runner.invoke(app, ["5", "--age", "20", "--score", "100.5"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Invalid value for '--score': 100.5 is not in the range x<=100." + in result.output + or "Invalid value for '--score': 100.5 is bigger than the maximum valid value" + in result.output + ) + + +def test_negative_score(): + result = runner.invoke(app, ["5", "--age", "20", "--score", "-5"]) + assert result.exit_code == 0 + assert "ID is 5" in result.output + assert "--age is 20" in result.output + assert "--score is -5.0" 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_number/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial002_an.py new file mode 100644 index 0000000000..31f9729cc9 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial002_an.py @@ -0,0 +1,42 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.number import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_invalid_id(): + result = runner.invoke(app, ["1002"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Invalid value for 'ID': 1002 is not in the range 0<=x<=1000" in result.output + or "Invalid value for 'ID': 1002 is not in the valid range of 0 to 1000." + in result.output + ) + + +def test_clamped(): + result = runner.invoke(app, ["5", "--rank", "11", "--score", "-5"]) + assert result.exit_code == 0 + assert "ID is 5" in result.output + assert "--rank is 10" in result.output + assert "--score is 0" 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_number/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial003_an.py new file mode 100644 index 0000000000..4e2a0c7190 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial003_an.py @@ -0,0 +1,58 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.number 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 "Verbose level is 0" in result.output + + +def test_verbose_1(): + result = runner.invoke(app, ["--verbose"]) + assert result.exit_code == 0 + assert "Verbose level is 1" in result.output + + +def test_verbose_3(): + result = runner.invoke(app, ["--verbose", "--verbose", "--verbose"]) + assert result.exit_code == 0 + assert "Verbose level is 3" in result.output + + +def test_verbose_short_1(): + result = runner.invoke(app, ["-v"]) + assert result.exit_code == 0 + assert "Verbose level is 1" in result.output + + +def test_verbose_short_3(): + result = runner.invoke(app, ["-v", "-v", "-v"]) + assert result.exit_code == 0 + assert "Verbose level is 3" in result.output + + +def test_verbose_short_3_condensed(): + result = runner.invoke(app, ["-vvv"]) + assert result.exit_code == 0 + assert "Verbose level is 3" 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_path/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial001_an.py new file mode 100644 index 0000000000..efa6097c2f --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial001_an.py @@ -0,0 +1,55 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.path import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_no_path(tmpdir): + Path(tmpdir) / "config.txt" + result = runner.invoke(app) + assert result.exit_code == 1 + assert "No config file" in result.output + assert "Aborted" in result.output + + +def test_not_exists(tmpdir): + config_file = Path(tmpdir) / "config.txt" + if config_file.exists(): # pragma no cover + config_file.unlink() + result = runner.invoke(app, ["--config", f"{config_file}"]) + assert result.exit_code == 0 + assert "The config doesn't exist" in result.output + + +def test_exists(tmpdir): + config_file = Path(tmpdir) / "config.txt" + config_file.write_text("some settings") + result = runner.invoke(app, ["--config", f"{config_file}"]) + config_file.unlink() + assert result.exit_code == 0 + assert "Config file contents: some settings" in result.output + + +def test_dir(): + result = runner.invoke(app, ["--config", "./"]) + assert result.exit_code == 0 + assert "Config is a directory, will use all its config files" 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_path/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py new file mode 100644 index 0000000000..ae93fae160 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_path/test_tutorial002_an.py @@ -0,0 +1,48 @@ +import subprocess +import sys +from pathlib import Path + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.path import tutorial002_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_not_exists(tmpdir): + config_file = Path(tmpdir) / "config.txt" + if config_file.exists(): # pragma no cover + config_file.unlink() + result = runner.invoke(app, ["--config", f"{config_file}"]) + assert result.exit_code != 0 + assert "Invalid value for '--config': File" in result.output + assert "does not exist" in result.output + + +def test_exists(tmpdir): + config_file = Path(tmpdir) / "config.txt" + config_file.write_text("some settings") + result = runner.invoke(app, ["--config", f"{config_file}"]) + config_file.unlink() + assert result.exit_code == 0 + assert "Config file contents: some settings" in result.output + + +def test_dir(): + result = runner.invoke(app, ["--config", "./"]) + assert result.exit_code != 0 + assert "Invalid value for '--config': File './' is a directory." 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/_typing.py b/typer/_typing.py new file mode 100644 index 0000000000..6711ab8ad2 --- /dev/null +++ b/typer/_typing.py @@ -0,0 +1,627 @@ +# Copied from pydantic 1.9.2 (the latest version to support python 3.6.) +# https://github.com/pydantic/pydantic/blob/v1.9.2/pydantic/typing.py + +import sys +from os import PathLike +from typing import ( # type: ignore + TYPE_CHECKING, + AbstractSet, + Any, + ClassVar, + Dict, + Generator, + Iterable, + List, + Mapping, + NewType, + Optional, + Sequence, + Set, + Tuple, + Type, + Union, + _eval_type, + cast, + get_type_hints, +) + +from typing_extensions import Annotated, Literal + +try: + from typing import _TypingBase as typing_base # type: ignore +except ImportError: + from typing import _Final as typing_base # type: ignore + +try: + from typing import GenericAlias as TypingGenericAlias # type: ignore +except ImportError: + # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on) + TypingGenericAlias = () + +try: + from types import UnionType as TypesUnionType # type: ignore +except ImportError: + # python < 3.10 does not have UnionType (str | int, byte | bool and so on) + TypesUnionType = () + + +if sys.version_info < (3, 7): + if TYPE_CHECKING: + + class ForwardRef: + def __init__(self, arg: Any): + pass + + def _eval_type(self, globalns: Any, localns: Any) -> Any: + pass + + else: + from typing import _ForwardRef as ForwardRef +else: + from typing import ForwardRef + + +if sys.version_info < (3, 7): + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + return type_._eval_type(globalns, localns) + +elif sys.version_info < (3, 9): + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + return type_._evaluate(globalns, localns) + +else: + + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: + # Even though it is the right signature for python 3.9, mypy complains with + # `error: Too many arguments for "_evaluate" of "ForwardRef"` hence the cast... + return cast(Any, type_)._evaluate(globalns, localns, set()) + + +if sys.version_info < (3, 9): + # Ensure we always get all the whole `Annotated` hint, not just the annotated type. + # For 3.6 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, + # so it already returns the full annotation + get_all_type_hints = get_type_hints + +else: + + def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> Any: + return get_type_hints(obj, globalns, localns, include_extras=True) + + +if sys.version_info < (3, 7): + from typing import Callable as Callable + + AnyCallable = Callable[..., Any] + NoArgAnyCallable = Callable[[], Any] +else: + from collections.abc import Callable as Callable + from typing import Callable as TypingCallable + + AnyCallable = TypingCallable[..., Any] + NoArgAnyCallable = TypingCallable[[], Any] + + +# Annotated[...] is implemented by returning an instance of one of these classes, depending on +# python/typing_extensions version. +AnnotatedTypeNames = {"AnnotatedMeta", "_AnnotatedAlias"} + + +if sys.version_info < (3, 8): + + def get_origin(t: Type[Any]) -> Optional[Type[Any]]: + if type(t).__name__ in AnnotatedTypeNames: + return cast( + Type[Any], Annotated + ) # mypy complains about _SpecialForm in py3.6 + return getattr(t, "__origin__", None) + +else: + from typing import get_origin as _typing_get_origin + + def get_origin(tp: Type[Any]) -> Optional[Type[Any]]: + """ + We can't directly use `typing.get_origin` since we need a fallback to support + custom generic classes like `ConstrainedList` + It should be useless once https://github.com/cython/cython/issues/3537 is + solved and https://github.com/samuelcolvin/pydantic/pull/1753 is merged. + """ + if type(tp).__name__ in AnnotatedTypeNames: + return cast(Type[Any], Annotated) # mypy complains about _SpecialForm + return _typing_get_origin(tp) or getattr(tp, "__origin__", None) + + +if sys.version_info < (3, 7): # noqa: C901 (ignore complexity) + + def get_args(t: Type[Any]) -> Tuple[Any, ...]: + """Simplest get_args compatibility layer possible. + + The Python 3.6 typing module does not have `_GenericAlias` so + this won't work for everything. In particular this will not + support the `generics` module (we don't support generic models in + python 3.6). + + """ + if type(t).__name__ in AnnotatedTypeNames: + return t.__args__ + t.__metadata__ + return getattr(t, "__args__", ()) + +elif sys.version_info < (3, 8): # noqa: C901 + from typing import _GenericAlias + + def get_args(t: Type[Any]) -> Tuple[Any, ...]: + """Compatibility version of get_args for python 3.7. + + Mostly compatible with the python 3.8 `typing` module version + and able to handle almost all use cases. + """ + if type(t).__name__ in AnnotatedTypeNames: + return t.__args__ + t.__metadata__ + if isinstance(t, _GenericAlias): + res = t.__args__ + if t.__origin__ is Callable and res and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return getattr(t, "__args__", ()) + +else: + from typing import get_args as _typing_get_args + + def _generic_get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """ + In python 3.9, `typing.Dict`, `typing.List`, ... + do have an empty `__args__` by default (instead of the generic ~T for example). + In order to still support `Dict` for example and consider it as `Dict[Any, Any]`, + we retrieve the `_nparams` value that tells us how many parameters it needs. + """ + if hasattr(tp, "_nparams"): + return (Any,) * tp._nparams + return () + + def get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if type(tp).__name__ in AnnotatedTypeNames: + return tp.__args__ + tp.__metadata__ + # the fallback is needed for the same reasons as `get_origin` (see above) + return ( + _typing_get_args(tp) or getattr(tp, "__args__", ()) or _generic_get_args(tp) + ) + + +if sys.version_info < (3, 9): + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """Python 3.9 and older only supports generics from `typing` module. + They convert strings to ForwardRef automatically. + + Examples:: + typing.List['Hero'] == typing.List[ForwardRef('Hero')] + """ + return tp + +else: + from typing import _UnionGenericAlias # type: ignore + + from typing_extensions import _AnnotatedAlias + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """ + Recursively searches for `str` type hints and replaces them with ForwardRef. + + Examples:: + convert_generics(list['Hero']) == list[ForwardRef('Hero')] + convert_generics(dict['Hero', 'Team']) == dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(typing.Dict['Hero', 'Team']) == typing.Dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(list[str | 'Hero'] | int) == list[str | ForwardRef('Hero')] | int + """ + origin = get_origin(tp) + if not origin or not hasattr(tp, "__args__"): + return tp + + args = get_args(tp) + + # typing.Annotated needs special treatment + if origin is Annotated: + return _AnnotatedAlias(convert_generics(args[0]), args[1:]) + + # recursively replace `str` instances inside of `GenericAlias` with `ForwardRef(arg)` + converted = tuple( + ForwardRef(arg) + if isinstance(arg, str) and isinstance(tp, TypingGenericAlias) + else convert_generics(arg) + for arg in args + ) + + if converted == args: + return tp + elif isinstance(tp, TypingGenericAlias): + return TypingGenericAlias(origin, converted) + elif isinstance(tp, TypesUnionType): + # recreate types.UnionType (PEP604, Python >= 3.10) + return _UnionGenericAlias(origin, converted) + else: + try: + setattr(tp, "__args__", converted) + except AttributeError: + pass + return tp + + +if sys.version_info < (3, 10): + + def is_union(tp: Optional[Type[Any]]) -> bool: + return tp is Union + + WithArgsTypes = (TypingGenericAlias,) + +else: + import types + import typing + + def is_union(tp: Optional[Type[Any]]) -> bool: + return tp is Union or tp is types.UnionType # noqa: E721 + + WithArgsTypes = (typing._GenericAlias, types.GenericAlias, types.UnionType) + + +if sys.version_info < (3, 9): + StrPath = Union[str, PathLike] +else: + StrPath = Union[str, PathLike] + # TODO: Once we switch to Cython 3 to handle generics properly + # (https://github.com/cython/cython/issues/2753), use following lines instead + # of the one above + # # os.PathLike only becomes subscriptable from Python 3.9 onwards + # StrPath = Union[str, PathLike[str]] + + +if TYPE_CHECKING: + from .fields import ModelField + + TupleGenerator = Generator[Tuple[str, Any], None, None] + DictStrAny = Dict[str, Any] + DictAny = Dict[Any, Any] + SetStr = Set[str] + ListStr = List[str] + IntStr = Union[int, str] + AbstractSetIntStr = AbstractSet[IntStr] + DictIntStrAny = Dict[IntStr, Any] + MappingIntStrAny = Mapping[IntStr, Any] + CallableGenerator = Generator[AnyCallable, None, None] + ReprArgs = Sequence[Tuple[Optional[str], Any]] + AnyClassMethod = classmethod[Any] + +__all__ = ( + "ForwardRef", + "Callable", + "AnyCallable", + "NoArgAnyCallable", + "NoneType", + "is_none_type", + "display_as_type", + "resolve_annotations", + "is_callable_type", + "is_literal_type", + "all_literal_values", + "is_namedtuple", + "is_typeddict", + "is_new_type", + "new_type_supertype", + "is_classvar", + "update_field_forward_refs", + "update_model_forward_refs", + "TupleGenerator", + "DictStrAny", + "DictAny", + "SetStr", + "ListStr", + "IntStr", + "AbstractSetIntStr", + "DictIntStrAny", + "CallableGenerator", + "ReprArgs", + "AnyClassMethod", + "CallableGenerator", + "WithArgsTypes", + "get_args", + "get_origin", + "get_sub_types", + "typing_base", + "get_all_type_hints", + "is_union", + "StrPath", +) + + +NoneType = None.__class__ + + +NONE_TYPES: Tuple[Any, Any, Any] = (None, NoneType, Literal[None]) + + +if sys.version_info < (3, 8): + # Even though this implementation is slower, we need it for python 3.6/3.7: + # In python 3.6/3.7 "Literal" is not a builtin type and uses a different + # mechanism. + # for this reason `Literal[None] is Literal[None]` evaluates to `False`, + # breaking the faster implementation used for the other python versions. + + def is_none_type(type_: Any) -> bool: + return type_ in NONE_TYPES + +elif sys.version_info[:2] == (3, 8): + # We can use the fast implementation for 3.8 but there is a very weird bug + # where it can fail for `Literal[None]`. + # We just need to redefine a useless `Literal[None]` inside the function body to fix this + + def is_none_type(type_: Any) -> bool: + Literal[None] # fix edge case + for none_type in NONE_TYPES: + if type_ is none_type: + return True + return False + +else: + + def is_none_type(type_: Any) -> bool: + for none_type in NONE_TYPES: + if type_ is none_type: + return True + return False + + +def display_as_type(v: Type[Any]) -> str: + if ( + not isinstance(v, typing_base) + and not isinstance(v, WithArgsTypes) + and not isinstance(v, type) + ): + v = v.__class__ + + if is_union(get_origin(v)): + return f'Union[{", ".join(map(display_as_type, get_args(v)))}]' + + if isinstance(v, WithArgsTypes): + # Generic alias are constructs like `list[int]` + return str(v).replace("typing.", "") + + try: + return v.__name__ + except AttributeError: + # happens with typing objects + return str(v).replace("typing.", "") + + +def resolve_annotations( + raw_annotations: Dict[str, Type[Any]], module_name: Optional[str] +) -> Dict[str, Type[Any]]: + """ + Partially taken from typing.get_type_hints. + + Resolve string or ForwardRef annotations into type objects if possible. + """ + base_globals: Optional[Dict[str, Any]] = None + if module_name: + try: + module = sys.modules[module_name] + except KeyError: + # happens occasionally, see https://github.com/samuelcolvin/pydantic/issues/2363 + pass + else: + base_globals = module.__dict__ + + annotations = {} + for name, value in raw_annotations.items(): + if isinstance(value, str): + if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= ( + 3, + 10, + 1, + ): + value = ForwardRef(value, is_argument=False, is_class=True) + elif sys.version_info >= (3, 7): + value = ForwardRef(value, is_argument=False) + else: + value = ForwardRef(value) + try: + value = _eval_type(value, base_globals, None) + except NameError: + # this is ok, it can be fixed with update_forward_refs + pass + annotations[name] = value + return annotations + + +def is_callable_type(type_: Type[Any]) -> bool: + return type_ is Callable or get_origin(type_) is Callable + + +if sys.version_info >= (3, 7): + + def is_literal_type(type_: Type[Any]) -> bool: + return Literal is not None and get_origin(type_) is Literal + + def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return get_args(type_) + +else: + + def is_literal_type(type_: Type[Any]) -> bool: + return ( + Literal is not None + and hasattr(type_, "__values__") + and type_ == Literal[type_.__values__] + ) + + def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return type_.__values__ + + +def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + """ + This method is used to retrieve all Literal values as + Literal can be used recursively (see https://www.python.org/dev/peps/pep-0586) + e.g. `Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]` + """ + if not is_literal_type(type_): + return (type_,) + + values = literal_values(type_) + return tuple(x for value in values for x in all_literal_values(value)) + + +def is_namedtuple(type_: Type[Any]) -> bool: + """ + Check if a given class is a named tuple. + It can be either a `typing.NamedTuple` or `collections.namedtuple` + """ + from .utils import lenient_issubclass + + return lenient_issubclass(type_, tuple) and hasattr(type_, "_fields") + + +def is_typeddict(type_: Type[Any]) -> bool: + """ + Check if a given class is a typed dict (from `typing` or `typing_extensions`) + In 3.10, there will be a public method (https://docs.python.org/3.10/library/typing.html#typing.is_typeddict) + """ + from .utils import lenient_issubclass + + return lenient_issubclass(type_, dict) and hasattr(type_, "__total__") + + +test_type = NewType("test_type", str) + + +def is_new_type(type_: Type[Any]) -> bool: + """ + Check whether type_ was created using typing.NewType + """ + return isinstance(type_, test_type.__class__) and hasattr(type_, "__supertype__") # type: ignore + + +def new_type_supertype(type_: Type[Any]) -> Type[Any]: + while hasattr(type_, "__supertype__"): + type_ = type_.__supertype__ + return type_ + + +def _check_classvar(v: Optional[Type[Any]]) -> bool: + if v is None: + return False + + return v.__class__ == ClassVar.__class__ and ( + sys.version_info < (3, 7) or getattr(v, "_name", None) == "ClassVar" + ) + + +def is_classvar(ann_type: Type[Any]) -> bool: + if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)): + return True + + # this is an ugly workaround for class vars that contain forward references and are therefore themselves + # forward references, see #3679 + if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith( + "ClassVar[" + ): + return True + + return False + + +def update_field_forward_refs(field: "ModelField", globalns: Any, localns: Any) -> None: + """ + Try to update ForwardRefs on fields based on this ModelField, globalns and localns. + """ + if field.type_.__class__ == ForwardRef: + field.type_ = evaluate_forwardref(field.type_, globalns, localns or None) + field.prepare() + + if field.sub_fields: + for sub_f in field.sub_fields: + update_field_forward_refs(sub_f, globalns=globalns, localns=localns) + + if field.discriminator_key is not None: + field.prepare_discriminated_union_sub_fields() + + +def update_model_forward_refs( + model: Type[Any], + fields: Iterable["ModelField"], + json_encoders: Dict[Union[Type[Any], str], AnyCallable], + localns: "DictStrAny", + exc_to_suppress: Tuple[Type[BaseException], ...] = (), +) -> None: + """ + Try to update model fields ForwardRefs based on model and localns. + """ + if model.__module__ in sys.modules: + globalns = sys.modules[model.__module__].__dict__.copy() + else: + globalns = {} + + globalns.setdefault(model.__name__, model) + + for f in fields: + try: + update_field_forward_refs(f, globalns=globalns, localns=localns) + except exc_to_suppress: + pass + + for key in set(json_encoders.keys()): + if isinstance(key, str): + fr: ForwardRef = ForwardRef(key) + elif isinstance(key, ForwardRef): + fr = key + else: + continue + + try: + new_key = evaluate_forwardref(fr, globalns, localns or None) + except exc_to_suppress: # pragma: no cover + continue + + json_encoders[new_key] = json_encoders.pop(key) + + +def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]: + """ + Tries to get the class of a Type[T] annotation. Returns True if Type is used + without brackets. Otherwise returns None. + """ + try: + origin = get_origin(type_) + if origin is None: # Python 3.6 + origin = type_ + if issubclass(origin, Type): # type: ignore + if not get_args(type_) or not isinstance(get_args(type_)[0], type): + return True + return get_args(type_)[0] + except (AttributeError, TypeError): + pass + return None + + +def get_sub_types(tp: Any) -> List[Any]: + """ + Return all the types that are allowed by type `tp` + `tp` can be a `Union` of allowed types or an `Annotated` type + """ + origin = get_origin(tp) + if origin is Annotated: + return get_sub_types(get_args(tp)[0]) + elif is_union(origin): + return [x for t in get_args(tp) for x in get_sub_types(t)] + else: + return [tp] diff --git a/typer/models.py b/typer/models.py index 0970a3148f..92e43337f8 100644 --- a/typer/models.py +++ b/typer/models.py @@ -184,6 +184,7 @@ def __init__( ] ] = None, autocompletion: Optional[Callable[..., Any]] = None, + default_factory: Optional[Callable[[], Any]] = None, # TyperArgument show_default: Union[bool, str] = True, show_choices: bool = True, @@ -225,6 +226,7 @@ def __init__( self.envvar = envvar self.shell_complete = shell_complete self.autocompletion = autocompletion + self.default_factory = default_factory # TyperArgument self.show_default = show_default self.show_choices = show_choices @@ -277,6 +279,7 @@ def __init__( ] ] = None, autocompletion: Optional[Callable[..., Any]] = None, + default_factory: Optional[Callable[[], Any]] = None, # Option show_default: bool = True, prompt: Union[bool, str] = False, @@ -327,6 +330,7 @@ def __init__( envvar=envvar, shell_complete=shell_complete, autocompletion=autocompletion, + default_factory=default_factory, # TyperArgument show_default=show_default, show_choices=show_choices, @@ -388,6 +392,7 @@ def __init__( ] ] = None, autocompletion: Optional[Callable[..., Any]] = None, + default_factory: Optional[Callable[[], Any]] = None, # TyperArgument show_default: Union[bool, str] = True, show_choices: bool = True, @@ -430,6 +435,7 @@ def __init__( envvar=envvar, shell_complete=shell_complete, autocompletion=autocompletion, + default_factory=default_factory, # TyperArgument show_default=show_default, show_choices=show_choices, diff --git a/typer/params.py b/typer/params.py index c833b552ee..82e72559de 100644 --- a/typer/params.py +++ b/typer/params.py @@ -10,7 +10,7 @@ def Option( # Parameter - default: Optional[Any], + default: Optional[Any] = ..., *param_decls: str, callback: Optional[Callable[..., Any]] = None, metavar: Optional[str] = None, @@ -24,6 +24,7 @@ def Option( ] ] = None, autocompletion: Optional[Callable[..., Any]] = None, + default_factory: Optional[Callable[[], Any]] = None, # Option show_default: bool = True, prompt: Union[bool, str] = False, @@ -75,6 +76,7 @@ def Option( envvar=envvar, shell_complete=shell_complete, autocompletion=autocompletion, + default_factory=default_factory, # Option show_default=show_default, prompt=prompt, @@ -119,7 +121,7 @@ def Option( def Argument( # Parameter - default: Optional[Any], + default: Optional[Any] = ..., *, callback: Optional[Callable[..., Any]] = None, metavar: Optional[str] = None, @@ -133,6 +135,7 @@ def Argument( ] ] = None, autocompletion: Optional[Callable[..., Any]] = None, + default_factory: Optional[Callable[[], Any]] = None, # TyperArgument show_default: Union[bool, str] = True, show_choices: bool = True, @@ -178,6 +181,7 @@ def Argument( envvar=envvar, shell_complete=shell_complete, autocompletion=autocompletion, + default_factory=default_factory, # TyperArgument show_default=show_default, show_choices=show_choices, diff --git a/typer/utils.py b/typer/utils.py index d015037576..008ea2a24d 100644 --- a/typer/utils.py +++ b/typer/utils.py @@ -1,7 +1,107 @@ import inspect -from typing import Any, Callable, Dict, get_type_hints +from copy import copy +from typing import Any, Callable, Dict, List, Tuple, Type, get_type_hints -from .models import ParamMeta +from typing_extensions import Annotated + +from ._typing import get_args, get_origin +from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta + + +def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str: + # Render a `ParameterInfo` subclass for use in error messages. + # User code doesn't call `*Info` directly, so errors should present the classes how + # they were (probably) defined in the user code. + if param_type is OptionInfo: + return "`Option`" + elif param_type is ArgumentInfo: + return "`Argument`" + return f"`{param_type.__name__}`" + + +class AnnotatedParamWithDefaultValueError(Exception): + argument_name: str + param_type: Type[ParameterInfo] + + def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): + self.argument_name = argument_name + self.param_type = param_type + + def __str__(self): + param_type_str = _param_type_to_user_string(self.param_type) + return ( + f"{param_type_str} default value cannot be set in `Annotated`" + f" for {self.argument_name!r}. Set the default value with `=` instead." + ) + + +class MixedAnnotatedAndDefaultStyleError(Exception): + argument_name: str + annotated_param_type: Type[ParameterInfo] + default_param_type: Type[ParameterInfo] + + def __init__( + self, + argument_name: str, + annotated_param_type: Type[ParameterInfo], + default_param_type: Type[ParameterInfo], + ): + self.argument_name = argument_name + self.annotated_param_type = annotated_param_type + self.default_param_type = default_param_type + + def __str__(self): + annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) + default_param_type_str = _param_type_to_user_string(self.default_param_type) + msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" + if self.annotated_param_type is self.default_param_type: + msg += " default value" + else: + msg += f" {default_param_type_str} as a default value" + msg += f" together for {self.argument_name!r}" + return msg + + +class MultipleTyperAnnotationsError(Exception): + argument_name: str + + def __init__(self, argument_name: str): + self.argument_name = argument_name + + def __str__(self): + return ( + "Cannot specify multiple `Annotated` Typer arguments" + f" for {self.argument_name!r}" + ) + + +class DefaultFactoryAndDefaultValueError(Exception): + argument_name: str + param_type: Type[ParameterInfo] + + def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): + self.argument_name = argument_name + self.param_type = param_type + + def __str__(self): + param_type_str = _param_type_to_user_string(self.param_type) + return ( + "Cannot specify `default_factory` and a default value together" + f" for {param_type_str}" + ) + + +def _split_annotation_from_typer_annotations( + base_annotation: Type[Any], +) -> Tuple[Type[Any], List[ParameterInfo]]: + if get_origin(base_annotation) is not Annotated: + return base_annotation, [] + base_annotation, *maybe_typer_annotations = get_args(base_annotation) + return base_annotation, [ + annotation + for annotation in maybe_typer_annotations + if isinstance(annotation, ParameterInfo) + ] def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: @@ -9,10 +109,78 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: type_hints = get_type_hints(func) params = {} for param in signature.parameters.values(): - annotation = param.annotation - if param.name in type_hints: + annotation, typer_annotations = _split_annotation_from_typer_annotations( + param.annotation, + ) + if len(typer_annotations) > 1: + raise MultipleTyperAnnotationsError(param.name) + + default = param.default + if typer_annotations: + # It's something like `my_param: Annotated[str, Argument()]` + [parameter_info] = typer_annotations + + # Forbid `my_param: Annotated[str, Argument()] = Argument("...")` + if isinstance(param.default, ParameterInfo): + raise MixedAnnotatedAndDefaultStyleError( + argument_name=param.name, + annotated_param_type=type(parameter_info), + default_param_type=type(param.default), + ) + + parameter_info = copy(parameter_info) + + # When used as a default, `Option` takes a default value and option names + # as positional arguments: + # `Option(some_value, "--some-argument", "-s")` + # When used in `Annotated` (ie, what this is handling), `Option` just takes + # option names as positional arguments: + # `Option("--some-argument", "-s")` + # In this case, the `default` attribute of `parameter_info` is actually + # meant to be the first item of `param_decls`. + if ( + isinstance(parameter_info, OptionInfo) + and parameter_info.default is not ... + ): + parameter_info.param_decls = ( + parameter_info.default, + *parameter_info.param_decls, + ) + parameter_info.default = ... + + # Forbid `my_param: Annotated[str, Argument('some-default')]` + if parameter_info.default is not ...: + raise AnnotatedParamWithDefaultValueError( + param_type=type(parameter_info), + argument_name=param.name, + ) + if param.default is not param.empty: + # Put the parameter's default (set by `=`) into `parameter_info`, where + # typer can find it. + parameter_info.default = param.default + + default = parameter_info + elif param.name in type_hints: + # Resolve forward references. annotation = type_hints[param.name] + + if isinstance(default, ParameterInfo): + parameter_info = copy(default) + # Click supports `default` as either + # - an actual value; or + # - a factory function (returning a default value.) + # The two are not interchangeable for static typing, so typer allows + # specifying `default_factory`. Move the `default_factory` into `default` + # so click can find it. + if parameter_info.default is ... and parameter_info.default_factory: + parameter_info.default = parameter_info.default_factory + elif parameter_info.default_factory: + raise DefaultFactoryAndDefaultValueError( + argument_name=param.name, param_type=type(parameter_info) + ) + default = parameter_info + params[param.name] = ParamMeta( - name=param.name, default=param.default, annotation=annotation + name=param.name, default=default, annotation=annotation ) return params