From afb04f915a1a08408da887305e0b010152370d4c Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 18:16:18 -0400 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20PEP-593?= =?UTF-8?q?=20`Annotated`=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 | 179 ++++- 130 files changed, 5264 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 e1f7057d5a..fac0faafa0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,9 @@ 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..349232dce7 100644 --- a/typer/utils.py +++ b/typer/utils.py @@ -1,7 +1,108 @@ 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`" + # This line shouldn't be reachable during normal use. + return f"`{param_type.__name__}`" # pragma: no cover + + +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 +110,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 From 1398fa028d70d9939cea95369286e15f6d1c668c Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 19:00:23 -0400 Subject: [PATCH 02/11] =?UTF-8?q?Revert=20things=20I=20did=20to=20get=20a?= =?UTF-8?q?=203.6=20virtualenv=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lot of the dev dependency packages weren't installable on 3.6, so I had to remove them from the pyproject.toml. This commit adds them back --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfbca17620..70f02c79f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,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.8.4,<0.10.0", + "pytest-sugar >=0.9.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", From 1deb8aaa19ee576db13e501872844f1989d9e942 Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 19:12:18 -0400 Subject: [PATCH 03/11] Fix mypy/lint errors --- typer/_typing.py | 1 + typer/utils.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/typer/_typing.py b/typer/_typing.py index 6711ab8ad2..d9c384db7e 100644 --- a/typer/_typing.py +++ b/typer/_typing.py @@ -1,5 +1,6 @@ # 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 +# mypy: ignore-errors import sys from os import PathLike diff --git a/typer/utils.py b/typer/utils.py index 349232dce7..44816e2420 100644 --- a/typer/utils.py +++ b/typer/utils.py @@ -1,6 +1,6 @@ import inspect from copy import copy -from typing import Any, Callable, Dict, List, Tuple, Type, get_type_hints +from typing import Any, Callable, Dict, List, Tuple, Type, cast, get_type_hints from typing_extensions import Annotated @@ -28,7 +28,7 @@ def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): self.argument_name = argument_name self.param_type = param_type - def __str__(self): + def __str__(self) -> str: param_type_str = _param_type_to_user_string(self.param_type) return ( f"{param_type_str} default value cannot be set in `Annotated`" @@ -51,7 +51,7 @@ def __init__( self.annotated_param_type = annotated_param_type self.default_param_type = default_param_type - def __str__(self): + def __str__(self) -> str: 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" @@ -69,7 +69,7 @@ class MultipleTyperAnnotationsError(Exception): def __init__(self, argument_name: str): self.argument_name = argument_name - def __str__(self): + def __str__(self) -> str: return ( "Cannot specify multiple `Annotated` Typer arguments" f" for {self.argument_name!r}" @@ -84,7 +84,7 @@ def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): self.argument_name = argument_name self.param_type = param_type - def __str__(self): + def __str__(self) -> str: param_type_str = _param_type_to_user_string(self.param_type) return ( "Cannot specify `default_factory` and a default value together" @@ -95,7 +95,7 @@ def __str__(self): def _split_annotation_from_typer_annotations( base_annotation: Type[Any], ) -> Tuple[Type[Any], List[ParameterInfo]]: - if get_origin(base_annotation) is not Annotated: + if get_origin(base_annotation) is not Annotated: # type: ignore return base_annotation, [] base_annotation, *maybe_typer_annotations = get_args(base_annotation) return base_annotation, [ @@ -144,8 +144,8 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: and parameter_info.default is not ... ): parameter_info.param_decls = ( - parameter_info.default, - *parameter_info.param_decls, + cast(str, parameter_info.default), + *(parameter_info.param_decls or ()), ) parameter_info.default = ... From 452fc6f33eb9c08fb5465094de2809174a331ba6 Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 19:22:25 -0400 Subject: [PATCH 04/11] Skip coverage on test lines that shouldn't execute --- tests/test_ambiguous_params.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 1b4dd4e18b..2fff09bb59 100644 --- a/tests/test_ambiguous_params.py +++ b/tests/test_ambiguous_params.py @@ -30,7 +30,7 @@ def test_forbid_default_value_in_annotated_argument(): # for param_decls too. @app.command() def cmd(my_param: Annotated[str, typer.Argument("foo")]): - ... + ... # pragma: no cover with pytest.raises(AnnotatedParamWithDefaultValueError) as excinfo: runner.invoke(app) @@ -65,7 +65,7 @@ def test_forbid_annotated_param_and_default_param(param, param_info_type): @app.command() def cmd(my_param: Annotated[str, param()] = param("foo")): - ... + ... # pragma: no cover with pytest.raises(MixedAnnotatedAndDefaultStyleError) as excinfo: runner.invoke(app) @@ -82,7 +82,7 @@ def test_forbid_multiple_typer_params_in_annotated(): @app.command() def cmd(my_param: Annotated[str, typer.Argument(), typer.Argument()]): - ... + ... # pragma: no cover with pytest.raises(MultipleTyperAnnotationsError) as excinfo: runner.invoke(app) @@ -118,7 +118,7 @@ def make_string(): @app.command() def cmd(my_param: Annotated[str, param(default_factory=make_string)] = "hello"): - ... + ... # pragma: no cover with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo: runner.invoke(app) @@ -166,7 +166,7 @@ def make_string(): @app.command() def cmd(my_param: str = param("hi", default_factory=make_string)): - ... + ... # pragma: no cover with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo: runner.invoke(app) From 4c0d058351bd3dd8da835fcde6148bf962726b64 Mon Sep 17 00:00:00 2001 From: Ryan Miller Galamb Date: Tue, 25 Apr 2023 19:51:47 -0400 Subject: [PATCH 05/11] Missed a spot --- tests/test_ambiguous_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 2fff09bb59..7938ac57c8 100644 --- a/tests/test_ambiguous_params.py +++ b/tests/test_ambiguous_params.py @@ -112,7 +112,7 @@ def cmd(my_param: Annotated[str, "someval", typer.Argument(), 4] = "hello"): ) def test_forbid_default_factory_and_default_value_in_annotated(param, param_info_type): def make_string(): - return "foo" + return "foo" # pragma: no cover app = typer.Typer() @@ -160,7 +160,7 @@ def cmd(my_param: str = param(default_factory=make_string)): ) def test_forbid_default_and_default_factory_with_default_param(param, param_info_type): def make_string(): - return "foo" + return "foo" # pragma: no cover app = typer.Typer() From 104cfdf880dbe7375a6d793b1106130eee09af4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 05:42:00 +0200 Subject: [PATCH 06/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Tweak=20examples=20a?= =?UTF-8?q?nd=20tests=20with=20Annotated,=20add=20extra=20examples=20and?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/arguments/default/tutorial002.py | 2 +- docs_src/arguments/optional/tutorial001.py | 2 +- docs_src/arguments/optional/tutorial002.py | 2 +- docs_src/arguments/optional/tutorial003.py | 9 ++++ docs_src/options/callback/tutorial002_an.py | 17 ++++++ docs_src/options/password/tutorial001_an.py | 13 +++++ docs_src/options/password/tutorial002_an.py | 16 ++++++ docs_src/options/required/tutorial001.py | 2 +- docs_src/options/required/tutorial001_an.py | 10 ++++ docs_src/options/required/tutorial002.py | 9 ++++ docs_src/options/version/tutorial001_an.py | 25 +++++++++ docs_src/options/version/tutorial002_an.py | 30 +++++++++++ .../options_autocompletion/tutorial001_an.py | 13 +++++ .../options_autocompletion/tutorial005_an.py | 30 +++++++++++ .../options_autocompletion/tutorial006_an.py | 18 +++++++ docs_src/testing/app02_an/__init__.py | 0 docs_src/testing/app02_an/main.py | 13 +++++ docs_src/testing/app02_an/test_main.py | 11 ++++ .../test_optional/test_tutorial003.py | 51 ++++++++++++++++++ .../test_name/test_tutorial001.py | 6 +++ .../test_name/test_tutorial001_an.py | 6 +++ ...est_tutorial002.py => test_tutorial001.py} | 0 .../test_required/test_tutorial001_an.py | 54 +++++++++++++++++++ .../test_testing/test_app02_an.py | 19 +++++++ 24 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 docs_src/arguments/optional/tutorial003.py create mode 100644 docs_src/options/callback/tutorial002_an.py create mode 100644 docs_src/options/password/tutorial001_an.py create mode 100644 docs_src/options/password/tutorial002_an.py create mode 100644 docs_src/options/required/tutorial001_an.py create mode 100644 docs_src/options/required/tutorial002.py create mode 100644 docs_src/options/version/tutorial001_an.py create mode 100644 docs_src/options/version/tutorial002_an.py create mode 100644 docs_src/options_autocompletion/tutorial001_an.py create mode 100644 docs_src/options_autocompletion/tutorial005_an.py create mode 100644 docs_src/options_autocompletion/tutorial006_an.py create mode 100644 docs_src/testing/app02_an/__init__.py create mode 100644 docs_src/testing/app02_an/main.py create mode 100644 docs_src/testing/app02_an/test_main.py create mode 100644 tests/test_tutorial/test_arguments/test_optional/test_tutorial003.py rename tests/test_tutorial/test_options/test_required/{test_tutorial002.py => test_tutorial001.py} (100%) create mode 100644 tests/test_tutorial/test_options/test_required/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_testing/test_app02_an.py diff --git a/docs_src/arguments/default/tutorial002.py b/docs_src/arguments/default/tutorial002.py index 71edea2754..2b516b20ac 100644 --- a/docs_src/arguments/default/tutorial002.py +++ b/docs_src/arguments/default/tutorial002.py @@ -7,7 +7,7 @@ def get_name(): return random.choice(["Deadpool", "Rick", "Morty", "Hiro"]) -def main(name: str = typer.Argument(get_name)): +def main(name: str = typer.Argument(default_factory=get_name)): print(f"Hello {name}") diff --git a/docs_src/arguments/optional/tutorial001.py b/docs_src/arguments/optional/tutorial001.py index 8a30344394..9c3a32b506 100644 --- a/docs_src/arguments/optional/tutorial001.py +++ b/docs_src/arguments/optional/tutorial001.py @@ -1,7 +1,7 @@ import typer -def main(name: str = typer.Argument(...)): +def main(name: str = typer.Argument()): print(f"Hello {name}") diff --git a/docs_src/arguments/optional/tutorial002.py b/docs_src/arguments/optional/tutorial002.py index 9d0014da04..2445b34016 100644 --- a/docs_src/arguments/optional/tutorial002.py +++ b/docs_src/arguments/optional/tutorial002.py @@ -3,7 +3,7 @@ import typer -def main(name: Optional[str] = typer.Argument(None)): +def main(name: Optional[str] = typer.Argument(default=None)): if name is None: print("Hello World!") else: diff --git a/docs_src/arguments/optional/tutorial003.py b/docs_src/arguments/optional/tutorial003.py new file mode 100644 index 0000000000..88a0e64cc7 --- /dev/null +++ b/docs_src/arguments/optional/tutorial003.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str = typer.Argument(default=...)): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/callback/tutorial002_an.py b/docs_src/options/callback/tutorial002_an.py new file mode 100644 index 0000000000..2f5a67f8be --- /dev/null +++ b/docs_src/options/callback/tutorial002_an.py @@ -0,0 +1,17 @@ +import typer +from typing_extensions import Annotated + + +def name_callback(value: str): + 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/password/tutorial001_an.py b/docs_src/options/password/tutorial001_an.py new file mode 100644 index 0000000000..3bd3e0d31e --- /dev/null +++ b/docs_src/options/password/tutorial001_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: str, + email: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)], +): + print(f"Hello {name}, your email is {email}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/password/tutorial002_an.py b/docs_src/options/password/tutorial002_an.py new file mode 100644 index 0000000000..921942a868 --- /dev/null +++ b/docs_src/options/password/tutorial002_an.py @@ -0,0 +1,16 @@ +import typer +from typing_extensions import Annotated + + +def main( + name: str, + password: Annotated[ + str, typer.Option(prompt=True, confirmation_prompt=True, hide_input=True) + ], +): + print(f"Hello {name}. Doing something very secure with password.") + print(f"...just kidding, here it is, very insecure: {password}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/required/tutorial001.py b/docs_src/options/required/tutorial001.py index f6f17cf992..78fc38ba17 100644 --- a/docs_src/options/required/tutorial001.py +++ b/docs_src/options/required/tutorial001.py @@ -1,7 +1,7 @@ import typer -def main(name: str, lastname: str = typer.Option(...)): +def main(name: str, lastname: str = typer.Option()): print(f"Hello {name} {lastname}") diff --git a/docs_src/options/required/tutorial001_an.py b/docs_src/options/required/tutorial001_an.py new file mode 100644 index 0000000000..d4c2e487c9 --- /dev/null +++ b/docs_src/options/required/tutorial001_an.py @@ -0,0 +1,10 @@ +import typer +from typing_extensions import Annotated + + +def main(name: str, lastname: Annotated[str, typer.Option()]): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/required/tutorial002.py b/docs_src/options/required/tutorial002.py new file mode 100644 index 0000000000..143cd0789e --- /dev/null +++ b/docs_src/options/required/tutorial002.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str, lastname: str = typer.Option(default=...)): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/version/tutorial001_an.py b/docs_src/options/version/tutorial001_an.py new file mode 100644 index 0000000000..10ad3680f1 --- /dev/null +++ b/docs_src/options/version/tutorial001_an.py @@ -0,0 +1,25 @@ +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 main( + name: Annotated[str, typer.Option()] = "World", + version: Annotated[ + Optional[bool], typer.Option("--version", callback=version_callback) + ] = None, +): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/version/tutorial002_an.py b/docs_src/options/version/tutorial002_an.py new file mode 100644 index 0000000000..35f6b804d7 --- /dev/null +++ b/docs_src/options/version/tutorial002_an.py @@ -0,0 +1,30 @@ +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") + + +def main( + name: Annotated[str, typer.Option(callback=name_callback)], + version: Annotated[ + Optional[bool], typer.Option("--version", callback=version_callback) + ] = None, +): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options_autocompletion/tutorial001_an.py b/docs_src/options_autocompletion/tutorial001_an.py new file mode 100644 index 0000000000..d39641253b --- /dev/null +++ b/docs_src/options_autocompletion/tutorial001_an.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer() + + +@app.command() +def main(name: Annotated[str, typer.Option(help="The name to say hi to.")] = "World"): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial005_an.py b/docs_src/options_autocompletion/tutorial005_an.py new file mode 100644 index 0000000000..bd1b62d7da --- /dev/null +++ b/docs_src/options_autocompletion/tutorial005_an.py @@ -0,0 +1,30 @@ +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): + 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[ + 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/tutorial006_an.py b/docs_src/options_autocompletion/tutorial006_an.py new file mode 100644 index 0000000000..ae208105b0 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial006_an.py @@ -0,0 +1,18 @@ +from typing import List + +import typer +from typing_extensions import Annotated + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[List[str], typer.Option(help="The name to say hi to.")] = ["World"] +): + for each_name in name: + print(f"Hello {each_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/testing/app02_an/__init__.py b/docs_src/testing/app02_an/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/testing/app02_an/main.py b/docs_src/testing/app02_an/main.py new file mode 100644 index 0000000000..56fb327be1 --- /dev/null +++ b/docs_src/testing/app02_an/main.py @@ -0,0 +1,13 @@ +import typer +from typing_extensions import Annotated + +app = typer.Typer() + + +@app.command() +def main(name: str, email: Annotated[str, typer.Option(prompt=True)]): + print(f"Hello {name}, your email is: {email}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/testing/app02_an/test_main.py b/docs_src/testing/app02_an/test_main.py new file mode 100644 index 0000000000..ffe01743ee --- /dev/null +++ b/docs_src/testing/app02_an/test_main.py @@ -0,0 +1,11 @@ +from typer.testing import CliRunner + +from .main import app + +runner = CliRunner() + + +def test_app(): + result = runner.invoke(app, ["Camila"], input="camila@example.com\n") + assert result.exit_code == 0 + assert "Hello Camila, your email is: camila@example.com" in result.stdout diff --git a/tests/test_tutorial/test_arguments/test_optional/test_tutorial003.py b/tests/test_tutorial/test_arguments/test_optional/test_tutorial003.py new file mode 100644 index 0000000000..b484ad2266 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_optional/test_tutorial003.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 tutorial003 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_options/test_name/test_tutorial001.py b/tests/test_tutorial/test_options/test_name/test_tutorial001.py index 2690ff24fa..da14c934c1 100644 --- a/tests/test_tutorial/test_options/test_name/test_tutorial001.py +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001.py @@ -26,6 +26,12 @@ def test_call(): assert "Hello Camila" in result.output +def test_call_no_args(): + result = runner.invoke(app, ["--name"]) + assert result.exit_code != 0 + assert "Option '--name' requires an argument" in result.output + + def test_script(): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], 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 index 87dfbc308b..c5c57b2ee8 100644 --- a/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py @@ -26,6 +26,12 @@ def test_call(): assert "Hello Camila" in result.output +def test_call_no_args(): + result = runner.invoke(app, ["--name"]) + assert result.exit_code != 0 + assert "Option '--name' requires an argument" in result.output + + def test_script(): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], diff --git a/tests/test_tutorial/test_options/test_required/test_tutorial002.py b/tests/test_tutorial/test_options/test_required/test_tutorial001.py similarity index 100% rename from tests/test_tutorial/test_options/test_required/test_tutorial002.py rename to tests/test_tutorial/test_options/test_required/test_tutorial001.py diff --git a/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py b/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py new file mode 100644 index 0000000000..48d82cf331 --- /dev/null +++ b/tests/test_tutorial/test_options/test_required/test_tutorial001_an.py @@ -0,0 +1,54 @@ +import subprocess +import sys + +import typer +import typer.core +from typer.testing import CliRunner + +from docs_src.options.required import tutorial001_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code != 0 + assert "Missing option '--lastname'." 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_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_help_no_rich(): + rich = typer.core.rich + typer.core.rich = None + 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 + typer.core.rich = rich + + +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_testing/test_app02_an.py b/tests/test_tutorial/test_testing/test_app02_an.py new file mode 100644 index 0000000000..140a7e37ca --- /dev/null +++ b/tests/test_tutorial/test_testing/test_app02_an.py @@ -0,0 +1,19 @@ +import subprocess +import sys + +from docs_src.testing.app02_an import main as mod +from docs_src.testing.app02_an.test_main import test_app + + +def test_app02_an(): + test_app() + + +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 From ce53b7a243113b42be8ea5cff125a1de3e891fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 05:42:50 +0200 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=94=A5=20Remove=20Pydantic-specific?= =?UTF-8?q?=20logic=20from=20=5Ftyping.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/_typing.py | 111 ++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/typer/_typing.py b/typer/_typing.py index d9c384db7e..d906e2227d 100644 --- a/typer/_typing.py +++ b/typer/_typing.py @@ -288,7 +288,8 @@ def is_union(tp: Optional[Type[Any]]) -> bool: if TYPE_CHECKING: - from .fields import ModelField + # Only in Pydantic + # from .fields import ModelField TupleGenerator = Generator[Tuple[str, Any], None, None] DictStrAny = Dict[str, Any] @@ -541,59 +542,61 @@ def is_classvar(ann_type: Type[Any]) -> bool: 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) +# Only in Pydantic +# 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() + + +# Only in Pydantic +# 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]]: From 95548f25f5b9e16ef643d19f16891f95cd2860e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 05:47:04 +0200 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20to=20use?= =?UTF-8?q?=20new=20Annotated=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/arguments/default.md | 39 ++++- docs/tutorial/arguments/envvar.md | 51 ++++-- docs/tutorial/arguments/help.md | 136 +++++++++++++--- docs/tutorial/commands/help.md | 76 +++++++-- docs/tutorial/commands/options.md | 17 +- .../arguments-with-multiple-values.md | 17 +- .../multiple-values/multiple-options.md | 34 +++- .../options-with-multiple-values.md | 17 +- docs/tutorial/options-autocompletion.md | 153 ++++++++++++++---- docs/tutorial/options/callback-and-context.md | 68 ++++++-- docs/tutorial/options/name.md | 97 ++++++++--- docs/tutorial/options/password.md | 34 +++- docs/tutorial/options/prompt.md | 51 ++++-- docs/tutorial/options/version.md | 51 ++++-- docs/tutorial/parameter-types/bool.md | 68 ++++++-- docs/tutorial/parameter-types/datetime.md | 17 +- docs/tutorial/parameter-types/enum.md | 17 +- docs/tutorial/parameter-types/file.md | 87 ++++++++-- docs/tutorial/parameter-types/number.md | 51 ++++-- docs/tutorial/parameter-types/path.md | 34 +++- docs/tutorial/testing.md | 17 +- 21 files changed, 927 insertions(+), 205 deletions(-) diff --git a/docs/tutorial/arguments/default.md b/docs/tutorial/arguments/default.md index 0ab7f15e01..3d5de98dfa 100644 --- a/docs/tutorial/arguments/default.md +++ b/docs/tutorial/arguments/default.md @@ -6,9 +6,20 @@ That way the *CLI argument* will be optional *and also* have a default value. We can also use `typer.Argument()` to make a *CLI argument* have a default value other than `None`: -```Python hl_lines="4" -{!../docs_src/arguments/default/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/default/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/default/tutorial001.py!} + ``` !!! tip Because now the value will be a `str` passed by the user or the default value of `"Wade Wilson"` which is also a `str`, we know the value will never be `None`, so we don't have to (and shouldn't) use `Optional[str]`. @@ -49,16 +60,30 @@ Hello Camila ## Dynamic default value -And we can even make the default value be dynamically generated by passing a function as the first function argument: +And we can even make the default value be dynamically generated by passing a function as the `default_factory` argument: -```Python hl_lines="6 7 10" -{!../docs_src/arguments/default/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7-8 11" + {!> ../docs_src/arguments/default/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6-7 10" + {!> ../docs_src/arguments/default/tutorial002.py!} + ``` In this case, we created the function `get_name` that will just return a random `str` each time. And we pass it as the first function argument to `typer.Argument()`. +!!! tip + The word "factory" in `default_factory` is just a fancy way of saying "function that will create the default value". + Check it:
diff --git a/docs/tutorial/arguments/envvar.md b/docs/tutorial/arguments/envvar.md index 4277870557..79637e551b 100644 --- a/docs/tutorial/arguments/envvar.md +++ b/docs/tutorial/arguments/envvar.md @@ -2,9 +2,20 @@ You can also configure a *CLI argument* to read a value from an environment vari To do that, use the `envvar` parameter for `typer.Argument()`: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/envvar/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/envvar/tutorial001.py!} + ``` In this case, the *CLI argument* `name` will have a default value of `"World"`, but will also read any value passed to the environment variable `AWESOME_NAME` if no value is provided in the command line: @@ -51,9 +62,20 @@ Hello Mr. Czernobog You are not restricted to a single environment variable, you can declare a list of environment variables that could be used to get a value if it was not passed in the command line: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6" + {!> ../docs_src/arguments/envvar/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/envvar/tutorial002.py!} + ``` Check it: @@ -90,9 +112,20 @@ Hello Mr. Anubis By default, environment variables used will be shown in the help text, but you can disable them with `show_envvar=False`: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/arguments/envvar/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/envvar/tutorial003.py!} + ``` Check it: diff --git a/docs/tutorial/arguments/help.md b/docs/tutorial/arguments/help.md index ddd2dfede3..8477823063 100644 --- a/docs/tutorial/arguments/help.md +++ b/docs/tutorial/arguments/help.md @@ -12,9 +12,20 @@ Now that you also know how to use `typer.Argument()`, let's use it to add docume You can use the `help` parameter to add a help text for a *CLI argument*: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/help/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/help/tutorial001.py!} + ``` And it will be used in the automatic `--help` option: @@ -41,9 +52,20 @@ Options: And of course, you can also combine that `help` with the docstring: -```Python hl_lines="4 5 6 7" -{!../docs_src/arguments/help/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5-8" + {!> ../docs_src/arguments/help/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4-7" + {!> ../docs_src/arguments/help/tutorial002.py!} + ``` And the `--help` option will combine all the information: @@ -72,9 +94,20 @@ Options: If you have a *CLI argument* with a default value, like `"World"`: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/help/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/help/tutorial003.py!} + ``` It will show that default value in the help text: @@ -101,9 +134,20 @@ Options: But you can disable that if you want to, with `show_default=False`: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/arguments/help/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/help/tutorial004.py!} + ``` And then it won't show the default value: @@ -138,9 +182,20 @@ Options: You can use the same `show_default` to pass a custom string (instead of a `bool`) to customize the default value to be shown in the help text: -```Python hl_lines="6" -{!../docs_src/arguments/help/tutorial005.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../docs_src/arguments/help/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6" + {!> ../docs_src/arguments/help/tutorial005.py!} + ``` And it will be used in the help text: @@ -187,9 +242,20 @@ But you can customize it with the `metavar` parameter for `typer.Argument()`. For example, let's say you don't want to have the default of `NAME`, you want to have `username`, in lowercase, and you really want ✨ emojis ✨ everywhere: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial006.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/help/tutorial006_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/help/tutorial006.py!} + ``` Now the generated help text will have `✨username✨` instead of `NAME`: @@ -217,9 +283,20 @@ You might want to show the help information for *CLI arguments* in different pan If you have installed Rich as described in the docs for [Printing and Colors](../printing.md){.internal-link target=_blank}, you can set the `rich_help_panel` parameter to the name of the panel where you want this *CLI argument* to be shown: -```Python hl_lines="7 10" -{!../docs_src/arguments/help/tutorial007.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="8 12" + {!> ../docs_src/arguments/help/tutorial007_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 10" + {!> ../docs_src/arguments/help/tutorial007.py!} + ``` Then, if you check the `--help` option, you will see a default panel named "`Arguments`" for the *CLI arguments* that don't have a custom `rich_help_panel`. @@ -267,9 +344,20 @@ If you want, you can make a *CLI argument* **not** show up in the `Arguments` se You will probably not want to do this normally, but it's possible: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial008.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/arguments/help/tutorial008_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/arguments/help/tutorial008.py!} + ``` Check it: diff --git a/docs/tutorial/commands/help.md b/docs/tutorial/commands/help.md index 0cc08c87fb..7cb306134f 100644 --- a/docs/tutorial/commands/help.md +++ b/docs/tutorial/commands/help.md @@ -2,9 +2,20 @@ The same as before, you can add help for the commands in the docstrings and the And the `typer.Typer()` application receives a parameter `help` that you can pass with the main help text for your CLI program: -```Python hl_lines="3 8 9 10 20 23 24 25 26 27 39 42 43 44 45 46 55 56 57" -{!../docs_src/commands/help/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="4 9-11 22 26-30 43 47-51 60-62" + {!> ../docs_src/commands/help/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 8-10 20 23-27 39 42-46 55-57" + {!> ../docs_src/commands/help/tutorial001.py!} + ``` Check it: @@ -190,9 +201,20 @@ Then you can use more formatting in the docstrings and the `help` parameter for If you set `rich_markup_mode="rich"` when creating the `typer.Typer()` app, you will be able to use Rich Console Markup in the docstring, and even in the help for the *CLI arguments* and options: -```Python hl_lines="3 9 13-15 20 22 24" -{!../docs_src/commands/help/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="4 10 14-16 21 24 27" + {!> ../docs_src/commands/help/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 9 13-15 20 22 24" + {!> ../docs_src/commands/help/tutorial004.py!} + ``` With that, you can use Rich Console Markup to format the text in the docstring for the command `create`, make the word "`create`" bold and green, and even use an emoji. @@ -255,9 +277,20 @@ $ python main.py delete --help If you set `rich_markup_mode="markdown"` when creating the `typer.Typer()` app, you will be able to use Markdown in the docstring: -```Python hl_lines="3 7 9-17 22 24-25" -{!../docs_src/commands/help/tutorial005.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="4 9 12-20 25 27-28" + {!> ../docs_src/commands/help/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3 7 9-17 22 24-25" + {!> ../docs_src/commands/help/tutorial005.py!} + ``` With that, you can use Markdown to format the text in the docstring for the command `create`, make the word "`create`" bold, show a list of items, and even use an emoji. @@ -330,9 +363,11 @@ If you installed ../docs_src/commands/help/tutorial006.py!} + ``` Commands without a panel will be shown in the default panel `Commands`, and the rest will be shown in the next panels: @@ -373,9 +408,20 @@ The same way, you can configure the panels for *CLI arguments* and *CLI options* And of course, in the same application you can also set the `rich_help_panel` for commands. -```Python hl_lines="12 16 21 30" -{!../docs_src/commands/help/tutorial007.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="15 21 27 37" + {!> ../docs_src/commands/help/tutorial007_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="12 16 21 30" + {!> ../docs_src/commands/help/tutorial007.py!} + ``` Then if you run the application you will see all the *CLI parameters* in their respective panels. diff --git a/docs/tutorial/commands/options.md b/docs/tutorial/commands/options.md index 8e14386173..872e81be37 100644 --- a/docs/tutorial/commands/options.md +++ b/docs/tutorial/commands/options.md @@ -2,9 +2,20 @@ Commands can also have their own *CLI options*. In fact, each command can have different *CLI arguments* and *CLI options*: -```Python hl_lines="7 13 14 24 33" -{!../docs_src/commands/options/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="8 14-17 27-29 38" + {!> ../docs_src/commands/options/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7 13-14 24 33" + {!> ../docs_src/commands/options/tutorial001.py!} + ``` Here we have multiple commands, with different *CLI parameters*: diff --git a/docs/tutorial/multiple-values/arguments-with-multiple-values.md b/docs/tutorial/multiple-values/arguments-with-multiple-values.md index 5783c8e83b..36287a401b 100644 --- a/docs/tutorial/multiple-values/arguments-with-multiple-values.md +++ b/docs/tutorial/multiple-values/arguments-with-multiple-values.md @@ -31,9 +31,20 @@ woohoo! If you want a specific number of values and types, you can use a tuple, and it can even have default values: -```Python hl_lines="7 8" -{!../docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="8-10" + {!> ../docs_src/multiple_values/arguments_with_multiple_values/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7-8" + {!> ../docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py!} + ``` Check it: diff --git a/docs/tutorial/multiple-values/multiple-options.md b/docs/tutorial/multiple-values/multiple-options.md index a6c2ca2e83..00754e93b4 100644 --- a/docs/tutorial/multiple-values/multiple-options.md +++ b/docs/tutorial/multiple-values/multiple-options.md @@ -4,9 +4,20 @@ For example, let's say you want to accept several users in a single execution. For this, use the standard Python `typing.List` to declare it as a `list` of `str`: -```Python hl_lines="1 6" -{!../docs_src/multiple_values/multiple_options/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="1 7" + {!> ../docs_src/multiple_values/multiple_options/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 6" + {!> ../docs_src/multiple_values/multiple_options/tutorial001.py!} + ``` You will receive the values as you declared them, as a `list` of `str`. @@ -39,9 +50,20 @@ Processing user: Morty The same way, you can use other types and they will be converted by **Typer** to their declared type: -```Python hl_lines="6" -{!../docs_src/multiple_values/multiple_options/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/multiple_values/multiple_options/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6" + {!> ../docs_src/multiple_values/multiple_options/tutorial002.py!} + ``` Check it: diff --git a/docs/tutorial/multiple-values/options-with-multiple-values.md b/docs/tutorial/multiple-values/options-with-multiple-values.md index 03a9c816e8..52154a7bd4 100644 --- a/docs/tutorial/multiple-values/options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/options-with-multiple-values.md @@ -4,9 +4,20 @@ You can set the number of values and types to anything you want, but it has to b For this, use the standard Python `typing.Tuple`: -```Python hl_lines="1 6" -{!../docs_src/multiple_values/options_with_multiple_values/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="1 7" + {!> ../docs_src/multiple_values/options_with_multiple_values/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 6" + {!> ../docs_src/multiple_values/options_with_multiple_values/tutorial001.py!} + ``` Each of the internal types defines the type of each value in the tuple. diff --git a/docs/tutorial/options-autocompletion.md b/docs/tutorial/options-autocompletion.md index c22f964aec..5d8a682670 100644 --- a/docs/tutorial/options-autocompletion.md +++ b/docs/tutorial/options-autocompletion.md @@ -14,9 +14,20 @@ To check it quickly without creating a new Python package, install [Typer CLI](. Then let's create small example program: -```Python -{!../docs_src/options_autocompletion/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python + {!> ../docs_src/options_autocompletion/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../docs_src/options_autocompletion/tutorial001.py!} + ``` And let's try it with **Typer CLI** to get completion: @@ -50,9 +61,20 @@ Right now we get completion for the *CLI option* names, but not for the values. We can provide completion for the values creating an `autocompletion` function, similar to the `callback` functions from [CLI Option Callback and Context](./callback-and-context.md){.internal-link target=_blank}: -```Python hl_lines="4-5 14" -{!../docs_src/options_autocompletion/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5-6 15" + {!> ../docs_src/options_autocompletion/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4-5 14" + {!> ../docs_src/options_autocompletion/tutorial002.py!} + ``` We return a `list` of strings from the `complete_name()` function. @@ -81,9 +103,20 @@ Modify the `complete_name()` function to receive a parameter of type `str`, it w Then we can check and return only the values that start with the incomplete value from the command line: -```Python hl_lines="6-11" -{!../docs_src/options_autocompletion/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7-12" + {!> ../docs_src/options_autocompletion/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6-11" + {!> ../docs_src/options_autocompletion/tutorial003.py!} + ``` Now let's try it: @@ -119,9 +152,20 @@ In the `complete_name()` function, instead of providing one `str` per completion So, in the end, we return a `list` of `tuples` of `str`: -```Python hl_lines="3 4 5 6 7 10 11 12 13 14 15 16" -{!../docs_src/options_autocompletion/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="4-8 11-17" + {!> ../docs_src/options_autocompletion/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="3-7 10-16" + {!> ../docs_src/options_autocompletion/tutorial004.py!} + ``` !!! tip If you want to have help text for each item, make sure each item in the list is a `tuple`. Not a `list`. @@ -156,9 +200,20 @@ Instead of creating and returning a list with values (`str` or `tuple`), we can That way our function will be a generator that **Typer** (actually Click) can iterate: -```Python hl_lines="10 11 12 13" -{!../docs_src/options_autocompletion/tutorial005.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="11-14" + {!> ../docs_src/options_autocompletion/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="10-13" + {!> ../docs_src/options_autocompletion/tutorial005.py!} + ``` That simplifies our code a bit and works the same. @@ -185,9 +240,20 @@ So, we will allow multiple `--name` *CLI options*. For this we use a `List` of `str`: -```Python hl_lines="8-11" -{!../docs_src/options_autocompletion/tutorial006.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="9-14" + {!> ../docs_src/options_autocompletion/tutorial006_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8-11" + {!> ../docs_src/options_autocompletion/tutorial006.py!} + ``` And then we can use it like: @@ -212,9 +278,20 @@ But you can access the context by declaring a function parameter of type `typer. And from that context you can get the current values for each parameter. -```Python hl_lines="12 13 15" -{!../docs_src/options_autocompletion/tutorial007.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="13-14 16" + {!> ../docs_src/options_autocompletion/tutorial007_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="12-13 15" + {!> ../docs_src/options_autocompletion/tutorial007.py!} + ``` We are getting the `names` already provided with `--name` in the command line before this completion was triggered. @@ -281,9 +358,20 @@ You can print to "standard error" with a **Rich** `Console(stderr=True)`. Using `stderr=True` tells **Rich** that the output should be shown in "standard error". -```Python hl_lines="12 15-16" -{!../docs_src/options_autocompletion/tutorial008.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="13 16-17" + {!> ../docs_src/options_autocompletion/tutorial008_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="12 15-16" + {!> ../docs_src/options_autocompletion/tutorial008.py!} + ``` !!! info If you can't install and use Rich, you can also use `print(lastname, file=sys.stderr)` or `typer.echo("some text", err=True)` instead. @@ -322,9 +410,20 @@ Sebastian -- The type hints guy. Of course, you can declare everything if you need it, the context, the raw *CLI parameters*, and the incomplete `str`: -```Python hl_lines="15" -{!../docs_src/options_autocompletion/tutorial009.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="16" + {!> ../docs_src/options_autocompletion/tutorial009_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="15" + {!> ../docs_src/options_autocompletion/tutorial009.py!} + ``` Check it: diff --git a/docs/tutorial/options/callback-and-context.md b/docs/tutorial/options/callback-and-context.md index 2e924226a9..11969f2aa2 100644 --- a/docs/tutorial/options/callback-and-context.md +++ b/docs/tutorial/options/callback-and-context.md @@ -6,9 +6,20 @@ In those cases you can use a *CLI parameter* callback function. For example, you could do some validation before the rest of the code is executed. -```Python hl_lines="4 5 6 7 10" -{!../docs_src/options/callback/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5-8 11" + {!> ../docs_src/options/callback/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4-7 10" + {!> ../docs_src/options/callback/tutorial001.py!} + ``` Here you pass a function to `typer.Option()` or `typer.Argument()` with the keyword argument `callback`. @@ -94,9 +105,20 @@ But the main **important point** is that it is all based on values printed by yo Let's say that when the callback is running, we want to show a message saying that it's validating the name: -```Python hl_lines="5" -{!../docs_src/options/callback/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6" + {!> ../docs_src/options/callback/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5" + {!> ../docs_src/options/callback/tutorial002.py!} + ``` And because the callback will be called when the shell calls your program asking for completion, that message `"Validating name"` will be printed and it will break completion. @@ -131,9 +153,20 @@ But you can access the context by declaring a function parameter of type `typer. The "context" has some additional data about the current execution of your program: -```Python hl_lines="4 5 6" -{!../docs_src/options/callback/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5-7" + {!> ../docs_src/options/callback/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4-6" + {!> ../docs_src/options/callback/tutorial003.py!} + ``` The `ctx.resilient_parsing` will be `True` when handling completion, so you can just return without printing anything else. @@ -165,9 +198,20 @@ Hello Camila The same way you can access the `typer.Context` by declaring a function parameter with its value, you can declare another function parameter with type `typer.CallbackParam` to get the specific Click `Parameter` object. -```Python hl_lines="4 7" -{!../docs_src/options/callback/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5 8" + {!> ../docs_src/options/callback/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4 7" + {!> ../docs_src/options/callback/tutorial004.py!} + ``` It's probably not very common, but you could do it if you need it. diff --git a/docs/tutorial/options/name.md b/docs/tutorial/options/name.md index 982e7e4d63..1a05fd50ba 100644 --- a/docs/tutorial/options/name.md +++ b/docs/tutorial/options/name.md @@ -10,7 +10,7 @@ def main(user_name: Optional[str] = None): or ```Python -def main(user_name: Optional[str] = typer.Option(None)): +def main(user_name: Annotated[Optional[str], typer.Option()] = None): pass ``` @@ -24,20 +24,33 @@ But you can customize it if you want to. Let's say the function parameter name is `user_name` as above, but you want the *CLI option* to be just `--name`. -You can pass the *CLI option* name that you want to have in the next positional argument passed to `typer.Option()`: +You can pass the *CLI option* name that you want to have in the following positional argument passed to `typer.Option()`: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/name/tutorial001_an.py!} + ``` + + Here you are passing the string `"--name"` as the first positional argument to `typer.Option()`. + +=== "Python 3.6+ non-Annotated" -Here you are passing the string `"--name"` as the second positional argument to `typer.Option()`. + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/name/tutorial001.py!} + ``` + + Here you are passing the string `"--name"` as the second positional argument to `typer.Option()`, as the first argument is `...` to mark it as required. !!! info "Positional" means that it's not a function argument with a keyword name. For example `show_default=True` is a keyword argument. "`show_default`" is the keyword. - But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()` after the `...` value passed in the first position. + But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()`. That's a "positional argument" in a function. @@ -172,7 +185,7 @@ tar: You must specify one of the blah, blah, error, error In **Typer** you can also define *CLI option* short names the same way you can customize the long names. -`typer.Option()` receives as a first function argument the default value, e.g. `None`, and all the next *positional* values are to define the *CLI option* name(s). +YOu can pass *positional* arguments to `typer.Option()` to define the *CLI option* name(s). !!! tip Remember the *positional* function arguments are those that don't have a keyword. @@ -183,9 +196,20 @@ You can overwrite the *CLI option* name to use as in the previous example, but y For example, extending the previous example, let's add a *CLI option* short name `-n`: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/name/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/name/tutorial002.py!} + ``` Here we are overwriting the *CLI option* name that by default would be `--user-name`, and we are defining it to be `--name`. And we are also declaring a *CLI option* short name of `-n`. @@ -218,9 +242,20 @@ Hello Camila If you only declare a short name like `-n` then that will be the only *CLI option* name. And neither `--name` nor `--user-name` will be available. -```Python hl_lines="4" -{!../docs_src/options/name/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/name/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/name/tutorial003.py!} + ``` Check it: @@ -250,9 +285,20 @@ Hello Camila Continuing with the example above, as **Typer** allows you to declare a *CLI option* as having only a short name, if you want to have the default long name plus a short name, you have to declare both explicitly: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/name/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/name/tutorial004.py!} + ``` Check it: @@ -288,9 +334,20 @@ You can create multiple short names and use them together. You don't have to do anything special for it to work (apart from declaring those short versions): -```Python hl_lines="5 6" -{!../docs_src/options/name/tutorial005.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6-7" + {!> ../docs_src/options/name/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5-6" + {!> ../docs_src/options/name/tutorial005.py!} + ``` !!! tip Notice that, again, we are declaring the long and short version of the *CLI option* names. diff --git a/docs/tutorial/options/password.md b/docs/tutorial/options/password.md index 2d2840f4bf..96c2a51cb8 100644 --- a/docs/tutorial/options/password.md +++ b/docs/tutorial/options/password.md @@ -1,8 +1,19 @@ Apart from having a prompt, you can make a *CLI option* have a `confirmation_prompt=True`: -```Python hl_lines="5" -{!../docs_src/options/password/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/options/password/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5" + {!> ../docs_src/options/password/tutorial001.py!} + ``` And the CLI program will ask for confirmation: @@ -30,9 +41,20 @@ You can achieve the same using `hide_input=True`. And if you combine it with `confirmation_prompt=True` you can easily receive a password with double confirmation: -```Python hl_lines="6 7 8" -{!../docs_src/options/password/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../docs_src/options/password/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6-8" + {!> ../docs_src/options/password/tutorial002.py!} + ``` Check it: diff --git a/docs/tutorial/options/prompt.md b/docs/tutorial/options/prompt.md index 92398dab7e..f77e4db491 100644 --- a/docs/tutorial/options/prompt.md +++ b/docs/tutorial/options/prompt.md @@ -1,8 +1,19 @@ It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`: -```Python hl_lines="4" -{!../docs_src/options/prompt/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/prompt/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/prompt/tutorial001.py!} + ``` And then your program will ask the user for it in the terminal: @@ -24,9 +35,20 @@ Hello Camila Gutiérrez You can also set a custom prompt, passing the string that you want to use instead of just `True`: -```Python hl_lines="5" -{!../docs_src/options/prompt/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/options/prompt/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5" + {!> ../docs_src/options/prompt/tutorial002.py!} + ``` And then your program will ask for it using with your custom prompt: @@ -52,9 +74,20 @@ You can do it passing the parameter `confirmation_prompt=True`. Let's say it's a CLI app to delete a project: -```Python hl_lines="4" -{!../docs_src/options/prompt/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6" + {!> ../docs_src/options/prompt/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/prompt/tutorial003.py!} + ``` And it will prompt the user for a value and then for the confirmation: diff --git a/docs/tutorial/options/version.md b/docs/tutorial/options/version.md index 40026692d9..51a3415d3a 100644 --- a/docs/tutorial/options/version.md +++ b/docs/tutorial/options/version.md @@ -6,9 +6,20 @@ It would show the version of your CLI program and then it would terminate it. Ev Let's see a first version of how it could look like: -```Python hl_lines="8-11 16-18" -{!../docs_src/options/version/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="9-12 17-19" + {!> ../docs_src/options/version/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8-11 16-18" + {!> ../docs_src/options/version/tutorial001.py!} + ``` !!! tip Notice that we don't have to get the `typer.Context` and check for `ctx.resilient_parsing` for completion to work, because we only print and modify the program when `--version` is passed, otherwise, nothing is printed or changed from the callback. @@ -57,9 +68,20 @@ Awesome CLI Version: 0.1.0 But now let's say that the `--name` *CLI option* that we declared before `--version` is required, and it has a callback that could exit the program: -```Python hl_lines="14-16 21-23" -{!../docs_src/options/version/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="15-17 22-24" + {!> ../docs_src/options/version/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="14-16 21-23" + {!> ../docs_src/options/version/tutorial002.py!} + ``` Then our CLI program could not work as expected in some cases as it is *right now*, because if we use `--version` after `--name` then the callback for `--name` will be processed before and we can get its error: @@ -89,9 +111,20 @@ For those cases, we can mark a *CLI parameter* (a *CLI option* or *CLI argument* That will tell **Typer** (actually Click) that it should process this *CLI parameter* before the others: -```Python hl_lines="22-24" -{!../docs_src/options/version/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="23-26" + {!> ../docs_src/options/version/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="22-24" + {!> ../docs_src/options/version/tutorial003.py!} + ``` Check it: diff --git a/docs/tutorial/parameter-types/bool.md b/docs/tutorial/parameter-types/bool.md index 1f493eca8d..0aca205e7a 100644 --- a/docs/tutorial/parameter-types/bool.md +++ b/docs/tutorial/parameter-types/bool.md @@ -8,9 +8,20 @@ Let's say that we want a `--force` *CLI option* only, we want to discard `--no-f We can do that by specifying the exact name we want: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/bool/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/bool/tutorial001.py!} + ``` Now there's only a `--force` *CLI option*: @@ -60,9 +71,20 @@ We might want to instead have `--accept` and `--reject`. We can do that by passing a single `str` with the 2 names for the `bool` *CLI option* separated by `/`: -```Python hl_lines="6" -{!../docs_src/parameter_types/bool/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../docs_src/parameter_types/bool/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="6" + {!> ../docs_src/parameter_types/bool/tutorial002.py!} + ``` Check it: @@ -105,9 +127,20 @@ The same way, you can declare short versions of the names for these *CLI options For example, let's say we want `-f` for `--force` and `-F` for `--no-force`: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/bool/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/bool/tutorial003.py!} + ``` Check it: @@ -145,9 +178,20 @@ If you want to (although it might not be a good idea), you can declare only *CLI To do that, use a space and a single `/` and pass the negative name after: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/bool/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/bool/tutorial004.py!} + ``` !!! tip Have in mind that it's a string with a preceding space and then a `/`. diff --git a/docs/tutorial/parameter-types/datetime.md b/docs/tutorial/parameter-types/datetime.md index a5a8b5183b..ddc80b656a 100644 --- a/docs/tutorial/parameter-types/datetime.md +++ b/docs/tutorial/parameter-types/datetime.md @@ -60,9 +60,20 @@ For example, let's imagine that you want to accept an ISO formatted datetime, bu ...It's a crazy example, but let's say you also needed that strange format: -```Python hl_lines="8" -{!../docs_src/parameter_types/datetime/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../docs_src/parameter_types/datetime/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8" + {!> ../docs_src/parameter_types/datetime/tutorial002.py!} + ``` !!! tip Notice the last string in `formats`: `"%m/%d/%Y"`. diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 6af834bf54..b6ba27873a 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -45,9 +45,20 @@ Error: Invalid value for '--network': invalid choice: capsule. (choose from simp You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter: -```Python hl_lines="13" -{!../docs_src/parameter_types/enum/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../docs_src/parameter_types/enum/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="13" + {!> ../docs_src/parameter_types/enum/tutorial002.py!} + ``` And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix: diff --git a/docs/tutorial/parameter-types/file.md b/docs/tutorial/parameter-types/file.md index 0afe92dbfc..d7dadeba42 100644 --- a/docs/tutorial/parameter-types/file.md +++ b/docs/tutorial/parameter-types/file.md @@ -40,11 +40,22 @@ instead of having `bytes`, e.g.: content = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o" ``` -You will get all the correct editor support, attributes, methods, etc for the file-like object: +You will get all the correct editor support, attributes, methods, etc for the file-like object:` -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/file/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/file/tutorial001.py!} + ``` Check it: @@ -71,9 +82,20 @@ Config line: some more settings For writing text, you can use `typer.FileTextWrite`: -```Python hl_lines="4 5" -{!../docs_src/parameter_types/file/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5-6" + {!> ../docs_src/parameter_types/file/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4-5" + {!> ../docs_src/parameter_types/file/tutorial002.py!} + ``` This would be for writing human text, like: @@ -114,9 +136,20 @@ You will receive `bytes` from it. It's useful for reading binary files like images: -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/file/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/file/tutorial003.py!} + ``` Check it: @@ -145,9 +178,20 @@ Have in mind that you have to pass `bytes` to its `.write()` method, not `str`. If you have a `str`, you have to encode it first to get `bytes`. -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial004.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/file/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/file/tutorial004.py!} + ```
@@ -197,9 +241,20 @@ You can override the `mode` from the defaults above. For example, you could use `mode="a"` to write "appending" to the same file: -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial005.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/file/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/file/tutorial005.py!} + ``` !!! tip As you are manually setting `mode="a"`, you can use `typer.FileText` or `typer.FileTextWrite`, both will work. diff --git a/docs/tutorial/parameter-types/number.md b/docs/tutorial/parameter-types/number.md index d41322d6fe..d22ac3f45a 100644 --- a/docs/tutorial/parameter-types/number.md +++ b/docs/tutorial/parameter-types/number.md @@ -1,8 +1,19 @@ You can define numeric validations with `max` and `min` values for `int` and `float` *CLI parameters*: -```Python hl_lines="5 6 7" -{!../docs_src/parameter_types/number/tutorial001.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6-8" + {!> ../docs_src/parameter_types/number/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5-7" + {!> ../docs_src/parameter_types/number/tutorial001.py!} + ``` *CLI arguments* and *CLI options* can both use these validations. @@ -75,9 +86,20 @@ You might want to, instead of showing an error, use the closest minimum or maxim You can do it with the `clamp` parameter: -```Python hl_lines="5 6 7" -{!../docs_src/parameter_types/number/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="6-8" + {!> ../docs_src/parameter_types/number/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5-7" + {!> ../docs_src/parameter_types/number/tutorial002.py!} + ``` And then, when you pass data that is out of the valid range, it will be "clamped", the closest valid value will be used: @@ -106,9 +128,20 @@ ID is 5 You can make a *CLI option* work as a counter with the `counter` parameter: -```Python hl_lines="4" -{!../docs_src/parameter_types/number/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/number/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/number/tutorial003.py!} + ``` It means that the *CLI option* will be like a boolean flag, e.g. `--verbose`. diff --git a/docs/tutorial/parameter-types/path.md b/docs/tutorial/parameter-types/path.md index 887ab37b80..44ca292c6a 100644 --- a/docs/tutorial/parameter-types/path.md +++ b/docs/tutorial/parameter-types/path.md @@ -2,9 +2,20 @@ You can declare a *CLI parameter* to be a standard Python ../docs_src/parameter_types/path/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="1 7" + {!> ../docs_src/parameter_types/path/tutorial001.py!} + ``` And again, as you receive a standard Python `Path` object the same as the type annotation, your editor will give you autocompletion for all its attributes and methods. @@ -59,9 +70,20 @@ You can perform several validations for `Path` *CLI parameters*: For example: -```Python hl_lines="9 10 11 12 13 14" -{!../docs_src/parameter_types/path/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="11-16" + {!> ../docs_src/parameter_types/path/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9-14" + {!> ../docs_src/parameter_types/path/tutorial002.py!} + ``` Check it: diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md index c8b2b76121..6c14ab22d9 100644 --- a/docs/tutorial/testing.md +++ b/docs/tutorial/testing.md @@ -103,9 +103,20 @@ test_main.py . If you have a CLI with prompts, like: -```Python hl_lines="7" -{!../docs_src/testing/app02/main.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../docs_src/testing/app02_an/main.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../docs_src/testing/app02/main.py!} + ``` That you would use like: From 8c119a10ac7ce57fb91e382127c82a2511065611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 05:48:00 +0200 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20introducing?= =?UTF-8?q?=20Annotated=20and=20previous=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/arguments/optional.md | 119 +++++++++++++++++++++++----- docs/tutorial/options/help.md | 67 +++++++++++----- docs/tutorial/options/required.md | 33 ++++++-- 3 files changed, 170 insertions(+), 49 deletions(-) diff --git a/docs/tutorial/arguments/optional.md b/docs/tutorial/arguments/optional.md index 4821ff923e..6b21cdc3fa 100644 --- a/docs/tutorial/arguments/optional.md +++ b/docs/tutorial/arguments/optional.md @@ -29,7 +29,7 @@ __init__.py test_tutorial
-### An alternative *CLI argument* declaration +## An alternative *CLI argument* declaration In the [First Steps](../first-steps.md#add-a-cli-argument){.internal-link target=_blank} you saw how to add a *CLI argument*: @@ -39,34 +39,39 @@ In the [First Steps](../first-steps.md#add-a-cli-argument){.internal-link target Now let's see an alternative way to create the same *CLI argument*: -```Python hl_lines="4" -{!../docs_src/arguments/optional/tutorial001.py!} + +```Python hl_lines="5" +{!> ../docs_src/arguments/optional/tutorial001_an.py!} ``` +!!! info + Typer added support for `Annotated` (and started recommending it) in version 0.9.0. + + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you Upgrade the Typer version to at least 0.9.0 before using `Annotated`. + Before, you had this function parameter: ```Python name: str ``` -And because `name` didn't have any default value it would be a **required parameter** for the Python function, in Python terms. - -**Typer** does the same and makes it a **required** *CLI argument*. - -And then we changed it to: +And now we wrap it with `Annotated`: ```Python -name: str = typer.Argument(...) +name: Annotated[str] ``` -But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms). +Both of these versions mean the same thing, `Annotated` is part of standard Python and is there for this. -As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, the first parameter to `typer.Argument()` serves the same purpose of defining that default value, or making it required. +But the second version using `Annotated` allows us to pass additional metadata that can be used by **Typer**: -To make it *required*, we pass `...` as the first function argument passed to `typer.Argument(...)`. +```Python +name: Annotated[str, typer.Argument()] +``` -!!! info - If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". +Now we are being explicit that `name` is a *CLI argument*. It's still a `str` and it's still required (it doesn't have a default value). All we did there achieves the same thing as before, a **required** *CLI argument*: @@ -85,28 +90,38 @@ Error: Missing argument 'NAME'. It's still not very useful, but it works correctly. -And being able to declare a **required** *CLI argument* using `name: str = typer.Argument(...)` that works exactly the same as `name: str` will come handy later. +And being able to declare a **required** *CLI argument* using + +```Python +name: Annoated[str, typer.Argument()] +``` + +...that works exactly the same as + +```Python +name: str +``` -### Make an optional *CLI argument* +...will come handy later. + +## Make an optional *CLI argument* Now, finally what we came for, an optional *CLI argument*. To make a *CLI argument* optional, use `typer.Argument()` and pass a different "default" as the first parameter to `typer.Argument()`, for example `None`: -```Python hl_lines="6" -{!../docs_src/arguments/optional/tutorial002.py!} +```Python hl_lines="7" +{!../docs_src/arguments/optional/tutorial002_an.py!} ``` Now we have: ```Python -name: Optional[str] = typer.Argument(None) +name: Annotated[Optional[str], typer.Argument()] = None ``` Because we are using `typer.Argument()` **Typer** will know that this is a *CLI argument* (no matter if *required* or *optional*). -And because the first parameter passed to `typer.Argument(None)` (the new "default" value) is `None`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `None`. - !!! tip By using `Optional` your editor will be able to know that the value *could* be `None`, and will be able to warn you if you do something assuming it is a `str` that would break if it was `None`. @@ -156,3 +171,65 @@ Hello Camila !!! tip Notice that "`Camila`" here is an optional *CLI argument*, not a *CLI option*, because we didn't use something like "`--name Camila`", we just passed "`Camila`" directly to the program. + +## Alternative (old) `typer.Argument()` as the default value + +**Typer** also supports another older alternative syntax for declaring *CLI arguments* with additional metadata. + +Instead of using `Annotated`, you can use `typer.Argument()` as the default value: + +```Python hl_lines="4" +{!> ../docs_src/arguments/optional/tutorial001.py!} +``` + +!!! tip + Prefer to use the `Annotated` version if possible. + +Before, because `name` didn't have any default value it would be a **required parameter** for the Python function, in Python terms. + +When using `typer.Argument()` as the default value **Typer** does the same and makes it a **required** *CLI argument*. + +We changed it to: + +```Python +name: str = typer.Argument() +``` + +But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms). + +As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, `typer.Argument()` receives a first parameter `default` that serves the same purpose of defining that default value, or making it required. + +Not passing any value to the `default` argument is the same as marking it as required. But you can also explicitly mark it as *required* by passing `...` as the `default` argument, passed to `typer.Argument(default=...)`. + +```Python +name: str = typer.Argument(default=...) +``` + +!!! info + If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". + +```Python hl_lines="4" +{!> ../docs_src/arguments/optional/tutorial003.py!} +``` + +And the same way, you can make it optional by passing a different `default` value, for example `None`: + +```Python hl_lines="6" +{!> ../docs_src/arguments/optional/tutorial002.py!} +``` + +Because the first parameter passed to `typer.Argument(default=None)` (the new "default" value) is `None`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `None`. + +The `default` argument is the first one, so it's possible that you see code that passes the value without explicitly using `default=`, like: + +```Python +name: str = typer.Argument(...) +``` + +...or like: + +```Python +name: str = typer.Argument(None) +``` + +...but again, try to use `Annotated` if possible, that way your code in terms of Python will mean the same thing as with **Typer** and you won't have to remember any of these details. diff --git a/docs/tutorial/options/help.md b/docs/tutorial/options/help.md index d139d1f13b..b1ba0e1377 100644 --- a/docs/tutorial/options/help.md +++ b/docs/tutorial/options/help.md @@ -2,36 +2,37 @@ You already saw how to add a help text for *CLI arguments* with the `help` param Let's now do the same for *CLI options*: -```Python hl_lines="6 7" -{!../docs_src/options/help/tutorial001.py!} -``` +=== "Python 3.6+" -We are replacing the default values we had before with `typer.Option()`. + ```Python hl_lines="7-8" + {!> ../docs_src/options/help/tutorial001_an.py!} + ``` -As we no longer have a default value there, the first parameter to `typer.Option()` serves the same purpose of defining that default value. +=== "Python 3.6+ non-Annotated" -So, if we had: + !!! tip + Prefer to use the `Annotated` version if possible. -```Python -lastname: str = "" -``` + ```Python hl_lines="6-7" + {!> ../docs_src/options/help/tutorial001.py!} + ``` + +The same way as with `typer.Argument()`, we can put `typer.Option()` inside of `Annotated`. -now we write: +We can then pass the `help` keyword parameter: ```Python -lastname: str = typer.Option("") +lastname: Annotated[str, typer.Option(help="this option does this and that")] = "" ``` -And both forms achieve the same: a *CLI option* with a default value of an empty string (`""`). +...to create the help for that *CLI option*. -And then we can pass the `help` keyword parameter: +The same way as with `typer.Argument()`, **Typer** also supports the old style using the function parameter default value: ```Python -lastname: str = typer.Option("", help="this option does this and that") +lastname: str = typer.Option(default="", help="this option does this and that") ``` -to create the help for that *CLI option*. - Copy that example from above to a file `main.py`. Test it: @@ -68,9 +69,20 @@ The same as with *CLI arguments*, you can put the help for some *CLI options* in If you have installed Rich as described in the docs for [Printing and Colors](../printing.md){.internal-link target=_blank}, you can set the `rich_help_panel` parameter to the name of the panel you want for each *CLI option*: -```Python hl_lines="8 11" -{!../docs_src/options/help/tutorial002.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="11 17" + {!> ../docs_src/options/help/tutorial002_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="8 11" + {!> ../docs_src/options/help/tutorial002.py!} + ``` Now, when you check the `--help` option, you will see a default panel named "`Options`" for the *CLI options* that don't have a custom `rich_help_panel`. @@ -121,9 +133,20 @@ If you are in a hurry you can jump there, but otherwise, it would be better to c You can tell Typer to not show the default value in the help text with `show_default=False`: -```Python hl_lines="4" -{!../docs_src/options/help/tutorial003.py!} -``` +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/help/tutorial003_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/options/help/tutorial003.py!} + ``` And it will no longer show the default value in the help text: diff --git a/docs/tutorial/options/required.md b/docs/tutorial/options/required.md index c81f3ec173..ff37da10ea 100644 --- a/docs/tutorial/options/required.md +++ b/docs/tutorial/options/required.md @@ -7,18 +7,39 @@ Well, that's how they work *by default*, and that's the convention in many CLI p But if you really want, you can change that. -To make a *CLI option* required, pass `...` to `typer.Option()`. +To make a *CLI option* required, you can put `typer.Option()` inside of `Annotated` and leave the parameter without a default value. + +Let's make `--lastname` a required *CLI option*: + +=== "Python 3.6+" + + ```Python hl_lines="5" + {!> ../docs_src/options/required/tutorial001_an.py!} + ``` + +The same way as with `typer.Argument()`, the old style of using the function parameter default value is also supported, in that case you would just not pass anything to the `default` parameter. + +=== "Python 3.6+ non-Annotated" + + ```Python hl_lines="4" + {!> ../docs_src/options/required/tutorial001.py!} + ``` + +Or you can explictily pass `...` to `typer.Option(default=...)`: + +=== "Python 3.6+ non-Annotated" + + ```Python hl_lines="4" + {!> ../docs_src/options/required/tutorial002.py!} + ``` !!! info If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required. -Let's make `--lastname` a required *CLI option*: - -```Python hl_lines="4" -{!../docs_src/options/required/tutorial001.py!} -``` +!!! tip + Again, prefer to use the `Annotated` version if possible. That way your code will mean the same in standard Python and in **Typer**. And test it: From fc721a752dac2dcaddee1b052f9eb62e42a2ee04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 05:48:57 +0200 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=94=A7=20Add=20commented=20out=20Mk?= =?UTF-8?q?Docs=20config=20for=20highlighting=20docs=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 496d4a2792..99127f7406 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -103,6 +103,9 @@ markdown_extensions: alternate_style: true - mdx_include: base_path: docs + # Enable while writing docs to simplify highlighting + # - pymdownx.highlight: + # linenums: true extra: analytics: From ba312c597b373124f42cbfcbd1bf62f06b70d266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 2 May 2023 06:12:53 +0200 Subject: [PATCH 11/11] =?UTF-8?q?=E2=9C=85=20Fix=20tests=20for=20Click=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_options/test_name/test_tutorial001.py | 6 +++++- .../test_options/test_name/test_tutorial001_an.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_options/test_name/test_tutorial001.py b/tests/test_tutorial/test_options/test_name/test_tutorial001.py index da14c934c1..fd0f5d59c6 100644 --- a/tests/test_tutorial/test_options/test_name/test_tutorial001.py +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001.py @@ -29,7 +29,11 @@ def test_call(): def test_call_no_args(): result = runner.invoke(app, ["--name"]) assert result.exit_code != 0 - assert "Option '--name' requires an argument" in result.output + # TODO: when deprecating Click 7, remove second option + assert ( + "Option '--name' requires an argument" in result.output + or "--name option requires an argument" in result.output + ) def test_script(): 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 index c5c57b2ee8..5bf1aa7d9c 100644 --- a/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py +++ b/tests/test_tutorial/test_options/test_name/test_tutorial001_an.py @@ -29,7 +29,11 @@ def test_call(): def test_call_no_args(): result = runner.invoke(app, ["--name"]) assert result.exit_code != 0 - assert "Option '--name' requires an argument" in result.output + # TODO: when deprecating Click 7, remove second option + assert ( + "Option '--name' requires an argument" in result.output + or "--name option requires an argument" in result.output + ) def test_script():