diff --git a/README.md b/README.md
index 32ead01746..5fc595293b 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
```
@@ -357,6 +357,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`.
@@ -377,7 +378,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 31baa86594..bea0edaf15 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
```
@@ -363,6 +363,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`.
@@ -383,7 +384,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 2749092c55..d96bea2310 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -673,7 +673,7 @@ Updated docs [SubCommand Name and Help](https://typer.tiangolo.com/tutorial/subc
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.
@@ -699,7 +699,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
new file mode 100644
index 0000000000..9623d86275
--- /dev/null
+++ b/docs/tutorial/parameter-types/pydantic-types.md
@@ -0,0 +1,37 @@
+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.
+
+Pydantic is installed automatically when installing Typer with its extra standard dependencies:
+
+
+
+```console
+// Pydantic comes with typer
+$ pip install typer
+---> 100%
+Successfully installed typer rich shellingham pydantic
+
+// Alternatively, you can install Pydantic independently
+$ pip install pydantic
+---> 100%
+Successfully installed pydantic
+
+// Or if you want to use EmailStr
+$ pip install "pydantic[email]"
+---> 100%
+Successfully installed pydantic, email-validator
+```
+
+
+
+
+You can then use them as parameter types.
+
+{* docs_src/parameter_types/pydantic_types/tutorial001_an_py39.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_py39.py hl[9] *}
+
+{* docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py hl[9] *}
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_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py
new file mode 100644
index 0000000000..fb251f1607
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial001_an_py39.py
@@ -0,0 +1,15 @@
+from typing import Annotated
+
+import typer
+from pydantic import AnyHttpUrl
+
+app = typer.Typer()
+
+
+@app.command()
+def main(url_arg: Annotated[AnyHttpUrl, typer.Argument()]):
+ typer.echo(f"url_arg: {url_arg}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial001_py39.py b/docs_src/parameter_types/pydantic_types/tutorial001_py39.py
new file mode 100644
index 0000000000..f6e2cbbfc8
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial001_py39.py
@@ -0,0 +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__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py
new file mode 100644
index 0000000000..809c5ff237
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial002_an_py39.py
@@ -0,0 +1,15 @@
+from typing import Annotated
+
+import typer
+from pydantic import AnyHttpUrl
+
+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__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial002_py39.py b/docs_src/parameter_types/pydantic_types/tutorial002_py39.py
new file mode 100644
index 0000000000..6566a0dad9
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial002_py39.py
@@ -0,0 +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__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py
new file mode 100644
index 0000000000..9d028c9d76
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial003_an_py39.py
@@ -0,0 +1,17 @@
+from typing import Annotated
+
+import typer
+from pydantic import AnyHttpUrl
+
+app = typer.Typer()
+
+
+@app.command()
+def main(
+ urls: Annotated[list[AnyHttpUrl], typer.Option("--url", default_factory=list)],
+):
+ typer.echo(f"urls: {urls}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial003_py39.py b/docs_src/parameter_types/pydantic_types/tutorial003_py39.py
new file mode 100644
index 0000000000..c9281cfe82
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial003_py39.py
@@ -0,0 +1,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__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py
new file mode 100644
index 0000000000..3f6b649f0b
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial004_an_py39.py
@@ -0,0 +1,23 @@
+from typing import Annotated
+
+import typer
+from pydantic import AnyHttpUrl, IPvAnyAddress
+
+app = typer.Typer()
+
+
+@app.command()
+def main(
+ server: Annotated[
+ tuple[str, IPvAnyAddress, AnyHttpUrl],
+ typer.Option(help="User name, age, email and social media URL"),
+ ],
+):
+ name, address, url = server
+ typer.echo(f"name: {name}")
+ typer.echo(f"address: {address}")
+ typer.echo(f"url: {url}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/parameter_types/pydantic_types/tutorial004_py39.py b/docs_src/parameter_types/pydantic_types/tutorial004_py39.py
new file mode 100644
index 0000000000..c5e0fd4d7d
--- /dev/null
+++ b/docs_src/parameter_types/pydantic_types/tutorial004_py39.py
@@ -0,0 +1,20 @@
+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"
+ ),
+):
+ name, address, url = server
+ typer.echo(f"name: {name}")
+ typer.echo(f"address: {address}")
+ typer.echo(f"url: {url}")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/mkdocs.yml b/mkdocs.yml
index 744954ce2c..80d95f6319 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -116,6 +116,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 5897e1aa8a..495eccfce4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ Changelog = "https://typer.tiangolo.com/release-notes/"
standard = [
"shellingham >=1.3.0",
"rich >=10.11.0",
+ "pydantic >=2.0.0",
]
[dependency-groups]
@@ -81,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",
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/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..02f7e2a0a1
--- /dev/null
+++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial001.py
@@ -0,0 +1,48 @@
+import importlib
+import subprocess
+import sys
+from types import ModuleType
+
+import pytest
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+@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(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(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(mod: ModuleType):
+ 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..7e9756087a
--- /dev/null
+++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial002.py
@@ -0,0 +1,48 @@
+import importlib
+import subprocess
+import sys
+from types import ModuleType
+
+import pytest
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+@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(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(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(mod: ModuleType):
+ 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..0c39e50653
--- /dev/null
+++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial003.py
@@ -0,0 +1,53 @@
+import importlib
+import subprocess
+import sys
+from types import ModuleType
+
+import pytest
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+@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(mod: ModuleType):
+ result = runner.invoke(
+ 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(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(mod: ModuleType):
+ 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..5853afce21
--- /dev/null
+++ b/tests/test_tutorial/test_parameter_types/test_pydantic_types/test_tutorial004.py
@@ -0,0 +1,60 @@
+import importlib
+import subprocess
+import sys
+from types import ModuleType
+
+import pytest
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+@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(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(mod: ModuleType):
+ result = runner.invoke(
+ 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(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(mod: ModuleType):
+ 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 db87f83e3f..af91c4284d 100644
--- a/typer/completion.py
+++ b/typer/completion.py
@@ -131,7 +131,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 e8c6b9e429..d1d9eb0344 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -12,11 +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 TypeAlias
from ._typing import get_args, get_origin, is_literal_type, is_union, literal_values
from .completion import get_completion_inspect_parameters
@@ -51,6 +52,40 @@
)
from .utils import get_params_from_function
+try:
+ import pydantic
+
+ def is_pydantic_type(type_: Any) -> bool:
+ if get_origin(type_) is Annotated:
+ # 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
+ )
+
+ 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]
+ adapter: pydantic.TypeAdapter[T] = pydantic.TypeAdapter(type_)
+
+ def convertor(value: str) -> T:
+ try:
+ return adapter.validate_python(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
@@ -809,6 +846,8 @@ def get_click_type(
literal_values(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
@@ -818,6 +857,11 @@ 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 get_origin(type_) is not None and not is_pydantic_type(type_)
+
+
def get_click_param(
param: ParamMeta,
) -> tuple[Union[click.Argument, click.Option], Any]:
@@ -851,6 +895,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):
@@ -865,14 +910,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(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):
types = []
for type_ in get_args(main_type):
- assert not get_origin(type_), (
+ assert not is_complex_subtype(type_), (
"Tuple types with complex sub-types are not currently supported"
)
types.append(
@@ -930,9 +975,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,
@@ -964,9 +1007,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,
diff --git a/uv.lock b/uv.lock
index aa96ca3fb5..6ff341d6c2 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" },
@@ -2252,6 +2253,7 @@ dev = [
{ name = "mypy" },
{ name = "pillow" },
{ name = "prek" },
+ { name = "pydantic" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-sugar" },
@@ -2286,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" },
@@ -2298,6 +2301,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" },
@@ -2318,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" },
@@ -2350,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" },