Skip to content

Commit befba35

Browse files
igorp-collaboranejch
authored andcommitted
feat(api)!: Make RESTObjectList typing generic
BREAKING CHANGE: Type narrowing of `list()` methods return objects from RESTObject to a concrete subclass (for example `MergeRequest`) can become redundant. Currently the RESTObjectList type hints yielded objects as base RESTObject. However, the ListMixin is now generic and will return the RESTObject subclass based on the RESTManager typing. Using `typing.Generic` it is possible to make RESTObjectList type hint a specific subclass of the RESTObject. Iterating over `list()` call the ListMixin will now yield the same object class because both `list` and `RESTObjectList` will have the same type hinted subclass. Signed-off-by: Igor Ponomarev <igor.ponomarev@collabora.com>
1 parent 9040dbe commit befba35

File tree

9 files changed

+44
-33
lines changed

9 files changed

+44
-33
lines changed

gitlab/base.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ def encoded_id(self) -> int | str | None:
252252
return obj_id
253253

254254

255-
class RESTObjectList:
255+
TObjCls = TypeVar("TObjCls", bound=RESTObject)
256+
257+
258+
class RESTObjectList(Generic[TObjCls]):
256259
"""Generator object representing a list of RESTObject's.
257260
258261
This generator uses the Gitlab pagination system to fetch new data when
@@ -268,7 +271,7 @@ class RESTObjectList:
268271
"""
269272

270273
def __init__(
271-
self, manager: RESTManager[Any], obj_cls: type[RESTObject], _list: GitlabList
274+
self, manager: RESTManager[TObjCls], obj_cls: type[TObjCls], _list: GitlabList
272275
) -> None:
273276
"""Creates an objects list from a GitlabList.
274277
@@ -284,16 +287,16 @@ def __init__(
284287
self._obj_cls = obj_cls
285288
self._list = _list
286289

287-
def __iter__(self) -> RESTObjectList:
290+
def __iter__(self) -> RESTObjectList[TObjCls]:
288291
return self
289292

290293
def __len__(self) -> int:
291294
return len(self._list)
292295

293-
def __next__(self) -> RESTObject:
296+
def __next__(self) -> TObjCls:
294297
return self.next()
295298

296-
def next(self) -> RESTObject:
299+
def next(self) -> TObjCls:
297300
data = self._list.next()
298301
return self._obj_cls(self.manager, data, created_from_list=True)
299302

@@ -334,9 +337,6 @@ def total(self) -> int | None:
334337
return self._list.total
335338

336339

337-
TObjCls = TypeVar("TObjCls", bound=RESTObject)
338-
339-
340340
class RESTManager(Generic[TObjCls]):
341341
"""Base class for CRUD operations on objects.
342342

gitlab/mixins.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ class ListMixin(HeadMixin[base.TObjCls]):
162162
_list_filters: tuple[str, ...] = ()
163163

164164
@exc.on_http_error(exc.GitlabListError)
165-
def list(self, **kwargs: Any) -> base.RESTObjectList | list[base.TObjCls]:
165+
def list(
166+
self, **kwargs: Any
167+
) -> base.RESTObjectList[base.TObjCls] | list[base.TObjCls]:
166168
"""Retrieve a list of objects.
167169
168170
Args:

gitlab/v4/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ def do_create(self) -> gitlab.base.RESTObject:
133133
cli.die("Impossible to create object", e)
134134
return result
135135

136-
def do_list(self) -> gitlab.base.RESTObjectList | list[gitlab.base.RESTObject]:
136+
def do_list(
137+
self,
138+
) -> (
139+
gitlab.base.RESTObjectList[gitlab.base.RESTObject]
140+
| list[gitlab.base.RESTObject]
141+
):
137142
if TYPE_CHECKING:
138143
assert isinstance(self.mgr, gitlab.mixins.ListMixin)
139144
message_details = gitlab.utils.WarnMessageData(

gitlab/v4/objects/ldap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class LDAPGroupManager(RESTManager[LDAPGroup]):
1818
_list_filters = ("search", "provider")
1919

2020
@exc.on_http_error(exc.GitlabListError)
21-
def list(self, **kwargs: Any) -> list[LDAPGroup] | RESTObjectList:
21+
def list(self, **kwargs: Any) -> list[LDAPGroup] | RESTObjectList[LDAPGroup]:
2222
"""Retrieve a list of objects.
2323
2424
Args:

