From e105f3f3bbd2716bd1d37087641fa19a7efb516a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 5 Feb 2024 11:09:19 +0100 Subject: [PATCH] Add: Add shell completion for all pontos CLI Implement shell completion via shtab and cleanup CLI arguments. --- pontos/changelog/main.py | 7 +++- pontos/github/argparser.py | 62 +++++++++++++++-------------- pontos/github/cmds.py | 2 +- pontos/github/script/__init__.py | 1 + pontos/github/script/parser.py | 3 ++ pontos/nvd/cpe/__init__.py | 3 ++ pontos/nvd/cve/__init__.py | 3 ++ pontos/nvd/cve_changes/__init__.py | 3 ++ pontos/release/parser.py | 31 +++++---------- pontos/updateheader/updateheader.py | 12 +++--- pontos/version/_parser.py | 15 +++++-- 11 files changed, 80 insertions(+), 62 deletions(-) diff --git a/pontos/changelog/main.py b/pontos/changelog/main.py index 2d1bb88db..2789ab4ab 100644 --- a/pontos/changelog/main.py +++ b/pontos/changelog/main.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import NoReturn, Optional, Sequence +import shtab + from pontos.changelog.conventional_commits import ChangelogBuilder from pontos.errors import PontosError from pontos.terminal.null import NullTerminal @@ -26,6 +28,7 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace: " text from conventional commits between the current and next release.", prog="pontos-changelog", ) + shtab.add_argument_to(parser) parser.add_argument( "--config", @@ -33,7 +36,7 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace: type=Path, help="Optional. Conventional commits config file (toml), including " "conventions. If not provided defaults are used.", - ) + ).complete = shtab.FILE # type: ignore[attr-defined] parser.add_argument( "--project", @@ -83,7 +86,7 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace: "-o", type=Path, help="Write changelog to this file.", - ) + ).complete = shtab.FILE # type: ignore[attr-defined] parser.add_argument( "--quiet", diff --git a/pontos/github/argparser.py b/pontos/github/argparser.py index 316e5bc10..1040c9388 100644 --- a/pontos/github/argparser.py +++ b/pontos/github/argparser.py @@ -10,6 +10,9 @@ from pathlib import Path from typing import List, Optional +import shtab + +from pontos.enum import enum_choice, enum_type from pontos.github.cmds import ( create_pull_request, create_release, @@ -32,10 +35,6 @@ def from_env(name: str) -> str: return os.environ.get(name, name) -def get_repository_type(rtype: str) -> RepositoryType: - return RepositoryType[rtype] - - def parse_args( args: Optional[List[str]] = None, ) -> Namespace: @@ -43,14 +42,13 @@ def parse_args( Parsing args for Pontos GitHub Arguments: - args The program arguments passed by exec - term The terminal to print + args The program arguments passed by exec """ parser = ArgumentParser( description="Greenbone GitHub API.", ) - + shtab.add_argument_to(parser) parser.add_argument( "--quiet", "-q", @@ -62,20 +60,21 @@ def parse_args( "--log-file", dest="log_file", type=str, - help="Acivate logging using the given file path", - ) + help="Activate logging using the given file path", + ).complete = shtab.FILE # type: ignore[attr-defined] subparsers = parser.add_subparsers( title="subcommands", - description="valid subcommands", - required=True, - help="additional help", + description="Valid subcommands", + help="Additional help", dest="command", ) # create a PR from command line pr_parser = subparsers.add_parser( - "pull-request", aliases=["pr", "PR", "pullrequest"] + "pull-request", + aliases=["pr", "PR", "pullrequest"], + help="Pull request related commands", ) pr_parser.set_defaults(func=pull_request) @@ -95,8 +94,8 @@ def parse_args( title="method", dest="pr_method", metavar="name", - description="valid pull request method", - help="pull request method", + description="Valid pull request method", + help="Pull request method", required=True, ) @@ -136,7 +135,7 @@ def parse_args( ) update_pr_parser = pr_subparsers.add_parser( - "update", help="update Pull Request" + "update", help="Update Pull Request" ) update_pr_parser.set_defaults(pr_func=update_pull_request) @@ -166,7 +165,7 @@ def parse_args( # get files file_status_parser = subparsers.add_parser( - "file-status", aliases=["status", "FS"] + "file-status", aliases=["status", "FS"], help="File status" ) file_status_parser.set_defaults(func=file_status) @@ -182,7 +181,7 @@ def parse_args( file_status_parser.add_argument( "-s", "--status", - choices=FileStatus, + choices=enum_choice(FileStatus), default=[FileStatus.ADDED, FileStatus.MODIFIED], nargs="+", help="What file status should be returned. Default: %(default)s", @@ -212,7 +211,9 @@ def parse_args( ) # labels - label_parser = subparsers.add_parser("labels", aliases=["L"]) + label_parser = subparsers.add_parser( + "labels", aliases=["L"], help="Issue/pull Request label handling" + ) label_parser.set_defaults(func=labels) @@ -243,7 +244,9 @@ def parse_args( ) # orga-repos - repos_parser = subparsers.add_parser("repos", aliases=["R"]) + repos_parser = subparsers.add_parser( + "repos", aliases=["R"], help="Repository information" + ) repos_parser.set_defaults(func=repos) @@ -262,8 +265,8 @@ def parse_args( repos_parser.add_argument( "--type", - choices=RepositoryType, - type=get_repository_type, + choices=enum_choice(RepositoryType), + type=enum_type(RepositoryType), default=RepositoryType.PUBLIC, help=( "Define the type of repositories that should be covered. " @@ -279,7 +282,7 @@ def parse_args( # create a release from command line re_parser = subparsers.add_parser( - "release", aliases=["re", "RE", "release"] + "release", aliases=["re", "RE", "release"], help="Release commands" ) re_parser.set_defaults(func=release) @@ -299,7 +302,7 @@ def parse_args( title="method", dest="re_method", metavar="name", - description="valid release method", + description="Valid release method", help="Release method", required=True, ) @@ -353,7 +356,9 @@ def parse_args( ) # Create a tag from command line - tag_parser = subparsers.add_parser("tag", aliases=["tag", "TAG"]) + tag_parser = subparsers.add_parser( + "tag", aliases=["tag", "TAG"], help="Tag commands" + ) tag_parser.set_defaults(func=tag) @@ -372,7 +377,7 @@ def parse_args( title="method", dest="tag_method", metavar="name", - description="valid tag method", + description="Valid tag method", help="Release method", required=True, ) @@ -425,7 +430,4 @@ def parse_args( " YYYY-MM-DDTHH:MM:SSZ." ), ) - - parsed_args = parser.parse_args(args) - - return parsed_args + return parser.parse_args(args) diff --git a/pontos/github/cmds.py b/pontos/github/cmds.py index bff1c0e43..33da0c2a8 100644 --- a/pontos/github/cmds.py +++ b/pontos/github/cmds.py @@ -231,7 +231,7 @@ async def repos(terminal: Terminal, args: Namespace): exists = await api.organizations.exists(args.orga) if not exists: terminal.error( - f"PR {args.orga} is not existing or authorisation failed." + f"Organization {args.orga} is not existing or authorisation failed." ) sys.exit(1) diff --git a/pontos/github/script/__init__.py b/pontos/github/script/__init__.py index 3ea67c9ba..2f5791143 100644 --- a/pontos/github/script/__init__.py +++ b/pontos/github/script/__init__.py @@ -66,6 +66,7 @@ def main(): child_parser = ArgumentParser(parents=[parser]) run_add_arguments_function(module, child_parser) args = child_parser.parse_args() + token = args.token timeout = args.timeout diff --git a/pontos/github/script/parser.py b/pontos/github/script/parser.py index ce59aac4b..73d323706 100644 --- a/pontos/github/script/parser.py +++ b/pontos/github/script/parser.py @@ -6,6 +6,8 @@ import os from argparse import ArgumentParser +import shtab + from pontos.github.api.helper import DEFAULT_TIMEOUT GITHUB_TOKEN = "GITHUB_TOKEN" @@ -19,6 +21,7 @@ def create_parser() -> ArgumentParser: A new ArgumentParser instance add the default arguments """ parser = ArgumentParser(add_help=False) + shtab.add_argument_to(parser) parser.add_argument( "--token", default=os.environ.get(GITHUB_TOKEN), diff --git a/pontos/nvd/cpe/__init__.py b/pontos/nvd/cpe/__init__.py index 0df57bbed..924281f40 100644 --- a/pontos/nvd/cpe/__init__.py +++ b/pontos/nvd/cpe/__init__.py @@ -8,6 +8,7 @@ from typing import Callable import httpx +import shtab from pontos.nvd.cpe.api import CPEApi @@ -34,6 +35,7 @@ async def query_cpes(args: Namespace) -> None: def cpe_main() -> None: parser = ArgumentParser() + shtab.add_argument_to(parser) parser.add_argument("--token", help="API key to use for querying.") parser.add_argument( "cpe_name_id", metavar="CPE Name ID", help="UUID of the CPE" @@ -44,6 +46,7 @@ def cpe_main() -> None: def cpes_main() -> None: parser = ArgumentParser() + shtab.add_argument_to(parser) parser.add_argument("--token", help="API key to use for querying.") parser.add_argument( "--cpe-match-string", diff --git a/pontos/nvd/cve/__init__.py b/pontos/nvd/cve/__init__.py index 54d42726c..93ec838b4 100644 --- a/pontos/nvd/cve/__init__.py +++ b/pontos/nvd/cve/__init__.py @@ -8,6 +8,7 @@ from typing import Callable import httpx +import shtab from pontos.nvd.cve.api import CVEApi @@ -36,6 +37,7 @@ async def query_cve(args: Namespace) -> None: def cves_main() -> None: parser = ArgumentParser() + shtab.add_argument_to(parser) parser.add_argument("--token", help="API key to use for querying.") parser.add_argument( "--keywords", @@ -73,6 +75,7 @@ def cves_main() -> None: def cve_main() -> None: parser = ArgumentParser() + shtab.add_argument_to(parser) parser.add_argument("--token", help="API key to use for querying.") parser.add_argument("cve_id", metavar="CVE-ID", help="ID of the CVE") diff --git a/pontos/nvd/cve_changes/__init__.py b/pontos/nvd/cve_changes/__init__.py index f7932ba16..d4459ab5b 100644 --- a/pontos/nvd/cve_changes/__init__.py +++ b/pontos/nvd/cve_changes/__init__.py @@ -5,6 +5,8 @@ import asyncio from argparse import ArgumentParser, Namespace +import shtab + from pontos.nvd.cve_changes.api import CVEChangesApi __all__ = ("CVEChangesApi",) @@ -23,6 +25,7 @@ async def query_changes(args: Namespace) -> None: def parse_args() -> Namespace: parser = ArgumentParser() + shtab.add_argument_to(parser) parser.add_argument("--token", help="API key to use for querying.") parser.add_argument("--cve-id", help="Get changes for a specific CVE") parser.add_argument( diff --git a/pontos/release/parser.py b/pontos/release/parser.py index e3cadbc5e..9e24c936a 100644 --- a/pontos/release/parser.py +++ b/pontos/release/parser.py @@ -7,14 +7,15 @@ import os from argparse import ( ArgumentParser, - ArgumentTypeError, BooleanOptionalAction, Namespace, ) -from enum import Enum from pathlib import Path -from typing import Callable, Optional, Tuple, Type +from typing import Optional, Tuple +import shtab + +from pontos.enum import enum_choice, enum_type, to_choices from pontos.release.helper import ReleaseType from pontos.release.show import OutputFormat, show from pontos.version.schemes import ( @@ -30,22 +31,6 @@ DEFAULT_SIGNING_KEY = "0ED1E580" -def to_choices(enum: Type[Enum]) -> str: - return ", ".join([t.value for t in enum]) - - -def enum_type(enum: Type[Enum]) -> Callable[[str], Enum]: - def convert(value: str) -> Enum: - try: - return enum(value) - except ValueError: - raise ArgumentTypeError( - f"invalid value {value}. Expected one of {to_choices(enum)}." - ) from None - - return convert - - class ReleaseVersionAction( argparse._StoreAction ): # pylint: disable=protected-access @@ -62,6 +47,7 @@ def parse_args(args) -> Tuple[Optional[str], Optional[str], Namespace]: description="Release handling utility.", prog="pontos-release", ) + shtab.add_argument_to(parser) parser.add_argument( "--quiet", @@ -72,8 +58,8 @@ def parse_args(args) -> Tuple[Optional[str], Optional[str], Namespace]: subparsers = parser.add_subparsers( title="subcommands", - description="valid subcommands", - help="additional help", + description="Valid subcommands", + help="Additional help", dest="command", required=True, ) @@ -98,6 +84,7 @@ def parse_args(args) -> Tuple[Optional[str], Optional[str], Namespace]: help="Select the release type for calculating the release version. " f"Possible choices are: {to_choices(ReleaseType)}.", type=enum_type(ReleaseType), + choices=enum_choice(ReleaseType), ) create_parser.add_argument( "--release-version", @@ -281,6 +268,7 @@ def parse_args(args) -> Tuple[Optional[str], Optional[str], Namespace]: help="Select the release type for calculating the release version. " f"Possible choices are: {to_choices(ReleaseType)}.", type=enum_type(ReleaseType), + choices=enum_choice(ReleaseType), ) show_parser.add_argument( "--release-version", @@ -308,6 +296,7 @@ def parse_args(args) -> Tuple[Optional[str], Optional[str], Namespace]: help="Print in the desired output format. " f"Possible choices are: {to_choices(OutputFormat)}.", type=enum_type(OutputFormat), + choices=enum_choice(OutputFormat), ) parsed_args = parser.parse_args(args) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index f77d973fc..231dc72d2 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -16,6 +16,8 @@ from subprocess import CalledProcessError, run from typing import Dict, List, Optional, Tuple, Union +import shtab + from pontos.terminal import Terminal from pontos.terminal.null import NullTerminal from pontos.terminal.rich import RichTerminal @@ -301,8 +303,8 @@ def _parse_args(args=None): parser = ArgumentParser( description="Update copyright in source file headers.", - prog="pontos-update-header", ) + shtab.add_argument_to(parser) parser.add_argument( "--quiet", @@ -316,7 +318,7 @@ def _parse_args(args=None): dest="log_file", type=str, help="Acivate logging using the given file path", - ) + ).complete = shtab.FILE date_group = parser.add_mutually_exclusive_group() date_group.add_argument( @@ -360,13 +362,13 @@ def _parse_args(args=None): files_group = parser.add_mutually_exclusive_group(required=True) files_group.add_argument( "-f", "--files", nargs="+", help="Files to update." - ) + ).complete = shtab.FILE files_group.add_argument( "-d", "--directories", nargs="+", help="Directories to find files to update recursively.", - ) + ).complete = shtab.DIRECTORY parser.add_argument( "--exclude-file", @@ -379,7 +381,7 @@ def _parse_args(args=None): "not absolute as **/*.py" ), type=FileType("r"), - ) + ).complete = shtab.FILE parser.add_argument( "--cleanup", diff --git a/pontos/version/_parser.py b/pontos/version/_parser.py index 09a0604b1..1f2d02eda 100644 --- a/pontos/version/_parser.py +++ b/pontos/version/_parser.py @@ -6,6 +6,8 @@ import argparse from typing import List, Optional +import shtab + from pontos.errors import PontosError from pontos.version.schemes import ( VERSIONING_SCHEMES, @@ -25,6 +27,7 @@ def initialize_default_parser() -> argparse.ArgumentParser: description="Version handling utilities.", prog="version", ) + shtab.add_argument_to(parser) subparsers = parser.add_subparsers( title="subcommands", description="Valid subcommands", @@ -33,7 +36,9 @@ def initialize_default_parser() -> argparse.ArgumentParser: required=True, ) - verify_parser = subparsers.add_parser("verify") + verify_parser = subparsers.add_parser( + "verify", help="Verify version in the current project" + ) verify_parser.add_argument( "version", help="Version string to compare", @@ -48,7 +53,9 @@ def initialize_default_parser() -> argparse.ArgumentParser: type=versioning_scheme_argument_type, ) - show_parser = subparsers.add_parser("show") + show_parser = subparsers.add_parser( + "show", help="Show version information of the current project" + ) show_parser.add_argument( "--versioning-scheme", help="Versioning scheme to use for parsing and handling version " @@ -58,7 +65,9 @@ def initialize_default_parser() -> argparse.ArgumentParser: type=versioning_scheme_argument_type, ) - update_parser = subparsers.add_parser("update") + update_parser = subparsers.add_parser( + "update", help="Update version in the current project" + ) update_parser.add_argument( "version", help="Version string to use",