fastapi / typer

Typer, build great CLIs. Easy to code. Based on Python type hints.
https://typer.tiangolo.com/
MIT License
15.59k stars 663 forks source link

Default value for optional multi-value argument #518

Open mathiazom opened 1 year ago

mathiazom commented 1 year ago

First Check

Commit to Help

Example Code

import typer

def main(names: list[str] = typer.Argument(["Peter", "Paul", "Mary"])):
    for name in names:
        typer.echo(f"Hi, {name}")

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

Description

The provided example raises the following error:

Traceback (most recent call last):

  File "[...]\main.py", line 8, in <module>
    typer.run(main)

TypeError: 'default' is not supported for nargs=-1.

Looking at the Click docs, my understanding is that nargs=-1 is used to denote Variadic arguments, i.e. multi-value arguments.

So if default is not supported for nargs=-1, is there any way to have default values for multi-value arguments in Typer?

Desired behaviour: the default values are used if no argument values are provided, with a help message that looks something like this:

Usage: main.py [OPTIONS] [NAMES]...

Arguments:
  [NAMES]...  [default: [Peter, Paul, Mary]]

Options:
  --help  Show this message and exit.

Operating System

Windows

Operating System Details

No response

Typer Version

0.7.0

Python Version

3.10.9

Additional Context

I believe this is related to #108, where the conclusion was to use None as default to achieve an optional multi-value argument. However, it would be useful if other default values were supported as well. But this might just be a limitation of Click?

Love the project ā¤ļø

N4D1K-lgtm commented 1 year ago

This may or may not help but I got around this issue by manually creating a callback that detects if the given parameter is None and setting it to my desired default value.

I also set show_default=False in foo: List[bar] = typer.Argument(...) and added "[black]\[default: default][/black]" to the end of my help="parameter help string" in order to display an accurate default message when running my-command --help.

I can post the complete code if anyone needs it but I feel it's easy enough to understand from this.

Note Rich is required to use [color][/color] tags inside strings

InCogNiTo124 commented 1 year ago

Edit: nvm it works for:

def stdin_callback(value: Optional[Path]) -> Path:
    return value if value else [Path('/dev/stdin')]

I'm trying to make CLI which accepts multiple file paths (and stdin if no paths provided) and I get the same error.


def stdin_callback(value: Optional[Path]) -> Path:
    return value if value else Path('/dev/stdin')

def main(
    files: List[Path] = typer.Argument(
        default=[None].
        allow_dash=True,
        exists=True,
        dir_okay=False,
        readable=True,
        callback=stdin_callback)

TypeError: 'default' is not supported for nargs=-1.

Introducing a callback doesn't solve the issue unfortunately.

JP-Globality commented 1 year ago

Using a callback worked for me

def callback(value: Optional[list[int]]) -> list[int]:
    return value if value else []
jasonk commented 8 months ago

Trying to do something similar (a command that optionally takes any number of string arguments), I tried many variations, before I discovered that this is the only way that works:

names: List[str] = Argument( default = None )

However, this doesn't, even though it should mean the same thing, it still makes it required.

names: Annotated[List[str], Argument( default = None )]

These similar variations also still end up required:

names: Annotated[List[str], Argument( default = None )]
names: Annotated[List[str], Argument( default_factory = list )]

This one throws the "'default' is not supported for nargs=-1" error:

names: List[str] = Argument( default_factory = list )

And these variations do make it optional, but also turn it into a --names option instead of keeping it as an argument:

names: Annotated[List[str], Argument()] = None
names: Annotated[List[str], Argument()] = []
names: Annotated[Optional[List[str]], Argument()] = None
MaxG87 commented 1 month ago

Using typer=0.12.5 I still can reproduce the reported issue.

However, I use another workaround that might be interesting:

def main(names: list[str] | None = typer.Argument(None)):
    if names is None:
        names = ["Peter", "Paul", "Mary"]