Skip to content

Commit 856f003

Browse files
committed
Introduce class CommandArgumentParser [RHELDST-34542]
This change adds a new `CommandAtgumentParser` to support argparse-based commands while keeping the existing optparse CommandOptionParser. This change introduces `ArgparseCommand` as a base class for argparse-based commands, allowing optparse and argparse commands to coexist during migration. No existing optparse behavior is modified.
1 parent eec6a3d commit 856f003

4 files changed

Lines changed: 162 additions & 0 deletions

File tree

kobo/argcli.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import argparse
2+
import sys
3+
4+
from kobo.plugins import Plugin
5+
6+
7+
class ArgparseCommand(Plugin):
8+
"""
9+
Base class for argparse-based commands.
10+
11+
Commands must:
12+
- inherit from ArgparseCommand
13+
- implement add_arguments(self)
14+
- implement run(self, *args, **kwargs)
15+
"""
16+
17+
def __init__(self, parser):
18+
Plugin.__init__(self)
19+
self.parser = parser
20+
21+
def add_arguments(self):
22+
"""Register argparse arguments."""
23+
raise NotImplementedError
24+
25+
def run(self, *args, **kwargs):
26+
"""Execute command."""
27+
raise NotImplementedError
28+
29+
30+
class CommandArgumentParser(object):
31+
"""
32+
Argparse-based command dispatcher.
33+
34+
This intentionally does NOT inherit from argparse.ArgumentParser.
35+
It is a coordinator that:
36+
- parses the global command name
37+
- dispatches to an argparse-capable command
38+
"""
39+
40+
def __init__(
41+
self,
42+
command_container,
43+
prog=None,
44+
default_command=None,
45+
default_profile=None,
46+
):
47+
self.container = command_container
48+
self.default_command = default_command
49+
self.default_profile = default_profile
50+
51+
self.parser = argparse.ArgumentParser(
52+
prog=prog,
53+
usage="%(prog)s <command> [args] [--help]",
54+
formatter_class=argparse.RawTextHelpFormatter,
55+
)
56+
57+
self.parser.add_argument(
58+
"command",
59+
nargs="?",
60+
help="command to execute",
61+
)
62+
63+
# everything after the command name
64+
self.parser.add_argument(
65+
"args",
66+
nargs=argparse.REMAINDER,
67+
help=argparse.SUPPRESS,
68+
)
69+
70+
def _load_profile(self, profile):
71+
"""
72+
Hook for loading a profile.
73+
Implemented here only as a placeholder.
74+
"""
75+
pass
76+
77+
def run(self, argv=None):
78+
cmd, cmd_ns, cmd_args = self.parse_args(argv)
79+
80+
cmd_kwargs = vars(cmd_ns)
81+
82+
# load profile if requested
83+
if self.default_profile and "profile" in cmd_kwargs:
84+
self._load_profile(cmd_kwargs["profile"])
85+
86+
return cmd.run(*cmd_args, **cmd_kwargs)
87+
88+
def parse_args(self, argv=None):
89+
argv = argv if argv is not None else sys.argv[1:]
90+
ns = self.parser.parse_args(argv)
91+
92+
command_name = ns.command or self.default_command
93+
94+
if not command_name:
95+
self.parser.error("no command specified")
96+
97+
if command_name not in self.container.plugins:
98+
self.parser.error("unknown command: %s" % command_name)
99+
100+
CommandClass = self.container[command_name]
101+
102+
if not issubclass(CommandClass, ArgparseCommand):
103+
self.parser.error("command '%s' does not support argparse" % command_name)
104+
105+
cmd_parser = argparse.ArgumentParser(
106+
prog="%s %s" % (self.parser.prog, command_name),
107+
formatter_class=argparse.RawTextHelpFormatter,
108+
)
109+
110+
cmd_parser.container = self.container
111+
112+
cmd = CommandClass(cmd_parser)
113+
cmd.add_arguments()
114+
115+
# allow extra args (optparse compatibility)
116+
cmd_ns, remainder = cmd_parser.parse_known_args(ns.args)
117+
118+
return cmd, cmd_ns, remainder

tests/plugins/commands/__init__.py

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from kobo.argcli import ArgparseCommand
2+
3+
__all__ = ("Cmd_Fake_Echo",)
4+
5+
6+
class Cmd_Fake_Echo(ArgparseCommand):
7+
enabled = True
8+
name = "fake-echo"
9+
10+
def add_arguments(self):
11+
self.parser.add_argument("words", nargs="+")
12+
13+
def run(self, *args, **kwargs):
14+
# store result on container
15+
self.parser.container.result = kwargs["words"]

tests/test_argcli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import kobo.client
2+
from kobo.argcli import CommandArgumentParser
3+
from tests.plugins import commands
4+
5+
6+
class FakeCommandContainer(kobo.client.ClientCommandContainer):
7+
pass
8+
9+
10+
FakeCommandContainer.register_module(commands, prefix="cmd_")
11+
12+
13+
class FakeCommandArgumentParser(CommandArgumentParser):
14+
def load_pub_profile(self, profile=None):
15+
pass
16+
17+
18+
def test_argparse_command_registered_via_module():
19+
conf = kobo.conf.PyConfigParser()
20+
container = FakeCommandContainer(conf)
21+
22+
parser = FakeCommandArgumentParser(
23+
command_container=container,
24+
default_profile="default-profile",
25+
)
26+
27+
parser.run(["cmd-fake-echo", "hello", "world"])
28+
29+
assert container.result == ["hello", "world"]

0 commit comments

Comments
 (0)