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

Does Typer support Tuples as Multiple Multi Value Options - List[Tuple[str, str]] - like Click does? #387

Open
7 tasks done
jtfidje opened this issue Apr 20, 2022 · 12 comments
Labels
investigate question Question or problem

Comments

@jtfidje
Copy link

jtfidje commented Apr 20, 2022

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 typer
from typing import List, Tuple

app = typer.Typer()

@app.command()
def test(opts: List[Tuple[str, str]]):
    ...

Description

I'm converting a small Click app to Typer and hit the following issue; I can't seem to get Tuples as Multiple Multi Value Options working.

I would like to achieve the following:

python main.py test --opt "--some-arg" "value" --opt "--another-arg" "value"

This is achieved in Click with the following option:

@click.option(
    "--opt",
    "opts",
    multiple=True,
    type=(str, str),
    required=False,
    default=(),
    help=(
        "Append to current config string. Options are given in the following format: "
        '[--opt "--force" "true" --opt "--overwrite" "true"]'
    ),
)

Operating System

Linux

Operating System Details

No response

Typer Version

0.4.1

Python Version

3.7.3

Additional Context

No response

@jtfidje jtfidje added the question Question or problem label Apr 20, 2022
@jonatasleon
Copy link

I was struggling with same problem here. I'll post how I solved my problem, this could be useful for others.

Once I tried to define option type as List[Tuple[str, str]] was raised an AssertionError as following:

AssertionError: List types with complex sub-types are not currently supported

To workaround this situation I had to use List[str] as type for the option, then set a callback function to transform the input. Once I would like to use option as that:

my-command --opts key1 value1 --opts key2 value2

I had to use this other pattern:

my-command --opts key1=value1 --opts key2=value2

At the end, my code looks like:

# Since there is a `_parse_option`
def _parse_option(value):
    result = defaultdict(list)
    for value in values:
        k, v = value.split('=')
        result[k.strip()].append(v.strip())
    return result.items()

