diff --git a/gitlab/base.py b/gitlab/base.py index db2e149f2..5e5f57b1e 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -150,6 +150,10 @@ def _create_managers(self) -> None: # annotations. If an attribute is annotated as being a *Manager type # then we create the manager and assign it to the attribute. for attr, annotation in sorted(self.__annotations__.items()): + # We ignore creating a manager for the 'manager' attribute as that + # is done in the self.__init__() method + if attr in ("manager",): + continue if not isinstance(annotation, (type, str)): continue if isinstance(annotation, type): diff --git a/gitlab/v4/objects/epics.py b/gitlab/v4/objects/epics.py index b42ce98a9..38d244c83 100644 --- a/gitlab/v4/objects/epics.py +++ b/gitlab/v4/objects/epics.py @@ -1,3 +1,5 @@ +from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union + from gitlab import exceptions as exc from gitlab import types from gitlab.base import RequiredOptional, RESTManager, RESTObject @@ -42,11 +44,17 @@ class GroupEpicManager(CRUDMixin, RESTManager): ) _types = {"labels": types.ListAttribute} + def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GroupEpic: + return cast(GroupEpic, super().get(id=id, lazy=lazy, **kwargs)) + class GroupEpicIssue(ObjectDeleteMixin, SaveMixin, RESTObject): _id_attr = "epic_issue_id" + # Define type for 'manager' here So mypy won't complain about + # 'self.manager.update()' call in the 'save' method. + manager: "GroupEpicIssueManager" - def save(self, **kwargs): + def save(self, **kwargs: Any) -> None: """Save the changes made to the object to the server. The object is updated to match what the server returns. @@ -78,7 +86,9 @@ class GroupEpicIssueManager( _update_attrs = RequiredOptional(optional=("move_before_id", "move_after_id")) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + def create( + self, data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> GroupEpicIssue: """Create a new object. Args: @@ -94,9 +104,13 @@ def create(self, data, **kwargs): RESTObject: A new instance of the manage object class build with the data sent by the server """ + if TYPE_CHECKING: + assert data is not None CreateMixin._check_missing_create_attrs(self, data) path = f"{self.path}/{data.pop('issue_id')}" server_data = self.gitlab.http_post(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) # The epic_issue_id attribute doesn't exist when creating the resource, # but is used everywhere elese. Let's create it to be consistent client # side diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index cf17cd70b..ce7317d25 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -1,4 +1,7 @@ import base64 +from typing import Any, Callable, cast, Dict, List, Optional, TYPE_CHECKING + +import requests from gitlab import cli from gitlab import exceptions as exc @@ -22,6 +25,8 @@ class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "file_path" _short_print_attr = "file_path" + file_path: str + manager: "ProjectFileManager" def decode(self) -> bytes: """Returns the decoded content of the file. @@ -31,7 +36,11 @@ def decode(self) -> bytes: """ return base64.b64decode(self.content) - def save(self, branch, commit_message, **kwargs): + # NOTE(jlvillal): Signature doesn't match SaveMixin.save() so ignore + # type error + def save( # type: ignore + self, branch: str, commit_message: str, **kwargs: Any + ) -> None: """Save the changes made to the file to the server. The object is updated to match what the server returns. @@ -50,7 +59,12 @@ def save(self, branch, commit_message, **kwargs): self.file_path = self.file_path.replace("/", "%2F") super(ProjectFile, self).save(**kwargs) - def delete(self, branch, commit_message, **kwargs): + @exc.on_http_error(exc.GitlabDeleteError) + # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore + # type error + def delete( # type: ignore + self, branch: str, commit_message: str, **kwargs: Any + ) -> None: """Delete the file from the server. Args: @@ -80,7 +94,11 @@ class ProjectFileManager(GetMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa ) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) - def get(self, file_path, ref, **kwargs): + # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore + # type error + def get( # type: ignore + self, file_path: str, ref: str, **kwargs: Any + ) -> ProjectFile: """Retrieve a single file. Args: @@ -95,7 +113,7 @@ def get(self, file_path, ref, **kwargs): Returns: object: The generated RESTObject """ - return GetMixin.get(self, file_path, ref=ref, **kwargs) + return cast(ProjectFile, GetMixin.get(self, file_path, ref=ref, **kwargs)) @cli.register_custom_action( "ProjectFileManager", @@ -103,7 +121,9 @@ def get(self, file_path, ref, **kwargs): ("encoding", "author_email", "author_name"), ) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + def create( + self, data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> ProjectFile: """Create a new object. Args: @@ -120,15 +140,23 @@ def create(self, data, **kwargs): GitlabCreateError: If the server cannot perform the request """ + if TYPE_CHECKING: + assert data is not None self._check_missing_create_attrs(data) new_data = data.copy() file_path = new_data.pop("file_path").replace("/", "%2F") path = f"{self.path}/{file_path}" server_data = self.gitlab.http_post(path, post_data=new_data, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) return self._obj_cls(self, server_data) @exc.on_http_error(exc.GitlabUpdateError) - def update(self, file_path, new_data=None, **kwargs): + # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore + # type error + def update( # type: ignore + self, file_path: str, new_data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> Dict[str, Any]: """Update an object on the server. Args: @@ -149,13 +177,20 @@ def update(self, file_path, new_data=None, **kwargs): data["file_path"] = file_path path = f"{self.path}/{file_path}" self._check_missing_update_attrs(data) - return self.gitlab.http_put(path, post_data=data, **kwargs) + result = self.gitlab.http_put(path, post_data=data, **kwargs) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result @cli.register_custom_action( "ProjectFileManager", ("file_path", "branch", "commit_message") ) @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, file_path, branch, commit_message, **kwargs): + # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore + # type error + def delete( # type: ignore + self, file_path: str, branch: str, commit_message: str, **kwargs: Any + ) -> None: """Delete a file on the server. Args: @@ -175,8 +210,14 @@ def delete(self, file_path, branch, commit_message, **kwargs): @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabGetError) def raw( - self, file_path, ref, streamed=False, action=None, chunk_size=1024, **kwargs - ): + self, + file_path: str, + ref: str, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Optional[bytes]: """Return the content of a file for a commit. Args: @@ -203,11 +244,13 @@ def raw( result = self.gitlab.http_get( path, query_data=query_data, streamed=streamed, raw=True, **kwargs ) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectFileManager", ("file_path", "ref")) @exc.on_http_error(exc.GitlabListError) - def blame(self, file_path, ref, **kwargs): + def blame(self, file_path: str, ref: str, **kwargs: Any) -> List[Dict[str, Any]]: """Return the content of a file for a commit. Args: @@ -225,4 +268,7 @@ def blame(self, file_path, ref, **kwargs): file_path = file_path.replace("/", "%2F").replace(".", "%2E") path = f"{self.path}/{file_path}/blame" query_data = {"ref": ref} - return self.gitlab.http_list(path, query_data, **kwargs) + result = self.gitlab.http_list(path, query_data, **kwargs) + if TYPE_CHECKING: + assert isinstance(result, list) + return result diff --git a/gitlab/v4/objects/geo_nodes.py b/gitlab/v4/objects/geo_nodes.py index cde439847..7fffb6341 100644 --- a/gitlab/v4/objects/geo_nodes.py +++ b/gitlab/v4/objects/geo_nodes.py @@ -1,3 +1,5 @@ +from typing import Any, cast, Dict, List, TYPE_CHECKING, Union + from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject @@ -18,7 +20,7 @@ class GeoNode(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabRepairError) - def repair(self, **kwargs): + def repair(self, **kwargs: Any) -> None: """Repair the OAuth authentication of the geo node. Args: @@ -30,11 +32,13 @@ def repair(self, **kwargs): """ path = f"/geo_nodes/{self.get_id()}/repair" server_data = self.manager.gitlab.http_post(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("GeoNode") @exc.on_http_error(exc.GitlabGetError) - def status(self, **kwargs): + def status(self, **kwargs: Any) -> Dict[str, Any]: """Get the status of the geo node. Args: @@ -48,7 +52,10 @@ def status(self, **kwargs): dict: The status of the geo node """ path = f"/geo_nodes/{self.get_id()}/status" - return self.manager.gitlab.http_get(path, **kwargs) + result = self.manager.gitlab.http_get(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): @@ -58,9 +65,12 @@ class GeoNodeManager(RetrieveMixin, UpdateMixin, DeleteMixin, RESTManager): optional=("enabled", "url", "files_max_capacity", "repos_max_capacity"), ) + def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> GeoNode: + return cast(GeoNode, super().get(id=id, lazy=lazy, **kwargs)) + @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) - def status(self, **kwargs): + def status(self, **kwargs: Any) -> List[Dict[str, Any]]: """Get the status of all the geo nodes. Args: @@ -73,11 +83,14 @@ def status(self, **kwargs): Returns: list: The status of all the geo nodes """ - return self.gitlab.http_list("/geo_nodes/status", **kwargs) + result = self.gitlab.http_list("/geo_nodes/status", **kwargs) + if TYPE_CHECKING: + assert isinstance(result, list) + return result @cli.register_custom_action("GeoNodeManager") @exc.on_http_error(exc.GitlabGetError) - def current_failures(self, **kwargs): + def current_failures(self, **kwargs: Any) -> List[Dict[str, Any]]: """Get the list of failures on the current geo node. Args: @@ -90,4 +103,7 @@ def current_failures(self, **kwargs): Returns: list: The list of failures """ - return self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) + result = self.gitlab.http_list("/geo_nodes/current/failures", **kwargs) + if TYPE_CHECKING: + assert isinstance(result, list) + return result diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py index 5c397349b..8cd231768 100644 --- a/gitlab/v4/objects/issues.py +++ b/gitlab/v4/objects/issues.py @@ -1,3 +1,5 @@ +from typing import Any, cast, Dict, Tuple, TYPE_CHECKING, Union + from gitlab import cli from gitlab import exceptions as exc from gitlab import types @@ -65,6 +67,9 @@ class IssueManager(RetrieveMixin, RESTManager): ) _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} + def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Issue: + return cast(Issue, super().get(id=id, lazy=lazy, **kwargs)) + class GroupIssue(RESTObject): pass @@ -116,7 +121,7 @@ class ProjectIssue( @cli.register_custom_action("ProjectIssue", ("to_project_id",)) @exc.on_http_error(exc.GitlabUpdateError) - def move(self, to_project_id, **kwargs): + def move(self, to_project_id: int, **kwargs: Any) -> None: """Move the issue to another project. Args: @@ -130,11 +135,13 @@ def move(self, to_project_id, **kwargs): path = f"{self.manager.path}/{self.get_id()}/move" data = {"to_project_id": to_project_id} server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) - def related_merge_requests(self, **kwargs): + def related_merge_requests(self, **kwargs: Any) -> Dict[str, Any]: """List merge requests related to the issue. Args: @@ -148,11 +155,14 @@ def related_merge_requests(self, **kwargs): list: The list of merge requests. """ path = f"{self.manager.path}/{self.get_id()}/related_merge_requests" - return self.manager.gitlab.http_get(path, **kwargs) + result = self.manager.gitlab.http_get(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result @cli.register_custom_action("ProjectIssue") @exc.on_http_error(exc.GitlabGetError) - def closed_by(self, **kwargs): + def closed_by(self, **kwargs: Any) -> Dict[str, Any]: """List merge requests that will close the issue when merged. Args: @@ -166,7 +176,10 @@ def closed_by(self, **kwargs): list: The list of merge requests. """ path = f"{self.manager.path}/{self.get_id()}/closed_by" - return self.manager.gitlab.http_get(path, **kwargs) + result = self.manager.gitlab.http_get(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result class ProjectIssueManager(CRUDMixin, RESTManager): @@ -222,6 +235,11 @@ class ProjectIssueManager(CRUDMixin, RESTManager): ) _types = {"iids": types.ListAttribute, "labels": types.ListAttribute} + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectIssue: + return cast(ProjectIssue, super().get(id=id, lazy=lazy, **kwargs)) + class ProjectIssueLink(ObjectDeleteMixin, RESTObject): _id_attr = "issue_link_id" @@ -234,7 +252,11 @@ class ProjectIssueLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): _create_attrs = RequiredOptional(required=("target_project_id", "target_issue_iid")) @exc.on_http_error(exc.GitlabCreateError) - def create(self, data, **kwargs): + # NOTE(jlvillal): Signature doesn't match CreateMixin.create() so ignore + # type error + def create( # type: ignore + self, data: Dict[str, Any], **kwargs: Any + ) -> Tuple[RESTObject, RESTObject]: """Create a new object. Args: @@ -250,7 +272,12 @@ def create(self, data, **kwargs): GitlabCreateError: If the server cannot perform the request """ self._check_missing_create_attrs(data) + if TYPE_CHECKING: + assert self.path is not None server_data = self.gitlab.http_post(self.path, post_data=data, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) + assert self._parent is not None source_issue = ProjectIssue(self._parent.manager, server_data["source_issue"]) target_issue = ProjectIssue(self._parent.manager, server_data["target_issue"]) return source_issue, target_issue diff --git a/gitlab/v4/objects/jobs.py b/gitlab/v4/objects/jobs.py index 9f0ad8703..eba96480d 100644 --- a/gitlab/v4/objects/jobs.py +++ b/gitlab/v4/objects/jobs.py @@ -1,3 +1,7 @@ +from typing import Any, Callable, cast, Dict, Optional, TYPE_CHECKING, Union + +import requests + from gitlab import cli from gitlab import exceptions as exc from gitlab import utils @@ -13,7 +17,7 @@ class ProjectJob(RefreshMixin, RESTObject): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobCancelError) - def cancel(self, **kwargs): + def cancel(self, **kwargs: Any) -> Dict[str, Any]: """Cancel the job. Args: @@ -24,11 +28,14 @@ def cancel(self, **kwargs): GitlabJobCancelError: If the job could not be canceled """ path = f"{self.manager.path}/{self.get_id()}/cancel" - return self.manager.gitlab.http_post(path) + result = self.manager.gitlab.http_post(path) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobRetryError) - def retry(self, **kwargs): + def retry(self, **kwargs: Any) -> Dict[str, Any]: """Retry the job. Args: @@ -39,11 +46,14 @@ def retry(self, **kwargs): GitlabJobRetryError: If the job could not be retried """ path = f"{self.manager.path}/{self.get_id()}/retry" - return self.manager.gitlab.http_post(path) + result = self.manager.gitlab.http_post(path) + if TYPE_CHECKING: + assert isinstance(result, dict) + return result @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobPlayError) - def play(self, **kwargs): + def play(self, **kwargs: Any) -> None: """Trigger a job explicitly. Args: @@ -58,7 +68,7 @@ def play(self, **kwargs): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabJobEraseError) - def erase(self, **kwargs): + def erase(self, **kwargs: Any) -> None: """Erase the job (remove job artifacts and trace). Args: @@ -73,7 +83,7 @@ def erase(self, **kwargs): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabCreateError) - def keep_artifacts(self, **kwargs): + def keep_artifacts(self, **kwargs: Any) -> None: """Prevent artifacts from being deleted when expiration is set. Args: @@ -88,7 +98,7 @@ def keep_artifacts(self, **kwargs): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabCreateError) - def delete_artifacts(self, **kwargs): + def delete_artifacts(self, **kwargs: Any) -> None: """Delete artifacts of a job. Args: @@ -103,7 +113,13 @@ def delete_artifacts(self, **kwargs): @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): + def artifacts( + self, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Optional[bytes]: """Get the job artifacts. Args: @@ -120,17 +136,26 @@ def artifacts(self, streamed=False, action=None, chunk_size=1024, **kwargs): GitlabGetError: If the artifacts could not be retrieved Returns: - str: The artifacts if `streamed` is False, None otherwise. + bytes: The artifacts if `streamed` is False, None otherwise. """ path = f"{self.manager.path}/{self.get_id()}/artifacts" result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def artifact(self, path, streamed=False, action=None, chunk_size=1024, **kwargs): + def artifact( + self, + path: str, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Optional[bytes]: """Get a single artifact file from within the job's artifacts archive. Args: @@ -148,17 +173,25 @@ def artifact(self, path, streamed=False, action=None, chunk_size=1024, **kwargs) GitlabGetError: If the artifacts could not be retrieved Returns: - str: The artifacts if `streamed` is False, None otherwise. + bytes: The artifacts if `streamed` is False, None otherwise. """ path = f"{self.manager.path}/{self.get_id()}/artifacts/{path}" result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("ProjectJob") @exc.on_http_error(exc.GitlabGetError) - def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): + def trace( + self, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Dict[str, Any]: """Get the job trace. Args: @@ -181,10 +214,18 @@ def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) - return utils.response_content(result, streamed, action, chunk_size) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) + return_value = utils.response_content(result, streamed, action, chunk_size) + if TYPE_CHECKING: + assert isinstance(return_value, dict) + return return_value class ProjectJobManager(RetrieveMixin, RESTManager): _path = "/projects/{project_id}/jobs" _obj_cls = ProjectJob _from_parent_attrs = {"project_id": "id"} + + def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> ProjectJob: + return cast(ProjectJob, super().get(id=id, lazy=lazy, **kwargs)) diff --git a/gitlab/v4/objects/labels.py b/gitlab/v4/objects/labels.py index d2deaa527..f89985213 100644 --- a/gitlab/v4/objects/labels.py +++ b/gitlab/v4/objects/labels.py @@ -1,3 +1,5 @@ +from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union + from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject from gitlab.mixins import ( @@ -22,10 +24,11 @@ class GroupLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "name" + manager: "GroupLabelManager" # Update without ID, but we need an ID to get from list. @exc.on_http_error(exc.GitlabUpdateError) - def save(self, **kwargs): + def save(self, **kwargs: Any) -> None: """Saves the changes made to the object to the server. The object is updated to match what the server returns. @@ -56,7 +59,14 @@ class GroupLabelManager(ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTMa ) # Update without ID. - def update(self, name, new_data=None, **kwargs): + # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore + # type error + def update( # type: ignore + self, + name: Optional[str], + new_data: Optional[Dict[str, Any]] = None, + **kwargs: Any + ) -> Dict[str, Any]: """Update a Label on the server. Args: @@ -70,7 +80,9 @@ def update(self, name, new_data=None, **kwargs): # Delete without ID. @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, name, **kwargs): + # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore + # type error + def delete(self, name: str, **kwargs: Any) -> None: # type: ignore """Delete a Label on the server. Args: @@ -81,6 +93,8 @@ def delete(self, name, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ + if TYPE_CHECKING: + assert self.path is not None self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) @@ -88,10 +102,11 @@ class ProjectLabel( PromoteMixin, SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject ): _id_attr = "name" + manager: "ProjectLabelManager" # Update without ID, but we need an ID to get from list. @exc.on_http_error(exc.GitlabUpdateError) - def save(self, **kwargs): + def save(self, **kwargs: Any) -> None: """Saves the changes made to the object to the server. The object is updated to match what the server returns. @@ -123,8 +138,20 @@ class ProjectLabelManager( required=("name",), optional=("new_name", "color", "description", "priority") ) + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectLabel: + return cast(ProjectLabel, super().get(id=id, lazy=lazy, **kwargs)) + # Update without ID. - def update(self, name, new_data=None, **kwargs): + # NOTE(jlvillal): Signature doesn't match UpdateMixin.update() so ignore + # type error + def update( # type: ignore + self, + name: Optional[str], + new_data: Optional[Dict[str, Any]] = None, + **kwargs: Any + ) -> Dict[str, Any]: """Update a Label on the server. Args: @@ -138,7 +165,9 @@ def update(self, name, new_data=None, **kwargs): # Delete without ID. @exc.on_http_error(exc.GitlabDeleteError) - def delete(self, name, **kwargs): + # NOTE(jlvillal): Signature doesn't match DeleteMixin.delete() so ignore + # type error + def delete(self, name: str, **kwargs: Any) -> None: # type: ignore """Delete a Label on the server. Args: @@ -149,4 +178,6 @@ def delete(self, name, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabDeleteError: If the server cannot perform the request """ + if TYPE_CHECKING: + assert self.path is not None self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs) diff --git a/gitlab/v4/objects/milestones.py b/gitlab/v4/objects/milestones.py index 4d73451b0..8ba9d6161 100644 --- a/gitlab/v4/objects/milestones.py +++ b/gitlab/v4/objects/milestones.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, cast, TYPE_CHECKING, Union from gitlab import cli from gitlab import exceptions as exc @@ -26,7 +26,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("GroupMilestone") @exc.on_http_error(exc.GitlabListError) - def issues(self, **kwargs): + def issues(self, **kwargs: Any) -> RESTObjectList: """List issues related to this milestone. Args: @@ -47,13 +47,15 @@ def issues(self, **kwargs): path = f"{self.manager.path}/{self.get_id()}/issues" data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + if TYPE_CHECKING: + assert isinstance(data_list, RESTObjectList) 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("GroupMilestone") @exc.on_http_error(exc.GitlabListError) - def merge_requests(self, **kwargs): + def merge_requests(self, **kwargs: Any) -> RESTObjectList: """List the merge requests related to this milestone. Args: @@ -73,6 +75,8 @@ def merge_requests(self, **kwargs): """ path = f"{self.manager.path}/{self.get_id()}/merge_requests" data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + if TYPE_CHECKING: + assert isinstance(data_list, RESTObjectList) manager = GroupIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, GroupMergeRequest, data_list) @@ -91,6 +95,11 @@ class GroupMilestoneManager(CRUDMixin, RESTManager): _list_filters = ("iids", "state", "search") _types = {"iids": types.ListAttribute} + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> GroupMilestone: + return cast(GroupMilestone, super().get(id=id, lazy=lazy, **kwargs)) + class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = "title" @@ -98,7 +107,7 @@ class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectMilestone") @exc.on_http_error(exc.GitlabListError) - def issues(self, **kwargs): + def issues(self, **kwargs: Any) -> RESTObjectList: """List issues related to this milestone. Args: @@ -119,6 +128,8 @@ def issues(self, **kwargs): path = f"{self.manager.path}/{self.get_id()}/issues" data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + if TYPE_CHECKING: + assert isinstance(data_list, RESTObjectList) manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) # FIXME(gpocentek): the computed manager path is not correct return RESTObjectList(manager, ProjectIssue, data_list) @@ -145,6 +156,8 @@ def merge_requests(self, **kwargs: Any) -> RESTObjectList: """ path = f"{self.manager.path}/{self.get_id()}/merge_requests" data_list = self.manager.gitlab.http_list(path, as_list=False, **kwargs) + if TYPE_CHECKING: + assert isinstance(data_list, RESTObjectList) manager = ProjectMergeRequestManager( self.manager.gitlab, parent=self.manager._parent ) @@ -165,3 +178,8 @@ class ProjectMilestoneManager(CRUDMixin, RESTManager): ) _list_filters = ("iids", "state", "search") _types = {"iids": types.ListAttribute} + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectMilestone: + return cast(ProjectMilestone, super().get(id=id, lazy=lazy, **kwargs)) diff --git a/gitlab/v4/objects/pipelines.py b/gitlab/v4/objects/pipelines.py index 66199b2d0..56da896a9 100644 --- a/gitlab/v4/objects/pipelines.py +++ b/gitlab/v4/objects/pipelines.py @@ -1,3 +1,7 @@ +from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union + +import requests + from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RequiredOptional, RESTManager, RESTObject @@ -52,7 +56,7 @@ class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectPipeline") @exc.on_http_error(exc.GitlabPipelineCancelError) - def cancel(self, **kwargs): + def cancel(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: """Cancel the job. Args: @@ -67,7 +71,7 @@ def cancel(self, **kwargs): @cli.register_custom_action("ProjectPipeline") @exc.on_http_error(exc.GitlabPipelineRetryError) - def retry(self, **kwargs): + def retry(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: """Retry the job. Args: @@ -98,7 +102,14 @@ class ProjectPipelineManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManage ) _create_attrs = RequiredOptional(required=("ref",)) - def create(self, data, **kwargs): + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectPipeline: + return cast(ProjectPipeline, super().get(id=id, lazy=lazy, **kwargs)) + + def create( + self, data: Optional[Dict[str, Any]] = None, **kwargs: Any + ) -> ProjectPipeline: """Creates a new object. Args: @@ -114,8 +125,12 @@ def create(self, data, **kwargs): RESTObject: A new instance of the managed object class build with the data sent by the server """ + if TYPE_CHECKING: + assert self.path is not None path = self.path[:-1] # drop the 's' - return CreateMixin.create(self, data, path=path, **kwargs) + return cast( + ProjectPipeline, CreateMixin.create(self, data, path=path, **kwargs) + ) class ProjectPipelineJob(RESTObject): @@ -169,7 +184,7 @@ class ProjectPipelineSchedule(SaveMixin, ObjectDeleteMixin, RESTObject): @cli.register_custom_action("ProjectPipelineSchedule") @exc.on_http_error(exc.GitlabOwnershipError) - def take_ownership(self, **kwargs): + def take_ownership(self, **kwargs: Any) -> None: """Update the owner of a pipeline schedule. Args: @@ -181,11 +196,13 @@ def take_ownership(self, **kwargs): """ path = f"{self.manager.path}/{self.get_id()}/take_ownership" server_data = self.manager.gitlab.http_post(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) self._update_attrs(server_data) @cli.register_custom_action("ProjectPipelineSchedule") @exc.on_http_error(exc.GitlabPipelinePlayError) - def play(self, **kwargs): + def play(self, **kwargs: Any) -> Dict[str, Any]: """Trigger a new scheduled pipeline, which runs immediately. The next scheduled run of this pipeline is not affected. @@ -198,6 +215,8 @@ def play(self, **kwargs): """ path = f"{self.manager.path}/{self.get_id()}/play" server_data = self.manager.gitlab.http_post(path, **kwargs) + if TYPE_CHECKING: + assert isinstance(server_data, dict) self._update_attrs(server_data) return server_data @@ -213,6 +232,11 @@ class ProjectPipelineScheduleManager(CRUDMixin, RESTManager): optional=("description", "ref", "cron", "cron_timezone", "active"), ) + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectPipelineSchedule: + return cast(ProjectPipelineSchedule, super().get(id=id, lazy=lazy, **kwargs)) + class ProjectPipelineTestReport(RESTObject): _id_attr = None diff --git a/gitlab/v4/objects/repositories.py b/gitlab/v4/objects/repositories.py index e1067bdf9..18b0f8f84 100644 --- a/gitlab/v4/objects/repositories.py +++ b/gitlab/v4/objects/repositories.py @@ -3,23 +3,36 @@ Currently this module only contains repository-related methods for projects. """ +from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING, Union +import requests + +import gitlab from gitlab import cli from gitlab import exceptions as exc from gitlab import utils +if TYPE_CHECKING: + # When running mypy we use these as the base classes + _RestObjectBase = gitlab.base.RESTObject +else: + _RestObjectBase = object + -class RepositoryMixin: +class RepositoryMixin(_RestObjectBase): @cli.register_custom_action("Project", ("submodule", "branch", "commit_sha")) @exc.on_http_error(exc.GitlabUpdateError) - def update_submodule(self, submodule, branch, commit_sha, **kwargs): + def update_submodule( + self, submodule: str, branch: str, commit_sha: str, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Update a project submodule Args: submodule (str): Full path to the submodule branch (str): Name of the branch to commit into commit_sha (str): Full commit SHA to update the submodule to - commit_message (str): Commit message. If no message is provided, a default one will be set (optional) + commit_message (str): Commit message. If no message is provided, a + default one will be set (optional) Raises: GitlabAuthenticationError: If authentication is not correct @@ -35,7 +48,9 @@ def update_submodule(self, submodule, branch, commit_sha, **kwargs): @cli.register_custom_action("Project", tuple(), ("path", "ref", "recursive")) @exc.on_http_error(exc.GitlabGetError) - def repository_tree(self, path="", ref="", recursive=False, **kwargs): + def repository_tree( + self, path: str = "", ref: str = "", recursive: bool = False, **kwargs: Any + ) -> Union[gitlab.client.GitlabList, List[Dict[str, Any]]]: """Return a list of files in the repository. Args: @@ -57,7 +72,7 @@ def repository_tree(self, path="", ref="", recursive=False, **kwargs): list: The representation of the tree """ gl_path = f"/projects/{self.get_id()}/repository/tree" - query_data = {"recursive": recursive} + query_data: Dict[str, Any] = {"recursive": recursive} if path: query_data["path"] = path if ref: @@ -66,7 +81,9 @@ def repository_tree(self, path="", ref="", recursive=False, **kwargs): @cli.register_custom_action("Project", ("sha",)) @exc.on_http_error(exc.GitlabGetError) - def repository_blob(self, sha, **kwargs): + def repository_blob( + self, sha: str, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Return a file by blob SHA. Args: @@ -87,8 +104,13 @@ def repository_blob(self, sha, **kwargs): @cli.register_custom_action("Project", ("sha",)) @exc.on_http_error(exc.GitlabGetError) def repository_raw_blob( - self, sha, streamed=False, action=None, chunk_size=1024, **kwargs - ): + self, + sha: str, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Optional[bytes]: """Return the raw file contents for a blob. Args: @@ -112,11 +134,15 @@ def repository_raw_blob( result = self.manager.gitlab.http_get( path, streamed=streamed, raw=True, **kwargs ) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project", ("from_", "to")) @exc.on_http_error(exc.GitlabGetError) - def repository_compare(self, from_, to, **kwargs): + def repository_compare( + self, from_: str, to: str, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Return a diff between two branches/commits. Args: @@ -137,7 +163,9 @@ def repository_compare(self, from_, to, **kwargs): @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabGetError) - def repository_contributors(self, **kwargs): + def repository_contributors( + self, **kwargs: Any + ) -> Union[gitlab.client.GitlabList, List[Dict[str, Any]]]: """Return a list of contributors for the project. Args: @@ -161,8 +189,13 @@ def repository_contributors(self, **kwargs): @cli.register_custom_action("Project", tuple(), ("sha",)) @exc.on_http_error(exc.GitlabListError) def repository_archive( - self, sha=None, streamed=False, action=None, chunk_size=1024, **kwargs - ): + self, + sha: str = None, + streamed: bool = False, + action: Optional[Callable[..., Any]] = None, + chunk_size: int = 1024, + **kwargs: Any, + ) -> Optional[bytes]: """Return a tarball of the repository. Args: @@ -189,11 +222,13 @@ def repository_archive( result = self.manager.gitlab.http_get( path, query_data=query_data, raw=True, streamed=streamed, **kwargs ) + if TYPE_CHECKING: + assert isinstance(result, requests.Response) return utils.response_content(result, streamed, action, chunk_size) @cli.register_custom_action("Project") @exc.on_http_error(exc.GitlabDeleteError) - def delete_merged_branches(self, **kwargs): + def delete_merged_branches(self, **kwargs: Any) -> None: """Delete merged branches. Args: diff --git a/gitlab/v4/objects/services.py b/gitlab/v4/objects/services.py index 3d7d37727..a62fdf0c2 100644 --- a/gitlab/v4/objects/services.py +++ b/gitlab/v4/objects/services.py @@ -1,3 +1,5 @@ +from typing import Any, cast, Dict, List, Optional, Union + from gitlab import cli from gitlab.base import RESTManager, RESTObject from gitlab.mixins import ( @@ -253,7 +255,9 @@ class ProjectServiceManager(GetMixin, UpdateMixin, DeleteMixin, ListMixin, RESTM "youtrack": (("issues_url", "project_url"), ("description", "push_events")), } - def get(self, id, **kwargs): + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> ProjectService: """Retrieve a single object. Args: @@ -270,11 +274,16 @@ def get(self, id, **kwargs): GitlabAuthenticationError: If authentication is not correct GitlabGetError: If the server cannot perform the request """ - obj = super(ProjectServiceManager, self).get(id, **kwargs) + obj = cast(ProjectService, super(ProjectServiceManager, self).get(id, **kwargs)) obj.id = id return obj - def update(self, id=None, new_data=None, **kwargs): + def update( + self, + id: Optional[Union[str, int]] = None, + new_data: Optional[Dict[str, Any]] = None, + **kwargs: Any + ) -> Dict[str, Any]: """Update an object on the server. Args: @@ -290,11 +299,12 @@ def update(self, id=None, new_data=None, **kwargs): GitlabUpdateError: If the server cannot perform the request """ new_data = new_data or {} - super(ProjectServiceManager, self).update(id, new_data, **kwargs) + result = super(ProjectServiceManager, self).update(id, new_data, **kwargs) self.id = id + return result @cli.register_custom_action("ProjectServiceManager") - def available(self, **kwargs): + def available(self, **kwargs: Any) -> List[str]: """List the services known by python-gitlab. Returns: diff --git a/gitlab/v4/objects/sidekiq.py b/gitlab/v4/objects/sidekiq.py index dc1094aff..9e00fe4e4 100644 --- a/gitlab/v4/objects/sidekiq.py +++ b/gitlab/v4/objects/sidekiq.py @@ -1,3 +1,7 @@ +from typing import Any, Dict, Union + +import requests + from gitlab import cli from gitlab import exceptions as exc from gitlab.base import RESTManager @@ -16,7 +20,7 @@ class SidekiqManager(RESTManager): @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def queue_metrics(self, **kwargs): + def queue_metrics(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: """Return the registered queues information. Args: @@ -33,7 +37,9 @@ def queue_metrics(self, **kwargs): @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def process_metrics(self, **kwargs): + def process_metrics( + self, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Return the registered sidekiq workers. Args: @@ -50,7 +56,7 @@ def process_metrics(self, **kwargs): @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def job_stats(self, **kwargs): + def job_stats(self, **kwargs: Any) -> Union[Dict[str, Any], requests.Response]: """Return statistics about the jobs performed. Args: @@ -67,7 +73,9 @@ def job_stats(self, **kwargs): @cli.register_custom_action("SidekiqManager") @exc.on_http_error(exc.GitlabGetError) - def compound_metrics(self, **kwargs): + def compound_metrics( + self, **kwargs: Any + ) -> Union[Dict[str, Any], requests.Response]: """Return all available metrics and statistics. Args: diff --git a/pyproject.toml b/pyproject.toml index 12df1dfdd..8e0c4b469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,17 +12,6 @@ files = "." module = [ "docs.*", "docs.ext.*", - "gitlab.v4.objects.epics", - "gitlab.v4.objects.files", - "gitlab.v4.objects.geo_nodes", - "gitlab.v4.objects.issues", - "gitlab.v4.objects.jobs", - "gitlab.v4.objects.labels", - "gitlab.v4.objects.milestones", - "gitlab.v4.objects.pipelines", - "gitlab.v4.objects.repositories", - "gitlab.v4.objects.services", - "gitlab.v4.objects.sidekiq", "tests.functional.*", "tests.functional.api.*", "tests.meta.*",