Skip to content

Commit 7ec3189

Browse files
authored
fix(cli): allow exclusive arguments as optional (#2770)
* fix(cli): allow exclusive arguments as optional The CLI takes its arguments from the RequiredOptional, which has three fields: required, optional, and exclusive. In practice, the exclusive options are not defined as either required or optional, and would not be allowed in the CLI. This changes that, so that exclusive options are also added to the argument parser. * fix(cli): inform argument parser that options are mutually exclusive * fix(cli): use correct exclusive options, add unit test Closes #2769
1 parent 4e68d32 commit 7ec3189

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

gitlab/v4/cli.py

+9
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ def _populate_sub_parser_by_class(
262262
sub_parser_action.add_argument(
263263
f"--{x.replace('_', '-')}", required=False
264264
)
265+
if mgr_cls._create_attrs.exclusive:
266+
group = sub_parser_action.add_mutually_exclusive_group()
267+
for x in mgr_cls._create_attrs.exclusive:
268+
group.add_argument(f"--{x.replace('_', '-')}")
265269

266270
if action_name == "update":
267271
if cls._id_attr is not None:
@@ -280,6 +284,11 @@ def _populate_sub_parser_by_class(
280284
f"--{x.replace('_', '-')}", required=False
281285
)
282286

287+
if mgr_cls._update_attrs.exclusive:
288+
group = sub_parser_action.add_mutually_exclusive_group()
289+
for x in mgr_cls._update_attrs.exclusive:
290+
group.add_argument(f"--{x.replace('_', '-')}")
291+
283292
if cls.__name__ in cli.custom_actions:
284293
name = cls.__name__
285294
for action_name in cli.custom_actions[name]:

tests/unit/test_cli.py

+61
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import gitlab.base
1212
from gitlab import cli
1313
from gitlab.exceptions import GitlabError
14+
from gitlab.mixins import CreateMixin, UpdateMixin
15+
from gitlab.types import RequiredOptional
1416
from gitlab.v4 import cli as v4_cli
1517

1618

@@ -157,6 +159,65 @@ def test_v4_parser():
157159
assert actions["--name"].required
158160

159161

162+
def test_extend_parser():
163+
class ExceptionArgParser(argparse.ArgumentParser):
164+
def error(self, message):
165+
"Raise error instead of exiting on invalid arguments, to make testing easier"
166+
raise ValueError(message)
167+
168+
class Fake:
169+
_id_attr = None
170+
171+
class FakeManager(gitlab.base.RESTManager, CreateMixin, UpdateMixin):
172+
_obj_cls = Fake
173+
_create_attrs = RequiredOptional(
174+
required=("create",),
175+
optional=("opt_create",),
176+
exclusive=("create_a", "create_b"),
177+
)
178+
_update_attrs = RequiredOptional(
179+
required=("update",),
180+
optional=("opt_update",),
181+
exclusive=("update_a", "update_b"),
182+
)
183+
184+
parser = ExceptionArgParser()
185+
with mock.patch.dict(
186+
"gitlab.v4.objects.__dict__", {"FakeManager": FakeManager}, clear=True
187+
):
188+
v4_cli.extend_parser(parser)
189+
190+
assert parser.parse_args(["fake", "create", "--create", "1"])
191+
assert parser.parse_args(["fake", "create", "--create", "1", "--opt-create", "1"])
192+
assert parser.parse_args(["fake", "create", "--create", "1", "--create-a", "1"])
193+
assert parser.parse_args(["fake", "create", "--create", "1", "--create-b", "1"])
194+
195+
with pytest.raises(ValueError):
196+
# missing required "create"
197+
parser.parse_args(["fake", "create", "--opt_create", "1"])
198+
199+
with pytest.raises(ValueError):
200+
# both exclusive options
201+
parser.parse_args(
202+
["fake", "create", "--create", "1", "--create-a", "1", "--create-b", "1"]
203+
)
204+
205+
assert parser.parse_args(["fake", "update", "--update", "1"])
206+
assert parser.parse_args(["fake", "update", "--update", "1", "--opt-update", "1"])
207+
assert parser.parse_args(["fake", "update", "--update", "1", "--update-a", "1"])
208+
assert parser.parse_args(["fake", "update", "--update", "1", "--update-b", "1"])
209+
210+
with pytest.raises(ValueError):
211+
# missing required "update"
212+
parser.parse_args(["fake", "update", "--opt_update", "1"])
213+
214+
with pytest.raises(ValueError):
215+
# both exclusive options
216+
parser.parse_args(
217+
["fake", "update", "--update", "1", "--update-a", "1", "--update-b", "1"]
218+
)
219+
220+
160221
@pytest.mark.skipif(sys.version_info < (3, 8), reason="added in 3.8")
161222
def test_legacy_display_without_fields_warns(fake_object_no_id):
162223
printer = v4_cli.LegacyPrinter()

0 commit comments

Comments
 (0)