From 25a5c3718d61950dbfef79dcb615435e9d20a745 Mon Sep 17 00:00:00 2001 From: Louis-Amaury Chaib Date: Thu, 21 Dec 2023 11:28:36 +0100 Subject: [PATCH 1/9] [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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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