I am using Python 3.7 and Cement 3.0.4.
What I have is a base app with a controller and the controller has a command that takes a single optional argument. What I'm seeing is if I pass the command an invalid argument I get an invalid argument error as I expect but I am getting the "usage" output for the app itself rather than the command on the controller. Here is an example:
from cement import App, Controller, ex
class Base(Controller):
class Meta:
label = 'base'
@ex(help='example sub-command')
def cmd1(self):
print('Inside Base.cmd1()')
@ex(
arguments=[
(['-n', '--name'],
{'help': ''': The name you want printed out''',
'dest': 'name',
'required': False}),
],
help=' the help for cmd2.')
def cmd2(self):
print(self.app.pargs.name)
This app has a command called cmd2 and it takes an optional argument of -n
which, as the help states, will be printed out. So if I do this:
with MyApp(argv=['cmd2', '-n', 'bob']) as app:
app.run()
I will the output:
bob
as expected. However if I pass an invalid argument to cmd2:
with MyApp(argv=['cmd2', '-a', 'bob']) as app:
app.run()
I get:
usage: myapp [-h] [-d] [-q] {cmd1,cmd2} ...
myapp: error: unrecognized arguments: -a bob
What I would like to see, instead of the usage for myapp, would be the usage for the cmd2 command, similar to if I did -h
on the command:
with MyApp(argv=['cmd2', '-h']) as app:
app.run()
outputs
usage: myapp cmd2 [-h] [-n NAME]
optional arguments:
-h, --help show this help message and exit
-n NAME, --name NAME : The name you want printed out
I realize much of this is delegated to Argparse and is not handled by Cement. I've done some debugging and I'm seeing there are multiple ArgparseArgumentHandler
classes nested. So in the case above there is an ArgparseArgumentHandler
for myapp
and it has in it's actions a SubParsersAction
that has a choices
field that has a map containing my two commands on the controller, cmd1
and cmd2
mapped to their own ArgparseArgumentHandler
.
When the invalid argument is detected it is within the ArgparseArgumentHandler for myapp
and thus it calls print_usage()
on myapp
rather than on the ArgparseArgumentHandler
for the invoked command, cmd2
.
My knowledge of Argparse is limited and I do find it a bit complex to navigate. The only workaround I can think of right now is subclassing ArgparseArgumentHandler
, and overriding error()
and trying to determine if the error is due to recognized arguments and if so try to find the parser for it.. something like this pseudocode:
class ArgparseArgumentOverride(ext_argparse.ArgparseArgumentHandler):
def error(self, message):
# determine if there are unknown args
args, argv = self.parse_known_args(self.original_arguments, self.original_namespace)
# we are in an error state and have unrecognized args
if argv:
controller_namespace = args.__controller_namespace__
for action in self._actions:
if action.choices is not None:
# we found an choice with our namespace
if action.choices[controller_namespace]:
command_parser= action.choices[controller_namespace]
# this should be the show_usage for the command
complete_command.print_usage(sys.stderr)
Again above is pseudocode and actually doing something like that would feel very fragile, error prone, and unpredictable. I know there has to be an better way to do this, I'm just not finding it. I've been digging through the docs and source for hours and still haven't found what I'm looking for. Could anyone tell me what I'm missing? Any advice on how to proceed here would be really appreciated. Thanks much!