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.