Open jtfidje opened 2 years ago
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.
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())
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 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!
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.
@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.
This minor fix to make @jonatasleon's worthy workaround to support also values containing the =
char:
k, v = value.split('=', 1)
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}')
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
.
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 typer0.6.1
.
0.9.0
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) andpylance
complain about the line with the for loop with:error: "click.types.Tuple" object is not iterable [misc]
First Check
Commit to Help
Example Code
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:
This is achieved in Click with the following option:
Operating System
Linux
Operating System Details
No response
Typer Version
0.4.1
Python Version
3.7.3
Additional Context
No response