From 56280040e415b39ca0e9d032a927f0a39e734b9b Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sun, 1 May 2022 20:55:56 +0200 Subject: [PATCH] refactor: decouple CLI from custom method arguments --- gitlab/base.py | 25 +++++++++++++++++- gitlab/cli.py | 28 ++++++++++---------- gitlab/exceptions.py | 17 +++++-------- gitlab/mixins.py | 16 ++++++------ gitlab/types.py | 6 ++++- gitlab/v4/objects/artifacts.py | 17 +++++-------- gitlab/v4/objects/commits.py | 11 +++++--- gitlab/v4/objects/container_registry.py | 8 +++--- gitlab/v4/objects/deploy_keys.py | 5 ++-- gitlab/v4/objects/files.py | 24 +++++++++-------- gitlab/v4/objects/groups.py | 23 +++++++++++------ gitlab/v4/objects/issues.py | 5 ++-- gitlab/v4/objects/merge_requests.py | 12 ++++----- gitlab/v4/objects/packages.py | 14 +++++----- gitlab/v4/objects/projects.py | 34 +++++++++++++++---------- gitlab/v4/objects/repositories.py | 20 ++++++++++----- gitlab/v4/objects/runners.py | 8 +++--- 17 files changed, 159 insertions(+), 114 deletions(-) diff --git a/gitlab/base.py b/gitlab/base.py index 6e7d5c584..1838b4e0d 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -19,11 +19,12 @@ import pprint import textwrap from types import ModuleType -from typing import Any, Dict, Iterable, Optional, Type, Union +from typing import Any, Callable, Dict, Iterable, Optional, Type, Union import gitlab from gitlab import types as g_types from gitlab.exceptions import GitlabParsingError +from gitlab.types import F from .client import Gitlab, GitlabList @@ -328,6 +329,28 @@ def total(self) -> Optional[int]: return self._list.total +def custom_attrs( + required: tuple = (), optional: tuple = (), exclusive: tuple = () +) -> Callable[[F], F]: + """Decorates a custom method to add a RequiredOptional attribute. + + Args: + required: A tuple of API attributes required in the custom method + optional: A tuple of API attributes optional in the custom method + exclusive: A tuple of mutually exclusive API attributes in the custom method + """ + + def decorator(func: F) -> F: + setattr( + func, + "_custom_attrs", + g_types.RequiredOptional(required, optional, exclusive), + ) + return func + + return decorator + + class RESTManager: """Base class for CRUD operations on objects. diff --git a/gitlab/cli.py b/gitlab/cli.py index 979396407..5758555b7 100644 --- a/gitlab/cli.py +++ b/gitlab/cli.py @@ -23,12 +23,13 @@ import re import sys from types import ModuleType -from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, Union from requests.structures import CaseInsensitiveDict import gitlab.config from gitlab.base import RESTObject +from gitlab.types import F, RequiredOptional # This regex is based on: # https://github.com/jpvanhal/inflection/blob/master/inflection/__init__.py @@ -43,24 +44,18 @@ custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool]]] = {} -# For an explanation of how these type-hints work see: -# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators -# -# The goal here is that functions which get decorated will retain their types. -__F = TypeVar("__F", bound=Callable[..., Any]) - - def register_custom_action( cls_names: Union[str, Tuple[str, ...]], - mandatory: Tuple[str, ...] = (), - optional: Tuple[str, ...] = (), custom_action: Optional[str] = None, -) -> Callable[[__F], __F]: - def wrap(f: __F) -> __F: +) -> Callable[[F], F]: + def wrap(f: F) -> F: @functools.wraps(f) def wrapped_f(*args: Any, **kwargs: Any) -> Any: return f(*args, **kwargs) + action = custom_action or f.__name__.replace("_", "-") + custom_attrs = getattr(f, "_custom_attrs", RequiredOptional()) + # in_obj defines whether the method belongs to the obj or the manager in_obj = True if isinstance(cls_names, tuple): @@ -76,10 +71,13 @@ def wrapped_f(*args: Any, **kwargs: Any) -> Any: if final_name not in custom_actions: custom_actions[final_name] = {} - action = custom_action or f.__name__.replace("_", "-") - custom_actions[final_name][action] = (mandatory, optional, in_obj) + custom_actions[final_name][action] = ( + custom_attrs.required, + custom_attrs.optional, + in_obj, + ) - return cast(__F, wrapped_f) + return cast(F, wrapped_f) return wrap diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 3da399c54..c8254ec2c 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -16,7 +16,9 @@ # along with this program. If not, see . import functools -from typing import Any, Callable, cast, Optional, Type, TYPE_CHECKING, TypeVar, Union +from typing import Any, Callable, cast, Optional, Type, TYPE_CHECKING, Union + +from gitlab.types import F class GitlabError(Exception): @@ -286,14 +288,7 @@ class GitlabUnfollowError(GitlabOperationError): pass -# For an explanation of how these type-hints work see: -# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators -# -# The goal here is that functions which get decorated will retain their types. -__F = TypeVar("__F", bound=Callable[..., Any]) - - -def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]: +def on_http_error(error: Type[Exception]) -> Callable[[F], F]: """Manage GitlabHttpError exceptions. This decorator function can be used to catch GitlabHttpError exceptions @@ -303,7 +298,7 @@ def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]: The exception type to raise -- must inherit from GitlabError """ - def wrap(f: __F) -> __F: + def wrap(f: F) -> F: @functools.wraps(f) def wrapped_f(*args: Any, **kwargs: Any) -> Any: try: @@ -311,6 +306,6 @@ def wrapped_f(*args: Any, **kwargs: Any) -> Any: except GitlabHttpError as e: raise error(e.error_message, e.response_code, e.response_body) from e - return cast(__F, wrapped_f) + return cast(F, wrapped_f) return wrap diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 4dee7106a..e040192d6 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -547,9 +547,8 @@ class AccessRequestMixin(_RestObjectBase): _updated_attrs: Dict[str, Any] manager: base.RESTManager - @cli.register_custom_action( - ("ProjectAccessRequest", "GroupAccessRequest"), (), ("access_level",) - ) + @cli.register_custom_action(("ProjectAccessRequest", "GroupAccessRequest")) + @base.custom_attrs(optional=("access_level",)) @exc.on_http_error(exc.GitlabUpdateError) def approve( self, access_level: int = gitlab.const.DEVELOPER_ACCESS, **kwargs: Any @@ -721,7 +720,8 @@ def time_stats(self, **kwargs: Any) -> Dict[str, Any]: assert not isinstance(result, requests.Response) return result - @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) + @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) + @base.custom_attrs(required=("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) def time_estimate(self, duration: str, **kwargs: Any) -> Dict[str, Any]: """Set an estimated time of work for the object. @@ -759,7 +759,8 @@ def reset_time_estimate(self, **kwargs: Any) -> Dict[str, Any]: assert not isinstance(result, requests.Response) return result - @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",)) + @cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest")) + @base.custom_attrs(required=("duration",)) @exc.on_http_error(exc.GitlabTimeTrackingError) def add_spent_time(self, duration: str, **kwargs: Any) -> Dict[str, Any]: """Add time spent working on the object. @@ -833,9 +834,8 @@ def participants(self, **kwargs: Any) -> Dict[str, Any]: class BadgeRenderMixin(_RestManagerBase): - @cli.register_custom_action( - ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url") - ) + @cli.register_custom_action(("GroupBadgeManager", "ProjectBadgeManager")) + @base.custom_attrs(required=("link_url", "image_url")) @exc.on_http_error(exc.GitlabRenderError) def render(self, link_url: str, image_url: str, **kwargs: Any) -> Dict[str, Any]: """Preview link_url and image_url after interpolation. diff --git a/gitlab/types.py b/gitlab/types.py index f811a6f3e..17ed9aa25 100644 --- a/gitlab/types.py +++ b/gitlab/types.py @@ -16,7 +16,11 @@ # along with this program. If not, see . import dataclasses -from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING +from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, TypeVar + +# TypeVar for decorators so that decorated functions retain their signatures. +# See https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators +F = TypeVar("F", bound=Callable[..., Any]) @dataclasses.dataclass(frozen=True) diff --git a/gitlab/v4/objects/artifacts.py b/gitlab/v4/objects/artifacts.py index 541e5e2f4..a7fa9991d 100644 --- a/gitlab/v4/objects/artifacts.py +++ b/gitlab/v4/objects/artifacts.py @@ -9,7 +9,7 @@ from gitlab import cli from gitlab import exceptions as exc from gitlab import utils -from gitlab.base import RESTManager, RESTObject +from gitlab.base import custom_attrs, RESTManager, RESTObject __all__ = ["ProjectArtifact", "ProjectArtifactManager"] @@ -25,9 +25,8 @@ class ProjectArtifactManager(RESTManager): _path = "/projects/{project_id}/jobs/artifacts" _from_parent_attrs = {"project_id": "id"} - @cli.register_custom_action( - "Project", ("ref_name", "job"), ("job_token",), custom_action="artifacts" - ) + @cli.register_custom_action("Project", custom_action="artifacts") + @custom_attrs(required=("ref_name", "job"), optional=("job_token",)) def __call__( self, *args: Any, @@ -62,9 +61,8 @@ def delete(self, **kwargs: Any) -> None: assert path is not None self.gitlab.http_delete(path, **kwargs) - @cli.register_custom_action( - "ProjectArtifactManager", ("ref_name", "job"), ("job_token",) - ) + @cli.register_custom_action("ProjectArtifactManager") + @custom_attrs(required=("ref_name", "job"), optional=("job_token",)) @exc.on_http_error(exc.GitlabGetError) def download( self, @@ -105,9 +103,8 @@ def download( assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) - @cli.register_custom_action( - "ProjectArtifactManager", ("ref_name", "artifact_path", "job") - ) + @cli.register_custom_action("ProjectArtifactManager") + @custom_attrs(required=("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) def raw( self, diff --git a/gitlab/v4/objects/commits.py b/gitlab/v4/objects/commits.py index 8558ef9ea..30b70b98d 100644 --- a/gitlab/v4/objects/commits.py +++ b/gitlab/v4/objects/commits.py @@ -3,7 +3,7 @@ import requests import gitlab -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject from gitlab.mixins import CreateMixin, ListMixin, RefreshMixin, RetrieveMixin @@ -46,7 +46,8 @@ def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]: path = f"{self.manager.path}/{self.encoded_id}/diff" return self.manager.gitlab.http_list(path, **kwargs) - @cli.register_custom_action("ProjectCommit", ("branch",)) + @cli.register_custom_action("ProjectCommit") + @base.custom_attrs(required=("branch",)) @exc.on_http_error(exc.GitlabCherryPickError) def cherry_pick(self, branch: str, **kwargs: Any) -> None: """Cherry-pick a commit into a branch. @@ -63,7 +64,8 @@ def cherry_pick(self, branch: str, **kwargs: Any) -> None: post_data = {"branch": branch} self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) - @cli.register_custom_action("ProjectCommit", optional=("type",)) + @cli.register_custom_action("ProjectCommit") + @base.custom_attrs(optional=("type",)) @exc.on_http_error(exc.GitlabGetError) def refs( self, type: str = "all", **kwargs: Any @@ -105,7 +107,8 @@ def merge_requests( path = f"{self.manager.path}/{self.encoded_id}/merge_requests" return self.manager.gitlab.http_list(path, **kwargs) - @cli.register_custom_action("ProjectCommit", ("branch",)) + @cli.register_custom_action("ProjectCommit") + @base.custom_attrs(required=("branch",)) @exc.on_http_error(exc.GitlabRevertError) def revert( self, branch: str, **kwargs: Any diff --git a/gitlab/v4/objects/container_registry.py b/gitlab/v4/objects/container_registry.py index a144dc114..21da59e46 100644 --- a/gitlab/v4/objects/container_registry.py +++ b/gitlab/v4/objects/container_registry.py @@ -1,6 +1,6 @@ from typing import Any, cast, TYPE_CHECKING, Union -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject from gitlab.mixins import DeleteMixin, ListMixin, ObjectDeleteMixin, RetrieveMixin @@ -32,9 +32,9 @@ class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager): _from_parent_attrs = {"project_id": "project_id", "repository_id": "id"} _path = "/projects/{project_id}/registry/repositories/{repository_id}/tags" - @cli.register_custom_action( - "ProjectRegistryTagManager", - ("name_regex_delete",), + @cli.register_custom_action("ProjectRegistryTagManager") + @base.custom_attrs( + required=("name_regex_delete",), optional=("keep_n", "name_regex_keep", "older_than"), ) @exc.on_http_error(exc.GitlabDeleteError) diff --git a/gitlab/v4/objects/deploy_keys.py b/gitlab/v4/objects/deploy_keys.py index 0962b4a39..3737b0f3e 100644 --- a/gitlab/v4/objects/deploy_keys.py +++ b/gitlab/v4/objects/deploy_keys.py @@ -2,7 +2,7 @@ import requests -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab.base import RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin @@ -36,7 +36,8 @@ class ProjectKeyManager(CRUDMixin, RESTManager): _create_attrs = RequiredOptional(required=("title", "key"), optional=("can_push",)) _update_attrs = RequiredOptional(optional=("title", "can_push")) - @cli.register_custom_action("ProjectKeyManager", ("key_id",)) + @cli.register_custom_action("ProjectKeyManager") + @base.custom_attrs(required=("key_id",)) @exc.on_http_error(exc.GitlabProjectDeployKeyError) def enable( self, key_id: int, **kwargs: Any diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index aa86704c9..8d8451988 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -3,7 +3,7 @@ import requests -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RESTManager, RESTObject @@ -98,7 +98,8 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa optional=("encoding", "author_email", "author_name"), ) - @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) + @cli.register_custom_action("ProjectFileManager") + @base.custom_attrs(required=("file_path", "ref")) # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore # type error def get( # type: ignore @@ -120,10 +121,10 @@ def get( # type: ignore """ return cast(ProjectFile, GetMixin.get(self, file_path, ref=ref, **kwargs)) - @cli.register_custom_action( - "ProjectFileManager", - ("file_path", "branch", "content", "commit_message"), - ("encoding", "author_email", "author_name"), + @cli.register_custom_action("ProjectFileManager") + @base.custom_attrs( + required=("file_path", "branch", "content", "commit_message"), + optional=("encoding", "author_email", "author_name"), ) @exc.on_http_error(exc.GitlabCreateError) def create( @@ -187,9 +188,8 @@ def update( # type: ignore assert isinstance(result, dict) return result - @cli.register_custom_action( - "ProjectFileManager", ("file_path", "branch", "commit_message") - ) + @cli.register_custom_action("ProjectFileManager") + @base.custom_attrs(required=("file_path", "branch", "commit_message")) @exc.on_http_error(exc.GitlabDeleteError) # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore # type error @@ -213,7 +213,8 @@ def delete( # type: ignore data = {"branch": branch, "commit_message": commit_message} self.gitlab.http_delete(path, query_data=data, **kwargs) - @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) + @cli.register_custom_action("ProjectFileManager") + @base.custom_attrs(required=("file_path", "ref")) @exc.on_http_error(exc.GitlabGetError) def raw( self, @@ -254,7 +255,8 @@ def raw( assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) - @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) + @cli.register_custom_action("ProjectFileManager") + @base.custom_attrs(required=("file_path", "ref")) @exc.on_http_error(exc.GitlabListError) def blame(self, file_path: str, ref: str, **kwargs: Any) -> List[Dict[str, Any]]: """Return the content of a file for a commit. diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py index 33f5be16b..6fa964e48 100644 --- a/gitlab/v4/objects/groups.py +++ b/gitlab/v4/objects/groups.py @@ -3,7 +3,7 @@ import requests import gitlab -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RESTManager, RESTObject @@ -80,7 +80,8 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): variables: GroupVariableManager wikis: GroupWikiManager - @cli.register_custom_action("Group", ("project_id",)) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("project_id",)) @exc.on_http_error(exc.GitlabTransferProjectError) def transfer_project(self, project_id: int, **kwargs: Any) -> None: """Transfer a project to this group. @@ -96,7 +97,8 @@ def transfer_project(self, project_id: int, **kwargs: Any) -> None: path = f"/groups/{self.encoded_id}/projects/{project_id}" self.manager.gitlab.http_post(path, **kwargs) - @cli.register_custom_action("Group", (), ("group_id",)) + @cli.register_custom_action("Group") + @base.custom_attrs(optional=("group_id",)) @exc.on_http_error(exc.GitlabGroupTransferError) def transfer(self, group_id: Optional[int] = None, **kwargs: Any) -> None: """Transfer the group to a new parent group or make it a top-level group. @@ -118,7 +120,8 @@ def transfer(self, group_id: Optional[int] = None, **kwargs: Any) -> None: post_data["group_id"] = group_id self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) - @cli.register_custom_action("Group", ("scope", "search")) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) def search( self, scope: str, search: str, **kwargs: Any @@ -141,7 +144,8 @@ def search( path = f"/groups/{self.encoded_id}/search" return self.manager.gitlab.http_list(path, query_data=data, **kwargs) - @cli.register_custom_action("Group", ("cn", "group_access", "provider")) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("cn", "group_access", "provider")) @exc.on_http_error(exc.GitlabCreateError) def add_ldap_group_link( self, cn: str, group_access: int, provider: str, **kwargs: Any @@ -163,7 +167,8 @@ def add_ldap_group_link( data = {"cn": cn, "group_access": group_access, "provider": provider} self.manager.gitlab.http_post(path, post_data=data, **kwargs) - @cli.register_custom_action("Group", ("cn",), ("provider",)) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("cn",), optional=("provider",)) @exc.on_http_error(exc.GitlabDeleteError) def delete_ldap_group_link( self, cn: str, provider: Optional[str] = None, **kwargs: Any @@ -200,7 +205,8 @@ def ldap_sync(self, **kwargs: Any) -> None: path = f"/groups/{self.encoded_id}/ldap_sync" self.manager.gitlab.http_post(path, **kwargs) - @cli.register_custom_action("Group", ("group_id", "group_access"), ("expires_at",)) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("group_id", "group_access"), optional=("expires_at",)) @exc.on_http_error(exc.GitlabCreateError) def share( self, @@ -234,7 +240,8 @@ def share( assert isinstance(server_data, dict) self._update_attrs(server_data) - @cli.register_custom_action("Group", ("group_id",)) + @cli.register_custom_action("Group") + @base.custom_attrs(required=("group_id",)) @exc.on_http_error(exc.GitlabDeleteError) def unshare(self, group_id: int, **kwargs: Any) -> None: """Delete a shared group link within a group. diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py index 736bb5cdb..579f524e7 100644 --- a/gitlab/v4/objects/issues.py +++ b/gitlab/v4/objects/issues.py @@ -1,6 +1,6 @@ from typing import Any, cast, Dict, Tuple, TYPE_CHECKING, Union -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RESTManager, RESTObject @@ -120,7 +120,8 @@ class ProjectIssue( resourcemilestoneevents: ProjectIssueResourceMilestoneEventManager resourcestateevents: ProjectIssueResourceStateEventManager - @cli.register_custom_action("ProjectIssue", ("to_project_id",)) + @cli.register_custom_action("ProjectIssue") + @base.custom_attrs(required=("to_project_id",)) @exc.on_http_error(exc.GitlabUpdateError) def move(self, to_project_id: int, **kwargs: Any) -> None: """Move the issue to another project. diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py index 9eb965b93..54ab5a610 100644 --- a/gitlab/v4/objects/merge_requests.py +++ b/gitlab/v4/objects/merge_requests.py @@ -8,7 +8,7 @@ import requests import gitlab -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RESTManager, RESTObject, RESTObjectList @@ -260,7 +260,8 @@ def changes(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: path = f"{self.manager.path}/{self.encoded_id}/changes" return self.manager.gitlab.http_get(path, **kwargs) - @cli.register_custom_action("ProjectMergeRequest", (), ("sha",)) + @cli.register_custom_action("ProjectMergeRequest") + @base.custom_attrs(optional=("sha",)) @exc.on_http_error(exc.GitlabMRApprovalError) def approve(self, sha: Optional[str] = None, **kwargs: Any) -> Dict[str, Any]: """Approve the merge request. @@ -342,10 +343,9 @@ def merge_ref(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: path = f"{self.manager.path}/{self.encoded_id}/merge_ref" return self.manager.gitlab.http_get(path, **kwargs) - @cli.register_custom_action( - "ProjectMergeRequest", - (), - ( + @cli.register_custom_action("ProjectMergeRequest") + @base.custom_attrs( + optional=( "merge_commit_message", "should_remove_source_branch", "merge_when_pipeline_succeeds", diff --git a/gitlab/v4/objects/packages.py b/gitlab/v4/objects/packages.py index 882cb1a5a..479a4feda 100644 --- a/gitlab/v4/objects/packages.py +++ b/gitlab/v4/objects/packages.py @@ -9,7 +9,7 @@ import requests -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import utils from gitlab.base import RESTManager, RESTObject @@ -36,9 +36,9 @@ class GenericPackageManager(RESTManager): _obj_cls = GenericPackage _from_parent_attrs = {"project_id": "id"} - @cli.register_custom_action( - "GenericPackageManager", - ("package_name", "package_version", "file_name", "path"), + @cli.register_custom_action("GenericPackageManager") + @base.custom_attrs( + required=("package_name", "package_version", "file_name", "path") ) @exc.on_http_error(exc.GitlabUploadError) def upload( @@ -92,10 +92,8 @@ def upload( }, ) - @cli.register_custom_action( - "GenericPackageManager", - ("package_name", "package_version", "file_name"), - ) + @cli.register_custom_action("GenericPackageManager") + @base.custom_attrs(required=("package_name", "package_version", "file_name")) @exc.on_http_error(exc.GitlabGetError) def download( self, diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index 8674ee635..14c2e8045 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -2,7 +2,7 @@ import requests -from gitlab import cli, client +from gitlab import base, cli, client from gitlab import exceptions as exc from gitlab import types, utils from gitlab.base import RESTManager, RESTObject @@ -191,7 +191,8 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO variables: ProjectVariableManager wikis: ProjectWikiManager - @cli.register_custom_action("Project", ("forked_from_id",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("forked_from_id",)) @exc.on_http_error(exc.GitlabCreateError) def create_fork_relation(self, forked_from_id: int, **kwargs: Any) -> None: """Create a forked from/to relation between existing projects. @@ -309,9 +310,8 @@ def unarchive(self, **kwargs: Any) -> None: assert isinstance(server_data, dict) self._update_attrs(server_data) - @cli.register_custom_action( - "Project", ("group_id", "group_access"), ("expires_at",) - ) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("group_id", "group_access"), optional=("expires_at",)) @exc.on_http_error(exc.GitlabCreateError) def share( self, @@ -339,7 +339,8 @@ def share( } self.manager.gitlab.http_post(path, post_data=data, **kwargs) - @cli.register_custom_action("Project", ("group_id",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("group_id",)) @exc.on_http_error(exc.GitlabDeleteError) def unshare(self, group_id: int, **kwargs: Any) -> None: """Delete a shared project link within a group. @@ -356,7 +357,8 @@ def unshare(self, group_id: int, **kwargs: Any) -> None: self.manager.gitlab.http_delete(path, **kwargs) # variables not supported in CLI - @cli.register_custom_action("Project", ("ref", "token")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("ref", "token")) @exc.on_http_error(exc.GitlabCreateError) def trigger_pipeline( self, @@ -404,7 +406,8 @@ def housekeeping(self, **kwargs: Any) -> None: self.manager.gitlab.http_post(path, **kwargs) # see #56 - add file attachment features - @cli.register_custom_action("Project", ("filename", "filepath")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("filename", "filepath")) @exc.on_http_error(exc.GitlabUploadError) def upload( self, @@ -456,7 +459,8 @@ def upload( assert isinstance(data, dict) return {"alt": data["alt"], "url": data["url"], "markdown": data["markdown"]} - @cli.register_custom_action("Project", optional=("wiki",)) + @cli.register_custom_action("Project") + @base.custom_attrs(optional=("wiki",)) @exc.on_http_error(exc.GitlabGetError) def snapshot( self, @@ -493,7 +497,8 @@ def snapshot( assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) - @cli.register_custom_action("Project", ("scope", "search")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) def search( self, scope: str, search: str, **kwargs: Any @@ -531,7 +536,8 @@ def mirror_pull(self, **kwargs: Any) -> None: path = f"/projects/{self.encoded_id}/mirror/pull" self.manager.gitlab.http_post(path, **kwargs) - @cli.register_custom_action("Project", ("to_namespace",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("to_namespace",)) @exc.on_http_error(exc.GitlabTransferProjectError) def transfer(self, to_namespace: Union[int, str], **kwargs: Any) -> None: """Transfer a project to the given namespace ID @@ -550,7 +556,8 @@ def transfer(self, to_namespace: Union[int, str], **kwargs: Any) -> None: path, post_data={"namespace": to_namespace}, **kwargs ) - @cli.register_custom_action("Project", ("to_namespace",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("to_namespace",)) def transfer_project(self, *args: Any, **kwargs: Any) -> None: utils.warn( message=( @@ -561,7 +568,8 @@ def transfer_project(self, *args: Any, **kwargs: Any) -> None: ) return self.transfer(*args, **kwargs) - @cli.register_custom_action("Project", ("ref_name", "artifact_path", "job")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("ref_name", "artifact_path", "job")) @exc.on_http_error(exc.GitlabGetError) def artifact( self, diff --git a/gitlab/v4/objects/repositories.py b/gitlab/v4/objects/repositories.py index 5826d9d83..bd6387900 100644 --- a/gitlab/v4/objects/repositories.py +++ b/gitlab/v4/objects/repositories.py @@ -8,7 +8,7 @@ import requests import gitlab -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import utils @@ -20,7 +20,8 @@ class RepositoryMixin(_RestObjectBase): - @cli.register_custom_action("Project", ("submodule", "branch", "commit_sha")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("submodule", "branch", "commit_sha")) @exc.on_http_error(exc.GitlabUpdateError) def update_submodule( self, submodule: str, branch: str, commit_sha: str, **kwargs: Any @@ -46,7 +47,8 @@ def update_submodule( data["commit_message"] = kwargs["commit_message"] return self.manager.gitlab.http_put(path, post_data=data) - @cli.register_custom_action("Project", (), ("path", "ref", "recursive")) + @cli.register_custom_action("Project") + @base.custom_attrs(optional=("path", "ref", "recursive")) @exc.on_http_error(exc.GitlabGetError) def repository_tree( self, path: str = "", ref: str = "", recursive: bool = False, **kwargs: Any @@ -79,7 +81,8 @@ def repository_tree( query_data["ref"] = ref return self.manager.gitlab.http_list(gl_path, query_data=query_data, **kwargs) - @cli.register_custom_action("Project", ("sha",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("sha",)) @exc.on_http_error(exc.GitlabGetError) def repository_blob( self, sha: str, **kwargs: Any @@ -101,7 +104,8 @@ def repository_blob( path = f"/projects/{self.encoded_id}/repository/blobs/{sha}" return self.manager.gitlab.http_get(path, **kwargs) - @cli.register_custom_action("Project", ("sha",)) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("sha",)) @exc.on_http_error(exc.GitlabGetError) def repository_raw_blob( self, @@ -138,7 +142,8 @@ def repository_raw_blob( assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) - @cli.register_custom_action("Project", ("from_", "to")) + @cli.register_custom_action("Project") + @base.custom_attrs(required=("from_", "to")) @exc.on_http_error(exc.GitlabGetError) def repository_compare( self, from_: str, to: str, **kwargs: Any @@ -186,7 +191,8 @@ def repository_contributors( path = f"/projects/{self.encoded_id}/repository/contributors" return self.manager.gitlab.http_list(path, **kwargs) - @cli.register_custom_action("Project", (), ("sha", "format")) + @cli.register_custom_action("Project") + @base.custom_attrs(optional=("sha", "format")) @exc.on_http_error(exc.GitlabListError) def repository_archive( self, diff --git a/gitlab/v4/objects/runners.py b/gitlab/v4/objects/runners.py index 4f9d7ce57..2841fb85d 100644 --- a/gitlab/v4/objects/runners.py +++ b/gitlab/v4/objects/runners.py @@ -1,6 +1,6 @@ from typing import Any, cast, List, Optional, Union -from gitlab import cli +from gitlab import base, cli from gitlab import exceptions as exc from gitlab import types from gitlab.base import RESTManager, RESTObject @@ -71,7 +71,8 @@ class RunnerManager(CRUDMixin, RESTManager): _list_filters = ("scope", "tag_list") _types = {"tag_list": types.CommaSeparatedListAttribute} - @cli.register_custom_action("RunnerManager", (), ("scope",)) + @cli.register_custom_action("RunnerManager") + @base.custom_attrs(optional=("scope",)) @exc.on_http_error(exc.GitlabListError) def all(self, scope: Optional[str] = None, **kwargs: Any) -> List[Runner]: """List all the runners. @@ -100,7 +101,8 @@ def all(self, scope: Optional[str] = None, **kwargs: Any) -> List[Runner]: obj = self.gitlab.http_list(path, query_data, **kwargs) return [self._obj_cls(self, item) for item in obj] - @cli.register_custom_action("RunnerManager", ("token",)) + @cli.register_custom_action("RunnerManager") + @base.custom_attrs(required=("token",)) @exc.on_http_error(exc.GitlabVerifyError) def verify(self, token: str, **kwargs: Any) -> None: """Validates authentication credentials for a registered Runner.