4
4
Original notes by John L. Villalovos
5
5
6
6
"""
7
+ import dataclasses
8
+ import functools
7
9
import inspect
8
- from typing import Tuple , Type
10
+ from typing import Optional , Type
9
11
10
12
import _pytest
11
13
12
14
import gitlab .mixins
13
15
import gitlab .v4 .objects
14
16
15
17
18
+ @functools .total_ordering
19
+ @dataclasses .dataclass (frozen = True )
20
+ class ClassInfo :
21
+ name : str
22
+ type : Type
23
+
24
+ def __lt__ (self , other : object ) -> bool :
25
+ if not isinstance (other , ClassInfo ):
26
+ return NotImplemented
27
+ return (self .type .__module__ , self .name ) < (other .type .__module__ , other .name )
28
+
29
+ def __eq__ (self , other : object ) -> bool :
30
+ if not isinstance (other , ClassInfo ):
31
+ return NotImplemented
32
+ return (self .type .__module__ , self .name ) == (other .type .__module__ , other .name )
33
+
34
+
16
35
def pytest_generate_tests (metafunc : _pytest .python .Metafunc ) -> None :
17
36
"""Find all of the classes in gitlab.v4.objects and pass them to our test
18
37
function"""
@@ -35,38 +54,84 @@ def pytest_generate_tests(metafunc: _pytest.python.Metafunc) -> None:
35
54
if not class_name .endswith ("Manager" ):
36
55
continue
37
56
38
- class_info_set .add ((class_name , class_value ))
57
+ class_info_set .add (ClassInfo (name = class_name , type = class_value ))
58
+
59
+ metafunc .parametrize ("class_info" , sorted (class_info_set ))
39
60
40
- metafunc .parametrize ("class_info" , class_info_set )
61
+
62
+ GET_ID_METHOD_TEMPLATE = """
63
+ def get(
64
+ self, id: Union[str, int], lazy: bool = False, **kwargs: Any
65
+ ) -> {obj_cls.__name__}:
66
+ return cast({obj_cls.__name__}, super().get(id=id, lazy=lazy, **kwargs))
67
+
68
+ You may also need to add the following imports:
69
+ from typing import Any, cast, Union"
70
+ """
71
+
72
+ GET_WITHOUT_ID_METHOD_TEMPLATE = """
73
+ def get(
74
+ self, id: Optional[Union[int, str]] = None, **kwargs: Any
75
+ ) -> Optional[{obj_cls.__name__}]:
76
+ return cast(Optional[{obj_cls.__name__}], super().get(id=id, **kwargs))
77
+
78
+ You may also need to add the following imports:
79
+ from typing import Any, cast, Optional, Union"
80
+ """
41
81
42
82
43
83
class TestTypeHints :
44
- def test_check_get_function_type_hints (self , class_info : Tuple [ str , Type ] ) -> None :
84
+ def test_check_get_function_type_hints (self , class_info : ClassInfo ) -> None :
45
85
"""Ensure classes derived from GetMixin have defined a 'get()' method with
46
86
correct type-hints.
47
87
"""
48
- class_name , class_value = class_info
49
- if not class_name .endswith ("Manager" ):
50
- return
88
+ self .get_check_helper (
89
+ base_type = gitlab .mixins .GetMixin ,
90
+ class_info = class_info ,
91
+ method_template = GET_ID_METHOD_TEMPLATE ,
92
+ optional_return = False ,
93
+ )
51
94
52
- mro = class_value .mro ()
95
+ def test_check_get_without_id_function_type_hints (
96
+ self , class_info : ClassInfo
97
+ ) -> None :
98
+ """Ensure classes derived from GetMixin have defined a 'get()' method with
99
+ correct type-hints.
100
+ """
101
+ self .get_check_helper (
102
+ base_type = gitlab .mixins .GetWithoutIdMixin ,
103
+ class_info = class_info ,
104
+ method_template = GET_WITHOUT_ID_METHOD_TEMPLATE ,
105
+ optional_return = True ,
106
+ )
107
+
108
+ def get_check_helper (
109
+ self ,
110
+ * ,
111
+ base_type : Type ,
112
+ class_info : ClassInfo ,
113
+ method_template : str ,
114
+ optional_return : bool ,
115
+ ) -> None :
116
+ if not class_info .name .endswith ("Manager" ):
117
+ return
118
+ mro = class_info .type .mro ()
53
119
# The class needs to be derived from GetMixin or we ignore it
54
- if gitlab . mixins . GetMixin not in mro :
120
+ if base_type not in mro :
55
121
return
56
122
57
- obj_cls = class_value ._obj_cls
58
- signature = inspect .signature (class_value .get )
59
- filename = inspect .getfile (class_value )
123
+ obj_cls = class_info . type ._obj_cls
124
+ signature = inspect .signature (class_info . type .get )
125
+ filename = inspect .getfile (class_info . type )
60
126
61
127
fail_message = (
62
- f"class definition for { class_name !r} in file { filename !r} "
128
+ f"class definition for { class_info . name !r} in file { filename !r} "
63
129
f"must have defined a 'get' method with a return annotation of "
64
130
f"{ obj_cls } but found { signature .return_annotation } \n "
65
131
f"Recommend adding the followinng method:\n "
66
- f"def get(\n "
67
- f" self, id: Union[str, int], lazy: bool = False, **kwargs: Any\n "
68
- f" ) -> { obj_cls .__name__ } :\n "
69
- f" return cast({ obj_cls .__name__ } , super().get(id=id, lazy=lazy, "
70
- f"**kwargs))\n "
71
132
)
72
- assert obj_cls == signature .return_annotation , fail_message
133
+ fail_message += method_template .format (obj_cls = obj_cls )
134
+ check_type = obj_cls
135
+ if optional_return :
136
+ check_type = Optional [obj_cls ]
137
+ assert check_type == signature .return_annotation , fail_message
0 commit comments