Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5628004

Browse files
committed
refactor: decouple CLI from custom method arguments
1 parent 0e3c461 commit 5628004

17 files changed

+159
-114
lines changed

gitlab/base.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
import pprint
2020
import textwrap
2121
from types import ModuleType
22-
from typing import Any, Dict, Iterable, Optional, Type, Union
22+
from typing import Any, Callable, Dict, Iterable, Optional, Type, Union
2323

2424
import gitlab
2525
from gitlab import types as g_types
2626
from gitlab.exceptions import GitlabParsingError
27+
from gitlab.types import F
2728

2829
from .client import Gitlab, GitlabList
2930

@@ -328,6 +329,28 @@ def total(self) -> Optional[int]:
328329
return self._list.total
329330

330331

332+
def custom_attrs(
333+
required: tuple = (), optional: tuple = (), exclusive: tuple = ()
334+
) -> Callable[[F], F]:
335+
"""Decorates a custom method to add a RequiredOptional attribute.
336+
337+
Args:
338+
required: A tuple of API attributes required in the custom method
339+
optional: A tuple of API attributes optional in the custom method
340+
exclusive: A tuple of mutually exclusive API attributes in the custom method
341+
"""
342+
343+
def decorator(func: F) -> F:
344+
setattr(
345+
func,
346+
"_custom_attrs",
347+
g_types.RequiredOptional(required, optional, exclusive),
348+
)
349+
return func
350+
351+
return decorator
352+
353+
331354
class RESTManager:
332355
"""Base class for CRUD operations on objects.
333356

gitlab/cli.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@
2323
import re
2424
import sys
2525
from types import ModuleType
26-
from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, TypeVar, Union
26+
from typing import Any, Callable, cast, Dict, Optional, Tuple, Type, Union
2727

2828
from requests.structures import CaseInsensitiveDict
2929

3030
import gitlab.config
3131
from gitlab.base import RESTObject
32+
from gitlab.types import F, RequiredOptional
3233

3334
# This regex is based on:
3435
# https://github.com/jpvanhal/inflection/blob/master/inflection/__init__.py
@@ -43,24 +44,18 @@
4344
custom_actions: Dict[str, Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...], bool]]] = {}
4445

4546

46-
# For an explanation of how these type-hints work see:
47-
# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
48-
#
49-
# The goal here is that functions which get decorated will retain their types.
50-
__F = TypeVar("__F", bound=Callable[..., Any])
51-
52-
5347
def register_custom_action(
5448
cls_names: Union[str, Tuple[str, ...]],
55-
mandatory: Tuple[str, ...] = (),
56-
optional: Tuple[str, ...] = (),
5749
custom_action: Optional[str] = None,
58-
) -> Callable[[__F], __F]:
59-
def wrap(f: __F) -> __F:
50+
) -> Callable[[F], F]:
51+
def wrap(f: F) -> F:
6052
@functools.wraps(f)
6153
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
6254
return f(*args, **kwargs)
6355

56+
action = custom_action or f.__name__.replace("_", "-")
57+
custom_attrs = getattr(f, "_custom_attrs", RequiredOptional())
58+
6459
# in_obj defines whether the method belongs to the obj or the manager
6560
in_obj = True
6661
if isinstance(cls_names, tuple):
@@ -76,10 +71,13 @@ def wrapped_f(*args: Any, **kwargs: Any) -> Any:
7671
if final_name not in custom_actions:
7772
custom_actions[final_name] = {}
7873

79-
action = custom_action or f.__name__.replace("_", "-")
80-
custom_actions[final_name][action] = (mandatory, optional, in_obj)
74+
custom_actions[final_name][action] = (
75+
custom_attrs.required,
76+
custom_attrs.optional,
77+
in_obj,
78+
)
8179

82-
return cast(__F, wrapped_f)
80+
return cast(F, wrapped_f)
8381

8482
return wrap
8583

gitlab/exceptions.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import functools
19-
from typing import Any, Callable, cast, Optional, Type, TYPE_CHECKING, TypeVar, Union
19+
from typing import Any, Callable, cast, Optional, Type, TYPE_CHECKING, Union
20+
21+
from gitlab.types import F
2022

2123

2224
class GitlabError(Exception):
@@ -286,14 +288,7 @@ class GitlabUnfollowError(GitlabOperationError):
286288
pass
287289

288290

289-
# For an explanation of how these type-hints work see:
290-
# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
291-
#
292-
# The goal here is that functions which get decorated will retain their types.
293-
__F = TypeVar("__F", bound=Callable[..., Any])
294-
295-
296-
def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]:
291+
def on_http_error(error: Type[Exception]) -> Callable[[F], F]:
297292
"""Manage GitlabHttpError exceptions.
298293
299294
This decorator function can be used to catch GitlabHttpError exceptions
@@ -303,14 +298,14 @@ def on_http_error(error: Type[Exception]) -> Callable[[__F], __F]:
303298
The exception type to raise -- must inherit from GitlabError
304299
"""
305300

306-
def wrap(f: __F) -> __F:
301+
def wrap(f: F) -> F:
307302
@functools.wraps(f)
308303
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
309304
try:
310305
return f(*args, **kwargs)
311306
except GitlabHttpError as e:
312307
raise error(e.error_message, e.response_code, e.response_body) from e
313308

314-
return cast(__F, wrapped_f)
309+
return cast(F, wrapped_f)
315310

316311
return wrap

gitlab/mixins.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -547,9 +547,8 @@ class AccessRequestMixin(_RestObjectBase):
547547
_updated_attrs: Dict[str, Any]
548548
manager: base.RESTManager
549549

550-
@cli.register_custom_action(
551-
("ProjectAccessRequest", "GroupAccessRequest"), (), ("access_level",)
552-
)
550+
@cli.register_custom_action(("ProjectAccessRequest", "GroupAccessRequest"))
551+
@base.custom_attrs(optional=("access_level",))
553552
@exc.on_http_error(exc.GitlabUpdateError)
554553
def approve(
555554
self, access_level: int = gitlab.const.DEVELOPER_ACCESS, **kwargs: Any
@@ -721,7 +720,8 @@ def time_stats(self, **kwargs: Any) -> Dict[str, Any]:
721720
assert not isinstance(result, requests.Response)
722721
return result
723722

724-
@cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",))
723+
@cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
724+
@base.custom_attrs(required=("duration",))
725725
@exc.on_http_error(exc.GitlabTimeTrackingError)
726726
def time_estimate(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
727727
"""Set an estimated time of work for the object.
@@ -759,7 +759,8 @@ def reset_time_estimate(self, **kwargs: Any) -> Dict[str, Any]:
759759
assert not isinstance(result, requests.Response)
760760
return result
761761

