Skip to content

Commit b358dfa

Browse files
authored
Minor helptext generation improvements (#184)
* Adjust helptext behavior * Tests
1 parent 1a9448a commit b358dfa

8 files changed

+249
-16
lines changed

src/tyro/_parsers.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def from_callable_or_type(
145145
)
146146

147147
# Helptext for this field; used as description for grouping arguments.
148-
class_field_name = _strings.make_field_name([field.intern_name])
148+
class_field_name = _strings.make_field_name(
149+
[intern_prefix, field.intern_name]
150+
)
149151
if field.helptext is not None:
150152
helptext_from_intern_prefixed_field_name[class_field_name] = (
151153
field.helptext
@@ -235,10 +237,20 @@ def apply_args(
235237
"""Create defined arguments and subparsers."""
236238

237239
# Make argument groups.
238-
def format_group_name(prefix: str) -> str:
239-
return (prefix + " options").strip()
240+
def format_group_name(group_name: str) -> str:
241+
return (group_name + " options").strip()
242+
243+
def group_name_from_arg(arg: _arguments.ArgumentDefinition) -> str:
244+
prefix = arg.lowered.name_or_flag
245+
if prefix.startswith("--"):
246+
prefix = prefix[2:]
247+
if "." in prefix:
248+
prefix = prefix.rpartition(".")[0]
249+
else:
250+
prefix = ""
251+
return prefix
240252

241-
group_from_prefix: Dict[str, argparse._ArgumentGroup] = {
253+
group_from_group_name: Dict[str, argparse._ArgumentGroup] = {
242254
"": parser._action_groups[1],
243255
**{
244256
cast(str, group.title).partition(" ")[0]: group
@@ -251,9 +263,10 @@ def format_group_name(prefix: str) -> str:
251263
# Add each argument group. Note that groups with only suppressed arguments won't
252264
# be added.
253265
for arg in self.args:
266+
group_name = group_name_from_arg(arg)
254267
if (
255268
arg.lowered.help is not argparse.SUPPRESS
256-
and arg.extern_prefix not in group_from_prefix
269+
and group_name not in group_from_group_name
257270
):
258271
description = (
259272
parent.helptext_from_intern_prefixed_field_name.get(
@@ -262,24 +275,23 @@ def format_group_name(prefix: str) -> str:
262275
if parent is not None
263276
else None
264277
)
265-
group_from_prefix[arg.extern_prefix] = parser.add_argument_group(
266-
format_group_name(arg.extern_prefix),
278+
group_from_group_name[group_name] = parser.add_argument_group(
279+
format_group_name(group_name),
267280
description=description,
268281
)
269282

270-
# Add each argument.
271-
for arg in self.args:
283+
# Add each argument.
272284
if arg.field.is_positional():
273285
arg.add_argument(positional_group)
274286
continue
275287

276-
if arg.extern_prefix in group_from_prefix:
277-
arg.add_argument(group_from_prefix[arg.extern_prefix])
288+
if group_name in group_from_group_name:
289+
arg.add_argument(group_from_group_name[group_name])
278290
else:
279291
# Suppressed argument: still need to add them, but they won't show up in
280292
# the helptext so it doesn't matter which group.
281293
assert arg.lowered.help is argparse.SUPPRESS
282-
arg.add_argument(group_from_prefix[""])
294+
arg.add_argument(group_from_group_name[""])
283295

284296
for child in self.child_from_prefix.values():
285297
child.apply_args(parser, parent=self)

tests/test_conf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1497,8 +1497,8 @@ class DatasetConfig:
14971497
with pytest.raises(SystemExit), contextlib.redirect_stdout(target):
14981498
instantiate_dataclasses((OptimizerConfig, DatasetConfig), args=["--help"])
14991499
helptext = target.getvalue()
1500-
assert "OptimizerConfig options" in helptext
1501-
assert "DatasetConfig options" in helptext
1500+
assert "OptimizerConfig options" not in helptext
1501+
assert "DatasetConfig options" not in helptext
15021502

15031503

15041504
def test_counter_action() -> None:

tests/test_nested.py

+6
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ class B:
1818
class Nested:
1919
x: int
2020
b: B
21+
"""Helptext for b"""
2122

2223
assert tyro.cli(Nested, args=["--x", "1", "--b.y", "3"]) == Nested(x=1, b=B(y=3))
2324
with pytest.raises(SystemExit):
2425
tyro.cli(Nested, args=["--x", "1"])
2526

27+
def main(x: Nested):
28+
return x
29+
30+
assert "Helptext for b" in get_helptext_with_checks(main)
31+
2632

2733
def test_nested_annotated() -> None:
2834
@dataclasses.dataclass

tests/test_py311_generated/ok.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from dataclasses import dataclass
2+
from typing import Literal
3+
4+
import tyro
5+
6+
7+
@dataclass(frozen=True)
8+
class Container[T]:
9+
a: T
10+
11+
12+
tyro.cli(Container[Container[bool] | Container[Literal["1", "2"]]])

tests/test_py311_generated/test_conf_generated.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1492,8 +1492,8 @@ class DatasetConfig:
14921492
with pytest.raises(SystemExit), contextlib.redirect_stdout(target):
14931493
instantiate_dataclasses((OptimizerConfig, DatasetConfig), args=["--help"])
14941494
helptext = target.getvalue()
1495-
assert "OptimizerConfig options" in helptext
1496-
assert "DatasetConfig options" in helptext
1495+
assert "OptimizerConfig options" not in helptext
1496+
assert "DatasetConfig options" not in helptext
14971497

14981498

14991499
def test_counter_action() -> None:

tests/test_py311_generated/test_nested_generated.py

+6
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ class B:
2828
class Nested:
2929
x: int
3030
b: B
31+
"""Helptext for b"""
3132

3233
assert tyro.cli(Nested, args=["--x", "1", "--b.y", "3"]) == Nested(x=1, b=B(y=3))
3334
with pytest.raises(SystemExit):
3435
tyro.cli(Nested, args=["--x", "1"])
3536

37+
def main(x: Nested):
38+
return x
39+
40+
assert "Helptext for b" in get_helptext_with_checks(main)
41+
3642

3743
def test_nested_annotated() -> None:
3844
@dataclasses.dataclass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Adapted from: https://github.com/brentyi/tyro/issues/183"""
2+
3+
from typing import Annotated, NamedTuple, Set
4+
5+
from helptext_utils import get_helptext_with_checks
6+
from pydantic import BaseModel, Field
7+
8+
import tyro
9+
10+
11+
class MyRange(NamedTuple):
12+
low: int
13+
high: int
14+
15+
def __str__(self):
16+
return f"<{self.low}, {self.high}>"
17+
18+
@staticmethod
19+
def tyro_constructor(
20+
range_str: Annotated[
21+
str,
22+
tyro.conf.arg(name=""),
23+
],
24+
):
25+
import re
26+
27+
m = re.match("([0-9]+)(-([0-9]+))*", range_str)
28+
low = m[1] # type: ignore
29+
high = low if not m[3] else m[3] # type: ignore
30+
31+
return MyRange(int(low), int(high))
32+
33+
@staticmethod
34+
def tyro_constructor_set(
35+
range_str_set: Annotated[
36+
Set[str],
37+
tyro.conf.arg(name=""),
38+
],
39+
):
40+
return {MyRange.tyro_constructor(r) for r in range_str_set}
41+
42+
43+
class MySpec(BaseModel):
44+
some_set: Set[int] = Field(
45+
default={1, 2, 3},
46+
description="Some set of integers",
47+
title="Some set",
48+
)
49+
50+
some_string: str = Field(
51+
description="Some string without a default value.", title="SomeSTR"
52+
)
53+
54+
here_comes_the_trouble: Annotated[
55+
Set[MyRange],
56+
tyro.conf.arg(constructor=MyRange.tyro_constructor_set),
57+
] = Field(
58+
default={MyRange(0, 1024)},
59+
description="I would like this one in the same group as others",
60+
title="Please help",
61+
)
62+
63+
64+
def add_spec(spec: MySpec) -> MySpec:
65+
return spec
66+
67+
68+
def test_functionality() -> None:
69+
assert tyro.cli(
70+
add_spec, args=["--spec.some-set", "1", "2", "3", "--spec.some-string", "hello"]
71+
) == MySpec(
72+
some_set={1, 2, 3},
73+
some_string="hello",
74+
here_comes_the_trouble={MyRange(0, 1024)},
75+
)
76+
assert tyro.cli(
77+
add_spec,
78+
args=[
79+
"--spec.some-set",
80+
"1",
81+
"2",
82+
"3",
83+
"--spec.some-string",
84+
"hello",
85+
"--spec.here-comes-the-trouble",
86+
"0-512",
87+
],
88+
) == MySpec(
89+
some_set={1, 2, 3},
90+
some_string="hello",
91+
here_comes_the_trouble={MyRange(0, 512)},
92+
)
93+
94+
95+
def test_helptext() -> None:
96+
helptext = get_helptext_with_checks(add_spec)
97+
assert "spec options" in helptext
98+
assert "spec.here-comes-the-trouble-options" not in helptext
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Adapted from: https://github.com/brentyi/tyro/issues/183"""
2+
3+
from typing import NamedTuple, Set
4+
5+
from helptext_utils import get_helptext_with_checks
6+
from pydantic import BaseModel, Field
7+
from typing_extensions import Annotated
8+
9+
import tyro
10+
11+
12+
class MyRange(NamedTuple):
13+
low: int
14+
high: int
15+
16+
def __str__(self):
17+
return f"<{self.low}, {self.high}>"
18+
19+
@staticmethod
20+
def tyro_constructor(
21+
range_str: Annotated[
22+
str,
23+
tyro.conf.arg(name=""),
24+
],
25+
):
26+
import re
27+
28+
m = re.match("([0-9]+)(-([0-9]+))*", range_str)
29+
low = m[1] # type: ignore
30+
high = low if not m[3] else m[3] # type: ignore
31+
32+
return MyRange(int(low), int(high))
33+
34+
@staticmethod
35+
def tyro_constructor_set(
36+
range_str_set: Annotated[
37+
Set[str],
38+
tyro.conf.arg(name=""),
39+
],
40+
):
41+
return {MyRange.tyro_constructor(r) for r in range_str_set}
42+
43+
44+
class MySpec(BaseModel):
45+
some_set: Set[int] = Field(
46+
default={1, 2, 3},
47+
description="Some set of integers",
48+
title="Some set",
49+
)
50+
51+
some_string: str = Field(
52+
description="Some string without a default value.", title="SomeSTR"
53+
)
54+
55+
here_comes_the_trouble: Annotated[
56+
Set[MyRange],
57+
tyro.conf.arg(constructor=MyRange.tyro_constructor_set),
58+
] = Field(
59+
default={MyRange(0, 1024)},
60+
description="I would like this one in the same group as others",
61+
title="Please help",
62+
)
63+
64+
65+
def add_spec(spec: MySpec) -> MySpec:
66+
return spec
67+
68+
69+
def test_functionality() -> None:
70+
assert tyro.cli(
71+
add_spec, args=["--spec.some-set", "1", "2", "3", "--spec.some-string", "hello"]
72+
) == MySpec(
73+
some_set={1, 2, 3},
74+
some_string="hello",
75+
here_comes_the_trouble={MyRange(0, 1024)},
76+
)
77+
assert tyro.cli(
78+
add_spec,
79+
args=[
80+
"--spec.some-set",
81+
"1",
82+
"2",
83+
"3",
84+
"--spec.some-string",
85+
"hello",
86+
"--spec.here-comes-the-trouble",
87+
"0-512",
88+
],
89+
) == MySpec(
90+
some_set={1, 2, 3},
91+
some_string="hello",
92+
here_comes_the_trouble={MyRange(0, 512)},
93+
)
94+
95+
96+
def test_helptext() -> None:
97+
helptext = get_helptext_with_checks(add_spec)
98+
assert "spec options" in helptext
99+
assert "spec.here-comes-the-trouble-options" not in helptext

0 commit comments

Comments
 (0)