Skip to content

feat(api)!: Make RESTObjectList typing generic #3121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions gitlab/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ def encoded_id(self) -> int | str | None:
return obj_id


class RESTObjectList:
TObjCls = TypeVar("TObjCls", bound=RESTObject)


class RESTObjectList(Generic[TObjCls]):
"""Generator object representing a list of RESTObject's.

This generator uses the Gitlab pagination system to fetch new data when
Expand All @@ -268,7 +271,7 @@ class RESTObjectList:
"""

def __init__(
self, manager: RESTManager[Any], obj_cls: type[RESTObject], _list: GitlabList
self, manager: RESTManager[TObjCls], obj_cls: type[TObjCls], _list: GitlabList
) -> None:
"""Creates an objects list from a GitlabList.

Expand All @@ -284,16 +287,16 @@ def __init__(
self._obj_cls = obj_cls
self._list = _list

def __iter__(self) -> RESTObjectList:
def __iter__(self) -> RESTObjectList[TObjCls]:
return self

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

def __next__(self) -> RESTObject:
def __next__(self) -> TObjCls:
return self.next()

def next(self) -> RESTObject:
def next(self) -> TObjCls:
data = self._list.next()
return self._obj_cls(self.manager, data, created_from_list=True)

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


TObjCls = TypeVar("TObjCls", bound=RESTObject)


class RESTManager(Generic[TObjCls]):
"""Base class for CRUD operations on objects.

Expand Down
4 changes: 3 additions & 1 deletion gitlab/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ class ListMixin(HeadMixin[base.TObjCls]):
_list_filters: tuple[str, ...] = ()

@exc.on_http_error(exc.GitlabListError)
def list(self, **kwargs: Any) -> base.RESTObjectList | list[base.TObjCls]:
def list(
self, **kwargs: Any
) -> base.RESTObjectList[base.TObjCls] | list[base.TObjCls]:
"""Retrieve a list of objects.

Args:
Expand Down
7 changes: 6 additions & 1 deletion gitlab/v4/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ def do_create(self) -> gitlab.base.RESTObject:
cli.die("Impossible to create object", e)
return result

def do_list(self) -> gitlab.base.RESTObjectList | list[gitlab.base.RESTObject]:
def do_list(
self,
) -> (
gitlab.base.RESTObjectList[gitlab.base.RESTObject]
| list[gitlab.base.RESTObject]
):
if TYPE_CHECKING:
assert isinstance(self.mgr, gitlab.mixins.ListMixin)
message_details = gitlab.utils.WarnMessageData(
Expand Down
2 changes: 1 addition & 1 deletion gitlab/v4/objects/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class LDAPGroupManager(RESTManager[LDAPGroup]):
_list_filters = ("search", "provider")

@exc.on_http_error(exc.GitlabListError)
def list(self, **kwargs: Any) -> list[LDAPGroup] | RESTObjectList:
def list(self, **kwargs: Any) -> list[LDAPGroup] | RESTObjectList[LDAPGroup]:
"""Retrieve a list of objects.

Args:
Expand Down
6 changes: 3 additions & 3 deletions gitlab/v4/objects/merge_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def cancel_merge_when_pipeline_succeeds(self, **kwargs: Any) -> dict[str, str]:

@cli.register_custom_action(cls_names="ProjectMergeRequest")
@exc.on_http_error(exc.GitlabListError)
def related_issues(self, **kwargs: Any) -> RESTObjectList:
def related_issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
"""List issues related to this merge request."

Args:
Expand Down Expand Up @@ -232,7 +232,7 @@ def related_issues(self, **kwargs: Any) -> RESTObjectList:

@cli.register_custom_action(cls_names="ProjectMergeRequest")
@exc.on_http_error(exc.GitlabListError)
def closes_issues(self, **kwargs: Any) -> RESTObjectList:
def closes_issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
"""List issues that will close on merge."

Args:
Expand All @@ -257,7 +257,7 @@ def closes_issues(self, **kwargs: Any) -> RESTObjectList:

@cli.register_custom_action(cls_names="ProjectMergeRequest")
@exc.on_http_error(exc.GitlabListError)
def commits(self, **kwargs: Any) -> RESTObjectList:
def commits(self, **kwargs: Any) -> RESTObjectList[ProjectCommit]:
"""List the merge request commits.

Args:
Expand Down
22 changes: 13 additions & 9 deletions gitlab/v4/objects/milestones.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from gitlab import exceptions as exc
from gitlab import types
from gitlab.base import RESTObject, RESTObjectList
from gitlab.client import GitlabList
from gitlab.mixins import (
CRUDMixin,
ObjectDeleteMixin,
Expand All @@ -16,6 +17,7 @@
from .issues import GroupIssue, GroupIssueManager, ProjectIssue, ProjectIssueManager
from .merge_requests import (
GroupMergeRequest,
GroupMergeRequestManager,
ProjectMergeRequest,
ProjectMergeRequestManager,
)
Expand All @@ -33,7 +35,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject):

@cli.register_custom_action(cls_names="GroupMilestone")
@exc.on_http_error(exc.GitlabListError)
def issues(self, **kwargs: Any) -> RESTObjectList:
def issues(self, **kwargs: Any) -> RESTObjectList[GroupIssue]:
"""List issues related to this milestone.

