From 25a5c3718d61950dbfef79dcb615435e9d20a745 Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Thu, 21 Dec 2023 11:28:36 +0100 Subject: [PATCH 01/23] [FEATURE] Support for common pydantic types Fixes #181 --- .../parameter-types/pydantic-types.md | 84 +++++++++++++++++++ .../pydantic_types/__init__.py | 0 .../pydantic_types/tutorial001.py | 10 +++ .../pydantic_types/tutorial001_an.py | 11 +++ .../pydantic_types/tutorial002.py | 10 +++ .../pydantic_types/tutorial002_an.py | 11 +++ .../pydantic_types/tutorial003.py | 12 +++ .../pydantic_types/tutorial003_an.py | 15 ++++ .../pydantic_types/tutorial004.py | 20 +++++ .../pydantic_types/tutorial004_an.py | 22 +++++ mkdocs.yml | 1 + pyproject.toml | 1 + requirements-tests.txt | 1 + .../test_pydantic_types/test_tutorial001.py | 38 +++++++++ .../test_tutorial001_an.py | 38 +++++++++ .../test_pydantic_types/test_tutorial002.py | 38 +++++++++ .../test_tutorial002_an.py | 38 +++++++++ .../test_pydantic_types/test_tutorial003.py | 41 +++++++++ .../test_tutorial003_an.py | 41 +++++++++ .../test_pydantic_types/test_tutorial004.py | 45 ++++++++++ .../test_tutorial004_an.py | 45 ++++++++++ typer/completion.py | 2 +- typer/main.py | 59 +++++++++++-- 23 files changed, 574 insertions(+), 9 deletions(-) create mode 100644 docs/tutorial/parameter-types/pydantic-types.md create mode 100644 docs_src/parameter_types/pydantic_types/__init__.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial001.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial001_an.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial002.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial002_an.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial003.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial003_an.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial004.py create mode 100644 docs_src/parameter_types/pydantic_types/tutorial004_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md new file mode 100644 index 0000000000..e6d116263b --- /dev/null +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -0,0 +1,84 @@ +Pydantic types such as [AnyUrl](https://docs.pydantic.dev/latest/api/networks/#pydantic.networks.AnyUrl) or [EmailStr](https://docs.pydantic.dev/latest/api/networks/#pydantic.networks.EmailStr) can be very convenient to describe and validate some parameters. + +You can add pydantic from typer's optional dependencies + +
+ +```console +// Pydantic comes with typer[standard] +$ pip install "typer[standard]" +---> 100% +Successfully installed typer rich pydantic + +// Alternatively, you can install Pydantic independently +$ pip install pydantic +---> 100% +Successfully installed pydantic +``` + +
+ + +You can then use them as parameter types. + +=== "Python 3.6+ Argument" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/pydantic_types/tutorial001_an.py!} + ``` + +=== "Python 3.6+ Argument non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/pydantic_types/tutorial001.py!} + ``` + +=== "Python 3.6+ Option" + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/pydantic_types/tutorial002_an.py!} + ``` + +=== "Python 3.6+ Option non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="4" + {!> ../docs_src/parameter_types/pydantic_types/tutorial002.py!} + ``` + +These types are also supported in lists or tuples + +=== "Python 3.6+ list" + + ```Python hl_lines="6" + {!> ../docs_src/parameter_types/pydantic_types/tutorial003_an.py!} + ``` + +=== "Python 3.6+ list non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/pydantic_types/tutorial003.py!} + ``` + +=== "Python 3.6+ tuple" + + ```Python hl_lines="6" + {!> ../docs_src/parameter_types/pydantic_types/tutorial004_an.py!} + ``` + +=== "Python 3.6+ tuple non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="5" + {!> ../docs_src/parameter_types/pydantic_types/tutorial004.py!} + ``` diff --git a/docs_src/parameter_types/pydantic_types/__init__.py b/docs_src/parameter_types/pydantic_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/parameter_types/pydantic_types/tutorial001.py b/docs_src/parameter_types/pydantic_types/tutorial001.py new file mode 100644 index 0000000000..4aec54161a --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial001.py @@ -0,0 +1,10 @@ +import typer +from pydantic import EmailStr + + +def main(email_arg: EmailStr): + typer.echo(f"email_arg: {email_arg}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_an.py b/docs_src/parameter_types/pydantic_types/tutorial001_an.py new file mode 100644 index 0000000000..c92dbce546 --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial001_an.py @@ -0,0 +1,11 @@ +import typer +from pydantic import EmailStr +from typing_extensions import Annotated + + +def main(email_arg: Annotated[EmailStr, typer.Argument()]): + typer.echo(f"email_arg: {email_arg}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial002.py b/docs_src/parameter_types/pydantic_types/tutorial002.py new file mode 100644 index 0000000000..14ef540743 --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial002.py @@ -0,0 +1,10 @@ +import typer +from pydantic import EmailStr + + +def main(email_opt: EmailStr = typer.Option("tiangolo@gmail.com")): + typer.echo(f"email_opt: {email_opt}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an.py b/docs_src/parameter_types/pydantic_types/tutorial002_an.py new file mode 100644 index 0000000000..bcf7cf5e15 --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial002_an.py @@ -0,0 +1,11 @@ +import typer +from pydantic import EmailStr +from typing_extensions import Annotated + + +def main(email_opt: Annotated[EmailStr, typer.Option()] = "tiangolo@gmail.com"): + typer.echo(f"email_opt: {email_opt}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial003.py b/docs_src/parameter_types/pydantic_types/tutorial003.py new file mode 100644 index 0000000000..c1b13964be --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial003.py @@ -0,0 +1,12 @@ +from typing import List + +import typer +from pydantic import AnyHttpUrl + + +def main(urls: List[AnyHttpUrl] = typer.Option([], "--url")): + typer.echo(f"urls: {urls}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an.py b/docs_src/parameter_types/pydantic_types/tutorial003_an.py new file mode 100644 index 0000000000..61b816243e --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial003_an.py @@ -0,0 +1,15 @@ +from typing import List + +import typer +from pydantic import AnyHttpUrl +from typing_extensions import Annotated + + +def main( + urls: Annotated[List[AnyHttpUrl], typer.Option("--url", default_factory=list)], +): + typer.echo(f"urls: {urls}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial004.py b/docs_src/parameter_types/pydantic_types/tutorial004.py new file mode 100644 index 0000000000..66b7b71a25 --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial004.py @@ -0,0 +1,20 @@ +from typing import Tuple + +import typer +from pydantic import AnyHttpUrl, EmailStr + + +def main( + user: Tuple[str, int, EmailStr, AnyHttpUrl] = typer.Option( + ..., help="User name, age, email and social media URL" + ), +): + name, age, email, url = user + typer.echo(f"name: {name}") + typer.echo(f"age: {age}") + typer.echo(f"email: {email}") + typer.echo(f"url: {url}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an.py b/docs_src/parameter_types/pydantic_types/tutorial004_an.py new file mode 100644 index 0000000000..9fa0ee5494 --- /dev/null +++ b/docs_src/parameter_types/pydantic_types/tutorial004_an.py @@ -0,0 +1,22 @@ +from typing import Tuple + +import typer +from pydantic import AnyHttpUrl, EmailStr +from typing_extensions import Annotated + + +def main( + user: Annotated[ + Tuple[str, int, EmailStr, AnyHttpUrl], + typer.Option(help="User name, age, email and social media URL"), + ], +): + name, age, email, url = user + typer.echo(f"name: {name}") + typer.echo(f"age: {age}") + typer.echo(f"email: {email}") + typer.echo(f"url: {url}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/mkdocs.yml b/mkdocs.yml index ead95508a0..105d3faef0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -114,6 +114,7 @@ nav: - tutorial/parameter-types/path.md - tutorial/parameter-types/file.md - tutorial/parameter-types/custom-types.md + - tutorial/parameter-types/pydantic-types.md - SubCommands - Command Groups: - tutorial/subcommands/index.md - tutorial/subcommands/add-typer.md diff --git a/pyproject.toml b/pyproject.toml index ce9d61afa3..92cda7d9d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ Changelog = "https://typer.tiangolo.com/release-notes/" standard = [ "shellingham >=1.3.0", "rich >=10.11.0", + "pydantic[email] >=2.0.0", ] [tool.pdm] diff --git a/requirements-tests.txt b/requirements-tests.txt index 04ca55bba8..36f90749a2 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,3 +10,4 @@ ruff ==0.6.5 # Needed explicitly by typer-slim rich >=10.11.0 shellingham >=1.3.0 +pydantic[email] >=2.0.0 diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py new file mode 100644 index 0000000000..e8d226088b --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types import tutorial001 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + + +def test_email_arg(): + result = runner.invoke(app, ["tiangolo@gmail.com"]) + assert result.exit_code == 0 + assert "email_arg: tiangolo@gmail.com" in result.output + + +def test_email_arg_invalid(): + result = runner.invoke(app, ["invalid"]) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py new file mode 100644 index 0000000000..167c1ce3a8 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types 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 + + +def test_email_arg(): + result = runner.invoke(app, ["tiangolo@gmail.com"]) + assert result.exit_code == 0 + assert "email_arg: tiangolo@gmail.com" in result.output + + +def test_email_arg_invalid(): + result = runner.invoke(app, ["invalid"]) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py new file mode 100644 index 0000000000..265e1d3191 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types import tutorial002 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + + +def test_email_opt(): + result = runner.invoke(app, ["--email-opt", "tiangolo@gmail.com"]) + assert result.exit_code == 0 + assert "email_opt: tiangolo@gmail.com" in result.output + + +def test_email_opt_invalid(): + result = runner.invoke(app, ["--email-opt", "invalid"]) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py new file mode 100644 index 0000000000..1d0475d009 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py @@ -0,0 +1,38 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types 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 + + +def test_email_opt(): + result = runner.invoke(app, ["--email-opt", "tiangolo@gmail.com"]) + assert result.exit_code == 0 + assert "email_opt: tiangolo@gmail.com" in result.output + + +def test_email_opt_invalid(): + result = runner.invoke(app, ["--email-opt", "invalid"]) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py new file mode 100644 index 0000000000..b9a9018e04 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py @@ -0,0 +1,41 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + + +def test_url_list(): + result = runner.invoke( + app, ["--url", "https://example.com", "--url", "https://example.org"] + ) + assert result.exit_code == 0 + assert "https://example.com" in result.output + assert "https://example.org" in result.output + + +def test_url_invalid(): + result = runner.invoke(app, ["--url", "invalid", "--url", "https://example.org"]) + assert result.exit_code != 0 + assert "Input should be a valid URL" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py new file mode 100644 index 0000000000..487fffc55b --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py @@ -0,0 +1,41 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types 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 + + +def test_url_list(): + result = runner.invoke( + app, ["--url", "https://example.com", "--url", "https://example.org"] + ) + assert result.exit_code == 0 + assert "https://example.com" in result.output + assert "https://example.org" in result.output + + +def test_url_invalid(): + result = runner.invoke(app, ["--url", "invalid", "--url", "https://example.org"]) + assert result.exit_code != 0 + assert "Input should be a valid URL" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py new file mode 100644 index 0000000000..e51c2b5b89 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -0,0 +1,45 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + + +def test_tuple(): + result = runner.invoke( + app, ["--user", "Camila", "23", "camila@example.org", "https://example.com"] + ) + assert result.exit_code == 0 + assert "name: Camila" in result.output + assert "age: 23" in result.output + assert "email: camila@example.org" in result.output + assert "url: https://example.com" in result.output + + +def test_tuple_invalid(): + result = runner.invoke( + app, ["--user", "Camila", "23", "invalid", "https://example.com"] + ) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py new file mode 100644 index 0000000000..dde6671976 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py @@ -0,0 +1,45 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.pydantic_types 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 + + +def test_tuple(): + result = runner.invoke( + app, ["--user", "Camila", "23", "camila@example.org", "https://example.com"] + ) + assert result.exit_code == 0 + assert "name: Camila" in result.output + assert "age: 23" in result.output + assert "email: camila@example.org" in result.output + assert "url: https://example.com" in result.output + + +def test_tuple_invalid(): + result = runner.invoke( + app, ["--user", "Camila", "23", "invalid", "https://example.com"] + ) + assert result.exit_code != 0 + assert "value is not a valid email address" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/completion.py b/typer/completion.py index 1220a1b545..90c4984b30 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -135,7 +135,7 @@ def shell_complete( click.echo(f"Shell {shell} not supported.", err=True) return 1 - comp = comp_cls(cli, ctx_args, prog_name, complete_var) + comp = comp_cls(cli, dict(ctx_args), prog_name, complete_var) if instruction == "source": click.echo(comp.source()) diff --git a/typer/main.py b/typer/main.py index a621bda6ad..474943c9bc 100644 --- a/typer/main.py +++ b/typer/main.py @@ -13,6 +13,7 @@ import click from typing_extensions import get_args, get_origin +from typing_extensions import TypeAlias from ._typing import is_union from .completion import get_completion_inspect_parameters @@ -56,6 +57,40 @@ except ImportError: # pragma: no cover rich = None # type: ignore +try: + import pydantic + + def is_pydantic_type(type_: Any) -> bool: + return type_.__module__.startswith("pydantic") and not lenient_issubclass( + type_, pydantic.BaseModel + ) + + def pydantic_convertor(type_: type) -> Callable[[str], Any]: + """Create a convertor for a parameter annotated with a pydantic type.""" + T: TypeAlias = type_ # type: ignore[valid-type] + + @pydantic.validate_call + def internal_convertor(value: T) -> T: + return value + + def convertor(value: str) -> T: + try: + return internal_convertor(value) + except pydantic.ValidationError as e: + error_message = e.errors( + include_context=False, include_input=False, include_url=False + )[0]["msg"] + raise click.BadParameter(error_message) from e + + return convertor + +except ImportError: # pragma: no cover + pydantic = None # type: ignore + + def is_pydantic_type(type_: Any) -> bool: + return False + + _original_except_hook = sys.excepthook _typer_developer_exception_attr_name = "__typer_developer_exception__" @@ -622,6 +657,8 @@ def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]: convertor = param_path_convertor if lenient_issubclass(type_, Enum): convertor = generate_enum_convertor(type_) + if is_pydantic_type(type_): + convertor = pydantic_convertor(type_) return convertor @@ -797,6 +834,8 @@ def get_click_type( [item.value for item in annotation], case_sensitive=parameter_info.case_sensitive, ) + elif is_pydantic_type(annotation): + return click.STRING raise RuntimeError(f"Type not yet supported: {annotation}") # pragma: no cover @@ -806,6 +845,13 @@ def lenient_issubclass( return isinstance(cls, type) and issubclass(cls, class_or_tuple) +def is_complex_subtype(type_: Any) -> bool: + # For pydantic types, such as `AnyUrl`, there's an extra `Annotated` layer that we don't need to treat as complex + return getattr(type_, "__origin__", None) is not None and not is_pydantic_type( + type_ + ) + + def get_click_param( param: ParamMeta, ) -> Tuple[Union[click.Argument, click.Option], Any]: @@ -839,6 +885,7 @@ def get_click_param( is_flag = None origin = get_origin(main_type) + callback = parameter_info.callback if origin is not None: # Handle SomeType | None and Optional[SomeType] if is_union(origin): @@ -853,14 +900,14 @@ def get_click_param( # Handle Tuples and Lists if lenient_issubclass(origin, List): main_type = get_args(main_type)[0] - assert not get_origin( + assert not is_complex_subtype( main_type ), "List types with complex sub-types are not currently supported" is_list = True elif lenient_issubclass(origin, Tuple): # type: ignore types = [] for type_ in get_args(main_type): - assert not get_origin( + assert not is_complex_subtype( type_ ), "Tuple types with complex sub-types are not currently supported" types.append( @@ -919,9 +966,7 @@ def get_click_param( # Parameter required=required, default=default_value, - callback=get_param_callback( - callback=parameter_info.callback, convertor=convertor - ), + callback=get_param_callback(callback=callback, convertor=convertor), metavar=parameter_info.metavar, expose_value=parameter_info.expose_value, is_eager=parameter_info.is_eager, @@ -953,9 +998,7 @@ def get_click_param( hidden=parameter_info.hidden, # Parameter default=default_value, - callback=get_param_callback( - callback=parameter_info.callback, convertor=convertor - ), + callback=get_param_callback(callback=callback, convertor=convertor), metavar=parameter_info.metavar, expose_value=parameter_info.expose_value, is_eager=parameter_info.is_eager, From 57f0cb72f4e3e2fe10ed37e20458f9f43e85bd07 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 25 Jul 2024 16:18:54 +0200 Subject: [PATCH 02/23] Update docs to refer to Python 3.7+ instead of 3.6+ --- docs/tutorial/parameter-types/pydantic-types.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index e6d116263b..d2762b0e3e 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -21,13 +21,13 @@ Successfully installed pydantic You can then use them as parameter types. -=== "Python 3.6+ Argument" +=== "Python 3.7+ Argument" ```Python hl_lines="5" {!> ../docs_src/parameter_types/pydantic_types/tutorial001_an.py!} ``` -=== "Python 3.6+ Argument non-Annotated" +=== "Python 3.7+ Argument non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -36,13 +36,13 @@ You can then use them as parameter types. {!> ../docs_src/parameter_types/pydantic_types/tutorial001.py!} ``` -=== "Python 3.6+ Option" +=== "Python 3.7+ Option" ```Python hl_lines="5" {!> ../docs_src/parameter_types/pydantic_types/tutorial002_an.py!} ``` -=== "Python 3.6+ Option non-Annotated" +=== "Python 3.7+ Option non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -53,13 +53,13 @@ You can then use them as parameter types. These types are also supported in lists or tuples -=== "Python 3.6+ list" +=== "Python 3.7+ list" ```Python hl_lines="6" {!> ../docs_src/parameter_types/pydantic_types/tutorial003_an.py!} ``` -=== "Python 3.6+ list non-Annotated" +=== "Python 3.7+ list non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -68,13 +68,13 @@ These types are also supported in lists or tuples {!> ../docs_src/parameter_types/pydantic_types/tutorial003.py!} ``` -=== "Python 3.6+ tuple" +=== "Python 3.7+ tuple" ```Python hl_lines="6" {!> ../docs_src/parameter_types/pydantic_types/tutorial004_an.py!} ``` -=== "Python 3.6+ tuple non-Annotated" +=== "Python 3.7+ tuple non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. From 6039b4f96fd1abd428b69e43531a4072784de7e4 Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Fri, 26 Jul 2024 00:28:33 +0200 Subject: [PATCH 03/23] chore: remove email-validator from examples/testing dependencies Fixes #181 --- .../parameter-types/pydantic-types.md | 5 +++++ .../pydantic_types/tutorial001.py | 6 +++--- .../pydantic_types/tutorial001_an.py | 6 +++--- .../pydantic_types/tutorial002.py | 6 +++--- .../pydantic_types/tutorial002_an.py | 6 +++--- .../pydantic_types/tutorial004.py | 11 +++++----- .../pydantic_types/tutorial004_an.py | 11 +++++----- pyproject.toml | 2 +- requirements-tests.txt | 2 +- .../test_pydantic_types/test_tutorial001.py | 10 ++++----- .../test_tutorial001_an.py | 10 ++++----- .../test_pydantic_types/test_tutorial002.py | 12 +++++------ .../test_tutorial002_an.py | 12 +++++------ .../test_pydantic_types/test_tutorial004.py | 21 +++++++++++-------- .../test_tutorial004_an.py | 21 +++++++++++-------- 15 files changed, 75 insertions(+), 66 deletions(-) diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index d2762b0e3e..8a97903a58 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -14,6 +14,11 @@ Successfully installed typer rich pydantic $ pip install pydantic ---> 100% Successfully installed pydantic + +// Or if you want to use EmailStr +$ pip install "pydantic[email]" +---> 100% +Successfully installed pydantic, email-validator ``` diff --git a/docs_src/parameter_types/pydantic_types/tutorial001.py b/docs_src/parameter_types/pydantic_types/tutorial001.py index 4aec54161a..219aa7d788 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial001.py +++ b/docs_src/parameter_types/pydantic_types/tutorial001.py @@ -1,9 +1,9 @@ import typer -from pydantic import EmailStr +from pydantic import AnyHttpUrl -def main(email_arg: EmailStr): - typer.echo(f"email_arg: {email_arg}") +def main(url_arg: AnyHttpUrl): + typer.echo(f"url_arg: {url_arg}") if __name__ == "__main__": diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_an.py b/docs_src/parameter_types/pydantic_types/tutorial001_an.py index c92dbce546..b67db5b39a 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial001_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial001_an.py @@ -1,10 +1,10 @@ import typer -from pydantic import EmailStr +from pydantic import AnyHttpUrl from typing_extensions import Annotated -def main(email_arg: Annotated[EmailStr, typer.Argument()]): - typer.echo(f"email_arg: {email_arg}") +def main(url_arg: Annotated[AnyHttpUrl, typer.Argument()]): + typer.echo(f"url_arg: {url_arg}") if __name__ == "__main__": diff --git a/docs_src/parameter_types/pydantic_types/tutorial002.py b/docs_src/parameter_types/pydantic_types/tutorial002.py index 14ef540743..55491e43d7 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002.py @@ -1,9 +1,9 @@ import typer -from pydantic import EmailStr +from pydantic import AnyHttpUrl -def main(email_opt: EmailStr = typer.Option("tiangolo@gmail.com")): - typer.echo(f"email_opt: {email_opt}") +def main(url_opt: AnyHttpUrl = typer.Option("https://typer.tiangolo.com")): + typer.echo(f"url_opt: {url_opt}") if __name__ == "__main__": diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an.py b/docs_src/parameter_types/pydantic_types/tutorial002_an.py index bcf7cf5e15..3b31083dd6 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002_an.py @@ -1,10 +1,10 @@ import typer -from pydantic import EmailStr +from pydantic import AnyHttpUrl from typing_extensions import Annotated -def main(email_opt: Annotated[EmailStr, typer.Option()] = "tiangolo@gmail.com"): - typer.echo(f"email_opt: {email_opt}") +def main(url_opt: Annotated[AnyHttpUrl, typer.Option()] = "tiangolo@gmail.com"): + typer.echo(f"url_opt: {url_opt}") if __name__ == "__main__": diff --git a/docs_src/parameter_types/pydantic_types/tutorial004.py b/docs_src/parameter_types/pydantic_types/tutorial004.py index 66b7b71a25..0d1a889338 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004.py @@ -1,18 +1,17 @@ from typing import Tuple import typer -from pydantic import AnyHttpUrl, EmailStr +from pydantic import AnyHttpUrl, IPvAnyAddress def main( - user: Tuple[str, int, EmailStr, AnyHttpUrl] = typer.Option( - ..., help="User name, age, email and social media URL" + server: Tuple[str, IPvAnyAddress, AnyHttpUrl] = typer.Option( + ..., help="Server name, IP address and public URL" ), ): - name, age, email, url = user + name, address, url = server typer.echo(f"name: {name}") - typer.echo(f"age: {age}") - typer.echo(f"email: {email}") + typer.echo(f"address: {address}") typer.echo(f"url: {url}") diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an.py b/docs_src/parameter_types/pydantic_types/tutorial004_an.py index 9fa0ee5494..db24158c76 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004_an.py @@ -1,20 +1,19 @@ from typing import Tuple import typer -from pydantic import AnyHttpUrl, EmailStr +from pydantic import AnyHttpUrl, IPvAnyAddress from typing_extensions import Annotated def main( - user: Annotated[ - Tuple[str, int, EmailStr, AnyHttpUrl], + server: Annotated[ + Tuple[str, IPvAnyAddress, AnyHttpUrl], typer.Option(help="User name, age, email and social media URL"), ], ): - name, age, email, url = user + name, address, url = server typer.echo(f"name: {name}") - typer.echo(f"age: {age}") - typer.echo(f"email: {email}") + typer.echo(f"address: {address}") typer.echo(f"url: {url}") diff --git a/pyproject.toml b/pyproject.toml index 92cda7d9d0..d87e6361be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Changelog = "https://typer.tiangolo.com/release-notes/" standard = [ "shellingham >=1.3.0", "rich >=10.11.0", - "pydantic[email] >=2.0.0", + "pydantic >=2.0.0", ] [tool.pdm] diff --git a/requirements-tests.txt b/requirements-tests.txt index 36f90749a2..1a39e303e6 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,4 +10,4 @@ ruff ==0.6.5 # Needed explicitly by typer-slim rich >=10.11.0 shellingham >=1.3.0 -pydantic[email] >=2.0.0 +pydantic >=2.0.0 diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py index e8d226088b..6802abd41b 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py @@ -17,16 +17,16 @@ def test_help(): assert result.exit_code == 0 -def test_email_arg(): - result = runner.invoke(app, ["tiangolo@gmail.com"]) +def test_url_arg(): + result = runner.invoke(app, ["https://typer.tiangolo.com"]) assert result.exit_code == 0 - assert "email_arg: tiangolo@gmail.com" in result.output + assert "url_arg: https://typer.tiangolo.com" in result.output -def test_email_arg_invalid(): +def test_url_arg_invalid(): result = runner.invoke(app, ["invalid"]) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "Input should be a valid URL" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py index 167c1ce3a8..ab6d6213ab 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py @@ -17,16 +17,16 @@ def test_help(): assert result.exit_code == 0 -def test_email_arg(): - result = runner.invoke(app, ["tiangolo@gmail.com"]) +def test_url_arg(): + result = runner.invoke(app, ["https://typer.tiangolo.com"]) assert result.exit_code == 0 - assert "email_arg: tiangolo@gmail.com" in result.output + assert "url_arg: https://typer.tiangolo.com" in result.output -def test_email_arg_invalid(): +def test_url_arg_invalid(): result = runner.invoke(app, ["invalid"]) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "Input should be a valid URL" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py index 265e1d3191..54d33208d2 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py @@ -17,16 +17,16 @@ def test_help(): assert result.exit_code == 0 -def test_email_opt(): - result = runner.invoke(app, ["--email-opt", "tiangolo@gmail.com"]) +def test_url_opt(): + result = runner.invoke(app, ["--url-opt", "https://typer.tiangolo.com"]) assert result.exit_code == 0 - assert "email_opt: tiangolo@gmail.com" in result.output + assert "url_opt: https://typer.tiangolo.com" in result.output -def test_email_opt_invalid(): - result = runner.invoke(app, ["--email-opt", "invalid"]) +def test_url_opt_invalid(): + result = runner.invoke(app, ["--url-opt", "invalid"]) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "Input should be a valid URL" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py index 1d0475d009..6c9c598fdd 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py @@ -17,16 +17,16 @@ def test_help(): assert result.exit_code == 0 -def test_email_opt(): - result = runner.invoke(app, ["--email-opt", "tiangolo@gmail.com"]) +def test_url_opt(): + result = runner.invoke(app, ["--url-opt", "https://typer.tiangolo.com"]) assert result.exit_code == 0 - assert "email_opt: tiangolo@gmail.com" in result.output + assert "url_opt: https://typer.tiangolo.com" in result.output -def test_email_opt_invalid(): - result = runner.invoke(app, ["--email-opt", "invalid"]) +def test_url_opt_invalid(): + result = runner.invoke(app, ["--url-opt", "invalid"]) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "Input should be a valid URL" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py index e51c2b5b89..5fa549b0cb 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -18,22 +18,25 @@ def test_help(): def test_tuple(): - result = runner.invoke( - app, ["--user", "Camila", "23", "camila@example.org", "https://example.com"] - ) + result = runner.invoke(app, ["--server", "Example", "::1", "https://example.com"]) assert result.exit_code == 0 - assert "name: Camila" in result.output - assert "age: 23" in result.output - assert "email: camila@example.org" in result.output + assert "name: Example" in result.output + assert "address: ::1" in result.output assert "url: https://example.com" in result.output -def test_tuple_invalid(): +def test_tuple_invalid_ip(): result = runner.invoke( - app, ["--user", "Camila", "23", "invalid", "https://example.com"] + app, ["--server", "Invalid", "invalid", "https://example.com"] ) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "value is not a valid IPv4 or IPv6 address" in result.output + + +def test_tuple_invalid_url(): + result = runner.invoke(app, ["--server", "Invalid", "::1", "invalid"]) + assert result.exit_code != 0 + assert "Input should be a valid URL" in result.output def test_script(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py index dde6671976..7849a3e2ae 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py @@ -18,22 +18,25 @@ def test_help(): def test_tuple(): - result = runner.invoke( - app, ["--user", "Camila", "23", "camila@example.org", "https://example.com"] - ) + result = runner.invoke(app, ["--server", "Example", "::1", "https://example.com"]) assert result.exit_code == 0 - assert "name: Camila" in result.output - assert "age: 23" in result.output - assert "email: camila@example.org" in result.output + assert "name: Example" in result.output + assert "address: ::1" in result.output assert "url: https://example.com" in result.output -def test_tuple_invalid(): +def test_tuple_invalid_ip(): result = runner.invoke( - app, ["--user", "Camila", "23", "invalid", "https://example.com"] + app, ["--server", "Invalid", "invalid", "https://example.com"] ) assert result.exit_code != 0 - assert "value is not a valid email address" in result.output + assert "value is not a valid IPv4 or IPv6 address" in result.output + + +def test_tuple_invalid_url(): + result = runner.invoke(app, ["--server", "Invalid", "::1", "invalid"]) + assert result.exit_code != 0 + assert "Input should be a valid URL" in result.output def test_script(): From c38fefcf8844ec6d44695cd6590a3727f78fcece Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Fri, 26 Jul 2024 00:29:58 +0200 Subject: [PATCH 04/23] fix: correct is_pydantic_type for python 3.12 --- typer/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/typer/main.py b/typer/main.py index 474943c9bc..50f2816457 100644 --- a/typer/main.py +++ b/typer/main.py @@ -12,8 +12,7 @@ from uuid import UUID import click -from typing_extensions import get_args, get_origin -from typing_extensions import TypeAlias +from typing_extensions import Annotated, TypeAlias, get_args, get_origin from ._typing import is_union from .completion import get_completion_inspect_parameters @@ -61,6 +60,8 @@ import pydantic def is_pydantic_type(type_: Any) -> bool: + if get_origin(type_) is Annotated: + return is_pydantic_type(get_args(type_)[0]) return type_.__module__.startswith("pydantic") and not lenient_issubclass( type_, pydantic.BaseModel ) @@ -847,9 +848,7 @@ def lenient_issubclass( def is_complex_subtype(type_: Any) -> bool: # For pydantic types, such as `AnyUrl`, there's an extra `Annotated` layer that we don't need to treat as complex - return getattr(type_, "__origin__", None) is not None and not is_pydantic_type( - type_ - ) + return get_origin(type_) is not None and not is_pydantic_type(type_) def get_click_param( From e0ad01b81c6894d25d02cbda31f8a3db34a77632 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Fri, 26 Jul 2024 11:34:32 +0200 Subject: [PATCH 05/23] Fix default value to URL instead of email --- docs_src/parameter_types/pydantic_types/tutorial002_an.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an.py b/docs_src/parameter_types/pydantic_types/tutorial002_an.py index 3b31083dd6..649f4463d1 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002_an.py @@ -3,7 +3,7 @@ from typing_extensions import Annotated -def main(url_opt: Annotated[AnyHttpUrl, typer.Option()] = "tiangolo@gmail.com"): +def main(url_opt: Annotated[AnyHttpUrl, typer.Option()] = "https://typer.tiangolo.com"): typer.echo(f"url_opt: {url_opt}") From 0ff28bad697ccff3fc3315410a9ade5bf534ce75 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Fri, 26 Jul 2024 11:53:18 +0200 Subject: [PATCH 06/23] Clarify pydantic as additional extra dependency throughout the docs --- README.md | 5 +-- docs/index.md | 5 +-- docs/release-notes.md | 4 +-- docs/tutorial/index.md | 31 +++++++++++++++++++ .../parameter-types/pydantic-types.md | 8 ++--- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b1e8c17eb2..ad90516b60 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Create and activate a 100% -Successfully installed typer rich shellingham +Successfully installed typer rich shellingham pydantic ``` @@ -355,6 +355,7 @@ For a more complete example including more features, see the rich: to show nicely formatted errors automatically. +* pydantic: to support the usage of Pydantic types. * shellingham: to automatically detect the current shell when installing completion. * With `shellingham` you can just use `--install-completion`. * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. @@ -375,7 +376,7 @@ pip install typer pip install "typer-slim[standard]" ``` -The `standard` extra dependencies are `rich` and `shellingham`. +The `standard` extra dependencies are `rich`, `shellingham` and `pydantic`. **Note**: The `typer` command is only included in the `typer` package. diff --git a/docs/index.md b/docs/index.md index 355380729a..1a4d2b44ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,7 +59,7 @@ Create and activate a 100% -Successfully installed typer rich shellingham +Successfully installed typer rich shellingham pydantic ``` @@ -361,6 +361,7 @@ For a more complete example including more features, see the rich: to show nicely formatted errors automatically. +* pydantic: to support the usage of Pydantic types. * shellingham: to automatically detect the current shell when installing completion. * With `shellingham` you can just use `--install-completion`. * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. @@ -381,7 +382,7 @@ pip install typer pip install "typer-slim[standard]" ``` -The `standard` extra dependencies are `rich` and `shellingham`. +The `standard` extra dependencies are `rich`, `shellingham` and `pydantic`. **Note**: The `typer` command is only included in the `typer` package. diff --git a/docs/release-notes.md b/docs/release-notes.md index 42bb70e3d0..5c05ef4a84 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -164,7 +164,7 @@ Now you don't need to install `typer[all]`. When you install `typer` it comes with the default optional dependencies and the `typer` command. -If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead. +If you don't want the extra optional dependencies (`rich`, `shellingham` and `pydantic`), you can install `typer-slim` instead. You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command. @@ -190,7 +190,7 @@ By installing the latest version (`0.12.1`) it fixes it, for any previous versio In version `0.12.0`, the `typer` package depends on `typer-slim[standard]` which includes the default dependencies (instead of `typer[all]`) and `typer-cli` (that provides the `typer` command). -If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead. +If you don't want the extra optional dependencies (`rich`, `shellingham` and `pydantic`), you can install `typer-slim` instead. You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index bad806c88a..983dbe9493 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -62,3 +62,34 @@ Using it in your editor is what really shows you the benefits of **Typer**, seei And running the examples is what will really help you **understand** what is going on. You can learn a lot more by **running some examples** and **playing around** with them than by reading all the docs here. + +--- + +## Install **Typer** + +The first step is to install **Typer**: + +
+ +```console +$ pip install typer +---> 100% +Successfully installed typer click shellingham rich pydantic +``` + +
+ +By default, `typer` comes with `rich`, `shellingham` and `pydantic`. + +!!! note + If you are an advanced user and want to opt out of these default extra dependencies, you can instead install `typer-slim`. + + ```bash + pip install typer + ``` + +...includes the same optional dependencies as: + + ```bash + pip install "typer-slim[standard]" + ``` diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index 8a97903a58..b78d33d0e2 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -1,14 +1,14 @@ Pydantic types such as [AnyUrl](https://docs.pydantic.dev/latest/api/networks/#pydantic.networks.AnyUrl) or [EmailStr](https://docs.pydantic.dev/latest/api/networks/#pydantic.networks.EmailStr) can be very convenient to describe and validate some parameters. -You can add pydantic from typer's optional dependencies +Pydantic is installed automatically when installing Typer with its extra standard dependencies:
```console -// Pydantic comes with typer[standard] -$ pip install "typer[standard]" +// Pydantic comes with typer +$ pip install typer ---> 100% -Successfully installed typer rich pydantic +Successfully installed typer rich shellingham pydantic // Alternatively, you can install Pydantic independently $ pip install pydantic From 6dccd26840bf0e25343653bb277c3e09ff8afe23 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 26 Aug 2024 15:18:07 +0200 Subject: [PATCH 07/23] ignore type error for now --- typer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 50f2816457..f8e1ad14fa 100644 --- a/typer/main.py +++ b/typer/main.py @@ -60,7 +60,7 @@ import pydantic def is_pydantic_type(type_: Any) -> bool: - if get_origin(type_) is Annotated: + if get_origin(type_) is Annotated: # type: ignore return is_pydantic_type(get_args(type_)[0]) return type_.__module__.startswith("pydantic") and not lenient_issubclass( type_, pydantic.BaseModel From 10e79864a1986b9cc879308803efcef7422bd4ae Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 3 Sep 2024 15:23:01 +0200 Subject: [PATCH 08/23] remove unnecessary ignore statement --- typer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index f8e1ad14fa..50f2816457 100644 --- a/typer/main.py +++ b/typer/main.py @@ -60,7 +60,7 @@ import pydantic def is_pydantic_type(type_: Any) -> bool: - if get_origin(type_) is Annotated: # type: ignore + if get_origin(type_) is Annotated: return is_pydantic_type(get_args(type_)[0]) return type_.__module__.startswith("pydantic") and not lenient_issubclass( type_, pydantic.BaseModel From 893d86e95eb4d2abaff0549127e2a2bb2eb1a881 Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Wed, 4 Sep 2024 21:38:22 +0200 Subject: [PATCH 09/23] simplify pydantic type conversion using pydantic.TypeAdapter --- typer/main.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/typer/main.py b/typer/main.py index 50f2816457..bc1caa5bb5 100644 --- a/typer/main.py +++ b/typer/main.py @@ -8,7 +8,17 @@ from pathlib import Path from traceback import FrameSummary, StackSummary from types import TracebackType -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, +) from uuid import UUID import click @@ -69,14 +79,11 @@ def is_pydantic_type(type_: Any) -> bool: def pydantic_convertor(type_: type) -> Callable[[str], Any]: """Create a convertor for a parameter annotated with a pydantic type.""" T: TypeAlias = type_ # type: ignore[valid-type] - - @pydantic.validate_call - def internal_convertor(value: T) -> T: - return value + adapter: pydantic.TypeAdapter[T] = pydantic.TypeAdapter(type_) def convertor(value: str) -> T: try: - return internal_convertor(value) + return adapter.validate_python(value) except pydantic.ValidationError as e: error_message = e.errors( include_context=False, include_input=False, include_url=False From 7608cb4f5d05214860047a900dd120da4b7c5d1f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 07:17:09 +0000 Subject: [PATCH 10/23] =?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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index d979db1031..7a40121157 100644 --- a/typer/main.py +++ b/typer/main.py @@ -901,8 +901,9 @@ def get_click_param( # Handle Tuples and Lists if lenient_issubclass(origin, List): main_type = get_args(main_type)[0] - assert not is_complex_subtype(main_type), \ + assert not is_complex_subtype(main_type), ( "List types with complex sub-types are not currently supported" + ) is_list = True elif lenient_issubclass(origin, Tuple): # type: ignore types = [] From 3d967ca7e2847714446e2c4339d59e024e0cb53d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:26:52 +0000 Subject: [PATCH 11/23] =?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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 6028905ebf..509d7ab652 100644 --- a/typer/main.py +++ b/typer/main.py @@ -25,8 +25,8 @@ from uuid import UUID import click -from typing_extensions import Annotated, TypeAlias from typer._types import TyperChoice +from typing_extensions import Annotated, TypeAlias from ._typing import get_args, get_origin, is_union from .completion import get_completion_inspect_parameters From b1025d2f64aba6ff69e50a4d7e15dd8306ba2106 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 28 Aug 2025 14:33:49 +0200 Subject: [PATCH 12/23] update docs to new format --- .../parameter-types/pydantic-types.md | 62 ++----------------- 1 file changed, 5 insertions(+), 57 deletions(-) diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index b78d33d0e2..a798d6025d 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -26,64 +26,12 @@ Successfully installed pydantic, email-validator You can then use them as parameter types. -=== "Python 3.7+ Argument" +{* docs_src/parameter_types/pydantic_types/tutorial001_an.py hl[5] *} - ```Python hl_lines="5" - {!> ../docs_src/parameter_types/pydantic_types/tutorial001_an.py!} - ``` +{* docs_src/parameter_types/pydantic_types/tutorial002_an.py hl[5] *} -=== "Python 3.7+ Argument non-Annotated" +These types are also supported in lists or tuples: - !!! tip - Prefer to use the `Annotated` version if possible. +{* docs_src/parameter_types/pydantic_types/tutorial003_an.py hl[6] *} - ```Python hl_lines="4" - {!> ../docs_src/parameter_types/pydantic_types/tutorial001.py!} - ``` - -=== "Python 3.7+ Option" - - ```Python hl_lines="5" - {!> ../docs_src/parameter_types/pydantic_types/tutorial002_an.py!} - ``` - -=== "Python 3.7+ Option non-Annotated" - - !!! tip - Prefer to use the `Annotated` version if possible. - - ```Python hl_lines="4" - {!> ../docs_src/parameter_types/pydantic_types/tutorial002.py!} - ``` - -These types are also supported in lists or tuples - -=== "Python 3.7+ list" - - ```Python hl_lines="6" - {!> ../docs_src/parameter_types/pydantic_types/tutorial003_an.py!} - ``` - -=== "Python 3.7+ list non-Annotated" - - !!! tip - Prefer to use the `Annotated` version if possible. - - ```Python hl_lines="5" - {!> ../docs_src/parameter_types/pydantic_types/tutorial003.py!} - ``` - -=== "Python 3.7+ tuple" - - ```Python hl_lines="6" - {!> ../docs_src/parameter_types/pydantic_types/tutorial004_an.py!} - ``` - -=== "Python 3.7+ tuple non-Annotated" - - !!! tip - Prefer to use the `Annotated` version if possible. - - ```Python hl_lines="5" - {!> ../docs_src/parameter_types/pydantic_types/tutorial004.py!} - ``` +{* docs_src/parameter_types/pydantic_types/tutorial004_an.py hl[6] *} From eff275d69345483f9688a7fcd8c09aa4cee3e10d Mon Sep 17 00:00:00 2001 From: svlandeg Date: Fri, 3 Oct 2025 17:50:07 +0200 Subject: [PATCH 13/23] ignore statement from coverage (as discussed) --- typer/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 372bd48f3a..7c0107b25d 100644 --- a/typer/main.py +++ b/typer/main.py @@ -66,7 +66,8 @@ def is_pydantic_type(type_: Any) -> bool: if get_origin(type_) is Annotated: - return is_pydantic_type(get_args(type_)[0]) + # While this is excluded from coverage, we need this check for older versions of Pydantic 2, cf PR 723 + return is_pydantic_type(get_args(type_)[0]) # pragma: no cover return type_.__module__.startswith("pydantic") and not lenient_issubclass( type_, pydantic.BaseModel ) From 9d1ef994a2aa03b94ebd582a66df317f3bc34839 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 25 Nov 2025 16:33:36 +0100 Subject: [PATCH 14/23] update tutorial files to use explicit Typer() --- docs/tutorial/parameter-types/pydantic-types.md | 8 ++++---- docs_src/parameter_types/pydantic_types/tutorial001.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial001_an.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial002.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial002_an.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial003.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial003_an.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial004.py | 5 ++++- docs_src/parameter_types/pydantic_types/tutorial004_an.py | 5 ++++- .../test_pydantic_types/test_tutorial001.py | 5 +---- .../test_pydantic_types/test_tutorial001_an.py | 5 +---- .../test_pydantic_types/test_tutorial002.py | 5 +---- .../test_pydantic_types/test_tutorial002_an.py | 5 +---- .../test_pydantic_types/test_tutorial003.py | 5 +---- .../test_pydantic_types/test_tutorial003_an.py | 5 +---- .../test_pydantic_types/test_tutorial004.py | 5 +---- .../test_pydantic_types/test_tutorial004_an.py | 5 +---- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index a798d6025d..713fdc947b 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -26,12 +26,12 @@ Successfully installed pydantic, email-validator You can then use them as parameter types. -{* docs_src/parameter_types/pydantic_types/tutorial001_an.py hl[5] *} +{* docs_src/parameter_types/pydantic_types/tutorial001_an.py hl[9] *} -{* docs_src/parameter_types/pydantic_types/tutorial002_an.py hl[5] *} +{* docs_src/parameter_types/pydantic_types/tutorial002_an.py hl[9] *} These types are also supported in lists or tuples: -{* docs_src/parameter_types/pydantic_types/tutorial003_an.py hl[6] *} +{* docs_src/parameter_types/pydantic_types/tutorial003_an.py hl[9] *} -{* docs_src/parameter_types/pydantic_types/tutorial004_an.py hl[6] *} +{* docs_src/parameter_types/pydantic_types/tutorial004_an.py hl[9] *} diff --git a/docs_src/parameter_types/pydantic_types/tutorial001.py b/docs_src/parameter_types/pydantic_types/tutorial001.py index 219aa7d788..f6e2cbbfc8 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial001.py +++ b/docs_src/parameter_types/pydantic_types/tutorial001.py @@ -1,10 +1,13 @@ import typer from pydantic import AnyHttpUrl +app = typer.Typer() + +@app.command() def main(url_arg: AnyHttpUrl): typer.echo(f"url_arg: {url_arg}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_an.py b/docs_src/parameter_types/pydantic_types/tutorial001_an.py index b67db5b39a..a16be774ed 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial001_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial001_an.py @@ -2,10 +2,13 @@ from pydantic import AnyHttpUrl from typing_extensions import Annotated +app = typer.Typer() + +@app.command() def main(url_arg: Annotated[AnyHttpUrl, typer.Argument()]): typer.echo(f"url_arg: {url_arg}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial002.py b/docs_src/parameter_types/pydantic_types/tutorial002.py index 55491e43d7..6566a0dad9 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002.py @@ -1,10 +1,13 @@ import typer from pydantic import AnyHttpUrl +app = typer.Typer() + +@app.command() def main(url_opt: AnyHttpUrl = typer.Option("https://typer.tiangolo.com")): typer.echo(f"url_opt: {url_opt}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an.py b/docs_src/parameter_types/pydantic_types/tutorial002_an.py index 649f4463d1..9c5cc8ee56 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002_an.py @@ -2,10 +2,13 @@ from pydantic import AnyHttpUrl from typing_extensions import Annotated +app = typer.Typer() + +@app.command() def main(url_opt: Annotated[AnyHttpUrl, typer.Option()] = "https://typer.tiangolo.com"): typer.echo(f"url_opt: {url_opt}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial003.py b/docs_src/parameter_types/pydantic_types/tutorial003.py index c1b13964be..5db2942d20 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial003.py +++ b/docs_src/parameter_types/pydantic_types/tutorial003.py @@ -3,10 +3,13 @@ import typer from pydantic import AnyHttpUrl +app = typer.Typer() + +@app.command() def main(urls: List[AnyHttpUrl] = typer.Option([], "--url")): typer.echo(f"urls: {urls}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an.py b/docs_src/parameter_types/pydantic_types/tutorial003_an.py index 61b816243e..0adb6c5a40 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial003_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial003_an.py @@ -4,7 +4,10 @@ from pydantic import AnyHttpUrl from typing_extensions import Annotated +app = typer.Typer() + +@app.command() def main( urls: Annotated[List[AnyHttpUrl], typer.Option("--url", default_factory=list)], ): @@ -12,4 +15,4 @@ def main( if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial004.py b/docs_src/parameter_types/pydantic_types/tutorial004.py index 0d1a889338..6d00874ef5 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004.py @@ -3,7 +3,10 @@ import typer from pydantic import AnyHttpUrl, IPvAnyAddress +app = typer.Typer() + +@app.command() def main( server: Tuple[str, IPvAnyAddress, AnyHttpUrl] = typer.Option( ..., help="Server name, IP address and public URL" @@ -16,4 +19,4 @@ def main( if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an.py b/docs_src/parameter_types/pydantic_types/tutorial004_an.py index db24158c76..f8bc3435a1 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004_an.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004_an.py @@ -4,7 +4,10 @@ from pydantic import AnyHttpUrl, IPvAnyAddress from typing_extensions import Annotated +app = typer.Typer() + +@app.command() def main( server: Annotated[ Tuple[str, IPvAnyAddress, AnyHttpUrl], @@ -18,4 +21,4 @@ def main( if __name__ == "__main__": - typer.run(main) + app() diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py index 6802abd41b..c3599f39a1 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial001 as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py index ab6d6213ab..e7c5e901fb 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial001_an as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py index 54d33208d2..269fe682ed 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial002 as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py index 6c9c598fdd..ea5a323990 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial002_an as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py index b9a9018e04..ddb1ead76c 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial003 as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py index 487fffc55b..766e9e4b68 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial003_an as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py index 5fa549b0cb..5976773ae3 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial004 as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py index 7849a3e2ae..5b553bee24 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py @@ -1,15 +1,12 @@ import subprocess import sys -import typer from typer.testing import CliRunner from docs_src.parameter_types.pydantic_types import tutorial004_an as mod runner = CliRunner() - -app = typer.Typer() -app.command()(mod.main) +app = mod.app def test_help(): From 6fa8211e8166824fc40cf014ce7961f6f98a180f Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 10:33:35 +0100 Subject: [PATCH 15/23] rename test files to be compatible with current master --- .../{tutorial001_an.py => tutorial001_an_py39.py} | 0 .../pydantic_types/{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002_an.py => tutorial002_an_py39.py} | 0 .../pydantic_types/{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003_an.py => tutorial003_an_py39.py} | 0 .../pydantic_types/{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004_an.py => tutorial004_an_py39.py} | 0 .../pydantic_types/{tutorial004.py => tutorial004_py39.py} | 0 .../test_pydantic_types/test_tutorial001.py | 2 +- .../test_pydantic_types/test_tutorial001_an.py | 2 +- .../test_pydantic_types/test_tutorial002.py | 2 +- .../test_pydantic_types/test_tutorial002_an.py | 2 +- .../test_pydantic_types/test_tutorial003.py | 2 +- .../test_pydantic_types/test_tutorial003_an.py | 2 +- .../test_pydantic_types/test_tutorial004.py | 2 +- .../test_pydantic_types/test_tutorial004_an.py | 2 +- 16 files changed, 8 insertions(+), 8 deletions(-) rename docs_src/parameter_types/pydantic_types/{tutorial001_an.py => tutorial001_an_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial002_an.py => tutorial002_an_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial003_an.py => tutorial003_an_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial004_an.py => tutorial004_an_py39.py} (100%) rename docs_src/parameter_types/pydantic_types/{tutorial004.py => tutorial004_py39.py} (100%) diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_an.py b/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial001_an.py rename to docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial001.py b/docs_src/parameter_types/pydantic_types/tutorial001_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial001.py rename to docs_src/parameter_types/pydantic_types/tutorial001_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an.py b/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial002_an.py rename to docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial002.py b/docs_src/parameter_types/pydantic_types/tutorial002_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial002.py rename to docs_src/parameter_types/pydantic_types/tutorial002_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an.py b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial003_an.py rename to docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial003.py b/docs_src/parameter_types/pydantic_types/tutorial003_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial003.py rename to docs_src/parameter_types/pydantic_types/tutorial003_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an.py b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial004_an.py rename to docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py diff --git a/docs_src/parameter_types/pydantic_types/tutorial004.py b/docs_src/parameter_types/pydantic_types/tutorial004_py39.py similarity index 100% rename from docs_src/parameter_types/pydantic_types/tutorial004.py rename to docs_src/parameter_types/pydantic_types/tutorial004_py39.py diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py index c3599f39a1..fcf56ce997 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial001 as mod +from docs_src.parameter_types.pydantic_types import tutorial001_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py index e7c5e901fb..edea22ed5e 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial001_an as mod +from docs_src.parameter_types.pydantic_types import tutorial001_an_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py index 269fe682ed..ea2a17280a 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial002 as mod +from docs_src.parameter_types.pydantic_types import tutorial002_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py index ea5a323990..316fdac32a 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial002_an as mod +from docs_src.parameter_types.pydantic_types import tutorial002_an_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py index ddb1ead76c..7241808a14 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial003 as mod +from docs_src.parameter_types.pydantic_types import tutorial003_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py index 766e9e4b68..508c2fb477 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial003_an as mod +from docs_src.parameter_types.pydantic_types import tutorial003_an_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py index 5976773ae3..b5bce2fb14 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial004 as mod +from docs_src.parameter_types.pydantic_types import tutorial004_py39 as mod runner = CliRunner() app = mod.app diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py index 5b553bee24..ca06a31318 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial004_an as mod +from docs_src.parameter_types.pydantic_types import tutorial004_an_py39 as mod runner = CliRunner() app = mod.app From 8550962bd89907223a553e1a876aa4c5516f0e72 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 10:41:15 +0100 Subject: [PATCH 16/23] also fix renaming in markdown file --- docs/tutorial/parameter-types/pydantic-types.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/parameter-types/pydantic-types.md b/docs/tutorial/parameter-types/pydantic-types.md index 713fdc947b..9623d86275 100644 --- a/docs/tutorial/parameter-types/pydantic-types.md +++ b/docs/tutorial/parameter-types/pydantic-types.md @@ -26,12 +26,12 @@ Successfully installed pydantic, email-validator You can then use them as parameter types. -{* docs_src/parameter_types/pydantic_types/tutorial001_an.py hl[9] *} +{* docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py hl[9] *} -{* docs_src/parameter_types/pydantic_types/tutorial002_an.py hl[9] *} +{* docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py hl[9] *} These types are also supported in lists or tuples: -{* docs_src/parameter_types/pydantic_types/tutorial003_an.py hl[9] *} +{* docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py hl[9] *} -{* docs_src/parameter_types/pydantic_types/tutorial004_an.py hl[9] *} +{* docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py hl[9] *} From 8e0fb625f980bb4a2b771826db827f628fb343b9 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 10:57:01 +0100 Subject: [PATCH 17/23] use new format to generalize test files with a fixture --- .../test_pydantic_types/test_tutorial001.py | 33 +++++++++----- .../test_tutorial001_an.py | 35 --------------- .../test_pydantic_types/test_tutorial002.py | 33 +++++++++----- .../test_tutorial002_an.py | 35 --------------- .../test_pydantic_types/test_tutorial003.py | 33 +++++++++----- .../test_tutorial003_an.py | 38 ---------------- .../test_pydantic_types/test_tutorial004.py | 37 ++++++++++----- .../test_tutorial004_an.py | 45 ------------------- 8 files changed, 94 insertions(+), 195 deletions(-) delete mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py delete mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py delete mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py index fcf56ce997..02f7e2a0a1 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py @@ -1,32 +1,45 @@ +import importlib import subprocess import sys +from types import ModuleType +import pytest from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial001_py39 as mod - runner = CliRunner() -app = mod.app -def test_help(): - result = runner.invoke(app, ["--help"]) +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.parameter_types.pydantic_types.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) assert result.exit_code == 0 -def test_url_arg(): - result = runner.invoke(app, ["https://typer.tiangolo.com"]) +def test_url_arg(mod: ModuleType): + result = runner.invoke(mod.app, ["https://typer.tiangolo.com"]) assert result.exit_code == 0 assert "url_arg: https://typer.tiangolo.com" in result.output -def test_url_arg_invalid(): - result = runner.invoke(app, ["invalid"]) +def test_url_arg_invalid(mod: ModuleType): + result = runner.invoke(mod.app, ["invalid"]) assert result.exit_code != 0 assert "Input should be a valid URL" in result.output -def test_script(): +def test_script(mod: ModuleType): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], capture_output=True, diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py deleted file mode 100644 index edea22ed5e..0000000000 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001_an.py +++ /dev/null @@ -1,35 +0,0 @@ -import subprocess -import sys - -from typer.testing import CliRunner - -from docs_src.parameter_types.pydantic_types import tutorial001_an_py39 as mod - -runner = CliRunner() -app = mod.app - - -def test_help(): - result = runner.invoke(app, ["--help"]) - assert result.exit_code == 0 - - -def test_url_arg(): - result = runner.invoke(app, ["https://typer.tiangolo.com"]) - assert result.exit_code == 0 - assert "url_arg: https://typer.tiangolo.com" in result.output - - -def test_url_arg_invalid(): - result = runner.invoke(app, ["invalid"]) - assert result.exit_code != 0 - assert "Input should be a valid URL" in result.output - - -def test_script(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py index ea2a17280a..7e9756087a 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py @@ -1,32 +1,45 @@ +import importlib import subprocess import sys +from types import ModuleType +import pytest from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial002_py39 as mod - runner = CliRunner() -app = mod.app -def test_help(): - result = runner.invoke(app, ["--help"]) +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.parameter_types.pydantic_types.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) assert result.exit_code == 0 -def test_url_opt(): - result = runner.invoke(app, ["--url-opt", "https://typer.tiangolo.com"]) +def test_url_opt(mod: ModuleType): + result = runner.invoke(mod.app, ["--url-opt", "https://typer.tiangolo.com"]) assert result.exit_code == 0 assert "url_opt: https://typer.tiangolo.com" in result.output -def test_url_opt_invalid(): - result = runner.invoke(app, ["--url-opt", "invalid"]) +def test_url_opt_invalid(mod: ModuleType): + result = runner.invoke(mod.app, ["--url-opt", "invalid"]) assert result.exit_code != 0 assert "Input should be a valid URL" in result.output -def test_script(): +def test_script(mod: ModuleType): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], capture_output=True, diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py deleted file mode 100644 index 316fdac32a..0000000000 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002_an.py +++ /dev/null @@ -1,35 +0,0 @@ -import subprocess -import sys - -from typer.testing import CliRunner - -from docs_src.parameter_types.pydantic_types import tutorial002_an_py39 as mod - -runner = CliRunner() -app = mod.app - - -def test_help(): - result = runner.invoke(app, ["--help"]) - assert result.exit_code == 0 - - -def test_url_opt(): - result = runner.invoke(app, ["--url-opt", "https://typer.tiangolo.com"]) - assert result.exit_code == 0 - assert "url_opt: https://typer.tiangolo.com" in result.output - - -def test_url_opt_invalid(): - result = runner.invoke(app, ["--url-opt", "invalid"]) - assert result.exit_code != 0 - assert "Input should be a valid URL" in result.output - - -def test_script(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py index 7241808a14..cebb251f0d 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py @@ -1,35 +1,48 @@ +import importlib import subprocess import sys +from types import ModuleType +import pytest from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial003_py39 as mod - runner = CliRunner() -app = mod.app -def test_help(): - result = runner.invoke(app, ["--help"]) +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial003_py39"), + pytest.param("tutorial003_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.parameter_types.pydantic_types.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) assert result.exit_code == 0 -def test_url_list(): +def test_url_list(mod: ModuleType): result = runner.invoke( - app, ["--url", "https://example.com", "--url", "https://example.org"] + mod.app, ["--url", "https://example.com", "--url", "https://example.org"] ) assert result.exit_code == 0 assert "https://example.com" in result.output assert "https://example.org" in result.output -def test_url_invalid(): - result = runner.invoke(app, ["--url", "invalid", "--url", "https://example.org"]) +def test_url_invalid(mod: ModuleType): + result = runner.invoke(mod.app, ["--url", "invalid", "--url", "https://example.org"]) assert result.exit_code != 0 assert "Input should be a valid URL" in result.output -def test_script(): +def test_script(mod: ModuleType): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], capture_output=True, diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py deleted file mode 100644 index 508c2fb477..0000000000 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003_an.py +++ /dev/null @@ -1,38 +0,0 @@ -import subprocess -import sys - -from typer.testing import CliRunner - -from docs_src.parameter_types.pydantic_types import tutorial003_an_py39 as mod - -runner = CliRunner() -app = mod.app - - -def test_help(): - result = runner.invoke(app, ["--help"]) - assert result.exit_code == 0 - - -def test_url_list(): - result = runner.invoke( - app, ["--url", "https://example.com", "--url", "https://example.org"] - ) - assert result.exit_code == 0 - assert "https://example.com" in result.output - assert "https://example.org" in result.output - - -def test_url_invalid(): - result = runner.invoke(app, ["--url", "invalid", "--url", "https://example.org"]) - assert result.exit_code != 0 - assert "Input should be a valid URL" in result.output - - -def test_script(): - result = subprocess.run( - [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], - capture_output=True, - encoding="utf-8", - ) - assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py index b5bce2fb14..ec9708d8b1 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -1,42 +1,55 @@ +import importlib import subprocess import sys +from types import ModuleType +import pytest from typer.testing import CliRunner -from docs_src.parameter_types.pydantic_types import tutorial004_py39 as mod - runner = CliRunner() -app = mod.app -def test_help(): - result = runner.invoke(app, ["--help"]) +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.parameter_types.pydantic_types.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) assert result.exit_code == 0 -def test_tuple(): - result = runner.invoke(app, ["--server", "Example", "::1", "https://example.com"]) +def test_tuple(mod: ModuleType): + result = runner.invoke(mod.app, ["--server", "Example", "::1", "https://example.com"]) assert result.exit_code == 0 assert "name: Example" in result.output assert "address: ::1" in result.output assert "url: https://example.com" in result.output -def test_tuple_invalid_ip(): +def test_tuple_invalid_ip(mod: ModuleType): result = runner.invoke( - app, ["--server", "Invalid", "invalid", "https://example.com"] + mod.app, ["--server", "Invalid", "invalid", "https://example.com"] ) assert result.exit_code != 0 assert "value is not a valid IPv4 or IPv6 address" in result.output -def test_tuple_invalid_url(): - result = runner.invoke(app, ["--server", "Invalid", "::1", "invalid"]) +def test_tuple_invalid_url(mod: ModuleType): + result = runner.invoke(mod.app, ["--server", "Invalid", "::1", "invalid"]) assert result.exit_code != 0 assert "Input should be a valid URL" in result.output -def test_script(): +def test_script(mod: ModuleType): result = subprocess.run( [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], capture_output=True, diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py deleted file mode 100644 index ca06a31318..0000000000 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004_an.py +++ /dev/null @@ -1,45 +0,0 @@ -import subprocess -import sys - -from typer.testing import CliRunner - -from docs_src.parameter_types.pydantic_types import tutorial004_an_py39 as mod - -runner = CliRunner() -app = mod.app - - -def test_help(): - result = runner.invoke(app, ["--help"]) - assert result.exit_code == 0 - - -def test_tuple(): - result = runner.invoke(app, ["--server", "Example", "::1", "https://example.com"]) - assert result.exit_code == 0 - assert "name: Example" in result.output - assert "address: ::1" in result.output - assert "url: https://example.com" in result.output - - -def test_tuple_invalid_ip(): - result = runner.invoke( - app, ["--server", "Invalid", "invalid", "https://example.com"] - ) - assert result.exit_code != 0 - assert "value is not a valid IPv4 or IPv6 address" in result.output - - -def test_tuple_invalid_url(): - result = runner.invoke(app, ["--server", "Invalid", "::1", "invalid"]) - assert result.exit_code != 0 - assert "Input should be a valid URL" 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 From 471769694c449c6a7c56a4def0580299b9be710e Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 11:28:30 +0100 Subject: [PATCH 18/23] sync uv.lock --- uv.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uv.lock b/uv.lock index aa96ca3fb5..645486658f 100644 --- a/uv.lock +++ b/uv.lock @@ -2231,6 +2231,7 @@ source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, @@ -2298,6 +2299,7 @@ tests = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.0.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "rich", specifier = ">=10.11.0" }, { name = "shellingham", specifier = ">=1.3.0" }, { name = "typing-extensions", specifier = ">=3.7.4.3" }, From d59a36f67d282b964e955d19b5aeb75a02ccc892 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:29:09 +0000 Subject: [PATCH 19/23] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parameter_types/pydantic_types/tutorial001_an_py39.py | 3 ++- .../parameter_types/pydantic_types/tutorial002_an_py39.py | 3 ++- .../parameter_types/pydantic_types/tutorial003_an_py39.py | 3 +-- .../parameter_types/pydantic_types/tutorial004_an_py39.py | 3 +-- .../test_pydantic_types/test_tutorial003.py | 4 +++- .../test_pydantic_types/test_tutorial004.py | 4 +++- typer/main.py | 4 ++-- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py index a16be774ed..fb251f1607 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py @@ -1,6 +1,7 @@ +from typing import Annotated + import typer from pydantic import AnyHttpUrl -from typing_extensions import Annotated app = typer.Typer() diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py index 9c5cc8ee56..809c5ff237 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py @@ -1,6 +1,7 @@ +from typing import Annotated + import typer from pydantic import AnyHttpUrl -from typing_extensions import Annotated app = typer.Typer() diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py index 0adb6c5a40..f1fb0507db 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py @@ -1,8 +1,7 @@ -from typing import List +from typing import Annotated, List import typer from pydantic import AnyHttpUrl -from typing_extensions import Annotated app = typer.Typer() diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py index f8bc3435a1..dacad3491b 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py @@ -1,8 +1,7 @@ -from typing import Tuple +from typing import Annotated, Tuple import typer from pydantic import AnyHttpUrl, IPvAnyAddress -from typing_extensions import Annotated app = typer.Typer() diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py index cebb251f0d..0c39e50653 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py @@ -37,7 +37,9 @@ def test_url_list(mod: ModuleType): def test_url_invalid(mod: ModuleType): - result = runner.invoke(mod.app, ["--url", "invalid", "--url", "https://example.org"]) + result = runner.invoke( + mod.app, ["--url", "invalid", "--url", "https://example.org"] + ) assert result.exit_code != 0 assert "Input should be a valid URL" in result.output diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py index ec9708d8b1..5853afce21 100644 --- a/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py @@ -28,7 +28,9 @@ def test_help(mod: ModuleType): def test_tuple(mod: ModuleType): - result = runner.invoke(mod.app, ["--server", "Example", "::1", "https://example.com"]) + result = runner.invoke( + mod.app, ["--server", "Example", "::1", "https://example.com"] + ) assert result.exit_code == 0 assert "name: Example" in result.output assert "address: ::1" in result.output diff --git a/typer/main.py b/typer/main.py index b7d29a8fea..d1d9eb0344 100644 --- a/typer/main.py +++ b/typer/main.py @@ -12,12 +12,12 @@ from pathlib import Path from traceback import FrameSummary, StackSummary from types import TracebackType -from typing import Any, Callable, Optional, Union +from typing import Annotated, Any, Callable, Optional, Union from uuid import UUID import click from typer._types import TyperChoice -from typing_extensions import Annotated, TypeAlias +from typing_extensions import TypeAlias from ._typing import get_args, get_origin, is_literal_type, is_union, literal_values from .completion import get_completion_inspect_parameters From 168a79edfaaa12de21198e899007c7e6e90fa1db Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 11:34:04 +0100 Subject: [PATCH 20/23] use builtin tuple and list --- .../parameter_types/pydantic_types/tutorial003_an_py39.py | 4 ++-- docs_src/parameter_types/pydantic_types/tutorial003_py39.py | 4 +--- .../parameter_types/pydantic_types/tutorial004_an_py39.py | 4 ++-- docs_src/parameter_types/pydantic_types/tutorial004_py39.py | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py index f1fb0507db..9d028c9d76 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated import typer from pydantic import AnyHttpUrl @@ -8,7 +8,7 @@ @app.command() def main( - urls: Annotated[List[AnyHttpUrl], typer.Option("--url", default_factory=list)], + urls: Annotated[list[AnyHttpUrl], typer.Option("--url", default_factory=list)], ): typer.echo(f"urls: {urls}") diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_py39.py b/docs_src/parameter_types/pydantic_types/tutorial003_py39.py index 5db2942d20..c9281cfe82 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial003_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial003_py39.py @@ -1,5 +1,3 @@ -from typing import List - import typer from pydantic import AnyHttpUrl @@ -7,7 +5,7 @@ @app.command() -def main(urls: List[AnyHttpUrl] = typer.Option([], "--url")): +def main(urls: list[AnyHttpUrl] = typer.Option([], "--url")): typer.echo(f"urls: {urls}") diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py index dacad3491b..3f6b649f0b 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py @@ -1,4 +1,4 @@ -from typing import Annotated, Tuple +from typing import Annotated import typer from pydantic import AnyHttpUrl, IPvAnyAddress @@ -9,7 +9,7 @@ @app.command() def main( server: Annotated[ - Tuple[str, IPvAnyAddress, AnyHttpUrl], + tuple[str, IPvAnyAddress, AnyHttpUrl], typer.Option(help="User name, age, email and social media URL"), ], ): diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_py39.py b/docs_src/parameter_types/pydantic_types/tutorial004_py39.py index 6d00874ef5..c5e0fd4d7d 100644 --- a/docs_src/parameter_types/pydantic_types/tutorial004_py39.py +++ b/docs_src/parameter_types/pydantic_types/tutorial004_py39.py @@ -1,5 +1,3 @@ -from typing import Tuple - import typer from pydantic import AnyHttpUrl, IPvAnyAddress @@ -8,7 +6,7 @@ @app.command() def main( - server: Tuple[str, IPvAnyAddress, AnyHttpUrl] = typer.Option( + server: tuple[str, IPvAnyAddress, AnyHttpUrl] = typer.Option( ..., help="Server name, IP address and public URL" ), ): From 640957eb7d2856d6b27254b63ab93cdd55df0e2e Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 12:09:05 +0100 Subject: [PATCH 21/23] configure importlib to avoid conflicts --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f433622839..8d94534c62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,7 @@ source-includes = [""] addopts = [ "--strict-config", "--strict-markers", + "--import-mode=importlib", ] xfail_strict = true junit_family = "xunit2" From cf3ab7212274b46c9edc271163fe7ea6cb869464 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 12:13:14 +0100 Subject: [PATCH 22/23] add pydantic also to tests requirements in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8d94534c62..69912c2107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ github-actions = [ tests = [ "coverage[toml]>=6.2,<8.0", "mypy==1.19.1", + "pydantic >=2.0.0", "pytest>=4.4.0,<9.0.0", "pytest-cov>=2.10.0,<8.0.0", "pytest-sugar>=0.9.4,<1.2.0", From f7881f4b673efe41bb76ffc38ecf7b5462054aef Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 13 Jan 2026 12:16:46 +0100 Subject: [PATCH 23/23] more fixes --- pyproject.toml | 1 - .../test_parameter_types/test_pydantic_types/__init__.py | 0 uv.lock | 4 ++++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 tests/test_tutorial/test_parameter_types/test_pydantic_types/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 69912c2107..495eccfce4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,7 +151,6 @@ source-includes = [""] addopts = [ "--strict-config", "--strict-markers", - "--import-mode=importlib", ] xfail_strict = true junit_family = "xunit2" diff --git a/tests/test_tutorial/test_parameter_types/test_pydantic_types/__init__.py b/tests/test_tutorial/test_parameter_types/test_pydantic_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/uv.lock b/uv.lock index 645486658f..6ff341d6c2 100644 --- a/uv.lock +++ b/uv.lock @@ -2253,6 +2253,7 @@ dev = [ { name = "mypy" }, { name = "pillow" }, { name = "prek" }, + { name = "pydantic" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-sugar" }, @@ -2287,6 +2288,7 @@ tests = [ { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "mypy" }, + { name = "pydantic" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-sugar" }, @@ -2320,6 +2322,7 @@ dev = [ { name = "mypy", specifier = "==1.19.1" }, { name = "pillow", specifier = "==11.3.0" }, { name = "prek", specifier = "==0.2.24" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", specifier = ">=4.4.0,<9.0.0" }, { name = "pytest-cov", specifier = ">=2.10.0,<8.0.0" }, { name = "pytest-sugar", specifier = ">=0.9.4,<1.2.0" }, @@ -2352,6 +2355,7 @@ github-actions = [ tests = [ { name = "coverage", extras = ["toml"], specifier = ">=6.2,<8.0" }, { name = "mypy", specifier = "==1.19.1" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", specifier = ">=4.4.0,<9.0.0" }, { name = "pytest-cov", specifier = ">=2.10.0,<8.0.0" }, { name = "pytest-sugar", specifier = ">=0.9.4,<1.2.0" },