-
-
Notifications
You must be signed in to change notification settings - Fork 672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[QUESTION] How to wrap commands #296
Comments
I have a solution for you. The main problem here is the way Typer knows about command parameters. It uses function signature and type hints. I was tinkering how to do that elegantly and I found an article which does exactly that: https://chriswarrick.com/blog/2018/09/20/python-hackery-merging-signatures-of-two-python-functions/ The author created package The secondary problem is how to pass the new parameters. You cannot use I used the mention package and the from typing import Callable
import typer
from merge_args import merge_args
app = typer.Typer()
def from_city(
func: Callable,
) -> "wrapper":
@merge_args(func)
def wrapper(
ctx: typer.Context,
city: str = typer.Option(
..., "--city", "-c", prompt=True, help="The name of the city to say hi from"
),
state: str = typer.Option(
"VA", "--state", "-s", help="The state you are saying hi from"
),
**kwargs,
):
"""Setup for finding city."""
return func(
ctx=ctx,
**kwargs
)
return wrapper
@app.command()
@from_city
def hi_city(
ctx: typer.Context,
name: str = typer.Option(
..., "--name", "-n", prompt=True, help="Name of person to say hi to"
),
):
"""Say hello."""
kwargs = ctx.params
print(f"Hello, {name}. Welcome from {kwargs['city']}, {kwargs['state']}!")
if __name__ == "__main__":
app()
|
You can actually also use the Typer import typer
app = typer.Typer()
@app.callback()
def extras(
ctx: typer.Context,
city: str = typer.Option(
..., "--city", "-c", prompt=True, help="The name of the city to say hi from"
),
state: str = typer.Option(
"VA", "--state", "-s", help="The state you are saying hi from"
),
):
ctx.obj = ctx.params
pass
@app.command()
def hi_city(
ctx: typer.Context,
name: str = typer.Option(
..., "--name", "-n", prompt=True, help="Name of person to say hi to"
),
):
"""Say hello."""
kwargs = ctx.obj
print(f"Hello, {name}. Welcome from {kwargs['city']}, {kwargs['state']}!")
if __name__ == "__main__":
app() |
@captaincapitalism thank you for this! I had never heard of merge_args. I believe this will work for our use case. Will close after doing some more testing. |
This one wouldn't work for apps that already have a callback |
I found a was getting a error from pyflakes with @captaincapitalism 's example code. Removing I also found that it didn't work with sub-commands or multiple commands - the commands were not listed correctly. Here's a revised example with two commands:
I think this is something I can work with. |
Ugh, found a problem. I tried adding this to allow the common options to be specified before the command:
And, while it does allow the For example, this still prompts for city:
|
As mentioned in #405 (comment) I would also be interested in an elegant solution to this. :) |
Here's an addition to @robinbowes code that uses a pydantic model (1) to make it easy to declare (2) and obtain the options (3) in a way that's a little bit more statically typed (4). Basically, it avoids having to access the options from a dictionary by name/string. This still feels like too many hoops to jump through, but it feels slightly easier to maintain. Also, a vanilla from typing import Callable
from pydantic import BaseModel
import typer
from merge_args import merge_args
app = typer.Typer()
# 1) define the common options in a pydantic model
class FromCity(BaseModel):
city: str = typer.Option(
..., "--city", "-c", prompt=True, help="The name of the city to say hi from"
)
state: str = typer.Option(
"VA", "--state", "-s", help="The state you are saying hi from"
)
class Config:
arbitrary_types_allowed = True
def from_city(
func: Callable,
) -> "wrapper":
@merge_args(func)
def wrapper(
ctx: typer.Context,
# 2) get the TyperOptions for each of the options
city: str = FromCity().city, # <-- here
state: str = FromCity().state, # <-- here
**kwargs,
):
return func(ctx=ctx, **kwargs)
return wrapper
@app.command()
@from_city
def hi_city(
ctx: typer.Context,
name: str = typer.Option(
..., "--name", "-n", prompt=True, help="Name of person to say hi to"
),
):
"""Say hello."""
# 3) Convert the arguments into an object / instance of the pydantic model
from_city = FromCity(**ctx.params)
# 4) Access them as attributes of the objects
print(f"Hello, {name}. Welcome from {from_city.city}, {from_city.state}!")
if __name__ == "__main__":
app() |
@jimkring Which version of python are u using? I get into some errors with your solution on 3.10 like Seems like this one is really related to the changed behaviour of typing in 3.10. ====== EDIT: So to summarize - IDK if this is an issue with the |
How does this perform ? Is it slowing down building the CLI, in comparison to normal functions with normal signatures ? |
First check
Description
How can I wrap a command and extend the options?
I am able to do this via Click but haven't figured out a way to make it work. I want to be able to extend certain commands to give extra options when they are annotated.
Lets say I have a command defined as:
I can call this via
python cli.py hi
. Want I want is the ability to wrap the hi function to include a city and state if I wanted. Example:By adding the
@from_city
annotation, I want the additional city/state options to be available.Additional context
I can do this in Click with the following code:
Is there a way to do this in Typer?
The text was updated successfully, but these errors were encountered: