Commit 91c4f18d authored by Igor Ponomarev's avatar Igor Ponomarev Committed by Nejc Habjan
Browse files

feat(api): Make RESTManager generic on RESTObject class



Currently mixins like ListMixin are type hinted to return base
RESTObject instead of a specific class like `MergeRequest`.

The GetMixin and GetWithoutIdMixin solve this problem by defining
a new `get` method for every defined class. However, this creates
a lot of duplicated code.

Make RESTManager use `typing.Generic` as its base class and use
and assign the declared TypeVar to the `_obj_cls` attribute as
a type of the passed class.

Make both `_obj_cls` and `_path` attributes an abstract properties
so that type checkers can check that those attributes were properly
defined in subclasses. Mypy will only check then the class is
instantiated which makes non-final subclasses possible.
Unfortunately pylint will check the declarations not instantiations
so add `# pylint: disable=abstract-method` comments to all non-final
subclasses like ListMixin.

Make `_path` attribute always be `str` instead of sometimes `None`.
This eliminates unnecessary type checks.

Change all mixins like ListMixin or GetMixin to a subclass. This makes
the attribute declarations much cleaner as for example `_list_filters`
is now the only attribute defined by ListMixin.

Change SidekiqManager to not inherit from RESTManager and only
copy its `__init__` method. This is because SidekiqManager never
was a real manager and does not define `_path` or `_obj_cls`.

Delete `tests/unit/meta/test_ensure_type_hints.py` file as the `get`
method is no required to be defined for every class.

Signed-off-by: default avatarIgor Ponomarev <igor.ponomarev@collabora.com>
parent 46dfc509
Loading
Loading
Loading
Loading
+24 −12
Original line number Diff line number Diff line
@@ -4,7 +4,18 @@ import json
import pprint
import textwrap
from types import ModuleType
from typing import Any, Dict, Iterable, Optional, Type, TYPE_CHECKING, Union
from typing import (
    Any,
    ClassVar,
    Dict,
    Generic,
    Iterable,
    Optional,
    Type,
    TYPE_CHECKING,
    TypeVar,
    Union,
)

import gitlab
from gitlab import types as g_types
@@ -48,11 +59,11 @@ class RESTObject:
    _repr_attr: Optional[str] = None
    _updated_attrs: Dict[str, Any]
    _lazy: bool
    manager: "RESTManager"
    manager: "RESTManager[Any]"

    def __init__(
        self,
        manager: "RESTManager",
        manager: "RESTManager[Any]",
        attrs: Dict[str, Any],
        *,
        created_from_list: bool = False,
@@ -269,7 +280,7 @@ class RESTObjectList:
    """

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

@@ -335,7 +346,10 @@ class RESTObjectList:
        return self._list.total


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


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

    Derived class must define ``_path`` and ``_obj_cls``.
@@ -346,12 +360,12 @@ 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
    _path: ClassVar[str]
    _obj_cls: type[TObjCls]
    _from_parent_attrs: Dict[str, Any] = {}
    _types: Dict[str, Type[g_types.GitlabAttribute]] = {}

    _computed_path: Optional[str]
    _computed_path: str
    _parent: Optional[RESTObject]
    _parent_attrs: Dict[str, Any]
    gitlab: Gitlab
@@ -371,12 +385,10 @@ class RESTManager:
    def parent_attrs(self) -> Optional[Dict[str, Any]]:
        return self._parent_attrs

    def _compute_path(self, path: Optional[str] = None) -> Optional[str]:
    def _compute_path(self, path: Optional[str] = None) -> str:
        self._parent_attrs = {}
        if path is None:
            path = self._path
        if path is None:
            return None
        if self._parent is None or not self._from_parent_attrs:
            return path

@@ -390,5 +402,5 @@ class RESTManager:
        return path.format(**data)

    @property
    def path(self) -> Optional[str]:
    def path(self) -> str:
        return self._computed_path
+2 −3
Original line number Diff line number Diff line
@@ -397,11 +397,10 @@ class Gitlab:
        The `user` attribute will hold a `gitlab.objects.CurrentUser` object on
        success.
        """
        # pylint: disable=line-too-long
        self.user = self._objects.CurrentUserManager(self).get()  # type: ignore[assignment]
        self.user = self._objects.CurrentUserManager(self).get()

        if hasattr(self.user, "web_url") and hasattr(self.user, "username"):
            self._check_url(self.user.web_url, path=self.user.username)  # type: ignore[union-attr]
            self._check_url(self.user.web_url, path=self.user.username)

    def version(self) -> Tuple[str, str]:
        """Returns the version and revision of the gitlab server.
+48 −131
Original line number Diff line number Diff line
@@ -10,7 +10,6 @@ from typing import (
    Optional,
    overload,
    Tuple,
    Type,
    TYPE_CHECKING,
    Union,
)
@@ -48,14 +47,12 @@ __all__ = [

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 HeadMixin(_RestManagerBase):
class HeadMixin(base.RESTManager[base.TObjCls]):
    @exc.on_http_error(exc.GitlabHeadError)
    def head(
        self, id: Optional[Union[str, int]] = None, **kwargs: Any
@@ -73,9 +70,6 @@ class HeadMixin(_RestManagerBase):
            GitlabAuthenticationError: If authentication is not correct
            GitlabHeadError: If the server cannot perform the request
        """
        if TYPE_CHECKING:
            assert self.path is not None

        path = self.path
        if id is not None:
            path = f"{path}/{utils.EncodedId(id)}"
@@ -83,20 +77,13 @@ class HeadMixin(_RestManagerBase):
        return self.gitlab.http_head(path, **kwargs)


class GetMixin(HeadMixin, _RestManagerBase):
    _computed_path: Optional[str]
    _from_parent_attrs: Dict[str, Any]
    _obj_cls: Optional[Type[base.RESTObject]]
class GetMixin(HeadMixin[base.TObjCls]):
    _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:
    ) -> base.TObjCls:
        """Retrieve a single object.

        Args:
@@ -116,8 +103,6 @@ class GetMixin(HeadMixin, _RestManagerBase):
        if isinstance(id, str):
            id = utils.EncodedId(id)
        path = f"{self.path}/{id}"
        if TYPE_CHECKING:
            assert self._obj_cls is not None
        if lazy is True:
            if TYPE_CHECKING:
                assert self._obj_cls._id_attr is not None
@@ -128,18 +113,11 @@ class GetMixin(HeadMixin, _RestManagerBase):
        return self._obj_cls(self, server_data, lazy=lazy)


class GetWithoutIdMixin(HeadMixin, _RestManagerBase):
    _computed_path: Optional[str]
    _from_parent_attrs: Dict[str, Any]
    _obj_cls: Optional[Type[base.RESTObject]]
class GetWithoutIdMixin(HeadMixin[base.TObjCls]):
    _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, **kwargs: Any) -> base.RESTObject:
    def get(self, **kwargs: Any) -> base.TObjCls:
        """Retrieve a single object.

        Args:
@@ -152,12 +130,9 @@ class GetWithoutIdMixin(HeadMixin, _RestManagerBase):
            GitlabAuthenticationError: If authentication is not correct
            GitlabGetError: If the server cannot perform the request
        """
        if TYPE_CHECKING:
            assert self.path is not None
        server_data = self.gitlab.http_get(self.path, **kwargs)
        if TYPE_CHECKING:
            assert not isinstance(server_data, requests.Response)
            assert self._obj_cls is not None
        return self._obj_cls(self, server_data)


@@ -167,7 +142,7 @@ class RefreshMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @exc.on_http_error(exc.GitlabGetError)
    def refresh(self, **kwargs: Any) -> None:
@@ -194,18 +169,11 @@ class RefreshMixin(_RestObjectBase):
        self._update_attrs(server_data)


class ListMixin(HeadMixin, _RestManagerBase):
    _computed_path: Optional[str]
    _from_parent_attrs: Dict[str, Any]
class ListMixin(HeadMixin[base.TObjCls]):
    _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) -> Union[base.RESTObjectList, List[base.TObjCls]]:
        """Retrieve a list of objects.

        Args:
@@ -244,37 +212,20 @@ class ListMixin(HeadMixin, _RestManagerBase):
        # Allow to overwrite the path, handy for custom listings
        path = data.pop("path", self.path)

        if TYPE_CHECKING:
            assert self._obj_cls is not None
        obj = self.gitlab.http_list(path, **data)
        if isinstance(obj, list):
            return [self._obj_cls(self, item, created_from_list=True) for item in obj]
        return base.RESTObjectList(self, self._obj_cls, obj)


class RetrieveMixin(ListMixin, GetMixin):
    _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 RetrieveMixin(ListMixin[base.TObjCls], GetMixin[base.TObjCls]): ...

class CreateMixin(_RestManagerBase):
    _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(base.RESTManager[base.TObjCls]):
    @exc.on_http_error(exc.GitlabCreateError)
    def create(
        self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
    ) -> base.RESTObject:
    ) -> base.TObjCls:
        """Create a new object.

        Args:
@@ -303,7 +254,6 @@ class CreateMixin(_RestManagerBase):
        server_data = self.gitlab.http_post(path, post_data=data, files=files, **kwargs)
        if TYPE_CHECKING:
            assert not isinstance(server_data, requests.Response)
            assert self._obj_cls is not None
        return self._obj_cls(self, server_data)


@@ -314,19 +264,13 @@ class UpdateMethod(enum.IntEnum):
    PATCH = 3


class UpdateMixin(_RestManagerBase):
    _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]
class UpdateMixin(base.RESTManager[base.TObjCls]):
    # Update mixins attrs for easier implementation
    _update_method: UpdateMethod = UpdateMethod.PUT
    gitlab: gitlab.Gitlab

    def _get_update_method(
        self,
    ) -> Callable[..., Union[Dict[str, Any], requests.Response]]:
    ) -> Callable[..., Union[Dict[str, Any], "requests.Response"]]:
        """Return the HTTP method to use.

        Returns:
@@ -384,17 +328,9 @@ class UpdateMixin(_RestManagerBase):
        return result


class SetMixin(_RestManagerBase):
    _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 SetMixin(base.RESTManager[base.TObjCls]):
    @exc.on_http_error(exc.GitlabSetError)
    def set(self, key: str, value: str, **kwargs: Any) -> base.RESTObject:
    def set(self, key: str, value: str, **kwargs: Any) -> base.TObjCls:
        """Create or update the object.

        Args:
@@ -414,19 +350,10 @@ class SetMixin(_RestManagerBase):
        server_data = self.gitlab.http_put(path, post_data=data, **kwargs)
        if TYPE_CHECKING:
            assert not isinstance(server_data, requests.Response)
            assert self._obj_cls is not None
        return self._obj_cls(self, server_data)


class DeleteMixin(_RestManagerBase):
    _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 DeleteMixin(base.RESTManager[base.TObjCls]):
    @exc.on_http_error(exc.GitlabDeleteError)
    def delete(self, id: Optional[Union[str, int]] = None, **kwargs: Any) -> None:
        """Delete an object on the server.
@@ -444,29 +371,24 @@ class DeleteMixin(_RestManagerBase):
        else:
            path = f"{self.path}/{utils.EncodedId(id)}"

        if TYPE_CHECKING:
            assert path is not None
        self.gitlab.http_delete(path, **kwargs)


class CRUDMixin(GetMixin, ListMixin, CreateMixin, UpdateMixin, DeleteMixin):
    _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 CRUDMixin(
    GetMixin[base.TObjCls],
    ListMixin[base.TObjCls],
    CreateMixin[base.TObjCls],
    UpdateMixin[base.TObjCls],
    DeleteMixin[base.TObjCls],
): ...


class NoUpdateMixin(GetMixin, ListMixin, CreateMixin, DeleteMixin):
    _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[base.TObjCls],
    ListMixin[base.TObjCls],
    CreateMixin[base.TObjCls],
    DeleteMixin[base.TObjCls],
): ...


class SaveMixin(_RestObjectBase):
@@ -477,7 +399,7 @@ class SaveMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    def _get_updated_data(self) -> Dict[str, Any]:
        updated_data = {}
