Skip to content

Commit 3df0d8a

Browse files
Ensure get() methods have correct type-hints
Fix classes which don't have correct 'get()' methods.
1 parent 472b300 commit 3df0d8a

File tree

8 files changed

+211
-0
lines changed

8 files changed

+211
-0
lines changed

gitlab/v4/objects/award_emojis.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RequiredOptional, RESTManager, RESTObject
24
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin
35

@@ -27,6 +29,11 @@ class ProjectIssueAwardEmojiManager(NoUpdateMixin, RESTManager):
2729
_from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"}
2830
_create_attrs = RequiredOptional(required=("name",))
2931

32+
def get(
33+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
34+
) -> ProjectIssueAwardEmoji:
35+
return cast(ProjectIssueAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
36+
3037

3138
class ProjectIssueNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
3239
pass
@@ -44,6 +51,11 @@ class ProjectIssueNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
4451
}
4552
_create_attrs = RequiredOptional(required=("name",))
4653

54+
def get(
55+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
56+
) -> ProjectIssueNoteAwardEmoji:
57+
return cast(ProjectIssueNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
58+
4759

4860
class ProjectMergeRequestAwardEmoji(ObjectDeleteMixin, RESTObject):
4961
pass
@@ -55,6 +67,13 @@ class ProjectMergeRequestAwardEmojiManager(NoUpdateMixin, RESTManager):
5567
_from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
5668
_create_attrs = RequiredOptional(required=("name",))
5769

70+
def get(
71+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
72+
) -> ProjectMergeRequestAwardEmoji:
73+
return cast(
74+
ProjectMergeRequestAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
75+
)
76+
5877

5978
class ProjectMergeRequestNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
6079
pass
@@ -73,6 +92,13 @@ class ProjectMergeRequestNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
7392
}
7493
_create_attrs = RequiredOptional(required=("name",))
7594

95+
def get(
96+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
97+
) -> ProjectMergeRequestNoteAwardEmoji:
98+
return cast(
99+
ProjectMergeRequestNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
100+
)
101+
76102

77103
class ProjectSnippetAwardEmoji(ObjectDeleteMixin, RESTObject):
78104
pass
@@ -84,6 +110,11 @@ class ProjectSnippetAwardEmojiManager(NoUpdateMixin, RESTManager):
84110
_from_parent_attrs = {"project_id": "project_id", "snippet_id": "id"}
85111
_create_attrs = RequiredOptional(required=("name",))
86112

113+
def get(
114+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
115+
) -> ProjectSnippetAwardEmoji:
116+
return cast(ProjectSnippetAwardEmoji, super().get(id=id, lazy=lazy, **kwargs))
117+
87118

88119
class ProjectSnippetNoteAwardEmoji(ObjectDeleteMixin, RESTObject):
89120
pass
@@ -101,3 +132,10 @@ class ProjectSnippetNoteAwardEmojiManager(NoUpdateMixin, RESTManager):
101132
"note_id": "id",
102133
}
103134
_create_attrs = RequiredOptional(required=("name",))
135+
136+
def get(
137+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
138+
) -> ProjectSnippetNoteAwardEmoji:
139+
return cast(
140+
ProjectSnippetNoteAwardEmoji, super().get(id=id, lazy=lazy, **kwargs)
141+
)

gitlab/v4/objects/custom_attributes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RESTManager, RESTObject
24
from gitlab.mixins import DeleteMixin, ObjectDeleteMixin, RetrieveMixin, SetMixin
35

@@ -39,3 +41,8 @@ class UserCustomAttributeManager(RetrieveMixin, SetMixin, DeleteMixin, RESTManag
3941
_path = "/users/%(user_id)s/custom_attributes"
4042
_obj_cls = UserCustomAttribute
4143
_from_parent_attrs = {"user_id": "id"}
44+
45+
def get(
46+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
47+
) -> UserCustomAttribute:
48+
return cast(UserCustomAttribute, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/discussions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RequiredOptional, RESTManager, RESTObject
24
from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin
35

@@ -30,6 +32,11 @@ class ProjectCommitDiscussionManager(RetrieveMixin, CreateMixin, RESTManager):
3032
_from_parent_attrs = {"project_id": "project_id", "commit_id": "id"}
3133
_create_attrs = RequiredOptional(required=("body",), optional=("created_at",))
3234

35+
def get(
36+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
37+
) -> ProjectCommitDiscussion:
38+
return cast(ProjectCommitDiscussion, super().get(id=id, lazy=lazy, **kwargs))
39+
3340

3441
class ProjectIssueDiscussion(RESTObject):
3542
notes: ProjectIssueDiscussionNoteManager
@@ -41,6 +48,11 @@ class ProjectIssueDiscussionManager(RetrieveMixin, CreateMixin, RESTManager):
4148
_from_parent_attrs = {"project_id": "project_id", "issue_iid": "iid"}
4249
_create_attrs = RequiredOptional(required=("body",), optional=("created_at",))
4350

51+
def get(
52+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
53+
) -> ProjectIssueDiscussion:
54+
return cast(ProjectIssueDiscussion, super().get(id=id, lazy=lazy, **kwargs))
55+
4456

4557
class ProjectMergeRequestDiscussion(SaveMixin, RESTObject):
4658
notes: ProjectMergeRequestDiscussionNoteManager
@@ -57,6 +69,13 @@ class ProjectMergeRequestDiscussionManager(
5769
)
5870
_update_attrs = RequiredOptional(required=("resolved",))
5971

72+
def get(
73+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
74+
) -> ProjectMergeRequestDiscussion:
75+
return cast(
76+
ProjectMergeRequestDiscussion, super().get(id=id, lazy=lazy, **kwargs)
77+
)
78+
6079

6180
class ProjectSnippetDiscussion(RESTObject):
6281
notes: ProjectSnippetDiscussionNoteManager
@@ -67,3 +86,8 @@ class ProjectSnippetDiscussionManager(RetrieveMixin, CreateMixin, RESTManager):
6786
_obj_cls = ProjectSnippetDiscussion
6887
_from_parent_attrs = {"project_id": "project_id", "snippet_id": "id"}
6988
_create_attrs = RequiredOptional(required=("body",), optional=("created_at",))
89+
90+
def get(
91+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
92+
) -> ProjectSnippetDiscussion:
93+
return cast(ProjectSnippetDiscussion, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/notes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RequiredOptional, RESTManager, RESTObject
24
from gitlab.mixins import (
35
CreateMixin,
@@ -147,6 +149,11 @@ class ProjectSnippetNoteManager(CRUDMixin, RESTManager):
147149
_create_attrs = RequiredOptional(required=("body",))
148150
_update_attrs = RequiredOptional(required=("body",))
149151

152+
def get(
153+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
154+
) -> ProjectSnippetNote:
155+
return cast(ProjectSnippetNote, super().get(id=id, lazy=lazy, **kwargs))
156+
150157

151158
class ProjectSnippetDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject):
152159
pass

gitlab/v4/objects/tags.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RequiredOptional, RESTManager, RESTObject
24
from gitlab.mixins import NoUpdateMixin, ObjectDeleteMixin
35

@@ -22,6 +24,9 @@ class ProjectTagManager(NoUpdateMixin, RESTManager):
2224
required=("tag_name", "ref"), optional=("message",)
2325
)
2426

27+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> ProjectTag:
28+
return cast(ProjectTag, super().get(id=id, lazy=lazy, **kwargs))
29+
2530

2631
class ProjectProtectedTag(ObjectDeleteMixin, RESTObject):
2732
_id_attr = "name"
@@ -35,3 +40,8 @@ class ProjectProtectedTagManager(NoUpdateMixin, RESTManager):
3540
_create_attrs = RequiredOptional(
3641
required=("name",), optional=("create_access_level",)
3742
)
43+
44+
def get(
45+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
46+
) -> ProjectProtectedTag:
47+
return cast(ProjectProtectedTag, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/templates.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, cast, Union
2+
13
from gitlab.base import RESTManager, RESTObject
24
from gitlab.mixins import RetrieveMixin
35

@@ -21,6 +23,9 @@ class DockerfileManager(RetrieveMixin, RESTManager):
2123
_path = "/templates/dockerfiles"
2224
_obj_cls = Dockerfile
2325

26+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Dockerfile:
27+
return cast(Dockerfile, super().get(id=id, lazy=lazy, **kwargs))
28+
2429

2530
class Gitignore(RESTObject):
2631
_id_attr = "name"
@@ -30,6 +35,9 @@ class GitignoreManager(RetrieveMixin, RESTManager):
3035
_path = "/templates/gitignores"
3136
_obj_cls = Gitignore
3237

38+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Gitignore:
39+
return cast(Gitignore, super().get(id=id, lazy=lazy, **kwargs))
40+
3341

3442
class Gitlabciyml(RESTObject):
3543
_id_attr = "name"
@@ -39,6 +47,11 @@ class GitlabciymlManager(RetrieveMixin, RESTManager):
3947
_path = "/templates/gitlab_ci_ymls"
4048
_obj_cls = Gitlabciyml
4149

50+
def get(
51+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
52+
) -> Gitlabciyml:
53+
return cast(Gitlabciyml, super().get(id=id, lazy=lazy, **kwargs))
54+
4255

4356
class License(RESTObject):
4457
_id_attr = "key"
@@ -49,3 +62,6 @@ class LicenseManager(RetrieveMixin, RESTManager):
4962
_obj_cls = License
5063
_list_filters = ("popular",)
5164
_optional_get_attrs = ("project", "fullname")
65+
66+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> License:
67+
return cast(License, super().get(id=id, lazy=lazy, **kwargs))

gitlab/v4/objects/users.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManag
7474
_obj_cls = CurrentUserEmail
7575
_create_attrs = RequiredOptional(required=("email",))
7676

77+
def get(
78+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
79+
) -> CurrentUserEmail:
80+
return cast(CurrentUserEmail, super().get(id=id, lazy=lazy, **kwargs))
81+
7782

7883
class CurrentUserGPGKey(ObjectDeleteMixin, RESTObject):
7984
pass
@@ -84,6 +89,11 @@ class CurrentUserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTMana
8489
_obj_cls = CurrentUserGPGKey
8590
_create_attrs = RequiredOptional(required=("key",))
8691

92+
def get(
93+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
94+
) -> CurrentUserGPGKey:
95+
return cast(CurrentUserGPGKey, super().get(id=id, lazy=lazy, **kwargs))
96+
8797

8898
class CurrentUserKey(ObjectDeleteMixin, RESTObject):
8999
_short_print_attr = "title"
@@ -94,6 +104,11 @@ class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager
94104
_obj_cls = CurrentUserKey
95105
_create_attrs = RequiredOptional(required=("title", "key"))
96106

107+
def get(
108+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
109+
) -> CurrentUserKey:
110+
return cast(CurrentUserKey, super().get(id=id, lazy=lazy, **kwargs))
111+
97112

98113
class CurrentUserStatus(SaveMixin, RESTObject):
99114
_id_attr = None
@@ -357,6 +372,9 @@ class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
357372
_from_parent_attrs = {"user_id": "id"}
358373
_create_attrs = RequiredOptional(required=("email",))
359374

375+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> UserEmail:
376+
return cast(UserEmail, super().get(id=id, lazy=lazy, **kwargs))
377+
360378

361379
class UserActivities(RESTObject):
362380
_id_attr = "username"
@@ -388,6 +406,9 @@ class UserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
388406
_from_parent_attrs = {"user_id": "id"}
389407
_create_attrs = RequiredOptional(required=("key",))
390408

409+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> UserGPGKey:
410+
return cast(UserGPGKey, super().get(id=id, lazy=lazy, **kwargs))
411+
391412

392413
class UserKey(ObjectDeleteMixin, RESTObject):
393414
pass
@@ -424,6 +445,11 @@ class UserImpersonationTokenManager(NoUpdateMixin, RESTManager):
424445
)
425446
_list_filters = ("state",)
426447

448+
def get(
449+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
450+
) -> UserImpersonationToken:
451+
return cast(UserImpersonationToken, super().get(id=id, lazy=lazy, **kwargs))
452+
427453

428454
class UserMembership(RESTObject):
429455
_id_attr = "source_id"
@@ -435,6 +461,11 @@ class UserMembershipManager(RetrieveMixin, RESTManager):
435461
_from_parent_attrs = {"user_id": "id"}
436462
_list_filters = ("type",)
437463

464+
def get(
465+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
466+
) -> UserMembership:
467+
return cast(UserMembership, super().get(id=id, lazy=lazy, **kwargs))
468+
438469

439470
# Having this outside projects avoids circular imports due to ProjectUser
440471
class UserProject(RESTObject):
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Ensure type-hints are setup correctly and detect if missing functions.
3+
4+
Original notes by John L. Villalovos
5+
6+
"""
7+
import inspect
8+
from typing import Tuple, Type
9+
10+
import toml
11+
12+
import gitlab.mixins
13+
import gitlab.v4.objects
14+
15+
16+
def pytest_generate_tests(metafunc):
17+
"""Find all of the classes in gitlab.v4.objects and pass them to our test
18+
function"""
19+
20+
# Ignore any modules that we are ignoring in our pyproject.toml
21+
excluded_modules = set()
22+
with open("pyproject.toml", "r") as in_file:
23+
pyproject = toml.load(in_file)
24+
overrides = pyproject.get("tool", {}).get("mypy", {}).get("overrides", [])
25+
for override in overrides:
26+
if override.get("ignore_errors") is True:
27+
for module in override.get("module", []):
28+
if module.startswith("gitlab.v4.objects"):
29+
excluded_modules.add(module)
30+
31+
class_info_list = []
32+
for module_name, module_value in inspect.getmembers(gitlab.v4.objects):
33+
if not inspect.ismodule(module_value):
34+
# We only care about the modules
35+
continue
36+
# Iterate through all the classes in our module
37+
for class_name, class_value in inspect.getmembers(module_value):
38+
if not inspect.isclass(class_value):
39+
continue
40+
41+
module_name = class_value.__module__
42+
# Ignore modules that mypy is ignoring
43+
if module_name in excluded_modules:
44+
continue
45+
46+
# Ignore imported classes from gitlab.base
47+
if module_name == "gitlab.base":
48+
continue
49+
50+
class_info_list.append((class_name, class_value))
51+
52+
metafunc.parametrize("class_info", class_info_list)
53+
54+
55+
class TestTypeHints:
56+
def test_check_get_function_type_hints(self, class_info: Tuple[str, Type]):
57+
"""Ensure classes derived from GetMixin have defined a 'get()' method with
58+
correct type-hints.
59+
"""
60+
class_name, class_value = class_info
61+
if not class_name.endswith("Manager"):
62+
return
63+
64+
mro = class_value.mro()
65+
# The class needs to be derived from GetMixin or we ignore it
66+
if gitlab.mixins.GetMixin not in mro:
67+
return
68+
69+
obj_cls = class_value._obj_cls
70+
signature = inspect.signature(class_value.get)
71+
filename = inspect.getfile(class_value)
72+
73+
fail_message = (
74+
f"class definition for {class_name!r} in file {filename!r} "
75+
f"must have defined a 'get' method with a return annotation of "
76+
f"{obj_cls} but found {signature.return_annotation}"
77+
)
78+
assert obj_cls == signature.return_annotation, fail_message

0 commit comments

Comments
 (0)