shtab icon indicating copy to clipboard operation
shtab copied to clipboard

Subparsers are ignored when help is not defined on the subparser

Open mpkocher opened this issue 7 months ago • 3 comments

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_parser with help= 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.

mpkocher avatar May 30 '25 07:05 mpkocher

This is by design, as some devs deliberately define subparsers sans-help as hidden/beta APIs

casperdcl avatar May 30 '25 08:05 casperdcl

This is by design, as some devs deliberately define subparsers sans-help as 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 of help=, the subparser is always displayed as at least positional arguments: {alpha, beta}
  • When you type example.py alpha --help, the description defined in sp.add_parser(name, description="Desc of Alpha") is printed (but not help= value is never shown here. The help= value is only shown at the root level). Defining the description= 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_subparsers has different semantics than .add_parser.
  • help= in .add_argument doesn't need to be defined. Set to argparse.SUPPRESS to 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).

mpkocher avatar Jun 01 '25 06:06 mpkocher

Thanks for the summary! Yes I agree the FAQs should be tweaked a bit.

casperdcl avatar Jun 01 '25 09:06 casperdcl