@@ -526,7 +448,7 @@ class ObjectDeleteMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    def delete(self, **kwargs: Any) -> None:
        """Delete the object from the server.
@@ -550,7 +472,7 @@ class UserAgentDetailMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(cls_names=("Snippet", "ProjectSnippet", "ProjectIssue"))
    @exc.on_http_error(exc.GitlabGetError)
@@ -577,7 +499,7 @@ class AccessRequestMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(
        cls_names=("ProjectAccessRequest", "GroupAccessRequest"),
@@ -612,7 +534,7 @@ class DownloadMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @overload
    def download(
@@ -689,15 +611,7 @@ class DownloadMixin(_RestObjectBase):
        )


class RotateMixin(_RestManagerBase):
    _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 RotateMixin(base.RESTManager[base.TObjCls]):
    @cli.register_custom_action(
        cls_names=(
            "PersonalAccessTokenManager",
@@ -708,7 +622,10 @@ class RotateMixin(_RestManagerBase):
    )
    @exc.on_http_error(exc.GitlabRotateError)
    def rotate(
        self, id: Union[str, int], expires_at: Optional[str] = None, **kwargs: Any
        self,
        id: Union[str, int],
        expires_at: Optional[str] = None,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        """Rotate an access token.

@@ -737,7 +654,7 @@ class ObjectRotateMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(
        cls_names=("PersonalAccessToken", "GroupAccessToken", "ProjectAccessToken"),
@@ -768,7 +685,7 @@ class SubscribableMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(
        cls_names=("ProjectIssue", "ProjectMergeRequest", "ProjectLabel", "GroupLabel")
@@ -817,7 +734,7 @@ class TodoMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
    @exc.on_http_error(exc.GitlabTodoError)
@@ -841,7 +758,7 @@ class TimeTrackingMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(cls_names=("ProjectIssue", "ProjectMergeRequest"))
    @exc.on_http_error(exc.GitlabTimeTrackingError)
@@ -956,7 +873,7 @@ class ParticipantsMixin(_RestObjectBase):
    _module: ModuleType
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    @cli.register_custom_action(cls_names=("ProjectMergeRequest", "ProjectIssue"))
    @exc.on_http_error(exc.GitlabListError)
@@ -986,7 +903,7 @@ class ParticipantsMixin(_RestObjectBase):
        return result


class BadgeRenderMixin(_RestManagerBase):
class BadgeRenderMixin(base.RESTManager[base.TObjCls]):
    @cli.register_custom_action(
        cls_names=("GroupBadgeManager", "ProjectBadgeManager"),
        required=("link_url", "image_url"),
@@ -1022,7 +939,7 @@ class PromoteMixin(_RestObjectBase):
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    _update_method: UpdateMethod = UpdateMethod.PUT
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    def _get_update_method(
        self,
@@ -1069,7 +986,7 @@ class UploadMixin(_RestObjectBase):
    _parent_attrs: Dict[str, Any]
    _updated_attrs: Dict[str, Any]
    _upload_path: str
    manager: base.RESTManager
    manager: base.RESTManager[Any]

    def _get_upload_path(self) -> str:
        """Formats _upload_path with object attributes.
+18 −14
Original line number Diff line number Diff line
@@ -28,25 +28,16 @@ class GitlabCLI:
        self.gl = gl
        self.args = args
        self.parent_args: Dict[str, Any] = {}
        self.mgr_cls: Union[
            Type[gitlab.mixins.CreateMixin],
            Type[gitlab.mixins.DeleteMixin],
            Type[gitlab.mixins.GetMixin],
            Type[gitlab.mixins.GetWithoutIdMixin],
            Type[gitlab.mixins.ListMixin],
            Type[gitlab.mixins.UpdateMixin],
        ] = getattr(gitlab.v4.objects, f"{self.cls.__name__}Manager")
        self.mgr_cls: Any = getattr(gitlab.v4.objects, f"{self.cls.__name__}Manager")
        # We could do something smart, like splitting the manager name to find
        # parents, build the chain of managers to get to the final object.
        # Instead we do something ugly and efficient: interpolate variables in
        # the class _path attribute, and replace the value with the result.
        if TYPE_CHECKING:
            assert self.mgr_cls._path is not None

        self._process_from_parent_attrs()

        self.mgr_cls._path = self.mgr_cls._path.format(**self.parent_args)
        self.mgr = self.mgr_cls(gl)
        self.mgr: Any = self.mgr_cls(gl)
        self.mgr._from_parent_attrs = self.parent_args
        if self.mgr_cls._types:
            for attr_name, type_cls in self.mgr_cls._types.items():
@@ -82,7 +73,9 @@ class GitlabCLI:
        return self.do_custom()

    def do_custom(self) -> Any:
        class_instance: Union[gitlab.base.RESTManager, gitlab.base.RESTObject]
        class_instance: Union[
            gitlab.base.RESTManager[gitlab.base.RESTObject], gitlab.base.RESTObject
        ]
        in_obj = cli.custom_actions[self.cls_name][self.resource_action].in_object

        # Get the object (lazy), then act
@@ -132,6 +125,8 @@ class GitlabCLI:
            assert isinstance(self.mgr, gitlab.mixins.CreateMixin)
        try:
            result = self.mgr.create(self.args)
            if TYPE_CHECKING:
                assert isinstance(result, gitlab.base.RESTObject)
        except Exception as e:  # pragma: no cover, cli.die is unit-tested
            cli.die("Impossible to create object", e)
        return result
@@ -159,6 +154,8 @@ class GitlabCLI:
        if isinstance(self.mgr, gitlab.mixins.GetWithoutIdMixin):
            try:
                result = self.mgr.get(id=None, **self.args)
                if TYPE_CHECKING:
                    assert isinstance(result, gitlab.base.RESTObject) or result is None
            except Exception as e:  # pragma: no cover, cli.die is unit-tested
                cli.die("Impossible to get object", e)
            return result
@@ -170,6 +167,8 @@ class GitlabCLI:
        id = self.args.pop(self.cls._id_attr)
        try:
            result = self.mgr.get(id, lazy=False, **self.args)
            if TYPE_CHECKING:
                assert isinstance(result, gitlab.base.RESTObject) or result is None
        except Exception as e:  # pragma: no cover, cli.die is unit-tested
            cli.die("Impossible to get object", e)
        return result
@@ -401,10 +400,15 @@ def extend_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
        if not isinstance(cls, type):
            continue
        if issubclass(cls, gitlab.base.RESTManager):
            if cls._obj_cls is not None:
            classes.add(cls._obj_cls)

    for cls in sorted(classes, key=operator.attrgetter("__name__")):
        if cls is gitlab.base.RESTObject:
            # Skip managers where _obj_cls is a plain RESTObject class
            # Those managers do not actually manage any objects and
            # can only be used to calls specific API paths.
            continue

        arg_name = cli.cls_to_gitlab_resource(cls)
        mgr_cls_name = f"{cls.__name__}Manager"
        mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
+11 −3
Original line number Diff line number Diff line
from gitlab.base import RESTManager, RESTObject
from gitlab.base import RESTObject
from gitlab.mixins import (
    AccessRequestMixin,
    CreateMixin,
@@ -19,7 +19,11 @@ class GroupAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
    pass


class GroupAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
class GroupAccessRequestManager(
    ListMixin[GroupAccessRequest],
    CreateMixin[GroupAccessRequest],
    DeleteMixin[GroupAccessRequest],
):
    _path = "/groups/{group_id}/access_requests"
    _obj_cls = GroupAccessRequest
    _from_parent_attrs = {"group_id": "id"}
@@ -29,7 +33,11 @@ class ProjectAccessRequest(AccessRequestMixin, ObjectDeleteMixin, RESTObject):
    pass


class ProjectAccessRequestManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
class ProjectAccessRequestManager(
    ListMixin[ProjectAccessRequest],
    CreateMixin[ProjectAccessRequest],
    DeleteMixin[ProjectAccessRequest],
):
    _path = "/projects/{project_id}/access_requests"
    _obj_cls = ProjectAccessRequest
    _from_parent_attrs = {"project_id": "id"}
Loading