src/eric7/Utilities/ClassBrowsers/protoclbr.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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

eric ide

mercurial