pypae / pydantic-typer

Typer extension to enable pydantic support
MIT License
5 stars 0 forks source link

Allow sequences of pydantic models #6

Open pypae opened 2 months ago

pypae commented 2 months ago

If we want to allow sequences of pydantic models, things get unreadable quite fast.

Let's look at an example of a nested pydantic model with a sequence of another model: pets.py

from typing import Optional, List

import typer

import pydantic

class Pet(pydantic.BaseModel):
    name: str
    species: str

class Person(pydantic.BaseModel):
    name: str
    age: Optional[float] = None
    pets: List[Pet]

def main(person: Person):
    print(person, type(person))

if __name__ == "__main__":
    typer.run(main)

The script could be called like this:

$ python pets.py --person.name Jeff --person.pets.name Lassie --person.pets.species dog

If we want to add multiple pets, we can just supply --person.pets.name and person.pets.species multiple times.

$ python pets.py --person.name Jeff --person.pets.name Lassie --person.pets.species dog --person.pets.name Nala --person.pets.species cat

We don't explicitly state which pet names and species belong together and have to rely on the correct order of parameters. In my opinion this is potentially confusing for the CLI user and may lead to bugs.

Potential Solution

To make the mapping more explicit, we could allow to enable typer.Option lists to be indexed. Like for nested pydantic models, I suggest sticking to the syntax traefik uses for lists. I.e. entrypoints.<name>.http.tls.domains[n].main

Indexed lists could be implemented independently of this PR and should work for all lists. I suggest adding an indexed flag on typer.Option like shown in the example below.

indexed_list.py

from typing import List

import typer

def main(indexed_list: List[int] = typer.Option(..., indexed=True)):
    print(indexed_list)

if __name__ == "__main__":
    typer.run(main)

This would then produce the following help text:

$ python indexed_list.py --help

 Usage: indexed_list.py [OPTIONS]                                    

╭─ Options ────────────────────────────────────────────────────────╮
│ *  --indexed-list[n]        INTEGER   [default: None] [required] │
│    --help                             Show this message and      │
│                                       exit.                      │
╰──────────────────────────────────────────────────────────────────╯

And could be used like this:

$ python indexed_list.py --indexed-list[1] 0 --indexed-list[0] 1 --indexed-list[2] 2
[1, 0, 2]

Note how the order of the input parameters doesn't matter anymore because the indices are given explicitly.

Notes on Implementation

Implementing this might not be trivial, but I think it could be possible by forwarding unknown options as described in the click docs.

Edit: This might actually be easier using token normalization.

Originally posted by @pypae in https://github.com/fastapi/typer/issues/803#issuecomment-2079341538

pavan-uppari commented 2 months ago

@pypae Instead of mentioning every field in the command, can we provide the input as dict and convert it into pydantic objects so that nested also will be handled by nested keys in dict.

Ref: https://github.com/fastapi/typer/issues/130#issuecomment-2337710751

So, in your case, command will be

python pets.py {"name": "Jeff", "pets": [{"name": "Lessie", "species": "dog"}, {"name": "Nala", "species": "cat"}]}