|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Parse a ProtoBuf protocol file and retrieve messages, enums, services and |
|
8 rpc methods. |
|
9 |
|
10 It is based on the Python class browser found in this package. |
|
11 """ |
|
12 |
|
13 import re |
|
14 |
|
15 import Utilities |
|
16 import Utilities.ClassBrowsers as ClassBrowsers |
|
17 from . import ClbrBaseClasses |
|
18 |
|
19 SUPPORTED_TYPES = [ClassBrowsers.PROTO_SOURCE] |
|
20 |
|
21 _getnext = re.compile( |
|
22 r""" |
|
23 (?P<String> |
|
24 " [^"\\\n]* (?: \\. [^"\\\n]*)* " |
|
25 ) |
|
26 |
|
27 | (?P<Comment> |
|
28 ^ [ \t]* // .*? $ |
|
29 | |
|
30 ^ [ \t]* /\* .*? \*/ |
|
31 ) |
|
32 |
|
33 | (?P<Message> |
|
34 ^ |
|
35 (?P<MessageIndent> [ \t]* ) |
|
36 message [ \t]+ |
|
37 (?P<MessageName> [a-zA-Z_] [a-zA-Z0-9_]* ) |
|
38 [ \t]* { |
|
39 ) |
|
40 |
|
41 | (?P<Enum> |
|
42 ^ |
|
43 (?P<EnumIndent> [ \t]* ) |
|
44 enum [ \t]+ |
|
45 (?P<EnumName> [a-zA-Z_] [a-zA-Z0-9_]* ) |
|
46 [ \t]* { |
|
47 ) |
|
48 |
|
49 | (?P<Service> |
|
50 ^ |
|
51 (?P<ServiceIndent> [ \t]* ) |
|
52 service [ \t]+ |
|
53 (?P<ServiceName> [a-zA-Z_] [a-zA-Z0-9_]* ) |
|
54 [ \t]* { |
|
55 ) |
|
56 |
|
57 | (?P<Method> |
|
58 ^ |
|
59 (?P<MethodIndent> [ \t]* ) |
|
60 rpc [ \t]+ |
|
61 (?P<MethodName> [a-zA-Z_] [a-zA-Z0-9_]* ) |
|
62 [ \t]* |
|
63 \( |
|
64 (?P<MethodSignature> [^)]+? ) |
|
65 \) |
|
66 [ \t]+ |
|
67 returns |
|
68 [ \t]* |
|
69 \( |
|
70 (?P<MethodReturn> [^)]+? ) |
|
71 \) |
|
72 [ \t]* |
|
73 ) |
|
74 |
|
75 | (?P<Begin> |
|
76 [ \t]* { |
|
77 ) |
|
78 |
|
79 | (?P<End> |
|
80 [ \t]* } [ \t]* ;? |
|
81 )""", |
|
82 re.VERBOSE | re.DOTALL | re.MULTILINE).search |
|
83 |
|
84 # function to replace comments |
|
85 _commentsub = re.compile(r"""//[^\n]*\n|//[^\n]*$""").sub |
|
86 # function to normalize whitespace |
|
87 _normalize = re.compile(r"""[ \t]{2,}""").sub |
|
88 |
|
89 _modules = {} # cache of modules we've seen |
|
90 |
|
91 |
|
92 class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase): |
|
93 """ |
|
94 Mixin class implementing the notion of visibility. |
|
95 """ |
|
96 def __init__(self): |
|
97 """ |
|
98 Constructor |
|
99 """ |
|
100 self.setPublic() |
|
101 |
|
102 |
|
103 class Message(ClbrBaseClasses.Module, VisibilityMixin): |
|
104 """ |
|
105 Class to represent a ProtoBuf Message. |
|
106 """ |
|
107 def __init__(self, module, name, file, lineno): |
|
108 """ |
|
109 Constructor |
|
110 |
|
111 @param module name of the module containing this message |
|
112 @type str |
|
113 @param name name of this message |
|
114 @type str |
|
115 @param file filename containing this message |
|
116 @type str |
|
117 @param lineno linenumber of the message definition |
|
118 @type int |
|
119 """ |
|
120 ClbrBaseClasses.Module.__init__(self, module, name, file, lineno) |
|
121 VisibilityMixin.__init__(self) |
|
122 |
|
123 |
|
124 class Enum(ClbrBaseClasses.Enum, VisibilityMixin): |
|
125 """ |
|
126 Class to represent a ProtoBuf Enum. |
|
127 """ |
|
128 def __init__(self, module, name, file, lineno): |
|
129 """ |
|
130 Constructor |
|
131 |
|
132 @param module name of the module containing this enum |
|
133 @type str |
|
134 @param name name of this enum |
|
135 @type str |
|
136 @param file filename containing this enum |
|
137 @type str |
|
138 @param lineno linenumber of the message enum |
|
139 @type int |
|
140 """ |
|
141 ClbrBaseClasses.Enum.__init__(self, module, name, file, lineno) |
|
142 VisibilityMixin.__init__(self) |
|
143 |
|
144 |
|
145 class Service(ClbrBaseClasses.Class, VisibilityMixin): |
|
146 """ |
|
147 Class to represent a ProtoBuf Service. |
|
148 """ |
|
149 def __init__(self, module, name, file, lineno): |
|
150 """ |
|
151 Constructor |
|
152 |
|
153 @param module name of the module containing this service |
|
154 @type str |
|
155 @param name name of this service |
|
156 @type str |
|
157 @param file filename containing this service |
|
158 @type str |
|
159 @param lineno linenumber of the service definition |
|
160 @type int |
|
161 """ |
|
162 ClbrBaseClasses.Class.__init__(self, module, name, None, file, |
|
163 lineno) |
|
164 VisibilityMixin.__init__(self) |
|
165 |
|
166 |
|
167 class ServiceMethod(ClbrBaseClasses.Function, VisibilityMixin): |
|
168 """ |
|
169 Class to represent a ProtoBuf Service Method. |
|
170 """ |
|
171 def __init__(self, name, file, lineno, signature, returns): |
|
172 """ |
|
173 Constructor |
|
174 |
|
175 @param name name of this service method |
|
176 @type str |
|
177 @param file filename containing this service method |
|
178 @type str |
|
179 @param lineno linenumber of the service method definition |
|
180 @type int |
|
181 @param signature parameter list of the service method |
|
182 @type str |
|
183 @param returns return type of the service method |
|
184 @type str |
|
185 """ |
|
186 ClbrBaseClasses.Function.__init__(self, None, name, file, lineno, |
|
187 signature, |
|
188 annotation="-> {0}".format(returns)) |
|
189 VisibilityMixin.__init__(self) |
|
190 |
|
191 |
|
192 def readmodule_ex(module, path=None): |
|
193 """ |
|
194 Read a ProtoBuf protocol file and return a dictionary of messages, enums, |
|
195 services and rpc methods. |
|
196 |
|
197 @param module name of the ProtoBuf protocol file |
|
198 @type str |
|
199 @param path path the file should be searched in |
|
200 @type list of str |
|
201 @return the resulting dictionary |
|
202 @rtype dict |
|
203 """ |
|
204 global _modules |
|
205 |
|
206 if module in _modules: |
|
207 # we've seen this file before... |
|
208 return _modules[module] |
|
209 |
|
210 # search the path for the file |
|
211 f = None |
|
212 fullpath = [] if path is None else path[:] |
|
213 f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath) |
|
214 if f: |
|
215 f.close() |
|
216 if type not in SUPPORTED_TYPES: |
|
217 # not ProtoBuf protocol source, can't do anything with this module |
|
218 _modules[module] = {} |
|
219 return {} |
|
220 |
|
221 try: |
|
222 src = Utilities.readEncodedFile(file)[0] |
|
223 except (UnicodeError, OSError): |
|
224 # can't do anything with this module |
|
225 _modules[module] = {} |
|
226 return {} |
|
227 |
|
228 _modules[module] = scan(src, file, module) |
|
229 return _modules[module] |
|
230 |
|
231 |
|
232 def scan(src, file, module): |
|
233 """ |
|
234 Public method to scan the given source text. |
|
235 |
|
236 @param src source text to be scanned |
|
237 @type str |
|
238 @param file file name associated with the source text |
|
239 @type str |
|
240 @param module module name associated with the source text |
|
241 @type str |
|
242 @return dictionary containing the extracted data |
|
243 @rtype dict |
|
244 """ |
|
245 def calculateEndline(lineno, lines): |
|
246 """ |
|
247 Function to calculate the end line. |
|
248 |
|
249 @param lineno line number to start at (one based) |
|
250 @type int |
|
251 @param lines list of source lines |
|
252 @type list of str |
|
253 @return end line (one based) |
|
254 @rtype int |
|
255 """ |
|
256 # convert lineno to be zero based |
|
257 lineno -= 1 |
|
258 # 1. search for opening brace '{' |
|
259 while lineno < len(lines) and "{" not in lines[lineno]: |
|
260 lineno += 1 |
|
261 depth = lines[lineno].count("{") - lines[lineno].count("}") |
|
262 # 2. search for ending line, i.e. matching closing brace '}' |
|
263 while depth > 0 and lineno < len(lines) - 1: |
|
264 lineno += 1 |
|
265 depth += lines[lineno].count("{") - lines[lineno].count("}") |
|
266 if depth == 0: |
|
267 # found a matching brace |
|
268 return lineno + 1 |
|
269 else: |
|
270 # nothing found |
|
271 return -1 |
|
272 |
|
273 # convert eol markers the Python style |
|
274 src = src.replace("\r\n", "\n").replace("\r", "\n") |
|
275 srcLines = src.splitlines() |
|
276 |
|
277 dictionary = {} |
|
278 |
|
279 classstack = [] # stack of (class, indent) pairs |
|
280 indent = 0 |
|
281 |
|
282 lineno, last_lineno_pos = 1, 0 |
|
283 i = 0 |
|
284 while True: |
|
285 m = _getnext(src, i) |
|
286 if not m: |
|
287 break |
|
288 start, i = m.span() |
|
289 |
|
290 if m.start("Method") >= 0: |
|
291 # found a method definition or function |
|
292 thisindent = indent |
|
293 meth_name = m.group("MethodName") |
|
294 meth_sig = m.group("MethodSignature") |
|
295 meth_sig = meth_sig and meth_sig.replace('\\\n', '') or '' |
|
296 meth_sig = _commentsub('', meth_sig) |
|
297 meth_sig = _normalize(' ', meth_sig) |
|
298 meth_return = m.group("MethodReturn") |
|
299 meth_return = meth_return and meth_return.replace('\\\n', '') or '' |
|
300 meth_return = _commentsub('', meth_return) |
|
301 meth_return = _normalize(' ', meth_return) |
|
302 lineno += src.count('\n', last_lineno_pos, start) |
|
303 last_lineno_pos = start |
|
304 # close all interfaces/modules indented at least as much |
|
305 while classstack and classstack[-1][1] >= thisindent: |
|
306 del classstack[-1] |
|
307 if classstack: |
|
308 # it's an interface/module method |
|
309 cur_class = classstack[-1][0] |
|
310 if isinstance(cur_class, Service): |
|
311 # it's a method |
|
312 f = ServiceMethod(meth_name, file, lineno, meth_sig, |
|
313 meth_return) |
|
314 cur_class._addmethod(meth_name, f) |
|
315 # else it's a nested def |
|
316 else: |
|
317 f = None |
|
318 else: |
|
319 # the file is incorrect, ignore the entry |
|
320 continue |
|
321 if f: |
|
322 endline = calculateEndline(lineno, srcLines) |
|
323 f.setEndLine(endline) |
|
324 classstack.append((f, thisindent)) # Marker for nested fns |
|
325 |
|
326 elif m.start("String") >= 0 or m.start("Comment") >= 0: |
|
327 pass |
|
328 |
|
329 elif m.start("Message") >= 0: |
|
330 # we found a message definition |
|
331 thisindent = indent |
|
332 indent += 1 |
|
333 lineno += src.count('\n', last_lineno_pos, start) |
|
334 last_lineno_pos = start |
|
335 message_name = m.group("MessageName") |
|
336 # close all messages/services indented at least as much |
|
337 while classstack and classstack[-1][1] >= thisindent: |
|
338 del classstack[-1] |
|
339 # remember this message |
|
340 cur_class = Message(module, message_name, file, lineno) |
|
341 endline = calculateEndline(lineno, srcLines) |
|
342 cur_class.setEndLine(endline) |
|
343 if not classstack: |
|
344 dictionary[message_name] = cur_class |
|
345 else: |
|
346 msg = classstack[-1][0] |
|
347 msg._addclass(message_name, cur_class) |
|
348 classstack.append((cur_class, thisindent)) |
|
349 |
|
350 elif m.start("Enum") >= 0: |
|
351 # we found a message definition |
|
352 thisindent = indent |
|
353 indent += 1 |
|
354 # close all messages/services indented at least as much |
|
355 while classstack and classstack[-1][1] >= thisindent: |
|
356 del classstack[-1] |
|
357 lineno += src.count('\n', last_lineno_pos, start) |
|
358 last_lineno_pos = start |
|
359 enum_name = m.group("EnumName") |
|
360 # remember this Enum |
|
361 cur_class = Enum(module, enum_name, file, lineno) |
|
362 endline = calculateEndline(lineno, srcLines) |
|
363 cur_class.setEndLine(endline) |
|
364 if not classstack: |
|
365 dictionary[enum_name] = cur_class |
|
366 else: |
|
367 enum = classstack[-1][0] |
|
368 enum._addclass(enum_name, cur_class) |
|
369 classstack.append((cur_class, thisindent)) |
|
370 |
|
371 elif m.start("Service") >= 0: |
|
372 # we found a message definition |
|
373 thisindent = indent |
|
374 indent += 1 |
|
375 # close all messages/services indented at least as much |
|
376 while classstack and classstack[-1][1] >= thisindent: |
|
377 del classstack[-1] |
|
378 lineno += src.count('\n', last_lineno_pos, start) |
|
379 last_lineno_pos = start |
|
380 service_name = m.group("ServiceName") |
|
381 # remember this Service |
|
382 cur_class = Service(module, service_name, file, lineno) |
|
383 endline = calculateEndline(lineno, srcLines) |
|
384 cur_class.setEndLine(endline) |
|
385 if not classstack: |
|
386 dictionary[service_name] = cur_class |
|
387 else: |
|
388 service = classstack[-1][0] |
|
389 service._addclass(service_name, cur_class) |
|
390 classstack.append((cur_class, thisindent)) |
|
391 |
|
392 elif m.start("Begin") >= 0: |
|
393 # a begin of a block we are not interested in |
|
394 indent += 1 |
|
395 |
|
396 elif m.start("End") >= 0: |
|
397 # an end of a block |
|
398 indent -= 1 |
|
399 |
|
400 return dictionary |