brentyi / tyro

CLI interfaces & config objects, from types
https://brentyi.github.io/tyro
MIT License
469 stars 24 forks source link

list of lists is showing as fixed; no way to edit that type of parameters #101

Closed breengles closed 9 months ago

breengles commented 9 months ago

First of all, thanks for a great easy-to-use library! In my some kind of weird usage need to specify list of lists of some variable (let it be int for now). Here is an example:

from dataclasses import dataclass, field

import tyro

@dataclass
class Config:
    a: list[list[int]] = field(default_factory=lambda: [[1], [1]])
    b: list[int] = field(default_factory=lambda: [1, 1])

print(tyro.cli(Config))

this yield in the following help

usage: test.py [-h] [--a {fixed}] [--b [INT [INT ...]]]

╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --a {fixed}             (fixed to: [[1], [1]])          │
│ --b [INT [INT ...]]     (default: 1 1)                  │
╰─────────────────────────────────────────────────────────╯

and obviously parser complains that this argument is fixed and cannot be parsed:

Error parsing --a: --a was passed in, but is a fixed argument that cannot be parsed

Is there any workaround or maybe plans to support such kind of data?

brentyi commented 9 months ago

Hi!

Yes, unfortunately annotations where we have a variable-length sequence type nested in another variable-length sequence type aren't compatible with how we convert sequence types by default. There are a few options here though:

(1) If the inner list has a known length, for example 3, you can also use an annotation like list[tuple[int, int, int]].

(2) tyro.conf.UseAppendAction[] lets you append to the list.

@dataclass
class Config:
    a: tyro.conf.UseAppendAction[list[list[int]]] = field(default_factory=lambda: [[1], [1]])
    b: list[int] = field(default_factory=lambda: [1, 1])

print(tyro.cli(Config))
$ python test.py --a 1 2 3 --a 4 5 6
Config(a=[[1], [1], [1, 2, 3], [4, 5, 6]], b=[1, 1])

(3) tyro.conf.arg(constructor=...) lets you hand-specify how a type is parsed. For example, we can take a JSON string from the commandline:

from dataclasses import dataclass, field
from typing import Annotated

import tyro
import json

@dataclass
class Config:
    a: Annotated[list[list[int]], tyro.conf.arg(constructor=json.loads, metavar="JSON")] = field(
        default_factory=lambda: [[1], [1]]
    )
    b: list[int] = field(default_factory=lambda: [1, 1])

print(tyro.cli(Config))
python test.py --a "[[1,2],[3,4]]"
Config(a=[[1, 2], [3, 4]], b=[1, 1])

Let me know if any of that is unclear!

breengles commented 9 months ago

Thanks a lot for such a quick reply! This indeed seems like a solution to my use case! I will close the issue :hugs: