Skip to content

Specific type hints #2067

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

Closed
wants to merge 4 commits into from
Closed
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
37 changes: 27 additions & 10 deletions gitlab/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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)

Expand Down Expand Up @@ -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``.
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion gitlab/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
80 changes: 42 additions & 38 deletions gitlab/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
Any,
Callable,
Dict,
List,
Generic,
Iterable,
Optional,
Tuple,
Type,
TYPE_CHECKING,
TypeVar,
Union,
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
)
Expand Down
4 changes: 2 additions & 2 deletions gitlab/v4/cli.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 5 additions & 4 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 RESTManager, RESTObject, RESTObjectList
from gitlab.client import GitlabList
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, PromoteMixin, SaveMixin
from gitlab.types import RequiredOptional

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
Expand Down
6 changes: 3 additions & 3 deletions gitlab/v4/objects/snippets.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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:
Expand Down
Loading