Skip to content

Commit c41a7c6

Browse files
authored
Merge pull request #57 from pavalos6401/add_argument_group
Add argument group
2 parents 37baa76 + 3a1d35c commit c41a7c6

3 files changed

Lines changed: 243 additions & 1 deletion

File tree

README.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,39 @@ Configuring a field with the Optional generic type:
185185
>>> print(parser.parse_args(["--name", "John", "--id", "1234"]))
186186
Options(name='John', id=1234)
187187
188+
189+
Creating argument groups by group title:
190+
191+
.. code-block:: pycon
192+
193+
>>> from dataclasses import dataclass, field
194+
>>> from argparse_dataclass import ArgumentParser
195+
>>> @dataclass
196+
... class Options:
197+
... foo: str = field(metadata=dict(group="string group"))
198+
... bar: str = field(metadata=dict(group=dict(title="dict group", description="using a dict")))
199+
... baz: str = field(metadata=dict(group=("sequence group", "using a sequence")))
200+
...
201+
>>> parser = ArgumentParser(Options)
202+
>>> parser.print_help()
203+
usage: [-h] --foo FOO --bar BAR --baz BAZ
204+
205+
options:
206+
-h, --help show this help message and exit
207+
208+
string group:
209+
--foo FOO
210+
211+
dict group:
212+
using a dict
213+
214+
--bar BAR
215+
216+
sequence group:
217+
using a sequence
218+
219+
--baz BAZ
220+
188221
Contributors
189222
------------
190223

argparse_dataclass.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,11 @@ def _add_dataclass_options(
416416
"For Union types other than 'Optional', a custom 'type' must be specified using "
417417
"'metadata'."
418418
)
419-
parser.add_argument(*args, **kwargs)
419+
420+
if "group" in field.metadata:
421+
_handle_argument_group(parser, field, args, kwargs)
422+
else:
423+
parser.add_argument(*args, **kwargs)
420424

421425

422426
def _get_kwargs(namespace: argparse.Namespace) -> Dict[str, Any]:
@@ -450,6 +454,30 @@ def _handle_bool_type(field: Field, args: list, kwargs: dict):
450454
kwargs["required"] = True
451455

452456

