Subparsers are ignored when help is not defined on the subparser
Issue:
- When calling
.add_parser("alpha")to add a subparser and "help" kwarg is not defined, the sub parser will be ignored/skipped.
Requirements:
- When calling
.add_parser("alpha"), the subcommand should be recognized and emitted in the autocomplete.
Workarounds:
- Always call
.add_parserwithhelp=to have any string (even an empty string works).
Example:
#!/usr/bin/env python
import logging
import sys
from pathlib import Path
import argparse
from argparse import ArgumentParser, Namespace
from typing import Callable
import shtab
logger = logging.getLogger(__name__)
def run_alpha(input_path: Path) -> int:
logger.info(f"Running alpha for {input_path}")
return 0
def run_beta(src: Path, dest: Path) -> int:
logger.info(f"Running alpha with {src=} {dest=}")
return 0
def _to_parser_alpha(p: ArgumentParser) -> ArgumentParser:
p.add_argument("-i", "--input", type=Path, required=True)
return p
def _to_parser_beta(p: ArgumentParser) -> ArgumentParser:
p.add_argument("-s", "--src", type=Path, required=True)
p.add_argument("-d", "--dest", type=Path, required=True)
return p
def get_parser() -> ArgumentParser:
p = ArgumentParser(prog="shtab_test.py")
sp = p.add_subparsers()
def _add(
name: str,
add_opts: Callable[[ArgumentParser], ArgumentParser],
func: Callable[[Namespace], int]
) -> argparse.ArgumentParser:
# ******* This has to set help something, otherwise shtab will skip it.
px = sp.add_parser(name)
add_opts(px)
px.set_defaults(func=func)
return px
_add("alpha", _to_parser_alpha, lambda ns: run_alpha(ns.input))
_add("beta", _to_parser_beta, lambda ns: run_beta(ns.src, ns.dest))
p.add_argument("--version", action="version", version="0.1.0")
shtab.add_argument_to(p)
return p
def main(argv: list[str]) -> int:
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
pargs = get_parser().parse_args(argv)
return pargs.func(pargs)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Yields.
python shtab_test.py --print-completion zsh
DEBUG:shtab:choices:_shtab_example_py:['alpha', 'beta']
DEBUG:shtab:skip:subcommand:alpha
DEBUG:shtab:skip:subcommand:beta
DEBUG:shtab:subcommands:_shtab_example_py:['_shtab_example_py']
#compdef example.py
...
Changing px = sp.add_parser(name) to px = sp.add_parser(name, help=f"Running {name}") resolves the issue.
DEBUG:shtab:choices:_shtab_shtab_test_py:['alpha', 'beta']
DEBUG:shtab:subcommand:alpha
DEBUG:shtab:subcommands:alpha:{'cmd': 'alpha', 'help': '', 'arguments': ['"(- : *)"{-h,--help}"[show this help message and exit]"', '{-i,--input}"[]:input:"'], 'paths': ['alpha'], 'commands': {}}
DEBUG:shtab:subcommand:beta
DEBUG:shtab:subcommands:beta:{'cmd': 'beta', 'help': '', 'arguments': ['"(- : *)"{-h,--help}"[show this help message and exit]"', '{-s,--src}"[]:src:"', '{-d,--dest}"[]:dest:"'], 'paths': ['beta'], 'commands': {}}
DEBUG:shtab:subcommands:_shtab_shtab_test_py:['_shtab_shtab_test_py', '_shtab_shtab_test_py_alpha', '_shtab_shtab_test_py_beta']
#compdef shtab_test.py
This is a simple enough workaround, however the current behavior is a bit surprising.
This is by design, as some devs deliberately define subparsers sans-help as hidden/beta APIs
This is by design, as some devs deliberately define subparsers sans-
helpas hidden/beta APIs
- When you define
sp.add_parser("alpha", help="Run Alpha")this only adds a help message detail for the subparser at the root help (e.g.,example.py --help. Regardless of the value ofhelp=, the subparser is always displayed as at leastpositional arguments: {alpha, beta} - When you type
example.py alpha --help, the description defined insp.add_parser(name, description="Desc of Alpha")is printed (but nothelp=value is never shown here. Thehelp=value is only shown at the root level). Defining thedescription=will propagate to the autocomplete layer. - I don't think there's a way to hide a specific subparser. https://github.com/python/cpython/issues/67037
.add_subparsershas different semantics than.add_parser.help=in.add_argumentdoesn't need to be defined. Set toargparse.SUPPRESSto hide it. The current docs are suggesting a different behavior. https://docs.iterative.ai/shtab/#faqs
At any rate, clarifying some of these specific issues might be useful. For example, you need to define help= to any value to avoid the subparser being skipped and also to define description= to get the description information passed to autocomplete layer. On the rare case you want to hide all subparsers, then sp = p.add_subparsers(help=argparse.SUPPRESS) can be used.
For example:
def get_parser() -> ArgumentParser:
p = ArgumentParser(prog="shtab_test.py")
# this can be empty, it doesn't mean it's hidden.
sp = p.add_subparsers()
def _add(
name: str,
add_opts: Callable[[ArgumentParser], ArgumentParser],
func: Callable[[Namespace], int]
) -> argparse.ArgumentParser:
# help will make sure it's not hidden. desc= will mean the description will show up in the completion
px = sp.add_parser(name, description=f"DESC Run {name}", help=f"Help run {name}")
add_opts(px)
px.set_defaults(func=func)
return px
_add("alpha", _to_parser_alpha, lambda ns: run_alpha(ns.input))
_add("beta", _to_parser_beta, lambda ns: run_beta(ns.src, ns.dest))
p.add_argument("--version", action="version", version="0.1.0")
shtab.add_argument_to(p)
return p
I find shtab to be really useful, but the subparser behavior was a bit confusing (perhaps more because of argparse's weird behavior at times).
Thanks for the summary! Yes I agree the FAQs should be tweaked a bit.