Open mezuzza opened 1 month ago
It depends on what you're trying to achieve, but here are 2 approaches:
from typing import Annotated
from cyclopts import App, Parameter
app = App()
ApiKeyType = Annotated[str, Parameter(help="User API key.")]
@app.command
def foo(api_key: ApiKeyType):
"""Foo short description."""
pass
@app.command
def bar(api_key: ApiKeyType):
"""Bar short description."""
pass
if __name__ == "__main__":
app()
from typing import Annotated
from cyclopts import App, Parameter
app = App()
API_KEY: str
@app.command def foo(): """Foo short description.""" print(f"foo {API_KEY=}")
@app.command def bar(count: int): """Bar short description.""" print(f"bar {count} {API_KEY=}")
@app.meta.default def meta( *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], api_key: str, ): """My application description.
Parameters
----------
api_key: str
User API key.
"""
global API_KEY
API_KEY = api_key
app(tokens)
if name == "main": app.meta() # NOTICE!!! This is "app.meta", not just "app"
```bash
$ python issue-212.py foo --api-key=SECRET
foo API_KEY='SECRET'
$ python issue-212.py bar 7 --api-key=SECRET
bar 7 API_KEY='SECRET'
Love the library btw. It's a really intuitive approach overall.
Thank you!
The idea is that commands can have configuration that's shared across sub commands with sub commands adding additional args.
Let's say the parent command has 10 parameters, it becomes quickly unmanageable. You can think about common flags like api keys, verbosity flags, etc.
Globals would work, but at that point, I think you'd end up in a world where you're using an argparse-ish library like simple_parsing
.
I almost think something like:
@app.command
def foo(*, api_key: str):
"""Foo short description.
Args:
api_key: some description
"""
@app.command
def bar(pos_arg: str, /):
"""Bar's description.
Args:
pos_arg: some arg
"""
print(pos_arg)
This would be a nice way of describing something like:
./my_cmd foo --api_key="some-key" bar "hello"
Of course, this has a number of problems, but the main point is that foo's configuration is shared with bar in a way that lets other subcommands exist.
Anyway, not really a complaint, just something I was dealing with. For my particular use case for now, it's fine to duplicate the flags across the commands, but I don't think I can really keep that up.
Looks like Typer doesn't have a good answer to this either afaict: https://github.com/fastapi/typer/issues/153
Maybe something like this is closer to what you want:
from dataclasses import dataclass
from typing import Annotated
from cyclopts import App, Parameter
@dataclass
class CommonConfig:
api_key: str
# Setting parse=False means that Cyclopts skips it;
# the config will be supplied from the meta app.
ConfigType = Annotated[CommonConfig, Parameter(parse=False)]
app = App()
@app.command
def foo(
*,
config: ConfigType,
):
"""Foo short description."""
print(f"foo {config.api_key=}")
@app.command
def bar(
count: int,
*,
config: ConfigType,
):
"""Bar short description."""
print(f"bar {count} {config.api_key=}")
@app.meta.default
def meta(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
api_key: str,
):
config = CommonConfig(api_key=api_key)
command, bound = app.parse_args(tokens)
return command(*bound.args, **bound.kwargs, config=config)
if __name__ == "__main__":
app.meta() # NOTICE!!! This is "app.meta", not just "app"
In the upcoming Cyclopts v3, you will be able to directly instantiate the CommonConfig
without having to explode out the arguments in the meta-function-signature.
Is there a good way to share flags between commands? It's a bit of a pain to rewrite them over and over. For example, I'd like to share an API key across all commands but right now I just copy that parameter across commands which is a bit tedious.
Love the library btw. It's a really intuitive approach overall.