From 4fac7e1f528a45f39f59338dc3680e303b136d5e Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Mon, 23 Aug 2021 08:59:10 -0700 Subject: [PATCH 1/3] Determine whether to add a subparser to completion by its presence in _get_subactions() This is the logic argparse uses for showing a subparser in the list of options, so shtab now matches that behavior. This is particularly important for subparsers which pass `add_help=False` in order to provide their *own* help implementation. --- shtab/__init__.py | 18 ++++++++++++++++-- tests/test_shtab.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/shtab/__init__.py b/shtab/__init__.py index 2c34b7b..4e700af 100644 --- a/shtab/__init__.py +++ b/shtab/__init__.py @@ -141,6 +141,16 @@ def wordify(string): return string.replace("-", "_").replace(".", " ").replace(" ", "_") +def get_public_subcommands(sub): + """Get all the publicly-visible subcommands for a given subparser.""" + # NOTE: public subcommands have their primary name listed in the result of + # `_get_subactions()`. We use this to get the parser for each subcommand and + # compare all the choices (including aliases!) to the set of known-public + # parsers. + public_parsers = {id(sub.choices[i.dest]) for i in sub._get_subactions()} + return {k for k, v in sub.choices.items() if id(v) in public_parsers} + + def get_bash_commands(root_parser, root_prefix, choice_functions=None): """ Recursive subcommand parser traversal, returning lists of information on @@ -204,6 +214,9 @@ def recurse(parser, prefix): # choices (including subparsers & shtab `.complete` functions) log.debug("choices:{}:{}".format(prefix, sorted(positional.choices))) + if isinstance(positional.choices, dict): + public_cmds = get_public_subcommands(positional) + this_positional_choices = [] for choice in positional.choices: if isinstance(choice, Choice): @@ -222,7 +235,7 @@ def recurse(parser, prefix): elif isinstance(positional.choices, dict): # subparser, so append to list of subparsers & recurse log.debug("subcommand:%s", choice) - if positional.choices[choice].add_help: + if choice in public_cmds: discovered_subparsers.append(str(choice)) this_positional_choices.append(str(choice)) ( @@ -577,8 +590,9 @@ def format_positional(opt): root_arguments.append(format_positional(opt)) else: # subparser log.debug("choices:{}:{}".format(root_prefix, sorted(sub.choices))) + public_cmds = get_public_subcommands(sub) for cmd, subparser in sub.choices.items(): - if not subparser.add_help: + if cmd not in public_cmds: log.debug("skip:subcommand:%s", cmd) continue log.debug("subcommand:%s", cmd) diff --git a/tests/test_shtab.py b/tests/test_shtab.py index 5ce9959..be7f71b 100644 --- a/tests/test_shtab.py +++ b/tests/test_shtab.py @@ -177,7 +177,7 @@ def test_custom_complete(shell, caplog): def test_subparser_custom_complete(shell, caplog): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() - sub = subparsers.add_parser("sub") + sub = subparsers.add_parser("sub", help="help message") sub.add_argument("posA").complete = {"bash": "_shtab_test_some_func"} preamble = {"bash": "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}"} with caplog.at_level(logging.INFO): @@ -194,6 +194,31 @@ def test_subparser_custom_complete(shell, caplog): assert not caplog.record_tuples +@fix_shell +def test_subparser_aliases(shell, caplog): + parser = ArgumentParser(prog="test") + subparsers = parser.add_subparsers() + sub = subparsers.add_parser("sub", aliases=["xsub", "ysub"], help="help message") + sub.add_argument("posA").complete = {"bash": "_shtab_test_some_func"} + preamble = {"bash": "_shtab_test_some_func() { compgen -W 'one two' -- $1 ;}"} + with caplog.at_level(logging.INFO): + completion = shtab.complete(parser, shell=shell, preamble=preamble) + print(completion) + + if shell == "bash": + shell = Bash(completion) + shell.compgen('-W "${_shtab_test_subparsers[*]}"', "s", "sub") + shell.compgen('-W "$_shtab_test_pos_0_choices"', "s", "sub") + shell.compgen('-W "${_shtab_test_subparsers[*]}"', "x", "xsub") + shell.compgen('-W "$_shtab_test_pos_0_choices"', "x", "xsub") + shell.compgen('-W "${_shtab_test_subparsers[*]}"', "y", "ysub") + shell.compgen('-W "$_shtab_test_pos_0_choices"', "y", "ysub") + shell.test('"$($_shtab_test_sub_pos_0_COMPGEN o)" = "one"') + shell.test('-z "$_shtab_test_COMPGEN"') + + assert not caplog.record_tuples + + @fix_shell def test_add_argument_to_optional(shell, caplog): parser = ArgumentParser(prog="test") @@ -213,7 +238,7 @@ def test_add_argument_to_optional(shell, caplog): def test_add_argument_to_positional(shell, caplog, capsys): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers() - sub = subparsers.add_parser("completion") + sub = subparsers.add_parser("completion", help="help message") shtab.add_argument_to(sub, "shell", parent=parser) from argparse import Namespace From 63095cbad9cf55303c8b8d516e002905f454770a Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 24 Aug 2021 23:46:26 +0100 Subject: [PATCH 2/3] misc tidy --- shtab/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/shtab/__init__.py b/shtab/__init__.py index 4e700af..4bbe448 100644 --- a/shtab/__init__.py +++ b/shtab/__init__.py @@ -143,10 +143,6 @@ def wordify(string): def get_public_subcommands(sub): """Get all the publicly-visible subcommands for a given subparser.""" - # NOTE: public subcommands have their primary name listed in the result of - # `_get_subactions()`. We use this to get the parser for each subcommand and - # compare all the choices (including aliases!) to the set of known-public - # parsers. public_parsers = {id(sub.choices[i.dest]) for i in sub._get_subactions()} return {k for k, v in sub.choices.items() if id(v) in public_parsers} @@ -214,9 +210,6 @@ def recurse(parser, prefix): # choices (including subparsers & shtab `.complete` functions) log.debug("choices:{}:{}".format(prefix, sorted(positional.choices))) - if isinstance(positional.choices, dict): - public_cmds = get_public_subcommands(positional) - this_positional_choices = [] for choice in positional.choices: if isinstance(choice, Choice): @@ -235,6 +228,7 @@ def recurse(parser, prefix): elif isinstance(positional.choices, dict): # subparser, so append to list of subparsers & recurse log.debug("subcommand:%s", choice) + public_cmds = get_public_subcommands(positional) if choice in public_cmds: discovered_subparsers.append(str(choice)) this_positional_choices.append(str(choice)) From b1bf591a335058aa5ab24dcace2d007957e8d4f1 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Tue, 24 Aug 2021 16:07:04 -0700 Subject: [PATCH 3/3] Skip test_subparser_aliases on Python 2 --- tests/test_shtab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_shtab.py b/tests/test_shtab.py index be7f71b..d3022ba 100644 --- a/tests/test_shtab.py +++ b/tests/test_shtab.py @@ -3,6 +3,7 @@ """ import logging import subprocess +import sys from argparse import ArgumentParser import pytest @@ -195,6 +196,7 @@ def test_subparser_custom_complete(shell, caplog): @fix_shell +@pytest.mark.skipif(sys.version_info[0] == 2, reason="requires Python 3.x") def test_subparser_aliases(shell, caplog): parser = ArgumentParser(prog="test") subparsers = parser.add_subparsers()