typer icon indicating copy to clipboard operation
typer copied to clipboard

[QUESTION] Option from set of choices, where choices are not valid python identifier names

Open EntilZha opened this issue 4 years ago • 4 comments

First check

  • [x ] I used the GitHub search to find a similar issue and didn't find it.
  • [x ] I searched the Typer documentation, with the integrated search.
  • [x ] I already searched in Google "How to X in Typer" and didn't find any information.
  • [x ] I already searched in Google "How to X in Click" and didn't find any information.

Description

How can I specify that a command argument should be from a set of choices? Based on the typer docs, the canonical way is to use enums https://typer.tiangolo.com/tutorial/parameter-types/enum/ and in click it would be with something like:

@click.option(
    "--model", default="1PL", type=click.Choice(models.keys(), case_sensitive=True)
)

The problem I'm having with using enums is that the keys in my dictionary start with numbers, so they are not valid variable names in an enum. Specifically, I have something like this:

models = {
    "1PL": Path("./stan/1PL.stan"),
    "2PL": Path("./stan/2PL.stan"),
    "3PL": Path("./stan/3PL.stan"),
}

Is it possible to pass click.Choice to typer or if not add a feature that would enable this?

Thanks!

EntilZha avatar Oct 29 '20 17:10 EntilZha

@EntilZha Not an answer to your request, but a simple workaround

If you create an enum that has those keys as values, you can use that in the cli and to access your models dict

from enum import Enum
from pathlib import Path
import typer

class Test(Enum):
    ONE = "1PL"
    TWO = "2PL"
    THREE = "3PL"

app = typer.Typer()

models = {
    "1PL": Path("./stan/1PL.stan"),
    "2PL": Path("./stan/2PL.stan"),
    "3PL": Path("./stan/3PL.stan"),
}

@app.command()
def test(choice: Test = typer.Argument(...)):
    print(choice)
    print(models[choice.value])

python cli.py 1PL will print

Test.ONE
stan/1PL.stan

Andrew-Sheridan avatar Nov 04 '20 12:11 Andrew-Sheridan

That's workable for my use case, thanks! It would be nice to have a more "native" way, but I also recognize that my case is fairly niche.

EntilZha avatar Nov 04 '20 16:11 EntilZha

I needed to do something similar to this too. However, I wanted mine to be a prompt. Here's how I was able to achieve that:

from enum import Enum
from pathlib import Path
import typer

class Test(Enum):
    ONE = "1PL"
    TWO = "2PL"
    THREE = "3PL"

app = typer.Typer()

models = {
    "1PL": Path("./stan/1PL.stan"),
    "2PL": Path("./stan/2PL.stan"),
    "3PL": Path("./stan/3PL.stan"),
}

@app.command()
def test(
    choice: Test = typer.Option(
        None,
        "--choice",
        prompt=True,
        show_choices=True,
    )
) -> None:
    print(choice)
    print(models[choice.value])

Emmarex avatar Nov 30 '21 15:11 Emmarex

I agree with @EntilZha -- it would be nice to have a more "native" way. One drawback of using Enums for the "choices" is that enum members are subject to python object naming restrictions (e.g. cannot have dashes).

odoublewen avatar Oct 11 '22 20:10 odoublewen

Thanks for the help here everyone! 👏 🙇

If that solves the original problem, then you can close this issue ✔️

Have in mind that enums use the values, strings, not the identifiers.

Sorry for the long delay! 🙈 I wanted to personally address each issue/PR and they piled up through time, but now I'm checking each one in order.

tiangolo avatar Nov 11 '22 15:11 tiangolo

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

github-actions[bot] avatar Nov 22 '22 00:11 github-actions[bot]

I think I found the way:

# saved as test.py

from typing import Annotated

import click
import typer

app = typer.Typer()


@app.command()
def test(choice: Annotated[str, typer.Option(click_type=click.Choice(['1PL', '2PL', '3PL']))] = '1PL'):
    print(choice)


if __name__ == "__main__":
    app()

It can be recognized as a choice:

❯ python -m test --help

 Usage: test.py [OPTIONS]

╭─ Options ─────────────────────────────────────────────╮
│ --choice               [1PL|2PL|3PL]  [default: 1PL]  │
╰───────────────────────────────────────────────────────╯

If I go like this, it would raise error:

❯ python -m test 0PL
Usage: test.py [OPTIONS]
Try 'test.py --help' for help.
╭─ Error ──────────────────────────────────────────╮
│ Got unexpected extra argument (0PL)              │
╰──────────────────────────────────────────────────╯

HaiyiMei avatar Sep 06 '23 12:09 HaiyiMei