diff --git a/gitlab/base.py b/gitlab/base.py index 6e7d5c584..01f8c8264 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -19,7 +19,17 @@ import pprint import textwrap from types import ModuleType -from typing import Any, Dict, Iterable, Optional, Type, Union +from typing import ( + Any, + Dict, + Generic, + Iterable, + Iterator, + Optional, + Type, + TypeVar, + Union, +) import gitlab from gitlab import types as g_types @@ -246,7 +256,10 @@ def attributes(self) -> Dict[str, Any]: return d -class RESTObjectList: +T = TypeVar("T", bound=RESTObject) + + +class RESTObjectList(Generic[T], Iterable[T]): """Generator object representing a list of RESTObject's. This generator uses the Gitlab pagination system to fetch new data when @@ -262,7 +275,7 @@ class RESTObjectList: """ def __init__( - self, manager: "RESTManager", obj_cls: Type[RESTObject], _list: GitlabList + self, manager: "RESTManager", obj_cls: Type[T], _list: GitlabList ) -> None: """Creates an objects list from a GitlabList. @@ -278,16 +291,16 @@ def __init__( self._obj_cls = obj_cls self._list = _list - def __iter__(self) -> "RESTObjectList": + def __iter__(self) -> Iterator[T]: return self def __len__(self) -> int: return len(self._list) - def __next__(self) -> RESTObject: + def __next__(self) -> T: return self.next() - def next(self) -> RESTObject: + def next(self) -> T: data = self._list.next() return self._obj_cls(self.manager, data, created_from_list=True) @@ -328,7 +341,11 @@ def total(self) -> Optional[int]: return self._list.total -class RESTManager: +T_obj = TypeVar("T_obj", bound=RESTObject) +T_parent = TypeVar("T_parent", bound=Optional[RESTObject]) + + +class RESTManager(Generic[T_obj, T_parent]): """Base class for CRUD operations on objects. Derived class must define ``_path`` and ``_obj_cls``. @@ -340,16 +357,16 @@ class RESTManager: _create_attrs: g_types.RequiredOptional = g_types.RequiredOptional() _update_attrs: g_types.RequiredOptional = g_types.RequiredOptional() _path: Optional[str] = None - _obj_cls: Optional[Type[RESTObject]] = None + _obj_cls: Type[T_obj] _from_parent_attrs: Dict[str, Any] = {} _types: Dict[str, Type[g_types.GitlabAttribute]] = {} _computed_path: Optional[str] - _parent: Optional[RESTObject] + _parent: Optional[T_parent] _parent_attrs: Dict[str, Any] gitlab: Gitlab - def __init__(self, gl: Gitlab, parent: Optional[RESTObject] = None) -> None: + def __init__(self, gl: Gitlab, parent: Optional[T_parent] = None) -> None: """REST manager constructor. Args: diff --git a/gitlab/client.py b/gitlab/client.py index f5d12dfc1..b3e6b97e7 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -815,7 +815,7 @@ def http_list( as_list: Optional[bool] = None, # Deprecated in favor of `iterator` iterator: Optional[bool] = None, **kwargs: Any, - ) -> Union["GitlabList", List[Dict[str, Any]]]: + ) -> Union["GitlabList", List[Dict[str, int]]]: """Make a GET request to the Gitlab server for list-oriented queries. Args: diff --git a/gitlab/mixins.py b/gitlab/mixins.py index 4dee7106a..e3586ed1e 100644 --- a/gitlab/mixins.py +++ b/gitlab/mixins.py @@ -20,11 +20,13 @@ Any, Callable, Dict, - List, + Generic, + Iterable, Optional, Tuple, Type, TYPE_CHECKING, + TypeVar, Union, ) @@ -59,29 +61,34 @@ "BadgeRenderMixin", ] +T_obj = TypeVar("T_obj", bound=base.RESTObject) +T_parent = TypeVar("T_parent", bound=Optional[base.RESTObject]) + if TYPE_CHECKING: # When running mypy we use these as the base classes _RestManagerBase = base.RESTManager _RestObjectBase = base.RESTObject else: - _RestManagerBase = object - _RestObjectBase = object + + class _RestManagerBase(Generic[T_obj, T_parent]): + _obj_cls: Type[T_obj] + _parent: Optional[T_parent] + + class _RestObjectBase(Generic[T_obj, T_parent]): + _obj_cls: Type[T_obj] + _parent: Optional[T_parent] -class GetMixin(_RestManagerBase): +class GetMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] _optional_get_attrs: Tuple[str, ...] = () - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabGetError) - def get( - self, id: Union[str, int], lazy: bool = False, **kwargs: Any - ) -> base.RESTObject: + def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> T_obj: """Retrieve a single object. Args: @@ -113,12 +120,10 @@ def get( return self._obj_cls(self, server_data) -class GetWithoutIdMixin(_RestManagerBase): +class GetWithoutIdMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] _optional_get_attrs: Tuple[str, ...] = () - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @@ -181,18 +186,16 @@ def refresh(self, **kwargs: Any) -> None: self._update_attrs(server_data) -class ListMixin(_RestManagerBase): +class ListMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] _list_filters: Tuple[str, ...] = () - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @exc.on_http_error(exc.GitlabListError) - def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject]]: + def list(self, **kwargs: Any) -> Iterable[T_obj]: """Retrieve a list of objects. Args: @@ -234,21 +237,19 @@ def list(self, **kwargs: Any) -> Union[base.RESTObjectList, List[base.RESTObject return base.RESTObjectList(self, self._obj_cls, obj) -class RetrieveMixin(ListMixin, GetMixin): +class RetrieveMixin( + ListMixin[T_obj, T_parent], GetMixin[T_obj, T_parent], Generic[T_obj, T_parent] +): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab -class CreateMixin(_RestManagerBase): +class CreateMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @@ -287,11 +288,9 @@ def create( return self._obj_cls(self, server_data) -class UpdateMixin(_RestManagerBase): +class UpdateMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] _update_uses_post: bool = False @@ -352,11 +351,9 @@ def update( return result -class SetMixin(_RestManagerBase): +class SetMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @@ -386,11 +383,9 @@ def set(self, key: str, value: str, **kwargs: Any) -> base.RESTObject: return self._obj_cls(self, server_data) -class DeleteMixin(_RestManagerBase): +class DeleteMixin(_RestManagerBase[T_obj, T_parent], Generic[T_obj, T_parent]): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @@ -417,21 +412,30 @@ def delete(self, id: Optional[Union[str, int]] = None, **kwargs: Any) -> None: self.gitlab.http_delete(path, **kwargs) -class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin): +class CRUDMixin( + GetMixin[T_obj, T_parent], + ListMixin[T_obj, T_parent], + CreateMixin[T_obj, T_parent], + UpdateMixin[T_obj, T_parent], + DeleteMixin[T_obj, T_parent], + Generic[T_obj, T_parent], +): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab -class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin): +class NoUpdateMixin( + GetMixin[T_obj, T_parent], + ListMixin[T_obj, T_parent], + CreateMixin[T_obj, T_parent], + DeleteMixin[T_obj, T_parent], + Generic[T_obj, T_parent], +): _computed_path: Optional[str] _from_parent_attrs: Dict[str, Any] - _obj_cls: Optional[Type[base.RESTObject]] - _parent: Optional[base.RESTObject] _parent_attrs: Dict[str, Any] _path: Optional[str] gitlab: gitlab.Gitlab @@ -832,7 +836,7 @@ def participants(self, **kwargs: Any) -> Dict[str, Any]: return result -class BadgeRenderMixin(_RestManagerBase): +class BadgeRenderMixin(_RestManagerBase[T_obj, T_parent]): @cli.register_custom_action( ("GroupBadgeManager", "ProjectBadgeManager"), ("link_url", "image_url") ) diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py old mode 100644 new mode 100755 index 2b0d4ce72..026898b56 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -19,7 +19,7 @@ import argparse import operator import sys -from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING, Union +from typing import Any, Dict, Iterable, List, Optional, Type, TYPE_CHECKING, Union import gitlab import gitlab.base @@ -143,7 +143,7 @@ def do_create(self) -> gitlab.base.RESTObject: def do_list( self, - ) -> Union[gitlab.base.RESTObjectList, List[gitlab.base.RESTObject]]: + ) -> Iterable[gitlab.base.RESTObjectList]: if TYPE_CHECKING: assert isinstance(self.mgr, gitlab.mixins.ListMixin) try: diff --git a/gitlab/v4/objects/milestones.py b/gitlab/v4/objects/milestones.py index 2d82a59c7..43adddcde 100644 --- a/gitlab/v4/objects/milestones.py +++ b/gitlab/v4/objects/milestones.py @@ -4,6 +4,7 @@ from gitlab import exceptions as exc from gitlab import types from gitlab.base import RESTManager, RESTObject, RESTObjectList +from gitlab.client import GitlabList from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, PromoteMixin, SaveMixin from gitlab.types import RequiredOptional @@ -47,7 +48,7 @@ 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) @@ -73,7 +74,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 = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupMergeRequest, data_list) @@ -124,7 +125,7 @@ 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) @@ -150,7 +151,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 ) diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py index 597a3aaf0..512b97f0c 100644 --- a/gitlab/v4/objects/snippets.py +++ b/gitlab/v4/objects/snippets.py @@ -1,11 +1,11 @@ -from typing import Any, Callable, cast, List, Optional, TYPE_CHECKING, Union +from typing import Any, Callable, cast, Iterable, Optional, TYPE_CHECKING, Union import requests from gitlab import cli from gitlab import exceptions as exc from gitlab import utils -from gitlab.base import RESTManager, RESTObject, RESTObjectList +from gitlab.base import RESTManager, RESTObject from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin, UserAgentDetailMixin from gitlab.types import RequiredOptional @@ -71,7 +71,7 @@ class SnippetManager(CRUDMixin, RESTManager): ) @cli.register_custom_action("SnippetManager") - def public(self, **kwargs: Any) -> Union[RESTObjectList, List[RESTObject]]: + def public(self, **kwargs: Any) -> Iterable[Snippet]: """List all the public snippets. Args: diff --git a/gitlab/v4/objects/users.py b/gitlab/v4/objects/users.py index acd3b2f76..70eb40ede 100644 --- a/gitlab/v4/objects/users.py +++ b/gitlab/v4/objects/users.py @@ -3,14 +3,14 @@ https://docs.gitlab.com/ee/api/users.html https://docs.gitlab.com/ee/api/projects.html#list-projects-starred-by-a-user """ -from typing import Any, cast, Dict, List, Optional, Union +from typing import Any, cast, Dict, Iterable, Optional, Union import requests from gitlab import cli from gitlab import exceptions as exc from gitlab import types -from gitlab.base import RESTManager, RESTObject, RESTObjectList +from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ( CreateMixin, CRUDMixin, @@ -532,7 +532,7 @@ class UserProjectManager(ListMixin, CreateMixin, RESTManager): "id_before", ) - def list(self, **kwargs: Any) -> Union[RESTObjectList, List[RESTObject]]: + def list(self, **kwargs: Any) -> Iterable[UserProject]: """Retrieve a list of objects. Args: