From e5d1236815391aa663fb2b738489b9d1edadd812 Mon Sep 17 00:00:00 2001 From: solesensei Date: Sat, 12 Oct 2024 12:21:15 +0200 Subject: [PATCH 1/8] Add test for generate_list_converter --- docs_src/options/callback/tutorial005.py | 27 +++++++++ docs_src/options/callback/tutorial005_an.py | 30 ++++++++++ .../test_callback/test_tutorial005.py | 56 +++++++++++++++++++ .../test_callback/test_tutorial005_an.py | 56 +++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 docs_src/options/callback/tutorial005.py create mode 100644 docs_src/options/callback/tutorial005_an.py create mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial005.py create mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py diff --git a/docs_src/options/callback/tutorial005.py b/docs_src/options/callback/tutorial005.py new file mode 100644 index 0000000000..1435a0cc87 --- /dev/null +++ b/docs_src/options/callback/tutorial005.py @@ -0,0 +1,27 @@ +from typing import List, Optional + +import typer + + +def names_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if ctx.resilient_parsing: + return + print(f"Validating param: {param.name}") + if value is None: + return value + if "Camila" not in value: + raise typer.BadParameter("Camila must be in the list") + return value + + +def main( + names: Optional[List[str]] = typer.Option(None, "--name", callback=names_callback), +): + if names is None: + print("No names provided") + else: + print("Hello {}".format(", ".join(names))) + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/callback/tutorial005_an.py b/docs_src/options/callback/tutorial005_an.py new file mode 100644 index 0000000000..c5d0358edd --- /dev/null +++ b/docs_src/options/callback/tutorial005_an.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +import typer +from typing_extensions import Annotated + + +def names_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if ctx.resilient_parsing: + return + print(f"Validating param: {param.name}") + if value is None: + return value + if "Camila" not in value: + raise typer.BadParameter("Camila must be in the list") + return value + + +def main( + names: Annotated[ + Optional[List[str]], typer.Option("--name", callback=names_callback) + ] = None, +): + if names is None: + print("No names provided") + else: + print("Hello {}".format(", ".join(names))) + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial005.py b/tests/test_tutorial/test_options/test_callback/test_tutorial005.py new file mode 100644 index 0000000000..b104a6661f --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial005.py @@ -0,0 +1,56 @@ +import os +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.callback import tutorial005 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Camila", "--name", "Victor"]) + assert result.exit_code == 0 + assert "Validating param: name" in result.output + assert "Hello Camila, Victor" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick", "--name", "Victor"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Camila must be in the list" in result.output + + +def test_3(): + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "No names provided" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + capture_output=True, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL005.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial005.py --", + "COMP_CWORD": "1", + }, + ) + assert "--name" in result.stdout diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py new file mode 100644 index 0000000000..3cd409ec88 --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py @@ -0,0 +1,56 @@ +import os +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.options.callback import tutorial005_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["--name", "Camila", "--name", "Victor"]) + assert result.exit_code == 0 + assert "Validating param: name" in result.output + assert "Hello Camila, Victor" in result.output + + +def test_2(): + result = runner.invoke(app, ["--name", "rick", "--name", "Victor"]) + assert result.exit_code != 0 + assert "Invalid value for '--name': Camila must be in the list" in result.output + + +def test_3(): + result = runner.invoke(app, []) + assert result.exit_code == 0 + assert "No names provided" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout + + +def test_completion(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + capture_output=True, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL005_AN.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial005_an.py --", + "COMP_CWORD": "1", + }, + ) + assert "--name" in result.stdout From 07350b58836712560ef203188daa6ffc77415e6c Mon Sep 17 00:00:00 2001 From: solesensei Date: Sat, 12 Oct 2024 12:25:16 +0200 Subject: [PATCH 2/8] Fix generate_list_convertor to handle empty value parameter --- typer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 55d865c780..7f763af0d3 100644 --- a/typer/main.py +++ b/typer/main.py @@ -651,7 +651,7 @@ def generate_list_convertor( convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any] ) -> Callable[[Sequence[Any]], Optional[List[Any]]]: def internal_convertor(value: Sequence[Any]) -> Optional[List[Any]]: - if default_value is None and len(value) == 0: + if default_value is None and (value is None or len(value) == 0): return None return [convertor(v) if convertor else v for v in value] From 6faec30599fab5e828411c09471579025cc68a03 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 26 Dec 2024 15:50:51 +0100 Subject: [PATCH 3/8] add minimal test and remove the more verbose ones --- docs_src/options/callback/tutorial005.py | 27 --------- docs_src/options/callback/tutorial005_an.py | 30 ---------- tests/test_others.py | 22 ++++++++ .../test_callback/test_tutorial005.py | 56 ------------------- .../test_callback/test_tutorial005_an.py | 56 ------------------- 5 files changed, 22 insertions(+), 169 deletions(-) delete mode 100644 docs_src/options/callback/tutorial005.py delete mode 100644 docs_src/options/callback/tutorial005_an.py delete mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial005.py delete mode 100644 tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py diff --git a/docs_src/options/callback/tutorial005.py b/docs_src/options/callback/tutorial005.py deleted file mode 100644 index 1435a0cc87..0000000000 --- a/docs_src/options/callback/tutorial005.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List, Optional - -import typer - - -def names_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): - if ctx.resilient_parsing: - return - print(f"Validating param: {param.name}") - if value is None: - return value - if "Camila" not in value: - raise typer.BadParameter("Camila must be in the list") - return value - - -def main( - names: Optional[List[str]] = typer.Option(None, "--name", callback=names_callback), -): - if names is None: - print("No names provided") - else: - print("Hello {}".format(", ".join(names))) - - -if __name__ == "__main__": - typer.run(main) diff --git a/docs_src/options/callback/tutorial005_an.py b/docs_src/options/callback/tutorial005_an.py deleted file mode 100644 index c5d0358edd..0000000000 --- a/docs_src/options/callback/tutorial005_an.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import List, Optional - -import typer -from typing_extensions import Annotated - - -def names_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): - if ctx.resilient_parsing: - return - print(f"Validating param: {param.name}") - if value is None: - return value - if "Camila" not in value: - raise typer.BadParameter("Camila must be in the list") - return value - - -def main( - names: Annotated[ - Optional[List[str]], typer.Option("--name", callback=names_callback) - ] = None, -): - if names is None: - print("No names provided") - else: - print("Hello {}".format(", ".join(names))) - - -if __name__ == "__main__": - typer.run(main) diff --git a/tests/test_others.py b/tests/test_others.py index 1078e63d1f..09e6d2fc0c 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -144,6 +144,28 @@ def main(name: str = typer.Option(..., callback=name_callback)): assert "value is: Camila" in result.stdout +def test_callback_4_list_none(): + app = typer.Typer() + + def names_callback(ctx, param, values: typing.Optional[typing.List[str]]): + if values is None: + return values + return [value.upper() for value in values] + + @app.command() + def main(names: typing.Optional[typing.List[str]] = typer.Option(None, "--name", callback=names_callback)): + if names is None: + print("Hello World") + else: + print(f"Hello {', '.join(names)}") + + result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) + assert "Hello SIDESHOW, BOB" in result.stdout + + result = runner.invoke(app, []) + assert "Hello World" in result.stdout + + def test_completion_argument(): file_path = Path(__file__).parent / "assets/completion_argument.py" result = subprocess.run( diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial005.py b/tests/test_tutorial/test_options/test_callback/test_tutorial005.py deleted file mode 100644 index b104a6661f..0000000000 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial005.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import subprocess -import sys - -import typer -from typer.testing import CliRunner - -from docs_src.options.callback import tutorial005 as mod - -runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) - - -def test_1(): - result = runner.invoke(app, ["--name", "Camila", "--name", "Victor"]) - assert result.exit_code == 0 - assert "Validating param: name" in result.output - assert "Hello Camila, Victor" in result.output - - -def test_2(): - result = runner.invoke(app, ["--name", "rick", "--name", "Victor"]) - assert result.exit_code != 0 - assert "Invalid value for '--name': Camila must be in the list" in result.output - - -def test_3(): - result = runner.invoke(app, []) - assert result.exit_code == 0 - assert "No names provided" in result.output - - -def test_script(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout - - -def test_completion(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, " "], - capture_output=True, - encoding="utf-8", - env={ - **os.environ, - "_TUTORIAL005.PY_COMPLETE": "complete_bash", - "COMP_WORDS": "tutorial005.py --", - "COMP_CWORD": "1", - }, - ) - assert "--name" in result.stdout diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py deleted file mode 100644 index 3cd409ec88..0000000000 --- a/tests/test_tutorial/test_options/test_callback/test_tutorial005_an.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import subprocess -import sys - -import typer -from typer.testing import CliRunner - -from docs_src.options.callback import tutorial005_an as mod - -runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) - - -def test_1(): - result = runner.invoke(app, ["--name", "Camila", "--name", "Victor"]) - assert result.exit_code == 0 - assert "Validating param: name" in result.output - assert "Hello Camila, Victor" in result.output - - -def test_2(): - result = runner.invoke(app, ["--name", "rick", "--name", "Victor"]) - assert result.exit_code != 0 - assert "Invalid value for '--name': Camila must be in the list" in result.output - - -def test_3(): - result = runner.invoke(app, []) - assert result.exit_code == 0 - assert "No names provided" in result.output - - -def test_script(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout - - -def test_completion(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, " "], - capture_output=True, - encoding="utf-8", - env={ - **os.environ, - "_TUTORIAL005_AN.PY_COMPLETE": "complete_bash", - "COMP_WORDS": "tutorial005_an.py --", - "COMP_CWORD": "1", - }, - ) - assert "--name" in result.stdout From 92a56865fcd5461497af6f72c46f739e6e8217d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Dec 2024 14:51:03 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_others.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_others.py b/tests/test_others.py index 09e6d2fc0c..f5e409214a 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -153,7 +153,11 @@ def names_callback(ctx, param, values: typing.Optional[typing.List[str]]): return [value.upper() for value in values] @app.command() - def main(names: typing.Optional[typing.List[str]] = typer.Option(None, "--name", callback=names_callback)): + def main( + names: typing.Optional[typing.List[str]] = typer.Option( + None, "--name", callback=names_callback + ), + ): if names is None: print("Hello World") else: From 632695ac9294af8000754b7378ea62d4c0a06898 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 26 Dec 2024 15:58:18 +0100 Subject: [PATCH 5/8] fix type of list convertor --- typer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typer/main.py b/typer/main.py index 7f763af0d3..777fa4f99f 100644 --- a/typer/main.py +++ b/typer/main.py @@ -649,8 +649,8 @@ def convertor(value: Any) -> Any: def generate_list_convertor( convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any] -) -> Callable[[Sequence[Any]], Optional[List[Any]]]: - def internal_convertor(value: Sequence[Any]) -> Optional[List[Any]]: +) -> Callable[[Optional[Sequence[Any]]], Optional[List[Any]]]: + def internal_convertor(value: Optional[Sequence[Any]]) -> Optional[List[Any]]: if default_value is None and (value is None or len(value) == 0): return None return [convertor(v) if convertor else v for v in value] From 7d7ae7acf5f041707542fbfeca244ecbb3b7f6da Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 26 Dec 2024 16:28:17 +0100 Subject: [PATCH 6/8] fix list convertor properly --- typer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typer/main.py b/typer/main.py index 1617f67569..82a7c3a7bb 100644 --- a/typer/main.py +++ b/typer/main.py @@ -642,8 +642,8 @@ def generate_list_convertor( convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any] ) -> Callable[[Optional[Sequence[Any]]], Optional[List[Any]]]: def internal_convertor(value: Optional[Sequence[Any]]) -> Optional[List[Any]]: - if default_value is None and (value is None or len(value) == 0): - return None + if value is None or len(value) == 0: + return default_value return [convertor(v) if convertor else v for v in value] return internal_convertor From 62d4bcb3ff616e13966e8229175e629416205756 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 26 Dec 2024 16:38:46 +0100 Subject: [PATCH 7/8] add few more tests to ensure correct list generator functionality --- tests/test_others.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_others.py b/tests/test_others.py index f5e409214a..d5ce0728fe 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -170,6 +170,59 @@ def main( assert "Hello World" in result.stdout +def test_list_default_None(): + app = typer.Typer() + + @app.command() + def main(names: typing.Optional[typing.List[str]] = typer.Option(None, "--name")): + if not names: + print(f"Hello emptiness: {names}") + else: + print(f"Hello {', '.join(names)}") + + result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) + assert "Hello Sideshow, Bob" in result.stdout + + result = runner.invoke(app, []) + assert "Hello emptiness: None" in result.stdout + + +def test_list_default_empty(): + app = typer.Typer() + + @app.command() + def main(names: typing.Optional[typing.List[str]] = typer.Option([], "--name")): + if not names: + print(f"Hello emptiness: {names}") + else: + print(f"Hello {', '.join(names)}") + + result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) + assert "Hello Sideshow, Bob" in result.stdout + + result = runner.invoke(app, []) + assert "Hello emptiness: []" in result.stdout + + +def test_list_default_values(): + app = typer.Typer() + + @app.command() + def main( + names: typing.Optional[typing.List[str]] = typer.Option(["Darkness"], "--name"), + ): + if not names: + print(f"Hello emptiness: {names}") + else: + print(f"Hello {', '.join(names)}") + + result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) + assert "Hello Sideshow, Bob" in result.stdout + + result = runner.invoke(app, []) + assert "Hello Darkness" in result.stdout + + def test_completion_argument(): file_path = Path(__file__).parent / "assets/completion_argument.py" result = subprocess.run( From 3ea72875d298a0f00175a9a6f499aa1096fbc763 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 26 Dec 2024 16:41:02 +0100 Subject: [PATCH 8/8] remove redundant tests (they succeed on master) --- tests/test_others.py | 53 -------------------------------------------- 1 file changed, 53 deletions(-) diff --git a/tests/test_others.py b/tests/test_others.py index d5ce0728fe..f5e409214a 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -170,59 +170,6 @@ def main( assert "Hello World" in result.stdout -def test_list_default_None(): - app = typer.Typer() - - @app.command() - def main(names: typing.Optional[typing.List[str]] = typer.Option(None, "--name")): - if not names: - print(f"Hello emptiness: {names}") - else: - print(f"Hello {', '.join(names)}") - - result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) - assert "Hello Sideshow, Bob" in result.stdout - - result = runner.invoke(app, []) - assert "Hello emptiness: None" in result.stdout - - -def test_list_default_empty(): - app = typer.Typer() - - @app.command() - def main(names: typing.Optional[typing.List[str]] = typer.Option([], "--name")): - if not names: - print(f"Hello emptiness: {names}") - else: - print(f"Hello {', '.join(names)}") - - result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) - assert "Hello Sideshow, Bob" in result.stdout - - result = runner.invoke(app, []) - assert "Hello emptiness: []" in result.stdout - - -def test_list_default_values(): - app = typer.Typer() - - @app.command() - def main( - names: typing.Optional[typing.List[str]] = typer.Option(["Darkness"], "--name"), - ): - if not names: - print(f"Hello emptiness: {names}") - else: - print(f"Hello {', '.join(names)}") - - result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) - assert "Hello Sideshow, Bob" in result.stdout - - result = runner.invoke(app, []) - assert "Hello Darkness" in result.stdout - - def test_completion_argument(): file_path = Path(__file__).parent / "assets/completion_argument.py" result = subprocess.run(