Args:
Expand All @@ -53,14 +55,14 @@ def issues(self, **kwargs: Any) -> RESTObjectList:
path = f"{self.manager.path}/{self.encoded_id}/issues"
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
assert isinstance(data_list, GitlabList)
manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent)
# FIXME(gpocentek): the computed manager path is not correct
return RESTObjectList(manager, GroupIssue, data_list)

@cli.register_custom_action(cls_names="GroupMilestone")
@exc.on_http_error(exc.GitlabListError)
def merge_requests(self, **kwargs: Any) -> RESTObjectList:
def merge_requests(self, **kwargs: Any) -> RESTObjectList[GroupMergeRequest]:
"""List the merge requests related to this milestone.

Args:
Expand All @@ -79,8 +81,10 @@ def merge_requests(self, **kwargs: Any) -> RESTObjectList:
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent)
assert isinstance(data_list, GitlabList)
manager = GroupMergeRequestManager(
self.manager.gitlab, parent=self.manager._parent
)
# FIXME(gpocentek): the computed manager path is not correct
return RESTObjectList(manager, GroupMergeRequest, data_list)

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

@cli.register_custom_action(cls_names="ProjectMilestone")
@exc.on_http_error(exc.GitlabListError)
def issues(self, **kwargs: Any) -> RESTObjectList:
def issues(self, **kwargs: Any) -> RESTObjectList[ProjectIssue]:
"""List issues related to this milestone.

Args:
Expand All @@ -125,14 +129,14 @@ def issues(self, **kwargs: Any) -> RESTObjectList:
path = f"{self.manager.path}/{self.encoded_id}/issues"
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
assert isinstance(data_list, GitlabList)
manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent)
# FIXME(gpocentek): the computed manager path is not correct
return RESTObjectList(manager, ProjectIssue, data_list)

@cli.register_custom_action(cls_names="ProjectMilestone")
@exc.on_http_error(exc.GitlabListError)
def merge_requests(self, **kwargs: Any) -> RESTObjectList:
def merge_requests(self, **kwargs: Any) -> RESTObjectList[ProjectMergeRequest]:
"""List the merge requests related to this milestone.

Args:
Expand All @@ -151,7 +155,7 @@ def merge_requests(self, **kwargs: Any) -> RESTObjectList:
path = f"{self.manager.path}/{self.encoded_id}/merge_requests"
data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs)
if TYPE_CHECKING:
assert isinstance(data_list, RESTObjectList)
assert isinstance(data_list, GitlabList)
manager = ProjectMergeRequestManager(
self.manager.gitlab, parent=self.manager._parent
)
Expand Down
6 changes: 3 additions & 3 deletions gitlab/v4/objects/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class SnippetManager(CRUDMixin[Snippet]):
)

@cli.register_custom_action(cls_names="SnippetManager")
def list_public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
def list_public(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
"""List all public snippets.

Args:
Expand All @@ -129,7 +129,7 @@ def list_public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
return self.list(path="/snippets/public", **kwargs)

@cli.register_custom_action(cls_names="SnippetManager")
def list_all(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
def list_all(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
"""List all snippets.

Args:
Expand All @@ -148,7 +148,7 @@ def list_all(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
"""
return self.list(path="/snippets/all", **kwargs)

def public(self, **kwargs: Any) -> RESTObjectList | list[Snippet]:
def public(self, **kwargs: Any) -> RESTObjectList[Snippet] | list[Snippet]:
"""List all public snippets.

Args:
Expand Down
2 changes: 1 addition & 1 deletion gitlab/v4/objects/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ class UserProjectManager(ListMixin[UserProject], CreateMixin[UserProject]):
"id_before",
)

def list(self, **kwargs: Any) -> RESTObjectList | list[UserProject]:
def list(self, **kwargs: Any) -> RESTObjectList[UserProject] | list[UserProject]:
"""Retrieve a list of objects.

Args:
Expand Down
12 changes: 6 additions & 6 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
settings.save()

for project in gl.projects.list():
for deploy_token in project.deploytokens.list():
for project_deploy_token in project.deploytokens.list():
logging.info(
f"Deleting deploy token: {deploy_token.username!r} in "
f"Deleting deploy token: {project_deploy_token.username!r} in "
f"project: {project.path_with_namespace!r}"
)
helpers.safe_delete(deploy_token)
helpers.safe_delete(project_deploy_token)
logging.info(f"Deleting project: {project.path_with_namespace!r}")
helpers.safe_delete(project)

Expand All @@ -106,12 +106,12 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
)
continue

for deploy_token in group.deploytokens.list():
for group_deploy_token in group.deploytokens.list():
logging.info(
f"Deleting deploy token: {deploy_token.username!r} in "
f"Deleting deploy token: {group_deploy_token.username!r} in "
f"group: {group.path_with_namespace!r}"
)
helpers.safe_delete(deploy_token)
helpers.safe_delete(group_deploy_token)
logging.info(f"Deleting group: {group.full_path!r}")
helpers.safe_delete(group)
for topic in gl.topics.list():
Expand Down