Skip to content

Commit

Permalink
[FEATURE] Support for common pydantic types
Browse files Browse the repository at this point in the history
Fixes #181
  • Loading branch information
lachaib committed May 17, 2024
1 parent b4b6f25 commit 707839e
Show file tree
Hide file tree
Showing 23 changed files with 576 additions and 11 deletions.
84 changes: 84 additions & 0 deletions docs/tutorial/parameter-types/pydantic-types.md
Original file line number Diff line number Diff line change
@@ -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

<div class="termy">

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

</div>


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!}
```
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 EmailStr


def main(email_arg: EmailStr):
typer.echo(f"email_arg: {email_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 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)
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 EmailStr


def main(email_opt: EmailStr = typer.Option("[email protected]")):
typer.echo(f"email_opt: {email_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 EmailStr
from typing_extensions import Annotated


def main(email_opt: Annotated[EmailStr, typer.Option()] = "[email protected]"):
typer.echo(f"email_opt: {email_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)
20 changes: 20 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial004.py
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 22 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,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)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ nav:
- Path: tutorial/parameter-types/path.md
- File: tutorial/parameter-types/file.md
- Custom Types: tutorial/parameter-types/custom-types.md
- Pydantic Types: tutorial/parameter-types/pydantic-types.md
- SubCommands - Command Groups:
- SubCommands - Command Groups - Intro: tutorial/subcommands/index.md
- Add Typer: 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 @@ -44,6 +44,7 @@ homepage = "https://github.com/tiangolo/typer"
standard = [
"shellingham >=1.3.0",
"rich >=10.11.0",
"pydantic[email] >=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.2.0
# Needed explicitly by typer-slim
rich >=10.11.0
shellingham >=1.3.0
pydantic[email] >=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

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, ["[email protected]"])
assert result.exit_code == 0
assert "email_arg: [email protected]" 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
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_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, ["[email protected]"])
assert result.exit_code == 0
assert "email_arg: [email protected]" 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
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 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", "[email protected]"])
assert result.exit_code == 0
assert "email_opt: [email protected]" 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
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 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", "[email protected]"])
assert result.exit_code == 0
assert "email_opt: [email protected]" 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
Loading

0 comments on commit 707839e

Please sign in to comment.