Kwpolska / merge_args

Merge signatures of two functions with Advanced Hackery.
BSD 3-Clause "New" or "Revised" License
30 stars 6 forks source link

Help with merging Typer command args #8

Open robinbowes opened 1 year ago

robinbowes commented 1 year ago

Hi,

I'm using merge_args to add common options to several Typer commands. It seems to work OK for options with an argument (eg. --author Kwpolska) but when I try to add options with no argument (eg. --version) the merged option gets converted to require an argument.

I've created this gist showing what I mean: https://gist.github.com/robinbowes/fa815f2c0576fc6c76409ff3ba31b407

Any idea why the the version arg gets mangled when merged?

joanise commented 1 year ago

Hi @robinbowes we're just trying to combine typer and merge_args too, and I'm not sure I understand your implementation. My test common_opts() function is just simple like this:

def base_function(
    arg2: int,
    arg3: str = typer.Option("default arg3"),
    version: Optional[bool] = typer.Option(
        None, "--version"
    )
):
    if version:
        print("version")
    print("arg2:", arg2)
    print("arg3:", arg3)

and then this works fine, with --version working as you would expect as a switch:

@app.command()
@merge_args(base_function)
def test_three(arg1: str, *args, **kwargs):
    print("arg1:", arg1)
    return base_function(*args, **kwargs)
robinbowes commented 1 year ago

@joanise I'm not sure I understand it now, after so much hacking around 🤪

I've currently reverted to defining all the common options separately and simply adding the options explicitly to every command, eg:

options = SimpleNamespace(
    log_level=typer.Option("ERROR", callback=set_log_level, is_eager=True),
    sender=typer.Option(None),
    stuff=typer.Option(()),
    version=typer.Option(None,"--version",callback=show_version, is_eager=True),
)

@app.command()
def create(
    ctx: typer.Context,
    log_level: LogLevel = common.options.log_level,
    sender: str = common.options.sender,
    stuff: Optional[List[str]] = common.options.target,
    version: Optional[bool] = common.options.version,
):
  print("create stuff here")

@app.command()
def delete(
    ctx: typer.Context,
    log_level: LogLevel = common.options.log_level,
    sender: str = common.options.sender,
    stuff: Optional[List[str]] = common.options.target,
    version: Optional[bool] = common.options.version,
):
  print("delete stuff here")
Kwpolska commented 1 year ago

I'm afraid I don't know much about typer and its internals, so I might not be able to help.

If you compare inspect.signature() of a hand-written function, and then of a function merge_args with the same meaning, are there any differences? Perhaps the default values or annotations are somewhere typer doesn’t expect them?

joanise commented 1 year ago

@robinbowes This is how my colleague ended up solving it, defining both a base_function() and a dummy base_function_interface() for the purpose of using with merge_args. It works like a charm.

def base_function(arg1, arg2, arg3):
    print("arg1", arg1)
    print("arg2:", arg2)
    print("arg3:", arg3)

def base_function_interface(arg2: int = typer.Option(1), arg3: str = typer.Option("str")):
    """Base Function

    Args:
        arg2 (int, optional): _description_. Defaults to typer.Option(1).
        arg3 (str, optional): _description_. Defaults to typer.Option("str").
    """
    pass

@app.command()
@merge_args(base_function_interface)
def test_three(arg1: str = typer.Option("arg3 default value"), *args, **kwargs):
    """Test Three

    Args:
        arg1 (str, optional): _description_. Defaults to typer.Option("arg3 default value").

    Returns:
        _type_: _description_
    """
    return base_function(arg1, kwargs['arg2'], kwargs['arg3'])
joanise commented 1 year ago

@Kwpolska Well, even if you don't know much about typer, you did us an awesome service with this merge_args decorator. I was doing research into merging two signatures and it's really quite complex! typer builds the CLI by inspecting the signature of the function, so by giving us a tool to merge signature, you're effectively giving us a tool to provide base options in one function, and additional options in the command function.

So thank you very much for this module!

robinbowes commented 1 year ago

Thanks both. I shall review your suggestions and see if I can figure things out.

R.

adm271828 commented 7 months ago

Hi,

You might also want to have a look here: https://github.com/tiangolo/typer/issues/153#issuecomment-1922568794

I do not use merge_args but inspiration came by looking at merge_args's source code (needed a helper function to merge signatures, done differently though).

Regards.