BrianPugh / cyclopts

Intuitive, easy CLIs based on python type hints.
Apache License 2.0
285 stars 7 forks source link

[Feature Request] Dataclasses as commands #228

Open dominikandreas opened 6 days ago

dominikandreas commented 6 days ago

Given some existing config dataclass, I would like to parse arguments to create the config.

Example:


import sys

from cyclopts import App

@dataclass
class MyConfig:
    a: int = 1
    b: int = 2

config = object.__new__(MyConfig)

app = App()
app.default(config.__init__)

sys.argv = [__file__,  "--a", "3", "--b", "4"]
app()
print(config)

MyConfig(a=3, b=4)

This already works as is, but I was wondering if a prettier solution already exists, either currently working or planned for version 3?

Generally, I would prefer defining a CLI around dataclasses rather than function implementations (one reason being that it's easier to extend something based on classes using inheritance).

I assume someone else has already thought about this, so before I start reinventing the wheel, I thought I should ask here.

Thanks for the great work, really like it so far 👍

BrianPugh commented 6 days ago

This will kind of work in v3; not exactly as you describe, but the main big feature of v3 is that dataclasses/pydantic/attrs/typeddict will all be allowed as type-hints.

In v3 your example will look like:

from dataclasses import dataclass
from typing import Annotated

from cyclopts import App, Parameter

app = App(name="issue-228")

@dataclass
class Config:
    a: int = 1
    """Docstring for a."""

    b: Annotated[int, Parameter(name="bar")] = 2
    """This is the docstring for python parameter "b"."""

@app.default
def my_default_command(config: Annotated[Config, Parameter(name="*")]):
    # The name "*" is special and means "squash this name"
    # Without this, items would have to be identified as "--config.a" instead of "--a"
    print(f"{config=}")

if __name__ == "__main__":
    app()

And then invoking the program from the CLI:

$ python issue-228.py --help
Usage: issue-228 COMMAND [ARGS] [OPTIONS]

╭─ Commands ────────────────────────────────────────────────────────────────────╮
│ --help,-h  Display this message and exit.                                     │
│ --version  Display application version.                                       │
╰───────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ──────────────────────────────────────────────────────────────────╮
│ A,--a      Docstring for a.                                                   │
│ BAR,--bar  This is the docstring for python parameter "b".                    │
╰───────────────────────────────────────────────────────────────────────────────╯

$ python issue-228.py --a=100 --bar=200
config=Config(a=100, b=200)

You can play around with v3 in the dev-3.0.0 branch. Everything is mostly working, and the primary work that remains is:

For this specific example, it actually demonstrates that as of right now, positional arguments are not correctly being passed into Config, so I'll have to fix that! If you end up giving it a try, just know that things may change. If you have feedback, I would also be very open to it!

Thanks for the great work, really like it so far 👍

Thank you!

BrianPugh commented 6 days ago

ok, the positional arguments should now work on dev-3.0.0. The example provided above now fully works as expected.