fastapi / typer

Typer, build great CLIs. Easy to code. Based on Python type hints.

Home Page:https://typer.tiangolo.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add optional wrapper argument in Typer.command to wrap functions

dyollb opened this issue · comments

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to Typer but to Click.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

import SimpleITK as sitk
import sitk_cli
import typer

app = typer.Typer()

# sitk_cli.make_cli inspects the function and replaces images by pathlib.Path, and if the output is an image it inserts an argument `output: pathlib.Path`. Together with typer, this makes it trivial to create command lines from library code.


@app.command(wrapper=sitk_cli.make_cli)
def threshold_image(image: sitk.Image, value: float) -> sitk.Image:
    """Example function that has images as argument"""
    return sitk.BinaryThreshold(image, value)


if __name__ == "__main__":
    app()

Description

Proposed Change

I would like to submit a PR to add an argument called wrapper (or whatever), which by default does nothing, but could do any user-specified transformation of the function registered via Typer.command.

Motivation

I would like to use this mechanism to modify the signature of the original function so it can be used as command line, without duplicating the function - once with actual type used in the library and once with CLI compatible types (e.g. pathlib.Path).

See my example code above, using the simple package sitk-cli to wrap SimpleITK images.

Wanted Solution

The change would look like:

    def command(
        self,
        name: Optional[str] = None,
        *,
        cls: Optional[Type[TyperCommand]] = None,
        context_settings: Optional[Dict[Any, Any]] = None,
        help: Optional[str] = None,
        epilog: Optional[str] = None,
        short_help: Optional[str] = None,
        options_metavar: str = "[OPTIONS]",
        add_help_option: bool = True,
        no_args_is_help: bool = False,
        hidden: bool = False,
        deprecated: bool = False,
        wrapper: Callable[[CommandFunctionType], CommandFunctionType] = lambda f: f,
        # Rich settings
        rich_help_panel: Union[str, None] = Default(None),
    ) -> Callable[[CommandFunctionType], CommandFunctionType]:
        if cls is None:
            cls = TyperCommand

        def decorator(f: CommandFunctionType) -> CommandFunctionType:
            self.registered_commands.append(
                CommandInfo(
                    name=name,
                    cls=cls,
                    context_settings=context_settings,
                    callback=wrapper(f),
                    help=help,
                    epilog=epilog,
                    short_help=short_help,
                    options_metavar=options_metavar,
                    add_help_option=add_help_option,
                    no_args_is_help=no_args_is_help,
                    hidden=hidden,
                    deprecated=deprecated,
                    # Rich settings
                    rich_help_panel=rich_help_panel,
                )
            )
            return f

        return decorator

Wanted Code

See Example Code above

Alternatives

Instead of using a decorator i could explicitly register my function.

if __name__ == "__main__":
    app.command()(make_cli(threshold_image))
    app()

Operating System

Windows

Operating System Details

Linux, MacOS, Windows

Typer Version

0.7.0

Python Version

Python 3.7.8

Additional Context

I would like to use typer in combination with sitk-cli and SimpleITK to create nice command lines:

See https://pypi.org/project/sitk-cli/

Question here: why not just nest the decorators? Add your custom wrapper inside the call to app.command() so it get applied first. As a simple example:

def ceil_wrapper(f):
    @wraps(f)
    def wrap(x: float):
        return f(ceil(x))
    return wrap


@app.command()
@ceil_wrapper
def f(x: int) -> None:
    print(x)

It seems like the maintainers of sitk-cli (namely @dyollb themself) settled on this solution as well, so maybe this issue can be closed?

Okay with me