Skip to content

Odd implicit tuple conversion of default string value when using a parser function for type #2081

@nurpax

Description

@nurpax

I'm using the click.option type=<func> argument to convert input arguments from str to Tuple[float,float]. This (example below) code works as I'd expect in Click 7.1.2 but an odd implicit type conversion from string to tuple happens on Click 8.0.1.

Consider this code:

from typing import Optional, Tuple

import click

def parse_vec2(s: str) -> Tuple[float, float]:
    '''Parse a floating point 2-vector of syntax 'a,b'.

    Example:
        '0,1' returns (0,1)
    '''
    print (s)
    parts = s.split(',')
    if len(parts) == 2:
        return (float(parts[0]), float(parts[1]))
    raise ValueError(f'cannot parse 2-vector {s}')

@click.command()
@click.option('--translate', help='Translate XY-coordinate (e.g. \'0.3,1\')', type=parse_vec2, default='0,0')
def main(
    translate: Optional[Tuple[float,float]]
):
    print (translate)

if __name__ == "__main__":
    main()

On Click 7.1.2 this works as I'd expect: the default '0,0' is passed to parse_vec2 as a Python str value if it's not specified on the command line:

$ python clicktest.py
0,0
(0.0, 0.0)
$ python clicktest.py --translate='1,2'
1,2
(1.0, 2.0)

However, on 8.0.1, the default value is somehow implicitly converted to a tuple by Click when parse_vec2 is called.

On Click 8.0.1 parse_vec2 is in fact called twice, once with the original str '0,0' and a second time with (0.0, 0.0) On 7.1.2, parse_vec2 is called only once with the str value.

$ python clicktest.py
0,0
(0.0, 0.0)
Traceback (most recent call last):
  File "/home/janne/dev/clicktest.py", line 25, in <module>
    main() # pylint: disable=no-value-for-parameter
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 1061, in main
    with self.make_context(prog_name, args, **extra) as ctx:
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 923, in make_context
    self.parse_args(ctx, args)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 1379, in parse_args
    value, args = param.handle_parse_result(ctx, opts, args)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 2364, in handle_parse_result
    value = self.process_value(ctx, value)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 2320, in process_value
    value = self.type_cast_value(ctx, value)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/core.py", line 2307, in type_cast_value
    return convert(value)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/types.py", line 75, in __call__
    return self.convert(value, param, ctx)
  File "/home/janne/miniconda3/lib/python3.9/site-packages/click/types.py", line 170, in convert
    return self.func(value)
  File "/home/janne/dev/clicktest.py", line 12, in parse_vec2
    parts = s.split(',')
AttributeError: 'tuple' object has no attribute 'split'
$ python clicktest.py --translate='3,2'
3,2
(3.0, 2.0)

I don't understand where and why this type conversion happens and why is parse_vec2 being called twice. I'd expect the default value to be passed directly to the type parse.

Environment:

  • Python version: 3.9.5
  • Click version: 8.0.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions