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 Dec 21, 2023
1 parent 3a7264c commit 39f5d78
Show file tree
Hide file tree
Showing 21 changed files with 503 additions and 8 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[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.
8 changes: 8 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import typer
from pydantic import EmailStr

def main(email_arg: EmailStr):
typer.echo(f"email_arg: {email_arg}")

if __name__ == "__main__":
typer.run(main)
9 changes: 9 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,9 @@
import typer
from typing_extensions import Annotated
from pydantic import EmailStr

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

if __name__ == "__main__":
typer.run(main)
8 changes: 8 additions & 0 deletions docs_src/parameter_types/pydantic_types/tutorial002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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)
9 changes: 9 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,9 @@
import typer
from typing_extensions import Annotated
from pydantic import EmailStr

def main(email_opt: Annotated[EmailStr, typer.Option()]="[email protected]"):
typer.echo(f"email_opt: {email_opt}")

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

def main(urls: List[AnyHttpUrl] = typer.Option([],"--url")):
typer.echo(f"urls: {urls}")

if __name__ == "__main__":
typer.run(main)
10 changes: 10 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,10 @@
import typer
from typing import List
from typing_extensions import Annotated
from pydantic import AnyHttpUrl

def main(urls: Annotated[List[AnyHttpUrl], typer.Option("--url", default_factory=list)]):
typer.echo(f"urls: {urls}")

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

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)
15 changes: 15 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,15 @@
import typer
from typing import Tuple
from typing_extensions import Annotated
from pydantic import EmailStr, AnyHttpUrl

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 @@ -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
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout
Loading

0 comments on commit 39f5d78

Please sign in to comment.