-
-
Notifications
You must be signed in to change notification settings - Fork 671
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
[FEATURE] Support Pydantic Models as ParamTypes #111
Comments
I just noticed that this is very similar to #77. I will leave this open and change the name to specifically request support for Pydantic in order to differentiate it. |
As proof of concept of how support for Pydantic Models could be implemented, I built on the monkey-patched version of #!/usr/bin/env python3
from typing import Any
import click
import pydantic
import typer
_get_click_type = typer.main.get_click_type
def supersede_get_click_type(
*, annotation: Any, parameter_info: typer.main.ParameterInfo
) -> click.ParamType:
if hasattr(annotation, "parse_raw"):
class CustomParamType(click.ParamType):
def convert(self, value, param, ctx):
return annotation.parse_raw(value)
return CustomParamType()
else:
return _get_click_type(annotation=annotation, parameter_info=parameter_info)
typer.main.get_click_type = supersede_get_click_type
class User(pydantic.BaseModel):
id: int
name = "Jane Doe"
def main(num: int, user: User):
print(num, type(num))
print(user, type(user))
if __name__ == "__main__":
typer.run(main) $ ./typer_demo.py 1 '{"id": "2"}'
1 <class 'int'>
id=2 name='Jane Doe' <class '__main__.User'> Also, Typer could settle on an API for custom types (e.g. in keeping with Pydantic, the Typer docs could declare "all custom ParamTypes must implement a |
Oh, I didn't know about the The only reason to prefer a registration api imo is that the code not using pydantic types/classes can also be used in a CLI without making any changes to it. For ex - the typer import typer
def hello(name: str):
return f"Hello {name}"
if __name__ == "__main__":
app = typer.Typer()
app.command()(hello) |
+1 #!/usr/bin/env python3
import click
import pydantic
import typer
class User(pydantic.BaseModel):
id: int
name: str = "Jane Doe"
def main(num: int, user: User):
print(num, type(num))
print(user, type(user))
if __name__ == "__main__":
typer.run(main) The example above could then be called like this: $ ./typer_demo.py 1 --user.id 2 --user.name "John Doe"
1 <class 'int'>
id=2 name='John Doe' <class '__main__.User'> |
I was about to write a ticket along the lines of @PatDue's comment above, I would love something along those lines; I currently have several methods with large, almost identical signatures, which map to a pydantic model. Something like that would be fantastic! |
I'm working on a pull request to support this behaviour. Because there is no longer a 1-1 mapping between the typed function parameters and the Btw. @tiangolo how do you feel about this feature? |
@tiangolo can we get this green-lighted for a pull request? |
Has anyone tried pydantic-cli ? though it uses |
This would also help many other use cases:
It also doesn't seem, it would require a lot of changes on how typer works internally to implement this. There is already PR #304 |
Also would love to see this! Typer looks awesome, but I like to manage my configs with Pydantic, and I don't want to describe all my params twice. So currently looking at https://github.com/SupImDos/pydantic-argparse or https://pypi.org/project/pydantic-cli/ instead. But, alas, Btw, just found this: https://medium.com/short-bits/typer-the-command-line-script-framework-you-should-use-de9d07109f54 🤔 |
@tiangolo do you have a position on this feature request? If it were implemented well, would you accept? What might your definition of "implemented well" be? |
I agree that this feature would be very useful ! |
An integration of typer and pydantic would be totally awesome! Currently one needs to write an annoying amount of boilerplate to map from CLI arguments to model fields. In an ideal solution, one could bind pydantic fields directly to cli params e.g. using some extra type hint inside My dream solution would allow to have a well-integrated solution to have a common data model filled from various sources, such as:
For the latter part I'm using currently this approach to prompt fields for my config model: from rich import print
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
class MyBaseModel(BaseModel):
"""Tweaked BaseModel to manage the template settings.
Adds functionality to prompt user via CLI for values of fields.
Assumes that all fields have either a default value (None is acceptable,
even if the field it is not optional) or another nested model.
This ensures that the object can be constructed without being complete yet.
"""
model_config = ConfigDict(
str_strip_whitespace=True, str_min_length=1, validate_assignment=True
)
def check(self):
"""Run validation on this object again."""
self.model_validate(self.model_dump())
@staticmethod
def _unpack_annotation(ann):
"""Unpack an annotation from optional, raise exception if it is a non-trivial union."""
o, ts = get_origin(ann), get_args(ann)
is_union = o is Union
fld_types = [ann] if not is_union else [t for t in ts if t is not type(None)]
ret = []
for t in fld_types:
inner_kind = get_origin(t)
if inner_kind is Literal:
ret.append([a for a in get_args(t)])
elif inner_kind is Union:
raise TypeError("Complex nested types are not supported!")
else:
ret.append(t)
return ret
def _field_prompt(self, key: str, *, required_only: bool = False):
"""Interactive prompt for one primitive field of the object (one-shot, no retries)."""
fld = self.model_fields[key]
val = getattr(self, key, None)
if required_only and not fld.is_required():
return val
defval = val or fld.default
prompt_msg = f"\n[b]{key}[/b]"
if fld.description:
prompt_msg = f"\n[i]{fld.description}[/i]{prompt_msg}"
ann = self._unpack_annotation(fld.annotation)
fst, tail = ann[0], ann[1:]
choices = fst if isinstance(fst, list) else None
if fst is bool and not tail:
defval = bool(defval)
user_val = Confirm.ask(prompt_msg, default=defval)
else:
if not isinstance(defval, str) and defval is not None:
defval = str(defval)
user_val = Prompt.ask(prompt_msg, default=defval, choices=choices)
setattr(self, key, user_val) # assign (triggers validation)
return getattr(self, key) # return resulting parsed value
def prompt_field(
self,
key: str,
*,
recursive: bool = True,
missing_only: bool = False,
required_only: bool = False,
) -> Any:
"""Interactive prompt for one field of the object.
Will show field description to the user and pre-set the current value of the model as the default.
The resulting value is validated and assigned to the field of the object.
"""
val = getattr(self, key)
if isinstance(val, MyBaseModel):
if recursive:
val.prompt_fields(
missing_only=missing_only,
recursive=recursive,
required_only=required_only,
)
else: # no recursion -> just skip nested objects
return
# primitive case - prompt for value and retry if given invalid input
while True:
try:
# prompt, parse and return resulting value
return self._field_prompt(key, required_only=required_only)
except ValidationError as e:
print()
print(Panel.fit(str(e)))
print("[red]The provided value is not valid, please try again.[/red]")
def prompt_fields(
self,
*,
recursive: bool = True,
missing_only: bool = False,
required_only: bool = False,
exclude: List[str] = None,
):
"""Interactive prompt for all fields of the object. See `prompt_field`."""
excluded = set(exclude or [])
for key in self.model_fields.keys():
if missing_only and getattr(self, key, None) is None:
continue
if key not in excluded:
self.prompt_field(
key,
recursive=recursive,
missing_only=True,
required_only=required_only,
) So some instance can be completed interactively using Would this somehow make sense inside of typer as well (because the intended use-case strongly correlates with use-cases for typer), or would it make sense to create a little library for that? |
Is your feature request related to a problem
I would like to use Pydantic Models as type annotations in my Typer CLI program.
The solution you would like
Typer would call
Model.parse_raw
on the string that was passed to the CLIDescribe alternatives you've considered
Rather than having Typer be so aware of the Pydantic API, I tried to create a custom click.ParamType that did the parsing, but even that did not work as custom types do not currently seem to be supported in the get_click_type function.
Additional context
Here's a simple example:
This currently throws:
A major bonus would be if I could write the
user: User
type annotation directly, without creating theUserParamType
.Also - just want to say thank you for writing such an awesome python package and keep up the great work! 👏
The text was updated successfully, but these errors were encountered: