Skip to content
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

Specifying help for arguments & options in function docstrings #336

Open
7 tasks done
alexreg opened this issue Oct 19, 2021 · 11 comments
Open
7 tasks done

Specifying help for arguments & options in function docstrings #336

alexreg opened this issue Oct 19, 2021 · 11 comments
Labels
feature New feature, enhancement or request investigate

Comments

@alexreg
Copy link

alexreg commented Oct 19, 2021

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

This is the current way to add help text for arguments (and similarly for options).

import typer


def main(name: str = typer.Argument(..., help="The name of the user to greet")):
    """
    Say hi to NAME very gently, like Dirk.
    """
    typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

Description

It would be convenient to be able to specify help text for arguments and options via the function's docstring in addition to the current method. At the moment, of course, help can only be specified for the command itself via the function's docstring.

Wanted Solution

To be able to specify help text for command arguments and options via the function docstring, as below.

Wanted Code

This should behave in a totally equivalent way to the above example of code that already works.

import typer


def main(name: str = typer.Argument(...)):
    """
    Say hi to NAME very gently, like Dirk.

    :param name: The name of the user to greet
    """
    typer.echo(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

Note, the other standard syntax for parameter descriptions in functions is @param name: The name of the user to greet, and this should also be supported, I would think.

Alternatives

Just use the current method of specifying help text in the function signature, via arg = typer.Argument(..., help="<text>") or opt = typer.Option(..., help="<text>"). I argue that a) this can be seen as "polluting" the function signature and making it harder to read quickly/easily, b) it is most consistent to be able to specify help text for a command and its arguments 100% through docstrings (if so desired).

Operating System

macOS

Operating System Details

No response

Typer Version

0.4.0

Python Version

3.9.6

Additional Context

No response

@alexreg alexreg added the feature New feature, enhancement or request label Oct 19, 2021
@sathoune
Copy link

This is quite big feature that you propose.

Do you want to generate documentation based on docstrings? You can check typer-cli.

You could consider other alternative:
Moving typer.Argument(...)'s somewhere else:

machine_learning_model = typer.Argument(..., help="Specifiy predictive model that you'd like to use")
save_in_database=typer.Option(True, help="State your desire to save prediction result in our database for future reference")
...
@app.command()
def predict(model: str = machine_learning_model, to_save: bool=save_in_database):
...

This solution would also allow you to reuse some more elaborate setup of CLI.

On the other side help as a parameter allows you to be a bit more flexible, you can swap languages if your CLI's audience prefers something other than English.

If really you need this for your case, that should not be difficult to overwrite in your project.

There are two functions to overwrite:
get_help_record for TyperArgument:
https://github.com/tiangolo/typer/blob/a1520dcda685220a9a796288f5eaaebd00d68845/typer/core.py#L142
and TyperOption:
https://github.com/tiangolo/typer/blob/a1520dcda685220a9a796288f5eaaebd00d68845/typer/core.py#L274

These are used at get_click_param of Typer class:
https://github.com/tiangolo/typer/blob/a1520dcda685220a9a796288f5eaaebd00d68845/typer/main.py#L599

ctx has docstring at ctx.command.callback.__doc__
So creating three clases that inherit from above should do the job.

Another approach for that would be to modify help value for each argument before it is registered by the command.
You can do that by overwriting method of Typer class or I came up with something a little less sophisticated:

import typer
import typing
import functools

app = typer.Typer()


def update_help(the_default):
    the_default.help = "arbitraty value"
    return the_default


def my_command(app):
    @functools.wraps(app.command)
    def wrapper_0(*args, **kwargs):
        def wrapper_1(f):
            f.__defaults__ = tuple([update_help(arg) for arg in f.__defaults__])

            return app.command(*args, **kwargs)(f)

        return wrapper_1

    return wrapper_0


help_overwriting_command = my_command(app)


@help_overwriting_command()
def main(
    a: str = typer.Argument(..., help="oaneutsoua"),
    b: bool = typer.Option(False, help="I'd like this to be something else"),
):
    """

    HOHHOHO

    :param a: str My first arg
    :param b: bool My first option
    :return:
    """
    pass


if __name__ == "__main__":
    app()

with python main.py --help you should get help with custom messages. All you would need to do is to create appropriate parser for your case.

@alexreg
Copy link
Author

alexreg commented Nov 14, 2021

@captaincapitalism Thanks for your suggestion and the implementation info, which is useful indeed. I should have mentioned in my original report that I considered the same alternative solution that you did, although I still find it a little unwieldy, and it still suffers from mixing up help text with configuration for arguments and options. But it's still a reasonable solution in certain circumstances.

I'll probably have a look at your suggested way or overriding the default behaviour re help strings, and see what I can come up with. Appreciate it.

@pbarker
Copy link

pbarker commented Jan 6, 2022

This would be super useful! It's best practice to write docstrings, and also best practice to not repeat yourself. Much of this project aims to remove the repetition that Click brings. I see this feature as another extension of that philosophy

@alexreg
Copy link
Author

alexreg commented Jan 6, 2022

@pbarker Glad you agree. Given this project doesn't see a lot of activity these days, I'm thinking of forking it and adding this feature along with a few others, so stay tuned...

@pbarker
Copy link

pbarker commented Jan 7, 2022

@alexreg yeah thats been my concern with trying to put PRs in, it looks like most of them sit. If you fork please ping me!

alexreg added a commit to alexreg/typer-cloup that referenced this issue May 13, 2022
@alexreg
Copy link
Author

alexreg commented May 13, 2022

@pbarker Hey. So, I've gone ahead and forked to alexreg/typer, where I've fixed a few bugs, and added this feature. (See tests/test_help_docstring.py for an example how to use it.)

alexreg added a commit to alexreg/typer-cloup that referenced this issue May 13, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue May 13, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue May 13, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue May 15, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue May 15, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Jun 17, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Jul 2, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Jul 9, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Jul 21, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Jul 31, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Aug 7, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Aug 8, 2022
alexreg added a commit to alexreg/typer-cloup that referenced this issue Aug 8, 2022
@giacomov
Copy link

giacomov commented Sep 20, 2022

This is an alternative solution based on a simple decorator, leveraging docstring_parser. Save this into typer_easy_cli.py:

from docstring_parser import parse

def typer_easy_cli(func):
    """
    A decorator that takes a fully-annotated function and transforms it into a
    Typer command.
    
    At the moment, the function needs to have only keywords at the moment, so this is ok:
    
    def fun(*, par1: int, par2: float):
        ...
    
    but this is NOT ok:
    
    def fun(par1: int, par2: float):
        ...
    """

    # Parse docstring
    docstring = parse(func.__doc__)
    
    # Collect information about the parameters of the function
    parameters = {}
    
    # Parse the annotations first, so we have every parameter in the
    # dictionary
    for par, par_type in func.__annotations__.items():
        parameters[par] = {'default': ...}
    
    # Now loop over the parameters defined in the docstring to extract the
    # help message (if present)
    for par in docstring.params:
        parameters[par.arg_name]["help"] = par.description
    
    # Finally loop over the defaults to populate that
    for par, default in func.__kwdefaults__.items():
        parameters[par]["default"] = default
    
    # Transform the parameters into typer.Option instances
    typer_args = {par: typer.Option(**kw) for par, kw in parameters.items()}
    
    # Assign them to the function
    func.__kwdefaults__ = typer_args
    
    # Only keep the main description as docstring so the CLI won't print
    # the whole docstring, including the parameters
    func.__doc__ = "\n\n".join([docstring.short_description, docstring.long_description])

    return func

Then you can use it like this:

from docstring_parser import parse
from typer_easy_cli import typer_easy_cli
import typer

app = typer.Typer(add_completion=False)


@typer_easy_cli
@app.command()
def my_function(
    *,
    one: int,
    two: float = 3,
):
    """
    This is the description.
    
    This is the long description.
    
    :param one: the first parameter
    :param two: the second parameter
    """
    # Do something
    return one * two


if __name__ == "__main__":
    app()

This will give:

> python cli.py --help
Usage: cli.py [OPTIONS]                                                        
                                                                                
 This is the description.                                                       
 This is the long description.                                                  
                                                                                
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ *  --one         INTEGER  the first parameter [default: None] [required]     │
│    --two         FLOAT    the second parameter [default: 3]                  │
│    --help                 Show this message and exit.                        │
╰──────────────────────────────────────────────────────────────────────────────╯

With a little more effort it could be made to accept arguments (instead of only options).

@tiangolo if this is something useful I could make a PR and add it to the repo as a utility.

@alexreg
Copy link
Author

alexreg commented Sep 20, 2022

@giacomov That's a nice library. I may adapt my fork to use it instead of my 'homemade' solution.

alexreg added a commit to alexreg/typer-cloup that referenced this issue Nov 11, 2022
@StijnCaerts
Copy link

This feature would be very useful. Can we get this merged in typer please?

@sherbang
Copy link

This feature would be very useful. Can we get this merged in typer please?

Since I started following this issue, I found cyclopts which is inspired by Typer, but does parse docstrings.

@alexreg
Copy link
Author

alexreg commented Jan 16, 2024

@sherbang Good to know about, thanks. My own fork of typer also supports this, in case people are curious to try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature, enhancement or request investigate
Projects
None yet
Development

No branches or pull requests

7 participants