Skip to content

Commit

Permalink
[FEATURE] Support for common pydantic types
Browse files Browse the repository at this point in the history
  • Loading branch information
lachaib committed Mar 26, 2024
1 parent d6921fb commit f6c2b03
Show file tree
Hide file tree
Showing 23 changed files with 597 additions and 21 deletions.
84 changes: 84 additions & 0 deletions 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

<div class="termy">

```console
// Pydantic comes with typer[all]
$ pip install "typer[all]"
---> 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
@@ -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
@@ -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
@@ -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)
11 changes: 11 additions & 0 deletions 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)
12 changes: 12 additions & 0 deletions 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)
15 changes: 15 additions & 0 deletions 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)
20 changes: 20 additions & 0 deletions 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)
22 changes: 22 additions & 0 deletions 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)
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -61,6 +61,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
7 changes: 6 additions & 1 deletion pyproject.toml
Expand Up @@ -46,10 +46,11 @@ test = [
"coverage >=6.2,<7.0",
"pytest-xdist >=1.32.0,<4.0.0",
"pytest-sugar >=0.9.4,<0.10.0",
"mypy ==0.971",
"mypy ==1.4.1",
"black >=22.3.0,<23.0.0",
"isort >=5.0.6,<6.0.0",
"rich >=10.11.0,<14.0.0",
"pydantic[email] >=2.0.0",
]
doc = [
"mkdocs >=1.1.2,<2.0.0",
Expand All @@ -63,10 +64,14 @@ dev = [
"flake8 >=3.8.3,<4.0.0",
"pre-commit >=2.17.0,<3.0.0",
]
pydantic = [
"pydantic[email] >=2.0.0",
]
all = [
"colorama >=0.4.3,<0.5.0",
"shellingham >=1.3.0,<2.0.0",
"rich >=10.11.0,<14.0.0",
"pydantic[email] >=2.0.0",
]

[tool.isort]
Expand Down
@@ -0,0 +1,39 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
@@ -0,0 +1,39 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
@@ -0,0 +1,39 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
@@ -0,0 +1,39 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout

0 comments on commit f6c2b03

Please sign in to comment.