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

eric ide

mercurial