From e3f37b9ee0fc637a89d7368dcd3e0c8bde6fd0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 5 Feb 2024 10:30:24 +0100 Subject: [PATCH 1/4] Change: Move StrEnum to pontos.enum and add functions for argparse Create a dedicated enum module and add functions for using enum with argparse. --- pontos/enum.py | 53 +++++++++++++++++++++++++++++++++++++++ pontos/models/__init__.py | 12 +-------- tests/test_enum.py | 37 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 pontos/enum.py create mode 100644 tests/test_enum.py diff --git a/pontos/enum.py b/pontos/enum.py new file mode 100644 index 000000000..b3144e2f4 --- /dev/null +++ b/pontos/enum.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2024 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from argparse import ArgumentTypeError +from enum import Enum +from typing import Callable, Type, TypeVar, Union + + +class StrEnum(str, Enum): + # Should be replaced by enum.StrEnum when we require Python >= 3.11 + """ + An Enum that provides str like behavior + """ + + def __str__(self) -> str: + return self.value + + +def enum_choice(enum: Type[Enum]) -> list[str]: + """ + Return a sequence of choices for argparse from an enum + """ + return [str(e) for e in enum] + + +def to_choices(enum: Type[Enum]) -> str: + """ + Convert an enum to a comma separated string of choices. For example useful + in help messages for argparse. + """ + return ", ".join([str(t) for t in enum]) + + +T = TypeVar("T", bound=Enum) + + +def enum_type(enum: Type[T]) -> Callable[[Union[str, T]], T]: + """ + Create a argparse type function for converting the string input into an Enum + """ + + def convert(value: Union[str, T]) -> T: + if isinstance(value, str): + try: + return enum(value) + except ValueError: + raise ArgumentTypeError( + f"invalid value {value}. Expected one of {to_choices(enum)}." + ) from None + return value + + return convert diff --git a/pontos/models/__init__.py b/pontos/models/__init__.py index d956ed26b..e3a839eb9 100644 --- a/pontos/models/__init__.py +++ b/pontos/models/__init__.py @@ -5,12 +5,12 @@ from dataclasses import dataclass from datetime import date, datetime, timezone -from enum import Enum from inspect import isclass from typing import Any, Dict, Type, Union, get_args, get_origin, get_type_hints from dateutil import parser as dateparser +from pontos.enum import StrEnum from pontos.errors import PontosError __all__ = ( @@ -27,16 +27,6 @@ class ModelError(PontosError): """ -class StrEnum(str, Enum): - # Should be replaced by enum.StrEnum when we require Python >= 3.11 - """ - An Enum that provides str like behavior - """ - - def __str__(self) -> str: - return self.value - - def dotted_attributes(obj: Any, data: Dict[str, Any]) -> Any: """ Set dotted attributes on an object diff --git a/tests/test_enum.py b/tests/test_enum.py new file mode 100644 index 000000000..0d7c82a2f --- /dev/null +++ b/tests/test_enum.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2024 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + + +import unittest +from argparse import ArgumentTypeError + +from pontos.enum import StrEnum, enum_type + + +class EnumTypeTestCase(unittest.TestCase): + def test_enum_type(self): + class FooEnum(StrEnum): + ALL = "all" + NONE = "none" + + func = enum_type(FooEnum) + + self.assertEqual(func("all"), FooEnum.ALL) + self.assertEqual(func("none"), FooEnum.NONE) + + self.assertEqual(func(FooEnum.ALL), FooEnum.ALL) + self.assertEqual(func(FooEnum.NONE), FooEnum.NONE) + + def test_enum_type_error(self): + class FooEnum(StrEnum): + ALL = "all" + NONE = "none" + + func = enum_type(FooEnum) + + with self.assertRaisesRegex( + ArgumentTypeError, + r"invalid value foo. Expected one of all, none", + ): + func("foo") From c09e1d8f79b92f1867465714e0a2dc291e68e794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 5 Feb 2024 10:52:33 +0100 Subject: [PATCH 2/4] Change: Convert ReleaseType and OutoutFormat into StrEnum's With this change they can be used more easily. Especially for argument parsing. --- pontos/release/helper.py | 4 ++-- pontos/release/show.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pontos/release/helper.py b/pontos/release/helper.py index 724a5a11c..54c42438b 100644 --- a/pontos/release/helper.py +++ b/pontos/release/helper.py @@ -3,9 +3,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later # -from enum import Enum from typing import Optional +from pontos.enum import StrEnum from pontos.git import Git, GitError from pontos.terminal import Terminal from pontos.version import Version, VersionCalculator, VersionError @@ -14,7 +14,7 @@ DEFAULT_CHUNK_SIZE = 4096 -class ReleaseType(Enum): +class ReleaseType(StrEnum): """ Type of the release. Used to determine the next release version. diff --git a/pontos/release/show.py b/pontos/release/show.py index 68acdbdac..2caa2f4a6 100644 --- a/pontos/release/show.py +++ b/pontos/release/show.py @@ -4,9 +4,10 @@ import json from argparse import Namespace -from enum import Enum, IntEnum, auto +from enum import IntEnum, auto from typing import Optional +from pontos.enum import StrEnum from pontos.errors import PontosError from pontos.git import Git from pontos.github.actions import ActionIO @@ -29,7 +30,7 @@ class ShowReleaseReturnValue(IntEnum): NO_RELEASE_VERSION = auto() -class OutputFormat(Enum): +class OutputFormat(StrEnum): ENV = "env" JSON = "json" GITHUB_ACTION = "github-action" From 76e4f3bf635102875432b7d3a9d581dc098afeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 5 Feb 2024 11:05:22 +0100 Subject: [PATCH 3/4] Add: Add shtab for shell completion --- poetry.lock | 13 +------------ pyproject.toml | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 23a719e0c..506c0670c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -998,7 +998,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1006,16 +1005,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1032,7 +1023,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1040,7 +1030,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1452,4 +1441,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "7fc0de8b2f6a94b415a0b6b7b18db9b7b3cceaa73d2a2206beedf875c38c0b9a" +content-hash = "85966e2194383388c3d1c5d990be2392b51887f7ece8ee4f82a48f3502a99fe3" diff --git a/pyproject.toml b/pyproject.toml index 25b09b57d..0e7c07da2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ rich = ">=12.4.4" python-dateutil = ">=2.8.2" semver = ">=2.13" lxml = ">=4.9.0" +shtab = ">=1.6.5" [tool.poetry.group.dev.dependencies] autohooks = ">=22.7.0" From bdc2e72779b7440ff8483c073888156670360851 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 4/4] 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",