44Original notes by John L. Villalovos
55
66"""
7+ import dataclasses
8+ import functools
79import inspect
8- from typing import Tuple ,Type
10+ from typing import Optional ,Type
911
1012import _pytest
1113
1214import gitlab .mixins
1315import gitlab .v4 .objects
1416
1517
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+
1635def pytest_generate_tests (metafunc :_pytest .python .Metafunc )-> None :
1736"""Find all of the classes in gitlab.v4.objects and pass them to our test
1837 function"""
@@ -35,38 +54,84 @@ def pytest_generate_tests(metafunc: _pytest.python.Metafunc) -> None:
3554if not class_name .endswith ("Manager" ):
3655continue
3756
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 ))
3960
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+ """
4181
4282
4383class 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 :
4585"""Ensure classes derived from GetMixin have defined a 'get()' method with
4686 correct type-hints.
4787 """
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+ )
5194
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 ()
53119# 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 :
55121return
56122
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 )
60126
61127fail_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} "
63129f"must have defined a 'get' method with a return annotation of "
64130f"{ obj_cls } but found{ signature .return_annotation } \n "
65131f"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 "
71132 )
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