Open Jimmy2027 opened 3 years ago
This would be really helpful; in particular it would be easier to re-use a set of command line flags across different commands.
Maybe it's better to add helper that runs callable (main
from above), and returns its result.
Currently typer.run
is NoReturn
by it's behavior, because it calls exit()
deep inside of it.
I found typer
can pass arguments directly to any class, using it's __init__
as source for types of arguments and options.
But that requires to put code for main
to __init__
of that class.
With dataclasses it can be achieved in more elegant way - using theirs __post_init__
method.
With helper:
from dataclasses import dataclass, is_dataclass
from typing import Any, Callable, NoReturn, Type
import typer
T = TypeVar('T')
def run_dataclass(tp: Type[T], callback: Callable[[T], Any]) -> NoReturn:
assert is_dataclass(tp)
@dataclass
class BindType(tp): # type: ignore
def __post_init__(self):
super().__post_init__()
callback(self)
typer.run(BindType)
First snippet starts working:
from dataclasses import dataclass
@dataclass
class BaseFlags:
arg1: int = typer.Argument(help='something', default=1)
arg2: int = typer.Argument(help='something', default=2)
@dataclass
class Flags(BaseFlags):
arg3: int = typer.Argument(help='something', default=3)
arg4: int = typer.Argument(help='something', default=4)
def main(args: Flags):
print(args)
if __name__ == '__main__':
run_dataclass(Flags, main)
Hi @arquolo, thanks for your answer! If I try your code snipped, I get the error 'super' object has no attribute '__post_init__'
. Is it supposed to work as is?
Here's another helper which is based upon decorators. It might still be a bit rough around the edges (and I didn't test it with Typer because I need to get back to work) but at least the general idea should be clear:
from dataclasses import dataclass, is_dataclass
from typing import Callable, Type, TypeVar
@dataclass
class MyArguments:
name: str
formal: bool = False
T = TypeVar('T')
R = TypeVar('R')
# decorator generator
def inject_dataclass_args(cls: Type[T]):
assert is_dataclass(cls)
def decorator(func: Callable[[T], R]) -> R:
def wrapped(*args, **kwargs):
args_as_data_obj = cls(*args, **kwargs)
return func(args_as_data_obj)
wrapped.__annotations__ = cls.__init__.__annotations__
return wrapped
return decorator
# Usage:
@inject_dataclass_args(MyArguments)
def goodbye(data_obj: MyArguments):
if data_obj.formal:
print(f"Goodbye Ms. {data_obj.name}. Have a good day.")
else:
print(f"Bye {data_obj.name}!")
if __name__ == "__main__":
typer.run(goodbye)
One issue that I'm already foreseeing is that Typer won't notice that formal
is an optional parameter because it's not part of the annotations. I suppose Typer does some inspect
-module-related magic here to check for optional parameters, so one would have to do some similar reverse magic to ensure that inspect
yields the right results and not *args, **kwargs
.
A quick test shows that @arquolo's solution works better here because @dataclass
already sets up the right signature for MyArguments.__init__
. So to fix my solution one would either have to look into how @dataclass
achieves this or, inspired by @arquolo's suggestion, one could replace the above decorator with this monster:
def inject_dataclass_args(cls: Type[T]):
assert is_dataclass(cls)
def decorator(func: Callable[[T], R]) -> R:
@dataclass
class wrapped(cls):
def __post_init__(self):
super().__post_init__()
func(self)
return wrapped
return decorator
Either way, the nice thing about a decorator here is that it keeps the signature rewriting close to the function whose signature it's changing.
Side note: On a meta level this issue is somewhat related to determining the full type of a function (and "copying" it to another function) which is being discussed here.
I tweaked @codethief 's code and came up with this: https://gist.github.com/tbenthompson/9db0452445451767b59f5cb0611ab483
I have an already productionized version of this idea, and it's here: https://github.com/rec/dtyper
I have an already productionized version of this idea, and it's here: https://github.com/rec/dtyper
@rec From a skim of the docs, I think dtyper is doing the reverse of what's asked for here. It's making a dataclass from a CLI function. Instead, I already have a dataclass and I want to create a CLI function from that existing dataclass. Am I misunderstanding?
Oops, no, the misreading is mine. You are exactly right that is the reverse thing.
However, I don't quite see how you can really get this to work. How can you set the help for each argument? Or the names of command line flags?
On Fri, May 12, 2023 at 11:18 PM Ben Thompson @.***> wrote:
I have an already productionized version of this idea, and it's here: https://github.com/rec/dtyper
@rec https://github.com/rec From a skim of the docs, I think dtyper is doing the reverse of what's asked for here. It's making a dataclass from a CLI function. I already have a dataclass and I want to create a CLI function from that existing dataclass. Am I misunderstanding?
— Reply to this email directly, view it on GitHub https://github.com/tiangolo/typer/issues/197#issuecomment-1546312127, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAB53MQ7FAFIDNAEJXB3DF3XF2SKTANCNFSM4UBMIWRA . You are receiving this because you were mentioned.Message ID: @.***>
-- /t
PGP Key: @.** https://tom.ritchford.com https://tom.ritchford.com https://tom.swirly.com https://tom.swirly.com*
Oops, no, the misreading is mine. You are exactly right that is the reverse thing. However, I don't quite see how you can really get this to work. How can you set the help for each argument? Or the names of command line flags? …
The solution above manages this by copying the type signature of the dataclass' __init__
function. This gets you flag names and defaults:
@dataclass
class Test:
config: str = ""
hi: int = 1
bye: str = "bye"
@dataclass_cli
def main(c: Test):
"""docstring test"""
pass
you get a CLI like:
> python config.py --help
Usage: config.py [OPTIONS]
docstring test
╭─ Options
│ --config TEXT
│ --hi INTEGER [default: 1]
│ --bye TEXT [default: bye]
│ --help Show this message and exit.
╰─
This is enough for making a quick and easy CLI for personal use or a rough project.
Hi, is there a way to directly construct a dataclass from the command line inputs with typer? Like an argparser would do. For a large amount of arguments this would improve readability of the code, and would permit inheritances of dataclasses like in the example below:
Thanks a lot for the great package and your help!