44from collections .abc import Callable
55
66from mypy .build import PRI_MYPY
7- from mypy .errorcodes import ATTR_DEFINED
7+ from mypy .errorcodes import ATTR_DEFINED , ErrorCode
88from mypy .messages import format_type
9- from mypy .nodes import ARG_POS , MDEF , MypyFile , SymbolTableNode , TypeInfo , Var
9+ from mypy .nodes import ARG_POS , MDEF , FuncDef , MypyFile , SymbolTableNode , TypeInfo , Var
1010from mypy .plugin import (
1111 AttributeContext ,
1212 ClassDefContext ,
2424 Instance ,
2525 NoneType ,
2626 Type ,
27+ TypedDictType ,
2728 TypeOfAny ,
2829 UnionType ,
2930)
3031
32+ SERIALIZE_TYPEDDICT_CODE = ErrorCode (
33+ "serialize-typeddict" , "Serialize method must return a TypedDict" , "attr-defined"
34+ )
35+
36+ SERIALIZER_BASE_CLASS = "sentry.api.serializers.base.Serializer"
37+
38+
39+ def _is_typed_dict (typ : Type ) -> bool :
40+ """Check if a type is a TypedDict"""
41+ if isinstance (typ , TypedDictType ):
42+ return True
43+
44+ return False
45+
3146
3247def _make_using_required_str (ctx : FunctionSigContext ) -> CallableType :
3348 sig = ctx .default_signature
@@ -173,6 +188,44 @@ def _lazy_service_wrapper_attribute(ctx: AttributeContext, *, attr: str) -> Type
173188 return member
174189
175190
191+ def _check_serializer_class (ctx : ClassDefContext ) -> None :
192+ """
193+ Hook that checks if subclasses of Serializer have a serialize method that returns a TypedDict.
194+ """
195+ if not ctx .cls .info .has_base (SERIALIZER_BASE_CLASS ):
196+ return
197+
198+ # Look for the serialize method in the class
199+ for name , node in ctx .cls .info .names .items ():
200+ if name == "serialize" and isinstance (node .node , FuncDef ):
201+ # First check if the method has a return type annotation at all
202+ if not node .node .type :
203+ ctx .api .fail (
204+ "Method 'serialize' must have an explicit return type annotation in classes inheriting from Serializer" ,
205+ node .node ,
206+ code = SERIALIZE_TYPEDDICT_CODE ,
207+ )
208+ continue
209+
210+ # Then check if it's a callable with a proper return type
211+ if isinstance (node .node .type , CallableType ):
212+ ret_type = node .node .type .ret_type
213+ if not _is_typed_dict (ret_type ):
214+ print (ret_type , _is_typed_dict (ret_type ))
215+ ctx .api .fail (
216+ "Method 'serialize' must return a TypedDict in classes inheriting from Serializer" ,
217+ node .node ,
218+ code = SERIALIZE_TYPEDDICT_CODE ,
219+ )
220+ else :
221+ # This case should be rare, but handle it for completeness
222+ ctx .api .fail (
223+ "Method 'serialize' has an invalid type annotation" ,
224+ node .node ,
225+ code = SERIALIZE_TYPEDDICT_CODE ,
226+ )
227+
228+
176229class SentryMypyPlugin (Plugin ):
177230 def get_function_signature_hook (
178231 self , fullname : str
@@ -181,7 +234,9 @@ def get_function_signature_hook(
181234
182235 def get_base_class_hook (self , fullname : str ) -> Callable [[ClassDefContext ], None ] | None :
183236 # XXX: this is a hack -- I don't know if there's a better callback to modify a class
184- if fullname == "_io.BytesIO" :
237+ if fullname == SERIALIZER_BASE_CLASS :
238+ return _check_serializer_class
239+ elif fullname == "_io.BytesIO" :
185240 return _adjust_http_request_members
186241 elif fullname == "django.http.request.HttpRequest" :
187242 return _adjust_request_members
0 commit comments