Comments (15)
+1
I really like the idea of using Pydantic models.
Another approach to parse_raw
could be to traverse Pydantic models until we reach a supported type.
Parameters of type BaseModel
could be parsed into multiple cli options like for example the traefik cli does.
This does only work for options and not for arguments.
#!/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'>
from typer.
Going to try and implement this in #630, more or less exactly as described by @pypae
from typer.
@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?
from typer.
As proof of concept of how support for Pydantic Models could be implemented, I built on the monkey-patched version of get_click_type
that @ananis25 wrote in #77:
#!/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 parse_raw
method") and this would then cover the use-case requested in #77 without needing to implement a registration process.
from typer.
Has anyone tried pydantic-cli ? though it uses argparse
(from the README) rather than click
from typer.
Oh, I didn't know about the parse_raw
method provided by pydantic. This is definitely neater than keeping a global container of custom datatypes!
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 app.command
function also only registers the function being decorated instead of wrapping it, which is useful if the method hello
is defined somewhere you don't want to make edits.
import typer
def hello(name: str):
return f"Hello {name}"
if __name__ == "__main__":
app = typer.Typer()
app.command()(hello)
from typer.
This would also help many other use cases:
- I wanted to use, my own formatting for
datetime
, ex:today %H:%M
,%M:%H
(automatically uses date as today) - typer would be able to serialize any custom object
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
from typer.
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, argparse
...
Btw, just found this: https://medium.com/short-bits/typer-the-command-line-script-framework-you-should-use-de9d07109f54 🤔
from typer.
I agree that this feature would be very useful !
from typer.
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 Annotated
(that way it would not interfere with the pydantic field config).
My dream solution would allow to have a well-integrated solution to have a common data model filled from various sources, such as:
- CLI arguments (via typer)
- config files (via pydantic)
- interactive user inputs (via rich)
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 obj.prompt_field("key_name")
to ask for a single field value or obj.prompt_fields(...)
to walk through all (or configurable to ask only for missing values).
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?
from typer.
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.
from typer.
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!
from typer.
Another approach to
parse_raw
could be to traverse Pydantic models until we reach a supported type.
Parameters of typeBaseModel
could be parsed into multiple cli options like for example the traefik cli does.
This does only work for options and not for arguments.
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 click.Parameter
s, it involves quite a lot of changes.
Btw. @tiangolo how do you feel about this feature?
from typer.
@tiangolo can we get this green-lighted for a pull request?
from typer.
@tiangolo can we get this green-lighted for a pull request?
@jackric better to ask for forgiveness than permission 😉
from typer.
Related Issues (20)
- New setting to increase the width of the rich (exception) output HOT 1
- [QUESTION] Disable traceback globally on production HOT 4
- printing f-string returns nothing in terminal (win10) HOT 1
- How do I pass the None value explicitly? HOT 2
- Documentation is misleading. new `typer.run` behavior HOT 2
- See if rich 13.x is compatible HOT 4
- Using `some_type | None` syntax for type annotations causes error in python 3.11 HOT 12
- unlimited argument for an option with comma spliter HOT 1
- TAB completion is giving local directory files where command is called. HOT 2
- Support for bytes in Options and Arguments HOT 1
- Is it possible to include a Prolog in `--help` HOT 2
- Get the typer output with html format to provide it to termynal HOT 4
- how to use typer on class method __init__ with self argument, got this error: Error: Missing argument 'SELF'. HOT 2
- Support for localization of messages HOT 1
- Auto-completion when application works in 2 modes (GUI, CLI) HOT 1
- DOC: Documentation of passing multiple values in "option" vs "argument" isn't sufficiently explicit HOT 1
- SIGINT from docker is ignored HOT 4
- 🚀 Roadmap HOT 2
- Source distribution of 0.11.0 is missing the `docs_src` folder HOT 1
- 🐛 Upgrading from `typer<0.12.0` to `typer==0.12.0` breaks the install by partially removing the package/module files HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from typer.