22"""Protoc Plugin to generate mypy stubs."""
33from __future__ import annotations
44
5+ import ast
56import sys
67from collections import defaultdict
78from contextlib import contextmanager
@@ -149,12 +150,14 @@ def __init__(
149150 descriptors : Descriptors ,
150151 readable_stubs : bool ,
151152 relax_strict_optional_primitives : bool ,
153+ use_default_deprecation_warnings : bool ,
152154 grpc : bool ,
153155 ) -> None :
154156 self .fd = fd
155157 self .descriptors = descriptors
156158 self .readable_stubs = readable_stubs
157159 self .relax_strict_optional_primitives = relax_strict_optional_primitives
160+ self .use_default_depreaction_warnings = use_default_deprecation_warnings
158161 self .grpc = grpc
159162 self .lines : List [str ] = []
160163 self .indent = ""
@@ -165,6 +168,7 @@ def __init__(
165168 # if {z} is None, then it shortens to `from {x} import {y}`
166169 self .from_imports : Dict [str , Set [Tuple [str , str | None ]]] = defaultdict (set )
167170 self .typing_extensions_min : Optional [Tuple [int , int ]] = None
171+ self .deprecated_min : Optional [Tuple [int , int ]] = None
168172
169173 # Comments
170174 self .source_code_info_by_scl = {tuple (location .path ): location for location in fd .source_code_info .location }
@@ -180,6 +184,11 @@ def _import(self, path: str, name: str) -> str:
180184 self .typing_extensions_min = stabilization [name ]
181185 return "typing_extensions." + name
182186
187+ if path == "warnings" and name == "deprecated" :
188+ if not self .deprecated_min or self .deprecated_min < (3 , 11 ):
189+ self .deprecated_min = (3 , 13 )
190+ return name
191+
183192 imp = path .replace ("/" , "." )
184193 if self .readable_stubs :
185194 self .from_imports [imp ].add ((name , None ))
@@ -251,6 +260,51 @@ def _has_comments(self, scl: SourceCodeLocation) -> bool:
251260 sci_loc = self .source_code_info_by_scl .get (tuple (scl ))
252261 return sci_loc is not None and bool (sci_loc .leading_detached_comments or sci_loc .leading_comments or sci_loc .trailing_comments )
253262
263+ def _get_comments (self , scl : SourceCodeLocation ) -> List [str ]:
264+ """Return list of comment lines"""
265+ if not self ._has_comments (scl ):
266+ return []
267+
268+ sci_loc = self .source_code_info_by_scl .get (tuple (scl ))
269+ assert sci_loc is not None
270+
271+ leading_detached_lines = []
272+ leading_lines = []
273+ trailing_lines = []
274+ for leading_detached_comment in sci_loc .leading_detached_comments :
275+ leading_detached_lines = self ._break_text (leading_detached_comment )
276+ if sci_loc .leading_comments is not None :
277+ leading_lines = self ._break_text (sci_loc .leading_comments )
278+ # Trailing comments also go in the header - to make sure it gets into the docstring
279+ if sci_loc .trailing_comments is not None :
280+ trailing_lines = self ._break_text (sci_loc .trailing_comments )
281+
282+ lines = leading_detached_lines
283+ if leading_detached_lines and (leading_lines or trailing_lines ):
284+ lines .append ("" )
285+ lines .extend (leading_lines )
286+ lines .extend (trailing_lines )
287+
288+ return lines
289+
290+ def _write_deprecation_warning (self , scl : SourceCodeLocation , default_message : str ) -> None :
291+ msg = default_message
292+ if not self .use_default_depreaction_warnings and (comments := self ._get_comments (scl )):
293+ # Make sure the comment string is a valid python string literal
294+ joined = "\\ n" .join (comments )
295+ # Check that it is valid python string by using ast.parse
296+ try :
297+ ast .parse (f'"""{ joined } """' )
298+ msg = joined
299+ except SyntaxError as e :
300+ print (f"Warning: Deprecation comment { joined } could not be parsed as a python string literal. Using default deprecation message. { e } " , file = sys .stderr )
301+ pass
302+ self ._write_line (
303+ '@{}("""{}""")' ,
304+ self ._import ("warnings" , "deprecated" ),
305+ msg ,
306+ )
307+
254308 def _write_comments (self , scl : SourceCodeLocation ) -> bool :
255309 """Return true if any comments were written"""
256310 if not self ._has_comments (scl ):
@@ -364,6 +418,11 @@ def write_enums(
364418 )
365419 wl ("" )
366420
421+ if enum .options .deprecated :
422+ self ._write_deprecation_warning (
423+ scl + [d .EnumDescriptorProto .OPTIONS_FIELD_NUMBER ] + [d .EnumOptions .DEPRECATED_FIELD_NUMBER ],
424+ "This enum has been marked as deprecated using proto enum options." ,
425+ )
367426 if self ._has_comments (scl ):
368427 wl (f"class { class_name } ({ enum_helper_class } , metaclass={ etw_helper_class } ):" )
369428 with self ._indent ():
@@ -409,6 +468,11 @@ def write_messages(
409468
410469 class_name = desc .name if desc .name not in PYTHON_RESERVED else "_r_" + desc .name
411470 message_class = self ._import ("google.protobuf.message" , "Message" )
471+ if desc .options .deprecated :
472+ self ._write_deprecation_warning (
473+ scl_prefix + [i ] + [d .DescriptorProto .OPTIONS_FIELD_NUMBER ] + [d .MessageOptions .DEPRECATED_FIELD_NUMBER ],
474+ "This message has been marked as deprecated using proto message options." ,
475+ )
412476 wl ("@{}" , self ._import ("typing" , "final" ))
413477 wl (f"class { class_name } ({ message_class } { addl_base } ):" )
414478 with self ._indent ():
@@ -835,6 +899,11 @@ def write_grpc_services(
835899 self .write_grpc_type_vars (service )
836900
837901 # The stub client
902+ if service .options .deprecated :
903+ self ._write_deprecation_warning (
904+ scl + [d .ServiceDescriptorProto .OPTIONS_FIELD_NUMBER ] + [d .ServiceOptions .DEPRECATED_FIELD_NUMBER ],
905+ "This stub has been marked as deprecated using proto service options." ,
906+ )
838907 class_name = f"{ service .name } Stub"
839908 wl (
840909 "class {}({}[{}]):" ,
@@ -875,6 +944,11 @@ def write_grpc_services(
875944 wl ("" )
876945
877946 # The service definition interface
947+ if service .options .deprecated :
948+ self ._write_deprecation_warning (
949+ scl + [d .ServiceDescriptorProto .OPTIONS_FIELD_NUMBER ] + [d .ServiceOptions .DEPRECATED_FIELD_NUMBER ],
950+ "This servicer has been marked as deprecated using proto service options." ,
951+ )
878952 wl (
879953 "class {}Servicer(metaclass={}):" ,
880954 service .name ,
@@ -886,6 +960,11 @@ def write_grpc_services(
886960 self .write_grpc_methods (service , scl )
887961 server = self ._import ("grpc" , "Server" )
888962 aserver = self ._import ("grpc.aio" , "Server" )
963+ if service .options .deprecated :
964+ self ._write_deprecation_warning (
965+ scl + [d .ServiceDescriptorProto .OPTIONS_FIELD_NUMBER ] + [d .ServiceOptions .DEPRECATED_FIELD_NUMBER ],
966+ "This servicer has been marked as deprecated using proto service options." ,
967+ )
889968 wl (
890969 "def add_{}Servicer_to_server(servicer: {}Servicer, server: {}) -> None: ..." ,
891970 service .name ,
@@ -1001,7 +1080,7 @@ def write(self) -> str:
10011080 # n,n to force a reexport (from x import y as y)
10021081 self .from_imports [reexport_imp ].update ((n , n ) for n in names )
10031082
1004- if self .typing_extensions_min :
1083+ if self .typing_extensions_min or self . deprecated_min :
10051084 self .imports .add ("sys" )
10061085 for pkg in sorted (self .imports ):
10071086 self ._write_line (f"import { pkg } " )
@@ -1011,6 +1090,12 @@ def write(self) -> str:
10111090 self ._write_line (" import typing as typing_extensions" )
10121091 self ._write_line ("else:" )
10131092 self ._write_line (" import typing_extensions" )
1093+ if self .deprecated_min :
1094+ self ._write_line ("" )
1095+ self ._write_line (f"if sys.version_info >= { self .deprecated_min } :" )
1096+ self ._write_line (" from warnings import deprecated" )
1097+ self ._write_line ("else:" )
1098+ self ._write_line (" from typing_extensions import deprecated" )
10141099
10151100 for pkg , items in sorted (self .from_imports .items ()):
10161101 self ._write_line (f"from { pkg } import (" )
@@ -1041,13 +1126,15 @@ def generate_mypy_stubs(
10411126 quiet : bool ,
10421127 readable_stubs : bool ,
10431128 relax_strict_optional_primitives : bool ,
1129+ use_default_deprecation_warnings : bool ,
10441130) -> None :
10451131 for name , fd in descriptors .to_generate .items ():
10461132 pkg_writer = PkgWriter (
10471133 fd ,
10481134 descriptors ,
10491135 readable_stubs ,
10501136 relax_strict_optional_primitives ,
1137+ use_default_deprecation_warnings ,
10511138 grpc = False ,
10521139 )
10531140
@@ -1073,13 +1160,15 @@ def generate_mypy_grpc_stubs(
10731160 quiet : bool ,
10741161 readable_stubs : bool ,
10751162 relax_strict_optional_primitives : bool ,
1163+ use_default_deprecation_warnings : bool ,
10761164) -> None :
10771165 for name , fd in descriptors .to_generate .items ():
10781166 pkg_writer = PkgWriter (
10791167 fd ,
10801168 descriptors ,
10811169 readable_stubs ,
10821170 relax_strict_optional_primitives ,
1171+ use_default_deprecation_warnings ,
10831172 grpc = True ,
10841173 )
10851174 pkg_writer .write_grpc_async_hacks ()
@@ -1131,6 +1220,7 @@ def main() -> None:
11311220 "quiet" in request .parameter ,
11321221 "readable_stubs" in request .parameter ,
11331222 "relax_strict_optional_primitives" in request .parameter ,
1223+ "use_default_deprecation_warnings" in request .parameter ,
11341224 )
11351225
11361226
@@ -1143,6 +1233,7 @@ def grpc() -> None:
11431233 "quiet" in request .parameter ,
11441234 "readable_stubs" in request .parameter ,
11451235 "relax_strict_optional_primitives" in request .parameter ,
1236+ "use_default_deprecation_warnings" in request .parameter ,
11461237 )
11471238
11481239
0 commit comments