gitlab/v4/objects/merge_requests.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def cancel_merge_when_pipeline_succeeds(self, **kwargs: Any) -> dict[str, str]:
203203

204204
@cli.register_custom_action(cls_names="ProjectMergeRequest")
205205
@exc.on_http_error(exc.GitlabListError)
206-
def related_issues(self, **kwargs: Any) -> RESTObjectList:
206+
def related_issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
207207
"""List issues related to this merge request."
208208
209209
Args:
@@ -232,7 +232,7 @@ def related_issues(self, **kwargs: Any) -> RESTObjectList:
232232

233233
@cli.register_custom_action(cls_names="ProjectMergeRequest")
234234
@exc.on_http_error(exc.GitlabListError)
235-
def closes_issues(self, **kwargs: Any) -> RESTObjectList:
235+
def closes_issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
236236
"""List issues that will close on merge."
237237
238238
Args:
@@ -257,7 +257,7 @@ def closes_issues(self, **kwargs: Any) -> RESTObjectList:
257257

258258
@cli.register_custom_action(cls_names="ProjectMergeRequest")
259259
@exc.on_http_error(exc.GitlabListError)
260-
def commits(self, **kwargs: Any) -> RESTObjectList:
260+
def commits(self, **kwargs: Any) -> RESTObjectList[ProjectCommit]:
261261
"""List the merge request commits.
262262
263263
Args:

gitlab/v4/objects/milestones.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from gitlab import exceptions as exc
55
from gitlab import types
66
from gitlab.base import RESTObject, RESTObjectList
7+
from gitlab.client import GitlabList
78
from gitlab.mixins import (
89
CRUDMixin,
910
ObjectDeleteMixin,
@@ -16,6 +17,7 @@
1617
from .issues import GroupIssue, GroupIssueManager, ProjectIssue, ProjectIssueManager
1718
from .merge_requests import (
1819
GroupMergeRequest,
20+
GroupMergeRequestManager,
1921
ProjectMergeRequest,
2022
ProjectMergeRequestManager,
2123
)
@@ -33,7 +35,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):
3335

3436
@cli.register_custom_action(cls_names="GroupMilestone")
3537
@exc.on_http_error(exc.GitlabListError)
36-
def issues(self, **kwargs: Any) -> RESTObjectList:
38+
def issues(self, **kwargs: Any) -> RESTObjectList[GroupIssue]:
3739
"""List issues related to this milestone.
3840
3941
Args:
@@ -53,14 +55,14 @@ def issues(self, **kwargs: Any) -> RESTObjectList:
5355
path = f"{self.manager.path}/{self.encoded_id}/issues"
5456
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
5557
if TYPE_CHECKING:
56-
assert isinstance(data_list, RESTObjectList)
58+
assert isinstance(data_list, GitlabList)
5759
manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent)
5860
# FIXME(gpocentek): the computed manager path is not correct
5961
return RESTObjectList(manager, GroupIssue, data_list)
6062

6163
@cli.register_custom_action(cls_names="GroupMilestone")
6264
@exc.on_http_error(exc.GitlabListError)
63-
def merge_requests(self, **kwargs: Any) -> RESTObjectList:
65+
def merge_requests(self, **kwargs: Any) -> RESTObjectList[GroupMergeRequest]:
6466
"""List the merge requests related to this milestone.
6567
6668
Args:
@@ -79,8 +81,10 @@ def merge_requests(self, **kwargs: Any) -> RESTObjectList:
7981
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
8082
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
8183
if TYPE_CHECKING:
82-
assert isinstance(data_list, RESTObjectList)
83-
manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent)
84+
assert isinstance(data_list, GitlabList)
85+
manager = GroupMergeRequestManager(
86+
self.manager.gitlab, parent=self.manager._parent
87+
)
8488
# FIXME(gpocentek): the computed manager path is not correct
8589
return RESTObjectList(manager, GroupMergeRequest, data_list)
8690

@@ -105,7 +109,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
105109

106110
@cli.register_custom_action(cls_names="ProjectMilestone")
107111
@exc.on_http_error(exc.GitlabListError)
108-
def issues(self, **kwargs: Any) -> RESTObjectList:
112+
def issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
109113
"""List issues related to this milestone.
110114
111115
Args:
@@ -125,14 +129,14 @@ def issues(self, **kwargs: Any) -> RESTObjectList:
125129
path = f"{self.manager.path}/{self.encoded_id}/issues"
126130
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
127131
if TYPE_CHECKING:
128-
assert isinstance(data_list, RESTObjectList)
132+
assert isinstance(data_list, GitlabList)
129133
manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent)
130134
# FIXME(gpocentek): the computed manager path is not correct
131135
return RESTObjectList(manager, ProjectIssue, data_list)
132136

133137
@cli.register_custom_action(cls_names="ProjectMilestone")
134138
@exc.on_http_error(exc.GitlabListError)
135-
def merge_requests(self, **kwargs: Any) -> RESTObjectList:
139+
def merge_requests(self, **kwargs: Any) -> RESTObjectList[ProjectMergeRequest]:
136140
"""List the merge requests related to this milestone.
137141
138142
Args:
@@ -151,7 +155,7 @@ def merge_requests(self, **kwargs: Any) -> RESTObjectList:
151155
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
152156
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
153157
if TYPE_CHECKING:
154-
assert isinstance(data_list, RESTObjectList)
158+
assert isinstance(data_list, GitlabList)
155159
manager = ProjectMergeRequestManager(
156160
self.manager.gitlab, parent=self.manager._parent
157161
)

gitlab/v4/objects/snippets.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class SnippetManager(CRUDMixin[Snippet]):
109109
)
110110

111111
@cli.register_custom_action(cls_names="SnippetManager")
112-
def list_public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
112+
def list_public(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
113113
"""List all public snippets.
114114
115115
Args:
@@ -129,7 +129,7 @@ def list_public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
129129
return self.list(path="/snippets/public", **kwargs)
130130

131131
@cli.register_custom_action(cls_names="SnippetManager")
132-
def list_all(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
132+
def list_all(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
133133
"""List all snippets.
134134
135135
Args:
@@ -148,7 +148,7 @@ def list_all(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
148148
"""
149149
return self.list(path="/snippets/all", **kwargs)
150150

151-
def public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
151+
def public(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
152152
"""List all public snippets.
153153
154154
Args:

gitlab/v4/objects/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ class UserProjectManager(ListMixin[UserProject], CreateMixin[UserProject]):
623623
"id_before",
624624
)
625625

626-
def list(self, **kwargs: Any) -> RESTObjectList | list[UserProject]:
626+
def list(self, **kwargs: Any) -> RESTObjectList[UserProject] | list[UserProject]:
627627
"""Retrieve a list of objects.
628628
629629
Args:

tests/functional/conftest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
8787
settings.save()
8888

8989
for project in gl.projects.list():
90-
for deploy_token in project.deploytokens.list():
90+
for project_deploy_token in project.deploytokens.list():
9191
logging.info(
92-
f"Deleting deploy token: {deploy_token.username!r} in "
92+
f"Deleting deploy token: {project_deploy_token.username!r} in "
9393
f"project: {project.path_with_namespace!r}"
9494
)
95-
helpers.safe_delete(deploy_token)
95+
helpers.safe_delete(project_deploy_token)
9696
logging.info(f"Deleting project: {project.path_with_namespace!r}")
9797
helpers.safe_delete(project)
9898

@@ -106,12 +106,12 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
106106
)
107107
continue
108108

109-
for deploy_token in group.deploytokens.list():
109+
for group_deploy_token in group.deploytokens.list():
110110
logging.info(
111-
f"Deleting deploy token: {deploy_token.username!r} in "
111+
f"Deleting deploy token: {group_deploy_token.username!r} in "
112112
f"group: {group.path_with_namespace!r}"
113113
)
114-
helpers.safe_delete(deploy_token)
114+
helpers.safe_delete(group_deploy_token)
115115
logging.info(f"Deleting group: {group.full_path!r}")
116116
helpers.safe_delete(group)
117117
for topic in gl.topics.list():

0 commit comments

Comments
 (0)