From e85dd764429e9946f8a28e8ffc12345b87ede256 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Wed, 21 Sep 2022 07:29:13 -0600 Subject: [PATCH 01/21] =?UTF-8?q?=E2=9C=A8=20Support=20multiple=20options?= =?UTF-8?q?=20with=20multiple=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multiple-options-with-multiple-values.md | 39 +++++++++++ .../tutorial001.py | 19 ++++++ mkdocs.yml | 1 + .../__init__.py | 0 .../test_tutorial001.py | 68 +++++++++++++++++++ typer/main.py | 20 ++++-- 6 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 docs/tutorial/multiple-values/multiple-options-with-multiple-values.md create mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py create mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/__init__.py create mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md new file mode 100644 index 0000000000..7fd461bfd5 --- /dev/null +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -0,0 +1,39 @@ +For when simple doesn't quite cut it, you may also declare a *CLI option* that takes several values of different types and can be used multiple times. + +The same rules apply for the number of values for each use and their types; the types may be anything you want, but there must be a fixed number of values. + +For this, we use the standard Python `typing.List` and declare its internal type to be a `typing.Tuple`: + +```Python hl_lines="1 6" +{!../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py!} +``` + +Just as before, the types internal to the `Tuple` define the type of each value in the tuple. + +## Check it + +
+ +```console +$ python main.py + +Congratulations, you're debt-free! + +// Now let's borrow some money. +$ python main.py --borrow 2.5 Mark + +Borrowed 2.50 from Mark + +Total borrowed: 2.50 + +// And, of course, it may be used multiple times +$ python main.py --borrow 2.5 Mark --borrow 5.25 Sean --borrow 1.75 Wade + +Borrowed 2.50 from Mark +Borrowed 5.25 from Sean +Borrowed 1.75 from Wade + +Total borrowed: 9.50 +``` + +
diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py new file mode 100644 index 0000000000..fc011a679d --- /dev/null +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py @@ -0,0 +1,19 @@ +from typing import List, Tuple + +import typer + + +def main(borrow: List[Tuple[float, str]] = typer.Option([])): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/mkdocs.yml b/mkdocs.yml index a0b0f7cc48..9ab757a5a1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Multiple Values Intro: tutorial/multiple-values/index.md - Multiple CLI Options: tutorial/multiple-values/multiple-options.md - CLI Options with Multiple Values: tutorial/multiple-values/options-with-multiple-values.md + - Multiple CLI Options with Multiple Values: tutorial/multiple-values/multiple-options-with-multiple-values.md - CLI Arguments with Multiple Values: tutorial/multiple-values/arguments-with-multiple-values.md - Ask with Prompt: tutorial/prompt.md - Progress Bar: tutorial/progressbar.md diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/__init__.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py new file mode 100644 index 0000000000..a307e1a42a --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -0,0 +1,68 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.multiple_options_with_multiple_values import ( + tutorial001 as mod, +) + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Congratulations, you're debt-free!" in result.output + + +def test_borrow_1(): + result = runner.invoke(app, ["--borrow", "2.5", "Mark"]) + assert result.exit_code == 0 + assert "Borrowed 2.50 from Mark" in result.output + assert "Total borrowed: 2.50" in result.output + + +def test_borrow_many(): + result = runner.invoke( + app, + [ + "--borrow", + "2.5", + "Mark", + "--borrow", + "5.25", + "Sean", + "--borrow", + "1.75", + "Wade", + ], + ) + assert result.exit_code == 0 + assert "Borrowed 2.50 from Mark" in result.output + assert "Borrowed 5.25 from Sean" in result.output + assert "Borrowed 1.75 from Wade" in result.output + assert "Total borrowed: 9.50" in result.output + + +def test_invalid_borrow(): + result = runner.invoke(app, ["--borrow", "2.5"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Option '--borrow' requires 2 arguments" in result.output + or "--borrow option requires 2 arguments" in result.output + ) + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/main.py b/typer/main.py index 8aa1cf9b30..4d3009e5cb 100644 --- a/typer/main.py +++ b/typer/main.py @@ -825,10 +825,22 @@ def get_click_param( # Handle Tuples and Lists if lenient_issubclass(origin, List): main_type = main_type.__args__[0] - assert not getattr( - main_type, "__origin__", None + list_origin = getattr(main_type, "__origin__", None) + is_tuple = lenient_issubclass(list_origin, Tuple) + assert ( + is_tuple or not list_origin ), "List types with complex sub-types are not currently supported" is_list = True + if is_tuple: + types = [] + for type_ in main_type.__args__: + assert not getattr( + type_, "__origin__", None + ), "List[Tuple] types with complex Tuple sub-types are not currently supported" + types.append( + get_click_type(annotation=type_, parameter_info=parameter_info) + ) + parameter_type = tuple(types) elif lenient_issubclass(origin, Tuple): # type: ignore types = [] for type_ in main_type.__args__: @@ -845,10 +857,10 @@ def get_click_param( annotation=main_type, parameter_info=parameter_info ) convertor = determine_type_convertor(main_type) - if is_list: - convertor = generate_list_convertor(convertor) if is_tuple: convertor = generate_tuple_convertor(main_type.__args__) + if is_list: + convertor = generate_list_convertor(convertor) if isinstance(parameter_info, OptionInfo): if main_type is bool and not (parameter_info.is_flag is False): is_flag = True From cbd07beca0bcfe3ad5e51b13cdcf8fadc2341caa Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 20 Nov 2022 20:54:47 -0700 Subject: [PATCH 02/21] :rotating_light: Fix linting error --- typer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 034faa49f7..d76ac1a939 100644 --- a/typer/main.py +++ b/typer/main.py @@ -826,7 +826,7 @@ def get_click_param( if lenient_issubclass(origin, List): main_type = main_type.__args__[0] list_origin = getattr(main_type, "__origin__", None) - is_tuple = lenient_issubclass(list_origin, Tuple) + is_tuple = lenient_issubclass(list_origin, tuple) assert ( is_tuple or not list_origin ), "List types with complex sub-types are not currently supported" From f797a0918f840636b01336ef6bac85405ffcc5ec Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Fri, 16 Aug 2024 17:16:21 -0600 Subject: [PATCH 03/21] Fix tutorial docs, source, and tests to match current style --- .../multiple-options-with-multiple-values.md | 20 +++++- .../tutorial001_an.py | 20 ++++++ .../test_tutorial001.py | 3 +- .../test_tutorial001_an.py | 67 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index 7fd461bfd5..a2333817f1 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -4,10 +4,28 @@ The same rules apply for the number of values for each use and their types; the For this, we use the standard Python `typing.List` and declare its internal type to be a `typing.Tuple`: +//// tab | Python 3.7+ + +```Python hl_lines="1 7" +{!> ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible + +/// + ```Python hl_lines="1 6" -{!../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py!} +{!> ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py!} ``` +//// + Just as before, the types internal to the `Tuple` define the type of each value in the tuple. ## Check it diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py new file mode 100644 index 0000000000..75b5361eb5 --- /dev/null +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py @@ -0,0 +1,20 @@ +from typing import List, Tuple + +import typer +from typing_extensions import Annotated + + +def main(borrow: Annotated[List[Tuple[float, str]], typer.Option()] = []): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index a307e1a42a..fb855aa877 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -61,8 +61,7 @@ def test_invalid_borrow(): def test_script(): result = subprocess.run( ["coverage", "run", mod.__file__, "--help"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, encoding="utf-8", ) assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py new file mode 100644 index 0000000000..0954b58e26 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py @@ -0,0 +1,67 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.multiple_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 "Congratulations, you're debt-free!" in result.output + + +def test_borrow_1(): + result = runner.invoke(app, ["--borrow", "2.5", "Mark"]) + assert result.exit_code == 0 + assert "Borrowed 2.50 from Mark" in result.output + assert "Total borrowed: 2.50" in result.output + + +def test_borrow_many(): + result = runner.invoke( + app, + [ + "--borrow", + "2.5", + "Mark", + "--borrow", + "5.25", + "Sean", + "--borrow", + "1.75", + "Wade", + ], + ) + assert result.exit_code == 0 + assert "Borrowed 2.50 from Mark" in result.output + assert "Borrowed 5.25 from Sean" in result.output + assert "Borrowed 1.75 from Wade" in result.output + assert "Total borrowed: 9.50" in result.output + + +def test_invalid_borrow(): + result = runner.invoke(app, ["--borrow", "2.5"]) + assert result.exit_code != 0 + # TODO: when deprecating Click 7, remove second option + + assert ( + "Option '--borrow' requires 2 arguments" in result.output + or "--borrow option requires 2 arguments" in result.output + ) + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout From c191eee1e1d1aeb04fb4231a2250a8f869914628 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Fri, 16 Aug 2024 17:23:42 -0600 Subject: [PATCH 04/21] Add to per-file ignores, to match existing tutorial source --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b64b18d911..9a472a7e51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,7 @@ ignore = [ # Default mutable data structure "docs_src/options_autocompletion/tutorial006_an.py" = ["B006"] "docs_src/multiple_values/multiple_options/tutorial002_an.py" = ["B006"] +"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py" = ["B006"] "docs_src/options_autocompletion/tutorial007_an.py" = ["B006"] "docs_src/options_autocompletion/tutorial008_an.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an.py" = ["B006"] From e58468527ef11fff3166775dd753f66b932c624f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 18:51:28 +0000 Subject: [PATCH 05/21] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/typer/main.py b/typer/main.py index ada0af2da6..c57e72f1d1 100644 --- a/typer/main.py +++ b/typer/main.py @@ -846,16 +846,16 @@ def get_click_param( main_type = get_args(main_type)[0] list_origin = get_origin(main_type) is_tuple = lenient_issubclass(list_origin, tuple) - assert ( - is_tuple or not list_origin - ), "List types with complex sub-types are not currently supported" + assert is_tuple or not list_origin, ( + "List types with complex sub-types are not currently supported" + ) is_list = True if is_tuple: types = [] for type_ in main_type.__args__: - assert not getattr( - type_, "__origin__", None - ), "List[Tuple] types with complex Tuple sub-types are not currently supported" + assert not getattr(type_, "__origin__", None), ( + "List[Tuple] types with complex Tuple sub-types are not currently supported" + ) types.append( get_click_type(annotation=type_, parameter_info=parameter_info) ) From db56796cfc04e38438552c1f2dc98dbe3995d3c7 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Fri, 19 Sep 2025 21:16:37 +0200 Subject: [PATCH 06/21] Fix `is_tuple` and `is_list` conditions order --- typer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typer/main.py b/typer/main.py index c57e72f1d1..d89e38c8a6 100644 --- a/typer/main.py +++ b/typer/main.py @@ -876,12 +876,12 @@ def get_click_param( annotation=main_type, parameter_info=parameter_info ) convertor = determine_type_convertor(main_type) + if is_tuple: + convertor = generate_tuple_convertor(get_args(main_type)) if is_list: convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) - if is_tuple: - convertor = generate_tuple_convertor(get_args(main_type)) if isinstance(parameter_info, OptionInfo): if main_type is bool: is_flag = True From 979499d2c3b60fa7976fea867a6e8ff5cb026566 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Fri, 19 Sep 2025 21:40:11 +0200 Subject: [PATCH 07/21] Update code include format --- .../multiple-options-with-multiple-values.md | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index a2333817f1..6c12afb910 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -4,27 +4,7 @@ The same rules apply for the number of values for each use and their types; the For this, we use the standard Python `typing.List` and declare its internal type to be a `typing.Tuple`: -//// tab | Python 3.7+ - -```Python hl_lines="1 7" -{!> ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py!} -``` - -//// - -//// tab | Python 3.7+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible - -/// - -```Python hl_lines="1 6" -{!> ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py!} -``` - -//// +{* ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py hl[1,7] *} Just as before, the types internal to the `Tuple` define the type of each value in the tuple. From b38107675f6f5226bdcbcb03cf64821051d474a2 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 25 Nov 2025 17:02:32 +0100 Subject: [PATCH 08/21] update tutorial files to use explicit Typer() --- .../multiple-values/multiple-options-with-multiple-values.md | 2 +- .../multiple_options_with_multiple_values/tutorial001.py | 5 ++++- .../multiple_options_with_multiple_values/tutorial001_an.py | 5 ++++- .../test_tutorial001.py | 4 +--- .../test_tutorial001_an.py | 4 +--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index 6c12afb910..b0f9f21ab3 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -4,7 +4,7 @@ The same rules apply for the number of values for each use and their types; the For this, we use the standard Python `typing.List` and declare its internal type to be a `typing.Tuple`: -{* ../docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py hl[1,7] *} +{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py hl[1,10] *} Just as before, the types internal to the `Tuple` define the type of each value in the tuple. diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py index fc011a679d..ef3eb4803a 100644 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py @@ -2,7 +2,10 @@ import typer +app = typer.Typer() + +@app.command() def main(borrow: List[Tuple[float, str]] = typer.Option([])): if not borrow: print("Congratulations, you're debt-free!") @@ -16,4 +19,4 @@ def main(borrow: List[Tuple[float, str]] = typer.Option([])): if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py index 75b5361eb5..bc464be2b0 100644 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py @@ -3,7 +3,10 @@ import typer from typing_extensions import Annotated +app = typer.Typer() + +@app.command() def main(borrow: Annotated[List[Tuple[float, str]], typer.Option()] = []): if not borrow: print("Congratulations, you're debt-free!") @@ -17,4 +20,4 @@ def main(borrow: Annotated[List[Tuple[float, str]], typer.Option()] = []): if __name__ == "__main__": - typer.run(main) + app() diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index fb855aa877..c293ad954f 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -1,6 +1,5 @@ import subprocess -import typer from typer.testing import CliRunner from docs_src.multiple_values.multiple_options_with_multiple_values import ( @@ -8,8 +7,7 @@ ) runner = CliRunner() -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_main(): diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py index 0954b58e26..f75e66ef18 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py @@ -1,6 +1,5 @@ import subprocess -import typer from typer.testing import CliRunner from docs_src.multiple_values.multiple_options_with_multiple_values import ( @@ -8,8 +7,7 @@ ) runner = CliRunner() -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_main(): From 8644bb021fa42f67ad0e28f1329d99b0c1ffefd7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:28:15 +0000 Subject: [PATCH 09/21] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multiple_options_with_multiple_values/tutorial001_an.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py index bc464be2b0..e4aa2f19e4 100644 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py @@ -1,7 +1,6 @@ -from typing import List, Tuple +from typing import Annotated, List, Tuple import typer -from typing_extensions import Annotated app = typer.Typer() From 955eb13c684b586b5543b8afb559800f881142cb Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 16:49:19 -0700 Subject: [PATCH 10/21] Add tutorial code for py310, and add, combine, and clean up tests. --- .../multiple-options-with-multiple-values.md | 6 +- .../tutorial001_an_py310.py | 22 +++++++ ...torial001_an.py => tutorial001_an_py39.py} | 0 .../tutorial001_py310.py | 20 ++++++ .../{tutorial001.py => tutorial001_py39.py} | 0 .../test_tutorial001.py | 42 ++++++++---- .../test_tutorial001_an.py | 65 ------------------- 7 files changed, 73 insertions(+), 82 deletions(-) create mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py rename docs_src/multiple_values/multiple_options_with_multiple_values/{tutorial001_an.py => tutorial001_an_py39.py} (100%) create mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py rename docs_src/multiple_values/multiple_options_with_multiple_values/{tutorial001.py => tutorial001_py39.py} (100%) delete mode 100644 tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index b0f9f21ab3..cd63da0b05 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -2,11 +2,11 @@ For when simple doesn't quite cut it, you may also declare a *CLI option* that t The same rules apply for the number of values for each use and their types; the types may be anything you want, but there must be a fixed number of values. -For this, we use the standard Python `typing.List` and declare its internal type to be a `typing.Tuple`: +For this, we use the standard Python `list` and declare it as a list of `tuple`: -{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py hl[1,10] *} +{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[1,10] *} -Just as before, the types internal to the `Tuple` define the type of each value in the tuple. +Just as before, the types internal to the `tuple` define the type of each value in the tuple. ## Check it diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py new file mode 100644 index 0000000000..45dbbaf61e --- /dev/null +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(borrow: Annotated[list[tuple[float, str]], typer.Option()] = []): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py similarity index 100% rename from docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an.py rename to docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py new file mode 100644 index 0000000000..382a466625 --- /dev/null +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py @@ -0,0 +1,20 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(borrow: list[tuple[float, str]] = typer.Option([])): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py similarity index 100% rename from docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001.py rename to docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index c293ad954f..4a6fa2254e 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -1,31 +1,45 @@ +import importlib import subprocess +import sys +from types import ModuleType from typer.testing import CliRunner -from docs_src.multiple_values.multiple_options_with_multiple_values import ( - tutorial001 as mod, -) +from ....utils import needs_py310 runner = CliRunner() -app = mod.app + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.multiple_values.multiple_options_with_multiple_values.{request.param}" + mod = importlib.import_module(module_name) + return mod -def test_main(): - result = runner.invoke(app) +def test_main(mod: ModuleType): + result = runner.invoke(mod.app) assert result.exit_code == 0 assert "Congratulations, you're debt-free!" in result.output -def test_borrow_1(): - result = runner.invoke(app, ["--borrow", "2.5", "Mark"]) +def test_borrow_1(mod: ModuleType): + result = runner.invoke(mod.app, ["--borrow", "2.5", "Mark"]) assert result.exit_code == 0 assert "Borrowed 2.50 from Mark" in result.output assert "Total borrowed: 2.50" in result.output -def test_borrow_many(): +def test_borrow_many(mod: ModuleType): result = runner.invoke( - app, + mod.app, [ "--borrow", "2.5", @@ -45,8 +59,8 @@ def test_borrow_many(): assert "Total borrowed: 9.50" in result.output -def test_invalid_borrow(): - result = runner.invoke(app, ["--borrow", "2.5"]) +def test_invalid_borrow(mod: ModuleType): + result = runner.invoke(mod.app, ["--borrow", "2.5"]) assert result.exit_code != 0 # TODO: when deprecating Click 7, remove second option @@ -56,9 +70,9 @@ def test_invalid_borrow(): ) -def test_script(): +def test_script(mod: ModuleType): result = subprocess.run( - ["coverage", "run", mod.__file__, "--help"], + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], capture_output=True, encoding="utf-8", ) diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py deleted file mode 100644 index f75e66ef18..0000000000 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001_an.py +++ /dev/null @@ -1,65 +0,0 @@ -import subprocess - -from typer.testing import CliRunner - -from docs_src.multiple_values.multiple_options_with_multiple_values import ( - tutorial001_an as mod, -) - -runner = CliRunner() -app = mod.app - - -def test_main(): - result = runner.invoke(app) - assert result.exit_code == 0 - assert "Congratulations, you're debt-free!" in result.output - - -def test_borrow_1(): - result = runner.invoke(app, ["--borrow", "2.5", "Mark"]) - assert result.exit_code == 0 - assert "Borrowed 2.50 from Mark" in result.output - assert "Total borrowed: 2.50" in result.output - - -def test_borrow_many(): - result = runner.invoke( - app, - [ - "--borrow", - "2.5", - "Mark", - "--borrow", - "5.25", - "Sean", - "--borrow", - "1.75", - "Wade", - ], - ) - assert result.exit_code == 0 - assert "Borrowed 2.50 from Mark" in result.output - assert "Borrowed 5.25 from Sean" in result.output - assert "Borrowed 1.75 from Wade" in result.output - assert "Total borrowed: 9.50" in result.output - - -def test_invalid_borrow(): - result = runner.invoke(app, ["--borrow", "2.5"]) - assert result.exit_code != 0 - # TODO: when deprecating Click 7, remove second option - - assert ( - "Option '--borrow' requires 2 arguments" in result.output - or "--borrow option requires 2 arguments" in result.output - ) - - -def test_script(): - result = subprocess.run( - ["coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout From 9d4da5c48e47ca8b526e65f106ada3170db47fe5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:49:52 +0000 Subject: [PATCH 11/21] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial001.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index 4a6fa2254e..50f235acfe 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -9,6 +9,7 @@ runner = CliRunner() + @pytest.fixture( name="mod", params=[ From f2d40bd3d140390f9dd5d8548abb35f81991e250 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 16:52:41 -0700 Subject: [PATCH 12/21] Forgot to import pytest oops --- .../test_tutorial001.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index 50f235acfe..5dcb299d18 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -3,6 +3,7 @@ import sys from types import ModuleType +import pytest from typer.testing import CliRunner from ....utils import needs_py310 From c056d833f1e58c68bea259ada01b8585cfb5be4b Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 16:59:46 -0700 Subject: [PATCH 13/21] Linter doesn't understand --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6c5f33fcc8..c7cc3c77b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,7 +221,10 @@ ignore = [ # Default mutable data structure "docs_src/options_autocompletion/tutorial006_an_py39.py" = ["B006"] "docs_src/multiple_values/multiple_options/tutorial002_an_py39.py" = ["B006"] +"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py" = ["B006"] +"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py" = ["B006"] "docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py" = ["B006"] +"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py" = ["B006"] "docs_src/options_autocompletion/tutorial007_an_py39.py" = ["B006"] "docs_src/options_autocompletion/tutorial008_an_py39.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an_py39.py" = ["B006"] @@ -237,6 +240,8 @@ ignore = [ "docs_src/using_click/tutorial001_py39.py" = ["B007"] # No need to worry about rich imports in docs "docs_src/*" = ["TID"] +# Python 3.9 files use old typing format +"docs_src/**/*_py39.py" = ["UP006", "UP035"] [tool.ruff.lint.isort] known-third-party = ["typer", "click"] From 0e9628fc39813c751077fa3ef08a1e3d790ef919 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 17:03:19 -0700 Subject: [PATCH 14/21] Fix/limit lint scope for multiple_values python3.9 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7cc3c77b9..dae8eaebdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,8 +240,8 @@ ignore = [ "docs_src/using_click/tutorial001_py39.py" = ["B007"] # No need to worry about rich imports in docs "docs_src/*" = ["TID"] -# Python 3.9 files use old typing format -"docs_src/**/*_py39.py" = ["UP006", "UP035"] +# Python 3.9 files use old typing format for list, tuple +"docs_src/multiple_values/**/*_py39.py" = ["UP006", "UP035"] [tool.ruff.lint.isort] known-third-party = ["typer", "click"] From be69233a5b5acc8d6aa9aab26416f9109a671ddd Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 17:06:55 -0700 Subject: [PATCH 15/21] Incorporate change suggestion from @YuriiMotov --- typer/main.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/typer/main.py b/typer/main.py index ebd8659477..1074c9ae40 100644 --- a/typer/main.py +++ b/typer/main.py @@ -866,16 +866,18 @@ def get_click_param( if lenient_issubclass(origin, list): main_type = get_args(main_type)[0] list_origin = get_origin(main_type) - is_tuple = lenient_issubclass(list_origin, tuple) - assert is_tuple or not list_origin, ( - "List types with complex sub-types are not currently supported" - ) + list_origin_is_tuple = lenient_issubclass(list_origin, tuple) + if not list_origin_is_tuple: + assert not list_origin, ( + "List types with complex sub-types are not currently supported" + ) is_list = True - if is_tuple: + if list_origin_is_tuple: types = [] - for type_ in main_type.__args__: - assert not getattr(type_, "__origin__", None), ( - "List[Tuple] types with complex Tuple sub-types are not currently supported" + for type_ in get_args(main_type): + assert not get_origin(type_), ( + "List[Tuple] types with complex Tuple sub-types are not " + "currently supported" ) types.append( get_click_type(annotation=type_, parameter_info=parameter_info) @@ -897,12 +899,14 @@ def get_click_param( annotation=main_type, parameter_info=parameter_info ) convertor = determine_type_convertor(main_type) - if is_tuple: - convertor = generate_tuple_convertor(get_args(main_type)) if is_list: + if list_origin_is_tuple: + converter = generate_tuple_convertor(get_args(main_type)) convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) + if is_tuple: + convertor = generate_tuple_convertor(get_args(main_type)) if isinstance(parameter_info, OptionInfo): if main_type is bool: is_flag = True From d8c639b74c9ee35a8f6fae17ba3421d5288f2e5f Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 17:11:02 -0700 Subject: [PATCH 16/21] fix typo --- typer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 1074c9ae40..7c054efe04 100644 --- a/typer/main.py +++ b/typer/main.py @@ -901,7 +901,7 @@ def get_click_param( convertor = determine_type_convertor(main_type) if is_list: if list_origin_is_tuple: - converter = generate_tuple_convertor(get_args(main_type)) + convertor = generate_tuple_convertor(get_args(main_type)) convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) From d1402a565e749a48b2290e82e31297fab1b9fed5 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Sun, 4 Jan 2026 18:48:25 -0700 Subject: [PATCH 17/21] Fix docs highlighting --- .../multiple-values/multiple-options-with-multiple-values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index cd63da0b05..a2c0c1001d 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -4,7 +4,7 @@ The same rules apply for the number of values for each use and their types; the For this, we use the standard Python `list` and declare it as a list of `tuple`: -{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[1,10] *} +{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[9] *} Just as before, the types internal to the `tuple` define the type of each value in the tuple. From 9ac54fba55df9b7e6a10480558b8cab1c24634e8 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Thu, 8 Jan 2026 13:31:17 -0700 Subject: [PATCH 18/21] Mixed up my Python versions and their deprecations :sweat_smile: --- .../multiple-options-with-multiple-values.md | 6 +-- .../tutorial001_an_py310.py | 22 ---------- .../tutorial001_an_py39.py | 44 +++++++++---------- .../tutorial001_py310.py | 20 --------- .../tutorial001_py39.py | 42 +++++++++--------- pyproject.toml | 4 -- .../test_tutorial001.py | 2 - 7 files changed, 45 insertions(+), 95 deletions(-) delete mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py delete mode 100644 docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index a2c0c1001d..95471717f2 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -1,10 +1,10 @@ -For when simple doesn't quite cut it, you may also declare a *CLI option* that takes several values of different types and can be used multiple times. +# Multiple options with multiple values -The same rules apply for the number of values for each use and their types; the types may be anything you want, but there must be a fixed number of values. +For when simple doesn't quite cut it, you may also declare a *CLI option* that takes several values of different types and can be used multiple times. For this, we use the standard Python `list` and declare it as a list of `tuple`: -{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[9] *} +{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py hl[9] *} Just as before, the types internal to the `tuple` define the type of each value in the tuple. diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py deleted file mode 100644 index 45dbbaf61e..0000000000 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Annotated - -import typer - -app = typer.Typer() - - -@app.command() -def main(borrow: Annotated[list[tuple[float, str]], typer.Option()] = []): - if not borrow: - print("Congratulations, you're debt-free!") - raise typer.Exit(0) - total = 0.0 - for amount, person in borrow: - print(f"Borrowed {amount:.2f} from {person}") - total += amount - print() - print(f"Total borrowed: {total:.2f}") - - -if __name__ == "__main__": - app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py index e4aa2f19e4..45dbbaf61e 100644 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py @@ -1,22 +1,22 @@ -from typing import Annotated, List, Tuple - -import typer - -app = typer.Typer() - - -@app.command() -def main(borrow: Annotated[List[Tuple[float, str]], typer.Option()] = []): - if not borrow: - print("Congratulations, you're debt-free!") - raise typer.Exit(0) - total = 0.0 - for amount, person in borrow: - print(f"Borrowed {amount:.2f} from {person}") - total += amount - print() - print(f"Total borrowed: {total:.2f}") - - -if __name__ == "__main__": - app() +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(borrow: Annotated[list[tuple[float, str]], typer.Option()] = []): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py deleted file mode 100644 index 382a466625..0000000000 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -import typer - -app = typer.Typer() - - -@app.command() -def main(borrow: list[tuple[float, str]] = typer.Option([])): - if not borrow: - print("Congratulations, you're debt-free!") - raise typer.Exit(0) - total = 0.0 - for amount, person in borrow: - print(f"Borrowed {amount:.2f} from {person}") - total += amount - print() - print(f"Total borrowed: {total:.2f}") - - -if __name__ == "__main__": - app() diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py index ef3eb4803a..382a466625 100644 --- a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py +++ b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py @@ -1,22 +1,20 @@ -from typing import List, Tuple - -import typer - -app = typer.Typer() - - -@app.command() -def main(borrow: List[Tuple[float, str]] = typer.Option([])): - if not borrow: - print("Congratulations, you're debt-free!") - raise typer.Exit(0) - total = 0.0 - for amount, person in borrow: - print(f"Borrowed {amount:.2f} from {person}") - total += amount - print() - print(f"Total borrowed: {total:.2f}") - - -if __name__ == "__main__": - app() +import typer + +app = typer.Typer() + + +@app.command() +def main(borrow: list[tuple[float, str]] = typer.Option([])): + if not borrow: + print("Congratulations, you're debt-free!") + raise typer.Exit(0) + total = 0.0 + for amount, person in borrow: + print(f"Borrowed {amount:.2f} from {person}") + total += amount + print() + print(f"Total borrowed: {total:.2f}") + + +if __name__ == "__main__": + app() diff --git a/pyproject.toml b/pyproject.toml index dae8eaebdd..f159bd2cc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -222,9 +222,7 @@ ignore = [ "docs_src/options_autocompletion/tutorial006_an_py39.py" = ["B006"] "docs_src/multiple_values/multiple_options/tutorial002_an_py39.py" = ["B006"] "docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py" = ["B006"] -"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py" = ["B006"] "docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py" = ["B006"] -"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py" = ["B006"] "docs_src/options_autocompletion/tutorial007_an_py39.py" = ["B006"] "docs_src/options_autocompletion/tutorial008_an_py39.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an_py39.py" = ["B006"] @@ -240,8 +238,6 @@ ignore = [ "docs_src/using_click/tutorial001_py39.py" = ["B007"] # No need to worry about rich imports in docs "docs_src/*" = ["TID"] -# Python 3.9 files use old typing format for list, tuple -"docs_src/multiple_values/**/*_py39.py" = ["UP006", "UP035"] [tool.ruff.lint.isort] known-third-party = ["typer", "click"] diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index 5dcb299d18..280901827a 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -15,9 +15,7 @@ name="mod", params=[ pytest.param("tutorial001_py39"), - pytest.param("tutorial001_py310", marks=needs_py310), pytest.param("tutorial001_an_py39"), - pytest.param("tutorial001_an_py310", marks=needs_py310), ], ) def get_mod(request: pytest.FixtureRequest) -> ModuleType: From df934e811477d116e0a22a7bb190514f33c4870a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:31:49 +0000 Subject: [PATCH 19/21] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_tutorial001.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index 280901827a..623749f9be 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -6,8 +6,6 @@ import pytest from typer.testing import CliRunner -from ....utils import needs_py310 - runner = CliRunner() From c43d17318de3508efbf9c7d1421c966d0d72d288 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 17 Feb 2026 08:42:29 +0100 Subject: [PATCH 20/21] Update to Python 3.10 syntax --- .../{tutorial001_an_py39.py => tutorial001_an_py310.py} | 0 .../{tutorial001_py39.py => tutorial001_py310.py} | 0 pyproject.toml | 3 +-- .../test_tutorial001.py | 4 ++-- 4 files changed, 3 insertions(+), 4 deletions(-) rename docs_src/multiple_values/multiple_options_with_multiple_values/{tutorial001_an_py39.py => tutorial001_an_py310.py} (100%) rename docs_src/multiple_values/multiple_options_with_multiple_values/{tutorial001_py39.py => tutorial001_py310.py} (100%) diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py similarity index 100% rename from docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py rename to docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py diff --git a/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py b/docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py similarity index 100% rename from docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py rename to docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py310.py diff --git a/pyproject.toml b/pyproject.toml index 4b2a30a21a..9d37018a3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,8 +228,7 @@ ignore = [ # Default mutable data structure "docs_src/options_autocompletion/tutorial006_an_py310.py" = ["B006"] "docs_src/multiple_values/multiple_options/tutorial002_an_py310.py" = ["B006"] -"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_py39.py" = ["B006"] -"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py" = ["B006"] +"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py" = ["B006"] "docs_src/options_autocompletion/tutorial007_an_py310.py" = ["B006"] "docs_src/options_autocompletion/tutorial008_an_py310.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an_py310.py" = ["B006"] diff --git a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py index 623749f9be..305ae6791b 100644 --- a/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py +++ b/tests/test_tutorial/test_multiple_values/test_multiple_options_with_multiple_values/test_tutorial001.py @@ -12,8 +12,8 @@ @pytest.fixture( name="mod", params=[ - pytest.param("tutorial001_py39"), - pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_py310"), + pytest.param("tutorial001_an_py310"), ], ) def get_mod(request: pytest.FixtureRequest) -> ModuleType: From e20d1bd5275ecd68046b9feb3f4c3784294d6eeb Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 18 Feb 2026 09:23:13 +0100 Subject: [PATCH 21/21] Fix code include --- .../multiple-values/multiple-options-with-multiple-values.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md index 95471717f2..65d924f397 100644 --- a/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/multiple-options-with-multiple-values.md @@ -4,7 +4,7 @@ For when simple doesn't quite cut it, you may also declare a *CLI option* that t For this, we use the standard Python `list` and declare it as a list of `tuple`: -{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py39.py hl[9] *} +{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[9] *} Just as before, the types internal to the `tuple` define the type of each value in the tuple.