762-
@cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"), ("duration",))
762+
@cli.register_custom_action(("ProjectIssue", "ProjectMergeRequest"))
763+
@base.custom_attrs(required=("duration",))
763764
@exc.on_http_error(exc.GitlabTimeTrackingError)
764765
def add_spent_time(self, duration: str, **kwargs: Any) -> Dict[str, Any]:
765766
"""Add time spent working on the object.
@@ -833,9 +834,8 @@ def participants(self, **kwargs: Any) -> Dict[str, Any]:
833834

834835

835836
class BadgeRenderMixin(_RestManagerBase):
836-
@cli.register_custom_action(
837-
("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url")
838-
)
837+
@cli.register_custom_action(("GroupBadgeManager", "ProjectBadgeManager"))
838+
@base.custom_attrs(required=("link_url", "image_url"))
839839
@exc.on_http_error(exc.GitlabRenderError)
840840
def render(self, link_url: str, image_url: str, **kwargs: Any) -> Dict[str, Any]:
841841
"""Preview link_url and image_url after interpolation.

gitlab/types.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import dataclasses
19-
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
19+
from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, TypeVar
20+
21+
# TypeVar for decorators so that decorated functions retain their signatures.
22+
# See https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
23+
F = TypeVar("F", bound=Callable[..., Any])
2024

2125

2226
@dataclasses.dataclass(frozen=True)

gitlab/v4/objects/artifacts.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from gitlab import cli
1010
from gitlab import exceptions as exc
1111
from gitlab import utils
12-
from gitlab.base import RESTManager, RESTObject
12+
from gitlab.base import custom_attrs, RESTManager, RESTObject
1313

1414
__all__ = ["ProjectArtifact", "ProjectArtifactManager"]
1515

