Replies: 10 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 ? |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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_cityannotation, 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?
All reactions