eric6/Utilities/ClassBrowsers/pyclbr.py

changeset 6942
2602857055c5
parent 6815
b1b833693a38
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2005 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Parse a Python file and retrieve classes, functions/methods and attributes.
8
9 Parse enough of a Python file to recognize class and method definitions and
10 to find out the superclasses of a class as well as its attributes.
11
12 This is module is based on pyclbr found in the Python 2.2.2 distribution.
13 """
14
15
16 from __future__ import unicode_literals
17
18 import sys
19 import imp
20 import re
21
22 import Utilities
23 import Utilities.ClassBrowsers as ClassBrowsers
24 from . import ClbrBaseClasses
25 from functools import reduce
26
27 TABWIDTH = 4
28
29 SUPPORTED_TYPES = [ClassBrowsers.PY_SOURCE, ClassBrowsers.PTL_SOURCE]
30
31 _getnext = re.compile(
32 r"""
33 (?P<String>
34 \# .*? $ # ignore everything in comments
35 |
36 \""" [^"\\]* (?:
37 (?: \\. | "(?!"") )
38 [^"\\]*
39 )*
40 \"""
41
42 | ''' [^'\\]* (?:
43 (?: \\. | '(?!'') )
44 [^'\\]*
45 )*
46 '''
47
48 | " [^"\\\n]* (?: \\. [^"\\\n]*)* "
49
50 | ' [^'\\\n]* (?: \\. [^'\\\n]*)* '
51 )
52
53 | (?P<Publics>
54 ^
55 [ \t]* __all__ [ \t]* = [ \t]* \[
56 (?P<Identifiers> [^\]]*? )
57 \]
58 )
59
60 | (?P<MethodModifier>
61 ^
62 (?P<MethodModifierIndent> [ \t]* )
63 (?P<MethodModifierType> @classmethod | @staticmethod )
64 )
65
66 | (?P<Method>
67 ^
68 (?P<MethodIndent> [ \t]* )
69 (?: async [ \t]+ )? def [ \t]+
70 (?P<MethodName> \w+ )
71 (?: [ \t]* \[ (?: plain | html ) \] )?
72 [ \t]* \(
73 (?P<MethodSignature> (?: [^)] | \)[ \t]*,? )*? )
74 \) [ \t]*
75 (?P<MethodReturnAnnotation> (?: -> [ \t]* [^:]+ )? )
76 [ \t]* :
77 )
78
79 | (?P<Class>
80 ^
81 (?P<ClassIndent> [ \t]* )
82 class [ \t]+
83 (?P<ClassName> \w+ )
84 [ \t]*
85 (?P<ClassSupers> \( [^)]* \) )?
86 [ \t]* :
87 )
88
89 | (?P<Attribute>
90 ^
91 (?P<AttributeIndent> [ \t]* )
92 self [ \t]* \. [ \t]*
93 (?P<AttributeName> \w+ )
94 [ \t]* =
95 )
96
97 | (?P<Variable>
98 ^
99 (?P<VariableIndent> [ \t]* )
100 (?P<VariableName> \w+ )
101 [ \t]* =
102 )
103
104 | (?P<Main>
105 ^
106 if \s+ __name__ \s* == \s* [^:]+ : $
107 )
108
109 | (?P<ConditionalDefine>
110 ^
111 (?P<ConditionalDefineIndent> [ \t]* )
112 (?: (?: if | elif ) [ \t]+ [^:]* | else [ \t]* ) :
113 (?= \s* (?: async [ \t]+ )? def)
114 )
115
116 | (?P<Import>
117 ^ [ \t]* (?: import | from [ \t]+ \. [ \t]+ import ) [ \t]+
118 (?P<ImportList> (?: [^#;\\\n]* (?: \\\n )* )* )
119 )
120
121 | (?P<ImportFrom>
122 ^ [ \t]* from [ \t]+
123 (?P<ImportFromPath>
124 \.* \w+
125 (?:
126 [ \t]* \. [ \t]* \w+
127 )*
128 )
129 [ \t]+
130 import [ \t]+
131 (?P<ImportFromList>
132 (?: \( \s* .*? \s* \) )
133 |
134 (?: [^#;\\\n]* (?: \\\n )* )* )
135 )
136
137 | (?P<CodingLine>
138 ^ \# \s* [*_-]* \s* coding[:=] \s* (?P<Coding> [-\w_.]+ ) \s* [*_-]* $
139 )""",
140 re.VERBOSE | re.DOTALL | re.MULTILINE).search
141
142 _commentsub = re.compile(r"""#[^\n]*\n|#[^\n]*$""").sub
143
144 _modules = {} # cache of modules we've seen
145
146
147 class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase):
148 """
149 Mixin class implementing the notion of visibility.
150 """
151 def __init__(self):
152 """
153 Constructor
154 """
155 if self.name.startswith('__'):
156 self.setPrivate()
157 elif self.name.startswith('_'):
158 self.setProtected()
159 else:
160 self.setPublic()
161
162
163 class Class(ClbrBaseClasses.Class, VisibilityMixin):
164 """
165 Class to represent a Python class.
166 """
167 def __init__(self, module, name, superClasses, file, lineno):
168 """
169 Constructor
170
171 @param module name of the module containing this class
172 @param name name of this class
173 @param superClasses list of class names this class is inherited from
174 @param file filename containing this class
175 @param lineno linenumber of the class definition
176 """
177 ClbrBaseClasses.Class.__init__(self, module, name, superClasses, file,
178 lineno)
179 VisibilityMixin.__init__(self)
180
181
182 class Function(ClbrBaseClasses.Function, VisibilityMixin):
183 """
184 Class to represent a Python function.
185 """
186 def __init__(self, module, name, file, lineno, signature='', separator=',',
187 modifierType=ClbrBaseClasses.Function.General, annotation=""):
188 """
189 Constructor
190
191 @param module name of the module containing this function
192 @param name name of this function
193 @param file filename containing this class
194 @param lineno linenumber of the class definition
195 @param signature parameterlist of the method
196 @param separator string separating the parameters
197 @param modifierType type of the function
198 @param annotation return annotation
199 """
200 ClbrBaseClasses.Function.__init__(self, module, name, file, lineno,
201 signature, separator, modifierType,
202 annotation)
203 VisibilityMixin.__init__(self)
204
205
206 class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin):
207 """
208 Class to represent a class attribute.
209 """
210 def __init__(self, module, name, file, lineno):
211 """
212 Constructor
213
214 @param module name of the module containing this class
215 @param name name of this class
216 @param file filename containing this attribute
217 @param lineno linenumber of the class definition
218 """
219 ClbrBaseClasses.Attribute.__init__(self, module, name, file, lineno)
220 VisibilityMixin.__init__(self)
221
222
223 class Publics(object):
224 """
225 Class to represent the list of public identifiers.
226 """
227 def __init__(self, module, file, lineno, idents):
228 """
229 Constructor
230
231 @param module name of the module containing this function
232 @param file filename containing this class
233 @param lineno linenumber of the class definition
234 @param idents list of public identifiers
235 """
236 self.module = module
237 self.name = '__all__'
238 self.file = file
239 self.lineno = lineno
240 self.identifiers = [e.replace('"', '').replace("'", "").strip()
241 for e in idents.split(',')]
242
243
244 class Imports(object):
245 """
246 Class to represent the list of imported modules.
247 """
248 def __init__(self, module, file):
249 """
250 Constructor
251
252 @param module name of the module containing the import (string)
253 @param file file name containing the import (string)
254 """
255 self.module = module
256 self.name = 'import'
257 self.file = file
258 self.imports = {}
259
260 def addImport(self, moduleName, names, lineno):
261 """
262 Public method to add a list of imported names.
263
264 @param moduleName name of the imported module (string)
265 @param names list of names (list of strings)
266 @param lineno line number of the import
267 """
268 if moduleName not in self.imports:
269 module = ImportedModule(self.module, self.file, moduleName)
270 self.imports[moduleName] = module
271 else:
272 module = self.imports[moduleName]
273 module.addImport(lineno, names)
274
275 def getImport(self, moduleName):
276 """
277 Public method to get an imported module item.
278
279 @param moduleName name of the imported module (string)
280 @return imported module item (ImportedModule) or None
281 """
282 if moduleName in self.imports:
283 return self.imports[moduleName]
284 else:
285 return None
286
287 def getImports(self):
288 """
289 Public method to get all imported module names.
290
291 @return dictionary of imported module names with name as key and list
292 of line numbers of imports as value
293 """
294 return self.imports
295
296
297 class ImportedModule(object):
298 """
299 Class to represent an imported module.
300 """
301 def __init__(self, module, file, importedModule):
302 """
303 Constructor
304
305 @param module name of the module containing the import (string)
306 @param file file name containing the import (string)
307 @param importedModule name of the imported module (string)
308 """
309 self.module = module
310 self.name = 'import'
311 self.file = file
312 self.importedModuleName = importedModule
313 self.linenos = []
314 self.importedNames = {}
315 # dictionary of imported names with name as key and list of line
316 # numbers as value
317
318 def addImport(self, lineno, importedNames):
319 """
320 Public method to add a list of imported names.
321
322 @param lineno line number of the import
323 @param importedNames list of imported names (list of strings)
324 """
325 if lineno not in self.linenos:
326 self.linenos.append(lineno)
327
328 for name in importedNames:
329 if name not in self.importedNames:
330 self.importedNames[name] = [lineno]
331 else:
332 self.importedNames[name].append(lineno)
333
334
335 def readmodule_ex(module, path=None, inpackage=False, isPyFile=False):
336 """
337 Read a module file and return a dictionary of classes.
338
339 Search for MODULE in PATH and sys.path, read and parse the
340 module and return a dictionary with one entry for each class
341 found in the module.
342
343 @param module name of the module file (string)
344 @param path path the module should be searched in (list of strings)
345 @param inpackage flag indicating a module inside a package is scanned
346 @param isPyFile flag indicating a Python file (boolean)
347 @return the resulting dictionary
348 """
349 global _modules
350
351 dictionary = {}
352 dict_counts = {}
353
354 if module in _modules:
355 # we've seen this module before...
356 return _modules[module]
357 if module in sys.builtin_module_names:
358 # this is a built-in module
359 _modules[module] = dictionary
360 return dictionary
361
362 # search the path for the module
363 path = [] if path is None else path[:]
364 f = None
365 if inpackage:
366 try:
367 f, file, (suff, mode, type) = \
368 ClassBrowsers.find_module(module, path)
369 except ImportError:
370 f = None
371 if f is None:
372 fullpath = path[:] + sys.path[:]
373 f, file, (suff, mode, type) = \
374 ClassBrowsers.find_module(module, fullpath, isPyFile)
375 if module.endswith(".py") and type == imp.PKG_DIRECTORY:
376 return dictionary
377 if type == imp.PKG_DIRECTORY:
378 dictionary['__path__'] = [file]
379 _modules[module] = dictionary
380 path = [file] + path
381 f, file, (suff, mode, type) = \
382 ClassBrowsers.find_module('__init__', [file])
383 if f:
384 f.close()
385 if type not in SUPPORTED_TYPES:
386 # not Python source, can't do anything with this module
387 _modules[module] = dictionary
388 return dictionary
389
390 _modules[module] = dictionary
391 classstack = [] # stack of (class, indent) pairs
392 conditionalsstack = [] # stack of indents of conditional defines
393 deltastack = []
394 deltaindent = 0
395 deltaindentcalculated = 0
396 try:
397 src = Utilities.readEncodedFile(file)[0]
398 except (UnicodeError, IOError):
399 # can't do anything with this module
400 _modules[module] = dictionary
401 return dictionary
402
403 lineno, last_lineno_pos = 1, 0
404 lastGlobalEntry = None
405 cur_obj = None
406 i = 0
407 modifierType = ClbrBaseClasses.Function.General
408 modifierIndent = -1
409 while True:
410 m = _getnext(src, i)
411 if not m:
412 break
413 start, i = m.span()
414
415 if m.start("MethodModifier") >= 0:
416 modifierIndent = _indent(m.group("MethodModifierIndent"))
417 modifierType = m.group("MethodModifierType")
418
419 elif m.start("Method") >= 0:
420 # found a method definition or function
421 thisindent = _indent(m.group("MethodIndent"))
422 meth_name = m.group("MethodName")
423 meth_sig = m.group("MethodSignature")
424 meth_sig = meth_sig.replace('\\\n', '')
425 meth_sig = _commentsub('', meth_sig)
426 meth_ret = m.group("MethodReturnAnnotation")
427 meth_ret = meth_ret.replace('\\\n', '')
428 meth_ret = _commentsub('', meth_ret)
429 lineno = lineno + src.count('\n', last_lineno_pos, start)
430 last_lineno_pos = start
431 if modifierType and modifierIndent == thisindent:
432 if modifierType == "@staticmethod":
433 modifier = ClbrBaseClasses.Function.Static
434 elif modifierType == "@classmethod":
435 modifier = ClbrBaseClasses.Function.Class
436 else:
437 modifier = ClbrBaseClasses.Function.General
438 else:
439 modifier = ClbrBaseClasses.Function.General
440 # modify indentation level for conditional defines
441 if conditionalsstack:
442 if thisindent > conditionalsstack[-1]:
443 if not deltaindentcalculated:
444 deltastack.append(thisindent - conditionalsstack[-1])
445 deltaindent = reduce(lambda x, y: x + y, deltastack)
446 deltaindentcalculated = 1
447 thisindent -= deltaindent
448 else:
449 while conditionalsstack and \
450 conditionalsstack[-1] >= thisindent:
451 del conditionalsstack[-1]
452 if deltastack:
453 del deltastack[-1]
454 deltaindentcalculated = 0
455 # close all classes indented at least as much
456 while classstack and \
457 classstack[-1][1] >= thisindent:
458 if classstack[-1][0] is not None:
459 # record the end line
460 classstack[-1][0].setEndLine(lineno - 1)
461 del classstack[-1]
462 if classstack:
463 # it's a class method
464 cur_class = classstack[-1][0]
465 if cur_class:
466 # it's a method/nested def
467 f = Function(None, meth_name,
468 file, lineno, meth_sig, annotation=meth_ret,
469 modifierType=modifier)
470 cur_class._addmethod(meth_name, f)
471 else:
472 # it's a function
473 f = Function(module, meth_name,
474 file, lineno, meth_sig, annotation=meth_ret,
475 modifierType=modifier)
476 if meth_name in dict_counts:
477 dict_counts[meth_name] += 1
478 meth_name = "{0}_{1:d}".format(
479 meth_name, dict_counts[meth_name])
480 else:
481 dict_counts[meth_name] = 0
482 dictionary[meth_name] = f
483 if not classstack:
484 if lastGlobalEntry:
485 lastGlobalEntry.setEndLine(lineno - 1)
486 lastGlobalEntry = f
487 if cur_obj and isinstance(cur_obj, Function):
488 cur_obj.setEndLine(lineno - 1)
489 cur_obj = f
490 classstack.append((f, thisindent)) # Marker for nested fns
491
492 # reset the modifier settings
493 modifierType = ClbrBaseClasses.Function.General
494 modifierIndent = -1
495
496 elif m.start("String") >= 0:
497 pass
498
499 elif m.start("Class") >= 0:
500 # we found a class definition
501 thisindent = _indent(m.group("ClassIndent"))
502 # close all classes indented at least as much
503 while classstack and \
504 classstack[-1][1] >= thisindent:
505 if classstack[-1][0] is not None:
506 # record the end line
507 classstack[-1][0].setEndLine(lineno - 1)
508 del classstack[-1]
509 lineno = lineno + src.count('\n', last_lineno_pos, start)
510 last_lineno_pos = start
511 class_name = m.group("ClassName")
512 inherit = m.group("ClassSupers")
513 if inherit:
514 # the class inherits from other classes
515 inherit = inherit[1:-1].strip()
516 inherit = _commentsub('', inherit)
517 names = []
518 for n in inherit.split(','):
519 n = n.strip()
520 if n in dictionary:
521 # we know this super class
522 n = dictionary[n]
523 else:
524 c = n.split('.')
525 if len(c) > 1:
526 # super class
527 # is of the
528 # form module.class:
529 # look in
530 # module for class
531 m = c[-2]
532 c = c[-1]
533 if m in _modules:
534 d = _modules[m]
535 if c in d:
536 n = d[c]
537 names.append(n)
538 inherit = names
539 # remember this class
540 cur_class = Class(module, class_name, inherit,
541 file, lineno)
542 if not classstack:
543 if class_name in dict_counts:
544 dict_counts[class_name] += 1
545 class_name = "{0}_{1:d}".format(
546 class_name, dict_counts[class_name])
547 else:
548 dict_counts[class_name] = 0
549 dictionary[class_name] = cur_class
550 else:
551 classstack[-1][0]._addclass(class_name, cur_class)
552 if not classstack:
553 if lastGlobalEntry:
554 lastGlobalEntry.setEndLine(lineno - 1)
555 lastGlobalEntry = cur_class
556 classstack.append((cur_class, thisindent))
557
558 elif m.start("Attribute") >= 0:
559 lineno = lineno + src.count('\n', last_lineno_pos, start)
560 last_lineno_pos = start
561 index = -1
562 while index >= -len(classstack):
563 if classstack[index][0] is not None and \
564 not isinstance(classstack[index][0], Function):
565 attr = Attribute(
566 module, m.group("AttributeName"), file, lineno)
567 classstack[index][0]._addattribute(attr)
568 break
569 else:
570 index -= 1
571
572 elif m.start("Main") >= 0:
573 # 'main' part of the script, reset class stack
574 lineno = lineno + src.count('\n', last_lineno_pos, start)
575 last_lineno_pos = start
576 classstack = []
577
578 elif m.start("Variable") >= 0:
579 thisindent = _indent(m.group("VariableIndent"))
580 variable_name = m.group("VariableName")
581 lineno = lineno + src.count('\n', last_lineno_pos, start)
582 last_lineno_pos = start
583 if thisindent == 0 or not classstack:
584 # global variable, reset class stack first
585 classstack = []
586
587 if "@@Globals@@" not in dictionary:
588 dictionary["@@Globals@@"] = ClbrBaseClasses.ClbrBase(
589 module, "Globals", file, lineno)
590 dictionary["@@Globals@@"]._addglobal(
591 Attribute(module, variable_name, file, lineno))
592 if lastGlobalEntry:
593 lastGlobalEntry.setEndLine(lineno - 1)
594 lastGlobalEntry = None
595 else:
596 index = -1
597 while index >= -len(classstack):
598 if classstack[index][1] >= thisindent:
599 index -= 1
600 else:
601 if isinstance(classstack[index][0], Class):
602 classstack[index][0]._addglobal(
603 Attribute(module, variable_name, file, lineno))
604 break
605
606 elif m.start("Publics") >= 0:
607 idents = m.group("Identifiers")
608 lineno = lineno + src.count('\n', last_lineno_pos, start)
609 last_lineno_pos = start
610 pubs = Publics(module, file, lineno, idents)
611 dictionary['__all__'] = pubs
612
613 elif m.start("Import") >= 0:
614 # import module
615 names = [n.strip() for n in
616 "".join(m.group("ImportList").splitlines())
617 .replace("\\", "").split(',')]
618 lineno = lineno + src.count('\n', last_lineno_pos, start)
619 last_lineno_pos = start
620 if "@@Import@@" not in dictionary:
621 dictionary["@@Import@@"] = Imports(module, file)
622 for name in names:
623 dictionary["@@Import@@"].addImport(name, [], lineno)
624
625 elif m.start("ImportFrom") >= 0:
626 # from module import stuff
627 mod = m.group("ImportFromPath")
628 namesLines = (m.group("ImportFromList")
629 .replace("(", "").replace(")", "")
630 .replace("\\", "")
631 .strip().splitlines())
632 namesLines = [line.split("#")[0].strip()
633 for line in namesLines]
634 names = [n.strip() for n in
635 "".join(namesLines)
636 .split(',')]
637 lineno = lineno + src.count('\n', last_lineno_pos, start)
638 last_lineno_pos = start
639 if "@@Import@@" not in dictionary:
640 dictionary["@@Import@@"] = Imports(module, file)
641 dictionary["@@Import@@"].addImport(mod, names, lineno)
642
643 elif m.start("ConditionalDefine") >= 0:
644 # a conditional function/method definition
645 thisindent = _indent(m.group("ConditionalDefineIndent"))
646 while conditionalsstack and \
647 conditionalsstack[-1] >= thisindent:
648 del conditionalsstack[-1]
649 if deltastack:
650 del deltastack[-1]
651 conditionalsstack.append(thisindent)
652 deltaindentcalculated = 0
653
654 elif m.start("CodingLine") >= 0:
655 # a coding statement
656 coding = m.group("Coding")
657 lineno = lineno + src.count('\n', last_lineno_pos, start)
658 last_lineno_pos = start
659 if "@@Coding@@" not in dictionary:
660 dictionary["@@Coding@@"] = ClbrBaseClasses.Coding(
661 module, file, lineno, coding)
662
663 else:
664 assert 0, "regexp _getnext found something unexpected"
665
666 if '__all__' in dictionary:
667 # set visibility of all top level elements
668 pubs = dictionary['__all__']
669 for key in dictionary.keys():
670 if key == '__all__' or key.startswith("@@"):
671 continue
672 if key in pubs.identifiers:
673 dictionary[key].setPublic()
674 else:
675 dictionary[key].setPrivate()
676 del dictionary['__all__']
677
678 return dictionary
679
680
681 def _indent(ws):
682 """
683 Module function to return the indentation depth.
684
685 @param ws the whitespace to be checked (string)
686 @return length of the whitespace string (integer)
687 """
688 return len(ws.expandtabs(TABWIDTH))

eric ide

mercurial