@@ -25,9 +25,8 @@ class ProjectArtifactManager(RESTManager):
2525
_path = "/projects/{project_id}/jobs/artifacts"
2626
_from_parent_attrs = {"project_id": "id"}
2727

28-
@cli.register_custom_action(
29-
"Project", ("ref_name", "job"), ("job_token",), custom_action="artifacts"
30-
)
28+
@cli.register_custom_action("Project", custom_action="artifacts")
29+
@custom_attrs(required=("ref_name", "job"), optional=("job_token",))
3130
def __call__(
3231
self,
3332
*args: Any,
@@ -62,9 +61,8 @@ def delete(self, **kwargs: Any) -> None:
6261
assert path is not None
6362
self.gitlab.http_delete(path, **kwargs)
6463

65-
@cli.register_custom_action(
66-
"ProjectArtifactManager", ("ref_name", "job"), ("job_token",)
67-
)
64+
@cli.register_custom_action("ProjectArtifactManager")
65+
@custom_attrs(required=("ref_name", "job"), optional=("job_token",))
6866
@exc.on_http_error(exc.GitlabGetError)
6967
def download(
7068
self,
@@ -105,9 +103,8 @@ def download(
105103
assert isinstance(result, requests.Response)
106104
return utils.response_content(result, streamed, action, chunk_size)
107105

108-
@cli.register_custom_action(
109-
"ProjectArtifactManager", ("ref_name", "artifact_path", "job")
110-
)
106+
@cli.register_custom_action("ProjectArtifactManager")
107+
@custom_attrs(required=("ref_name", "artifact_path", "job"))
111108
@exc.on_http_error(exc.GitlabGetError)
112109
def raw(
113110
self,

gitlab/v4/objects/commits.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import requests
44

55
import gitlab
6-
from gitlab import cli
6+
from gitlab import base, cli
77
from gitlab import exceptions as exc
88
from gitlab.base import RESTManager, RESTObject
99
from gitlab.mixins import CreateMixin, ListMixin, RefreshMixin, RetrieveMixin
@@ -46,7 +46,8 @@ def diff(self, **kwargs: Any) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]:
4646
path = f"{self.manager.path}/{self.encoded_id}/diff"
4747
return self.manager.gitlab.http_list(path, **kwargs)
4848

49-
@cli.register_custom_action("ProjectCommit", ("branch",))
49+
@cli.register_custom_action("ProjectCommit")
50+
@base.custom_attrs(required=("branch",))
5051
@exc.on_http_error(exc.GitlabCherryPickError)
5152
def cherry_pick(self, branch: str, **kwargs: Any) -> None:
5253
"""Cherry-pick a commit into a branch.
@@ -63,7 +64,8 @@ def cherry_pick(self, branch: str, **kwargs: Any) -> None:
6364
post_data = {"branch": branch}
6465
self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
6566

66-
@cli.register_custom_action("ProjectCommit", optional=("type",))
67+
@cli.register_custom_action("ProjectCommit")
68+
@base.custom_attrs(optional=("type",))
6769
@exc.on_http_error(exc.GitlabGetError)
6870
def refs(
6971
self, type: str = "all", **kwargs: Any
@@ -105,7 +107,8 @@ def merge_requests(
105107
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
106108
return self.manager.gitlab.http_list(path, **kwargs)
107109

108-
@cli.register_custom_action("ProjectCommit", ("branch",))
110+
@cli.register_custom_action("ProjectCommit")
111+
@base.custom_attrs(required=("branch",))
109112
@exc.on_http_error(exc.GitlabRevertError)
110113
def revert(
111114
self, branch: str, **kwargs: Any

gitlab/v4/objects/container_registry.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any, cast, TYPE_CHECKING, Union
22

3-
from gitlab import cli
3+
from gitlab import base, cli
44
from gitlab import exceptions as exc
55
from gitlab.base import RESTManager, RESTObject
66
from gitlab.mixins import DeleteMixin, ListMixin, ObjectDeleteMixin, RetrieveMixin
@@ -32,9 +32,9 @@ class ProjectRegistryTagManager(DeleteMixin, RetrieveMixin, RESTManager):
3232
_from_parent_attrs = {"project_id": "project_id", "repository_id": "id"}
3333
_path = "/projects/{project_id}/registry/repositories/{repository_id}/tags"
3434

35-
@cli.register_custom_action(
36-
"ProjectRegistryTagManager",
37-
("name_regex_delete",),
35+
@cli.register_custom_action("ProjectRegistryTagManager")
36+
@base.custom_attrs(
37+
required=("name_regex_delete",),
3838
optional=("keep_n", "name_regex_keep", "older_than"),
3939
)
4040
@exc.on_http_error(exc.GitlabDeleteError)

gitlab/v4/objects/deploy_keys.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import requests
44

5-
from gitlab import cli
5+
from gitlab import base, cli
66
from gitlab import exceptions as exc
77
from gitlab.base import RESTManager, RESTObject
88
from gitlab.mixins import CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin
@@ -36,7 +36,8 @@ class ProjectKeyManager(CRUDMixin, RESTManager):
3636
_create_attrs = RequiredOptional(required=("title", "key"), optional=("can_push",))
3737
_update_attrs = RequiredOptional(optional=("title", "can_push"))
3838

39-
@cli.register_custom_action("ProjectKeyManager", ("key_id",))
39+
@cli.register_custom_action("ProjectKeyManager")
40+
@base.custom_attrs(required=("key_id",))
4041
@exc.on_http_error(exc.GitlabProjectDeployKeyError)
4142
def enable(
4243
self, key_id: int, **kwargs: Any

gitlab/v4/objects/files.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import requests
55

6-
from gitlab import cli
6+
from gitlab import base, cli
77
from gitlab import exceptions as exc
88
from gitlab import utils
99
from gitlab.base import RESTManager, RESTObject
@@ -98,7 +98,8 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa
9898
optional=("encoding", "author_email", "author_name"),
9999
)
100100

101-
@cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
101+
@cli.register_custom_action("ProjectFileManager")
102+
@base.custom_attrs(required=("file_path", "ref"))
102103
# NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore
103104
# type error
104105
def get( # type: ignore
@@ -120,10 +121,10 @@ def get( # type: ignore
120121
"""
121122
return cast(ProjectFile, GetMixin.get(self, file_path, ref=ref, **kwargs))
122123

123-
@cli.register_custom_action(
124-
"ProjectFileManager",
125-
("file_path", "branch", "content", "commit_message"),
126-
("encoding", "author_email", "author_name"),
124+
@cli.register_custom_action("ProjectFileManager")
125+
@base.custom_attrs(
126+
required=("file_path", "branch", "content", "commit_message"),
127+
optional=("encoding", "author_email", "author_name"),
127128
)
128129
@exc.on_http_error(exc.GitlabCreateError)
129130
def create(
@@ -187,9 +188,8 @@ def update( # type: ignore
187188
assert isinstance(result, dict)
188189
return result
189190

190-
@cli.register_custom_action(
191-
"ProjectFileManager", ("file_path", "branch", "commit_message")
192-
)
191+
@cli.register_custom_action("ProjectFileManager")
192+
@base.custom_attrs(required=("file_path", "branch", "commit_message"))
193193
@exc.on_http_error(exc.GitlabDeleteError)
194194
# NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore
195195
# type error
@@ -213,7 +213,8 @@ def delete( # type: ignore
213213
data = {"branch": branch, "commit_message": commit_message}
214214
self.gitlab.http_delete(path, query_data=data, **kwargs)
215215

216-
@cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
216+
@cli.register_custom_action("ProjectFileManager")
217+
@base.custom_attrs(required=("file_path", "ref"))
217218
@exc.on_http_error(exc.GitlabGetError)
218219
def raw(
219220
self,
@@ -254,7 +255,8 @@ def raw(
254255
assert isinstance(result, requests.Response)
255256
return utils.response_content(result, streamed, action, chunk_size)
256257

257-
@cli.register_custom_action("ProjectFileManager", ("file_path", "ref"))
258+
@cli.register_custom_action("ProjectFileManager")
259+
@base.custom_attrs(required=("file_path", "ref"))
258260
@exc.on_http_error(exc.GitlabListError)
259261
def blame(self, file_path: str, ref: str, **kwargs: Any) -> List[Dict[str, Any]]:
260262
"""Return the content of a file for a commit.

0 commit comments

Comments
 (0)