From 19d6fa3c50821fb6329cd05e84b9a841df197f92 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ale=C5=A1=20Mr=C3=A1zek?= Date: Tue, 8 Oct 2024 12:49:53 +0200 Subject: [PATCH] fixup! kresctl: tab-completion: implement suggestions/completion for first argument --- python/knot_resolver/client/command.py | 29 +++++++- python/knot_resolver/client/commands/cache.py | 14 +--- .../client/commands/completion.py | 69 ++++++------------- 3 files changed, 50 insertions(+), 62 deletions(-) diff --git a/python/knot_resolver/client/command.py b/python/knot_resolver/client/command.py index 960ac1f54..f2d59223e 100644 --- a/python/knot_resolver/client/command.py +++ b/python/knot_resolver/client/command.py @@ -1,7 +1,7 @@ import argparse from abc import ABC, abstractmethod # pylint: disable=[no-name-in-module] from pathlib import Path -from typing import Dict, List, Optional, Tuple, Type, TypeVar +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar from urllib.parse import quote from knot_resolver.constants import API_SOCK_FILE, CONFIG_FILE @@ -17,6 +17,33 @@ CompWords = Dict[str, Optional[str]] _registered_commands: List[Type["Command"]] = [] +def get_subparsers_words(subparser_actions: List[argparse.Action]) -> CompWords: + words: CompWords = {} + for action in subparser_actions: + if isinstance(action, argparse._SubParsersAction) and action.choices: + for choice, parser in action.choices.items(): + words[choice] = parser.description + else: + for opt in action.option_strings: + words[opt] = action.help + return words + + +def get_subparser_by_name(name: str, parser_actions: List[argparse.Action]) -> Optional[argparse.ArgumentParser]: + for action in parser_actions: + if isinstance(action, argparse._SubParsersAction): + if action.choices and name in action.choices: + return action.choices[name] + return None + + +def get_subparser_command(subparser: argparse.ArgumentParser) -> "Command": + defaults: Dict[str, Any] = subparser._defaults + if "command" in defaults: + return defaults["command"] + raise ValueError(f"missing 'command' default for '{subparser.prog}' parser") + + def register_command(cls: T) -> T: _registered_commands.append(cls) return cls diff --git a/python/knot_resolver/client/commands/cache.py b/python/knot_resolver/client/commands/cache.py index ef64302b3..b9c2e4662 100644 --- a/python/knot_resolver/client/commands/cache.py +++ b/python/knot_resolver/client/commands/cache.py @@ -3,12 +3,11 @@ import sys from enum import Enum from typing import Any, Dict, List, Optional, Tuple, Type -from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command +from knot_resolver.client.command import Command, CommandArgs, CompWords, get_subparsers_words, register_command from knot_resolver.datamodel.cache_schema import CacheClearRPCSchema from knot_resolver.utils.modeling.exceptions import AggregateDataValidationError, DataValidationError from knot_resolver.utils.modeling.parsing import DataFormat, parse_json from knot_resolver.utils.requests import request -from argparse import _SubParsersAction class CacheOperations(Enum): @@ -100,16 +99,7 @@ class CacheCommand(Command): @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - words = dict() - for action in parser._actions: - if isinstance(action, _SubParsersAction): - if action.choices is not None: - for choice in action.choices: - words[choice] = action.choices.get(choice) - else: - for opt in action.option_strings: - words[opt] = action.help - return words + return get_subparsers_words(parser._actions) def run(self, args: CommandArgs) -> None: if not self.operation: diff --git a/python/knot_resolver/client/commands/completion.py b/python/knot_resolver/client/commands/completion.py index 23871afd7..94499521d 100644 --- a/python/knot_resolver/client/commands/completion.py +++ b/python/knot_resolver/client/commands/completion.py @@ -2,8 +2,15 @@ import argparse from enum import Enum from typing import List, Tuple, Type -from knot_resolver.client.command import Command, CommandArgs, CompWords, register_command -from argparse import _SubParsersAction +from knot_resolver.client.command import ( + Command, + CommandArgs, + CompWords, + get_subparser_by_name, + get_subparser_command, + get_subparsers_words, + register_command, +) class Shells(Enum): @@ -11,38 +18,6 @@ class Shells(Enum): FISH = 1 -def parser_words(actions): - words = dict() - for action in actions: - if isinstance(action, _SubParsersAction): - if action.choices is not None: - for choice in action.choices: - words[choice] = action.choices.get(choice) - else: - for opt in action.option_strings: - words[opt] = action.help - - return words - - -def subparser_by_name(uarg: str, actions) -> argparse.ArgumentParser | None: - for action in actions: - if isinstance(action, _SubParsersAction): - if action.choices is not None: - for choice in action.choices: - # if uarg == choice: - if uarg in choice: - return action.choices.get(choice) - return None - - -def subparser_command(subparser: argparse.ArgumentParser) -> Command: - com_class: Command | None = subparser._defaults.get("command") - # NOTE: This is just a temporary bandage to silence pyright - assert(com_class is not None) - return com_class - - @register_command class CompletionCommand(Command): def __init__(self, namespace: argparse.Namespace) -> None: @@ -58,7 +33,10 @@ class CompletionCommand(Command): def register_args_subparser( subparser: "argparse._SubParsersAction[argparse.ArgumentParser]", ) -> Tuple[argparse.ArgumentParser, "Type[Command]"]: - completion = subparser.add_parser("completion", help="commands auto-completion") + completion = subparser.add_parser( + "completion", + help="commands auto-completion", + ) completion.add_argument( "--space", help="space after last word, returns all possible folowing options", @@ -82,38 +60,31 @@ class CompletionCommand(Command): @staticmethod def completion(args: List[str], parser: argparse.ArgumentParser) -> CompWords: - words: CompWords = {} - # for action in parser._actions: - # for opt in action.option_strings: - # words[opt] = action.help - # return words - return words + return get_subparsers_words(parser._actions) def run(self, args: CommandArgs) -> None: subparsers = args.parser._subparsers words: CompWords = {} if subparsers: - words = parser_words(subparsers._actions) + words = get_subparsers_words(subparsers._actions) uargs = iter(self.comp_args) - # skip kresctl - next(uargs) for uarg in uargs: - subparser = subparser_by_name(uarg, subparsers._actions) # pylint: disable=W0212 + subparser = get_subparser_by_name(uarg, subparsers._actions) # pylint: disable=W0212 if subparser: - cmd: Command = subparser_command(subparser) + cmd: Command = get_subparser_command(subparser) subparser_args = self.comp_args[self.comp_args.index(uarg) + 1 :] - if subparser_args: + if subparser_args or self.space: words = cmd.completion(subparser_args, subparser) break - elif uarg in ["-s", "--socket"]: + elif uarg in ["-s", "--socket", "-c", "--config"]: # if arg is socket config, skip next arg next(uargs) continue elif uarg in words: - # uarg is walid arg, continue + # uarg is valid arg, continue continue else: raise ValueError(f"unknown argument: {uarg}") -- 2.47.2