Skip to content

Conversation

@Rhiannon-Udall
Copy link

Hi @pypae - thank you for making this very nice extension! I've been hoping for something exactly like this, and I will very likely be using it in the future.

For this PR, I am not sure if it's the sort of feature you are interested in, but I have written it for my own purposes anyways so figured I would share it in case you are. Basically, it adds a decorator to make a function accept yaml(s) which are parsed initially to build the model, then accepts updates from the standard command line arguments. This is analogous to the way that configargparse extends the basic argparse functionality (though, in my opinion, a lot cleaner). If you are interested in adding this, I can work on tests/documentation and make any changes you might suggest.

To do this it does introduce two new dependencies (pydantic-yaml and makefun).

An example usage would be:

from pydantic import BaseModel, Field
from typing import Optional
from pydantic_typer.main import wrap_with_model_yaml, run

class ExampleSubModel(BaseModel):
    user : Optional[str] = Field(default=None, description="The user")
    account_id : Optional[int] = Field(default=None, description="The id of the account")

class ExampleModel(BaseModel):
    user : Optional[str] = Field(default=None, description="The user")
    account_id : Optional[int] = Field(default=None, description="The id of the account")
    sub_model : ExampleSubModel 

class AnotherExampleModel(BaseModel):
    user : Optional[str] = Field(default=None, description="The user")
    account_id : Optional[int] = Field(default=None, description="The id of the account")

@wrap_with_model_yaml
def example_wrapped_config(model_1 : ExampleModel, model_2 : AnotherExampleModel, not_a_model_arg : int = 3, untyped_arg = "hi"):
    print(model_1)
    print(model_2)

if __name__ == "__main__":
    run(example_wrapped_config)

which yields this help message

$ (dev-gw-project-db) [rhiannonu@rhiannon-laptop Experiments]$ python test_pydantic_typer.py --help
                                                                                                                                                              
 Usage: test_pydantic_typer.py [OPTIONS] CONFIG_MODEL_1 CONFIG_MODEL_2                                                                                        
                                                                                                                                                              
╭─ Arguments ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ *    config_model_1      TEXT  [default: None] [required]                                                                                                  │
│ *    config_model_2      TEXT  [default: None] [required]                                                                                                  │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --not-a-model-arg                     INTEGER  [default: 3]                                                                                                │
│ --untyped-arg                         TEXT     [default: hi]                                                                                               │
│ --model_1.user                        TEXT     The user [default: None]                                                                                    │
│ --model_1.account_id                  INTEGER  The id of the account [default: None]                                                                       │
│ --model_1.sub_model.user              TEXT     The user [default: None]                                                                                    │
│ --model_1.sub_model.account_id        INTEGER  The id of the account [default: None]                                                                       │
│ --model_2.user                        TEXT     The user [default: None]                                                                                    │
│ --model_2.account_id                  INTEGER  The id of the account [default: None]                                                                       │
│ --help                                         Show this message and exit.                                                                                 │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

and produces an output like this:

$ (dev-gw-project-db) [rhiannonu@rhiannon-laptop Experiments]$ python test_pydantic_typer.py example_model_config.yaml another_example_model_config.yaml --model_1.user "custom user"

user='custom user' account_id=4 sub_model=ExampleSubModel(user='sub user', account_id=16)
user='another user' account_id=111

This adds the decorator `wrap_with_model_yaml`, which takes
functions whose signature includes arguments typed as a pydantic model.
It adds to this function corresponding string arguments, presumed to point
towards yaml files which represent instances of the corresponding model.
These are then parsed as that model, and updated with any non-default arguments
passed by the model a la the original signature.
This amalgamated model is then passed to the original function.
This is meant to fill the same niche as the configargparse package, which provides
an analogous extension to argparse.
@pypae
Copy link
Owner

pypae commented Feb 6, 2025

Hey @Rhiannon-Udall, thanks for your PR!

I generally like the idea of parsing yaml configs with pydantic. I'm not sure if it makes sense to include that functionality in pydantic-typer though.

I didn't have a close look at your implementation; But I have two preliminary questions:

  • Do you only allow the yaml to be given as an argument? This makes it required, right? I would prefer to use another option instead (defaulting to something like --config-yaml) to keep it optional.

  • Can you combine yaml and commandline options? In particular, does the implementation allow for options to override values given in the yaml?

@Rhiannon-Udall
Copy link
Author

Hi @pypae - that makes sense. As I said I'd be happy to just maintain it as an extension-to-an-extension for my own purposes, just figured I'd offer.

To your questions:

  1. I made it an argument, but in principle I do think I could switch it to be an optional instead (it was a bit finicky about ordering already, but this is something which would likely need to be fixed anyways).

  2. Yes that is the idea! Any command-line option given which is not the default set in the model will override the yaml configuration passed (similar to the behavior of configargparse).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants