Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Support common pydantic types #723

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/"
```console
$ pip install typer
---> 100%
Successfully installed typer rich shellingham
Successfully installed typer rich shellingham pydantic
```

</div>
Expand Down Expand Up @@ -355,6 +355,7 @@ For a more complete example including more features, see the <a href="https://ty
By default it also comes with extra standard dependencies:

* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
* <a href="https://docs.pydantic.dev/latest/" class="external-link" target="_blank"><code>pydantic</code></a>: to support the usage of Pydantic types.
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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`.
Expand All @@ -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.

Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/"
```console
$ pip install typer
---> 100%
Successfully installed typer rich shellingham
Successfully installed typer rich shellingham pydantic
```

</div>
Expand Down Expand Up @@ -361,6 +361,7 @@ For a more complete example including more features, see the <a href="https://ty
By default it also comes with extra standard dependencies:

* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
* <a href="https://docs.pydantic.dev/latest/" class="external-link" target="_blank"><code>pydantic</code></a>: to support the usage of Pydantic types.
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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`.
Expand All @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down
31 changes: 31 additions & 0 deletions docs/tutorial/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

<div class="termy">

```console
$ pip install typer
---> 100%
Successfully installed typer click shellingham rich pydantic
```

</div>

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]"
```
89 changes: 89 additions & 0 deletions docs/tutorial/parameter-types/pydantic-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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.
lachaib marked this conversation as resolved.
Show resolved Hide resolved

Pydantic is installed automatically when installing Typer with its extra standard dependencies:

<div class="termy">

```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
```

</div>


You can then use them as parameter types.

=== "Python 3.7+ Argument"

```Python hl_lines="5"
{!> ../docs_src/parameter_types/pydantic_types/tutorial001_an.py!}
```

=== "Python 3.7+ 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.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!}
```
Empty file.
10 changes: 10 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer
from pydantic import AnyHttpUrl


def main(url_arg: AnyHttpUrl):
typer.echo(f"url_arg: {url_arg}")


if __name__ == "__main__":
typer.run(main)
11 changes: 11 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial001_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typer
from pydantic import AnyHttpUrl
from typing_extensions import Annotated


def main(url_arg: Annotated[AnyHttpUrl, typer.Argument()]):
typer.echo(f"url_arg: {url_arg}")


if __name__ == "__main__":
typer.run(main)
10 changes: 10 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer
from pydantic import AnyHttpUrl


def main(url_opt: AnyHttpUrl = typer.Option("https://typer.tiangolo.com")):
typer.echo(f"url_opt: {url_opt}")


if __name__ == "__main__":
typer.run(main)
11 changes: 11 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial002_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typer
from pydantic import AnyHttpUrl
from typing_extensions import Annotated


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)
12 changes: 12 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial003.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial003_an.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 19 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial004.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Tuple

import typer
from pydantic import AnyHttpUrl, IPvAnyAddress


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__":
typer.run(main)
21 changes: 21 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial004_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Tuple

import typer
from pydantic import AnyHttpUrl, IPvAnyAddress
from typing_extensions import Annotated


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__":
typer.run(main)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Changelog = "https://typer.tiangolo.com/release-notes/"
standard = [
"shellingham >=1.3.0",
"rich >=10.11.0",
"pydantic >=2.0.0",
]

[tool.pdm]
Expand Down
1 change: 1 addition & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ ruff ==0.6.5
# Needed explicitly by typer-slim
rich >=10.11.0
shellingham >=1.3.0
pydantic >=2.0.0
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure to skip these tests when pydantic and/or email-validator aren't installed (instead of running them and getting import errors and failing tests)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I made necessary changes to have everything installed for tests, I don't get the point of skipping the tests, except if we want to ensure that typer still works without pydantic, but running both flavours would be cumbersome

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think it's a valid use-case for someone to be developing on Typer and to be running the test suite locally, without wanting to install pydantic for Pydantic-related tests if they don't care about that...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience, it's better to not skip tests on such conditions because it's "easy" to silently break pydantic support because someone has removed the dependency in requirements-test.txt and CI didn't complain.

If a developer tests in local and doesn't have pydantic installed, I wouldn't expect them to run the full suite including tests on pydantic support (though, it's a best practice to run them, with the appropriate dependencies that the repository defines)


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_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
Loading
Loading