@app.command
def my_command(opts: List[str] = typer.Option(..., callback=_parse_option):
    print(opts)

# [('key1', ['value1']), ('key2', ['value2'])]

Note 1: _parse_option returns result.items() in order to provide a list to the option.
Otherwise, opts would receive only a list with the keys from result.

Note 2: I'm using defaultdict(list) because in my use case I would like to allow repeated keys in --opts option.

@nealian
Copy link

nealian commented Sep 14, 2022

Another, possibly naive way of handling this would be to use the Click Context and a bit of custom parsing, though as mentioned in my own question, I don't know how to add that to the help output.

This would look something like:

def _parse_extras(extras: List[str]):
    _extras = extras[:]
    extra_options = {
        'complex_option1': [],
        'complex_option2': [],
    }
    while len(_extras) > 0:
        if _extras[0] == '--complex-option1' and len(_extras) > 2:
            complex_values = _extras[1:2]
            _extras = _extras[3:]
            extra_options['complex_option1'].append(_parse_opt1(*complex_values))
        elif _extras[0] == '--complex-option2' and len(_extras) > 3:
            complex_values = _extras[1:3]
            _extras = _extras[4:]
            extra_options['complex_option2'].append(_parse_opt2(*complex_values))
        else:
            raise click.NoSuchOption(_extras[0])
    return extra_options

@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def my_command(ctx: typer.Context, other_normal_opts):
    extra_options = _parse_extras(ctx.args)
    complex_option1 = extra_options['complex_option1']
    complex_option2 = extra_options['complex_option2']
    print(dir())

noctuid added a commit to noctuid/typer that referenced this issue Mar 29, 2023
@noctuid
Copy link

noctuid commented Mar 29, 2023

It was pretty simple to get this working locally for something simple like List[Tuple[str, str]]. However I couldn't get the recursive type conversion working. I tried just doing generate_list_convertor(generate_tuple_convertor(main_type.__args__)) and actually adding a new convertor, but neither worked (tests still fail). I didn't have time to look more deeply into how the conversion works. Commit linked if anyone else wants to look at it.

noctuid added a commit to noctuid/zscroll that referenced this issue Mar 30, 2023
This requires a hack to maintain backwards-compatibility (i.e. setting sys.argv and manually running a typer command to parse it), but argparse also required a hack to preprocess the arguments.

This is blocked by fastapi/typer#387, which I have fixed locally (tests will not pass without typer as-is).

Functional improvements:
- Typer gives nicer help text and can generate completions for different shells
- --length now enforces the visual length of the entire text, making it possible to have zscroll take up a consistent amount of space even if after or before padding changes
- Add --shift-count flag (partially addresses #21)
- Allow using --match-text ".*" "<new options>" to just check the exit status of the corresponding match command

Code improvements:
- Split into multiple files and organize better
- Remove use of globals; instead create a Scroller class that stores used state
- Remove various confusing or unnecessary variables and functions: last_hidden_was_wide, next_hidden_was_wide, pad_with_space, needs_scrolling, should_restart_printing, build_display_text, etc.
- Simplify handling of full-width characters; it is only necessary to handle phasing out one side; the other side can automatically be handled when the visual length of the text is fixed (see visual_slice function that replaces make_visual_len)
- Way more testing

Meta improvements:
- Add .editorconfig file
- Use ruff for linting (remove flake8, isort, etc.)
- Additionally run tests on 3.10
- Do all configuration in pyproject.toml (remove .pylintrc and tox.ini)
- Switch from Makefile/make to poethepoet (fewer dependencies)
- Stop using setup.py (unnecessary duplication and deprecated); use poetry (and eventually poeblix for data_files support) instead
- Add a basic nix.shell file
@MitriSan
Copy link

MitriSan commented Apr 6, 2023

@noctuid Hi! I think i have a simple usecase in my project - List[Tuple[str, Optional[str]]]. Could you please briefly describe how did you manage to make it work? Or maybe what direction i should look for. Is it possible using click context as described above? Thx!

@noctuid
Copy link

noctuid commented Apr 6, 2023

I changed the Typer code itself (see WIP Support specifying options as a list of tuples). I'm not sure it will work with Optional[str] though. Does typer support Tuple[str, Optional[str]]? I'm not sure how parsing that would even work. It seems like you could run into ambiguous cases where it wouldn't be clear if a string was for that option or a later argument.

@MitriSan
Copy link

MitriSan commented Apr 7, 2023

@noctuid thanks for sharing this, hope it will be merged at some point! I re-thinked my approach - tuple with optional second argument is ambiguous indeed. For now it's working for me with using callback and List[str] param with a bit of parsing, as was suggested by @jonatasleon.

@ankostis
Copy link

This minor fix to make @jonatasleon's worthy workaround to support also values containing the = char:

k, v = value.split('=', 1)

@alex-janss
Copy link

Using click_type seemed to work for me:

@app.command()
def foo(pairs: list[click.Tuple] = typer.Option(click_type=click.Tuple([str, str]))):
    for x, y in pairs:
        print(f'x: {x}, y: {y}')

@diegoquintanav
Copy link

diegoquintanav commented Feb 27, 2024

Using click_type seemed to work for me:

@app.command()
def foo(pairs: list[click.Tuple] = typer.Option(click_type=click.Tuple([str, str]))):
    for x, y in pairs:
        print(f'x: {x}, y: {y}')

What version of typer are you using @alex-janss? I'm getting TypeError: Option() got an unexpected keyword argument 'click_type' with typer 0.6.1.

@alex-janss
Copy link

Using click_type seemed to work for me:

@app.command()
def foo(pairs: list[click.Tuple] = typer.Option(click_type=click.Tuple([str, str]))):
    for x, y in pairs:
        print(f'x: {x}, y: {y}')

What version of typer are you using @alex-janss? I'm getting TypeError: Option() got an unexpected keyword argument 'click_type' with typer 0.6.1.

0.9.0

@mgab
Copy link

mgab commented Jul 25, 2024

Using click_type seemed to work for me:

@app.command()
def foo(pairs: list[click.Tuple] = typer.Option(click_type=click.Tuple([str, str]))):
    for x, y in pairs:
        print(f'x: {x}, y: {y}')

This workaround works, but still it's a pitty that it does not allow type hinting to guess the type of the variable, thought. With this example both mypy (1.10.0) and pylance complain about the line with the for loop with:

 error: "click.types.Tuple" object is not iterable  [misc]

@jnareb
Copy link

jnareb commented Sep 28, 2024

Using click_type seemed to work for me:

@app.command()
def foo(pairs: list[click.Tuple] = typer.Option(click_type=click.Tuple([str, str]))):
    for x, y in pairs:
        print(f'x: {x}, y: {y}')

This workaround works, but still it's a pitty that it does not allow type hinting to guess the type of the variable, thought. With this example both mypy (1.10.0) and pylance complain about the line with the for loop with:

 error: "click.types.Tuple" object is not iterable  [misc]

I found the following workaround, with no sign of type errors by PyCharm linter / type checker. I did not check with mypy or pylance.

def parse_colon_separated_pair(value: str):
    return tuple(value.split(sep=':', maxsplit=2))

@app.command()
def foo(
    pairs: Annotated[
        Optional[List[click.Tuple]],
        typer.Option(
            metavar="KEY:VALUE",
            parser=parse_colon_separated_pair,
    ] = None,
):
    print(f"{type(pairs)=}, {pairs=}")
  • Python 3.10.4
  • PyCharm 2024.2.0.1 (Community Edition)
  • typer==0.12.5
  • click==8.1.7
  • typing_extensions==4.12.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
investigate question Question or problem
Projects
None yet
Development

No branches or pull requests