457+
def _handle_argument_group(
458+
parser: argparse.ArgumentParser, field: Field, args: list, kwargs: dict
459+
) -> None:
460+
"""Handles adding the argument to an argument group."""
461+
groups = {x.title: x for x in parser._action_groups}
462+
group = field.metadata.get("group")
463+
if isinstance(group, str):
464+
title = group
465+
description = None
466+
elif isinstance(group, dict):
467+
title = group.get("title")
468+
description = group.get("description")
469+
elif isinstance(group, Sequence):
470+
len_ = len(group)
471+
title = group[0] if len_ > 0 else None
472+
description = group[1] if len_ > 1 else None
473+
else:
474+
raise TypeError("'group' must be a group title, dictionary, or sequence")
475+
group = groups.get(title)
476+
if title is None or group is None:
477+
group = parser.add_argument_group(title, description)
478+
group.add_argument(*args, **kwargs)
479+
480+
453481
class ArgumentParser(argparse.ArgumentParser, Generic[OptionsType]):
454482
"""Command line argument parser that derives its options from a dataclass.
455483

tests/test_argumentgroups.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import argparse
2+
from dataclasses import dataclass, field
3+
4+
import unittest
5+
6+
from argparse_dataclass import ArgumentParser
7+
8+
9+
class ArgumentParserGroupsTests(unittest.TestCase):
10+
def test_basic_str(self):
11+
parser = argparse.ArgumentParser()
12+
group = parser.add_argument_group("title")
13+
group.add_argument("--x", required=True, type=int)
14+
expected = parser.format_help()
15+
16+
@dataclass
17+
class Opt:
18+
x: int = field(metadata={"group": "title"})
19+
20+
parser = ArgumentParser(Opt)
21+
out = parser.format_help()
22+
23+
self.assertEqual(expected, out)
24+
25+
def test_basic_dict(self):
26+
title = "title"
27+
28+
parser = argparse.ArgumentParser()
29+
group = parser.add_argument_group(title)
30+
group.add_argument("--x", required=True, type=int)
31+
expected = parser.format_help()
32+
33+
@dataclass
34+
class Opt:
35+
x: int = field(metadata={"group": {"title": title}})
36+
37+
parser = ArgumentParser(Opt)
38+
out = parser.format_help()
39+
40+
self.assertEqual(expected, out)
41+
42+
def test_basic_dict_description(self):
43+
title = "title"
44+
description = "description"
45+
46+
parser = argparse.ArgumentParser()
47+
group = parser.add_argument_group(title, description)
48+
group.add_argument("--x", required=True, type=int)
49+
expected = parser.format_help()
50+
51+
@dataclass
52+
class Opt:
53+
x: int = field(
54+
metadata={"group": {"title": title, "description": description}}
55+
)
56+
57+
parser = ArgumentParser(Opt)
58+
out = parser.format_help()
59+
60+
self.assertEqual(expected, out)
61+
62+
def test_basic_sequence(self):
63+
parser = argparse.ArgumentParser()
64+
group = parser.add_argument_group("group")
65+
group.add_argument("--x", required=True, type=int)
66+
expected = parser.format_help()
67+
68+
@dataclass
69+
class Opt:
70+
x: int = field(metadata={"group": ("group")})
71+
72+
parser = ArgumentParser(Opt)
73+
out = parser.format_help()
74+
75+
self.assertEqual(expected, out)
76+
77+
def test_basic_sequence_description(self):
78+
parser = argparse.ArgumentParser()
79+
group = parser.add_argument_group("group", "description")
80+
group.add_argument("--x", required=True, type=int)
81+
expected = parser.format_help()
82+
83+
@dataclass
84+
class Opt:
85+
x: int = field(metadata={"group": ("group", "description")})
86+
87+
parser = ArgumentParser(Opt)
88+
out = parser.format_help()
89+
90+
self.assertEqual(expected, out)
91+
92+
def test_basic_empty(self):
93+
parser = argparse.ArgumentParser()
94+
group = parser.add_argument_group()
95+
group.add_argument("--x", required=True, type=int)
96+
expected = parser.format_help()
97+
98+
@dataclass
99+
class Opt:
100+
x: int = field(metadata={"group": ()})
101+
102+
parser = ArgumentParser(Opt)
103+
out = parser.format_help()
104+
105+
self.assertEqual(expected, out)
106+
107+
def test_multiple_arguments(self):
108+
title = "title"
109+
110+
parser = argparse.ArgumentParser()
111+
group = parser.add_argument_group(title)
112+
group.add_argument("--x", required=True, type=int)
113+
group.add_argument("--y", required=True, type=int)
114+
expected = parser.format_help()
115+
116+
@dataclass
117+
class Opt:
118+
x: int = field(metadata={"group": title})
119+
y: int = field(metadata={"group": title})
120+
121+
parser = ArgumentParser(Opt)
122+
out = parser.format_help()
123+
124+
self.assertEqual(expected, out)
125+
126+
def test_multiple_groups_empty(self):
127+
parser = argparse.ArgumentParser()
128+
group_a = parser.add_argument_group()
129+
group_a.add_argument("--x", required=True, type=int)
130+
group_b = parser.add_argument_group()
131+
group_b.add_argument("--y", required=True, type=int)
132+
expected = parser.format_help()
133+
134+
@dataclass
135+
class Opt:
136+
x: int = field(metadata={"group": ()})
137+
y: int = field(metadata={"group": ()})
138+
139+
parser = ArgumentParser(Opt)
140+
out = parser.format_help()
141+
142+
self.assertEqual(expected, out)
143+
144+
def test_argument_groups(self):
145+
title_a = "Group A"
146+
title_b = "Group B"
147+
descr_b = "Description B"
148+
title_c = "Group C"
149+
descr_c = "Description C"
150+
151+
parser = argparse.ArgumentParser()
152+
group_a = parser.add_argument_group(title_a)
153+
group_a.add_argument("--arga1", required=True, type=int)
154+
group_a.add_argument("--arga2", required=True, type=int)
155+
group_b = parser.add_argument_group(title_b, descr_b)
156+
group_b.add_argument("--argb", required=True, type=int)
157+
group_c = parser.add_argument_group(title_c, descr_c)
158+
group_c.add_argument("--argc", required=True, type=int)
159+
group_d = parser.add_argument_group()
160+
group_d.add_argument("--argd", required=True, type=int)
161+
group_e = parser.add_argument_group()
162+
group_e.add_argument("--arge", required=True, type=int)
163+
group_a.add_argument("--arga3", required=True, type=int)
164+
expected = parser.format_help()
165+
166+
@dataclass
167+
class Opt:
168+
arga1: int = field(metadata={"group": title_a})
169+
arga2: int = field(metadata={"group": title_a})
170+
argb: int = field(
171+
metadata={"group": {"title": title_b, "description": descr_b}}
172+
)
173+
argc: int = field(metadata={"group": (title_c, descr_c)})
174+
argd: int = field(metadata={"group": ()})
175+
arge: int = field(metadata={"group": ()})
176+
arga3: int = field(metadata={"group": title_a})
177+
178+
parser = ArgumentParser(Opt)
179+
out = parser.format_help()
180+
181+
self.assertEqual(expected, out)

0 commit comments

Comments
 (0)