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:
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