Improvements to the base BPyTextPlugin module:
[blender.git] / release / scripts / bpymodules / BPyTextPlugin.py
1 """The BPyTextPlugin Module
2
3 Use get_cached_descriptor(txt) to retrieve information about the script held in
4 the txt Text object.
5
6 Use print_cache_for(txt) to print the information to the console.
7
8 Use line, cursor = current_line(txt) to get the logical line and cursor position
9
10 Use get_targets(line, cursor) to find out what precedes the cursor:
11         aaa.bbb.cc|c.ddd -> ['aaa', 'bbb', 'cc']
12
13 Use resolve_targets(txt, targets) to turn a target list into a usable object if
14 one is found to match.
15 """
16
17 import bpy, sys, os
18 import __builtin__, tokenize
19 from Blender.sys import time
20 from tokenize import generate_tokens, TokenError, \
21                 COMMENT, DEDENT, INDENT, NAME, NEWLINE, NL, STRING, NUMBER
22
23 class Definition():
24         """Describes a definition or defined object through its name, line number
25         and docstring. This is the base class for definition based descriptors.
26         """
27         
28         def __init__(self, name, lineno, doc=''):
29                 self.name = name
30                 self.lineno = lineno
31                 self.doc = doc
32
33 class ScriptDesc():
34         """Describes a script through lists of further descriptor objects (classes,
35         defs, vars) and dictionaries to built-in types (imports). If a script has
36         not been fully parsed, its incomplete flag will be set. The time of the last
37         parse is held by the time field and the name of the text object from which
38         it was parsed, the name field.
39         """
40         
41         def __init__(self, name, imports, classes, defs, vars, incomplete=False):
42                 self.name = name
43                 self.imports = imports
44                 self.classes = classes
45                 self.defs = defs
46                 self.vars = vars
47                 self.incomplete = incomplete
48                 self.parse_due = 0
49         
50         def set_delay(self, delay):
51                 self.parse_due = time() + delay
52
53 class ClassDesc(Definition):
54         """Describes a class through lists of further descriptor objects (defs and
55         vars). The name of the class is held by the name field and the line on
56         which it is defined is held in lineno.
57         """
58         
59         def __init__(self, name, defs, vars, lineno, doc=''):
60                 Definition.__init__(self, name, lineno, doc)
61                 self.defs = defs
62                 self.vars = vars
63
64 class FunctionDesc(Definition):
65         """Describes a function through its name and list of parameters (name,
66         params) and the line on which it is defined (lineno).
67         """
68         
69         def __init__(self, name, params, lineno, doc=''):
70                 Definition.__init__(self, name, lineno, doc)
71                 self.params = params
72
73 class VarDesc(Definition):
74         """Describes a variable through its name and type (if ascertainable) and the
75         line on which it is defined (lineno). If no type can be determined, type
76         will equal None.
77         """
78         
79         def __init__(self, name, type, lineno):
80                 Definition.__init__(self, name, lineno)
81                 self.type = type # None for unknown (supports: dict/list/str)
82
83 # Context types
84 CTX_UNSET = -1
85 CTX_NORMAL = 0
86 CTX_SINGLE_QUOTE = 1
87 CTX_DOUBLE_QUOTE = 2
88 CTX_COMMENT = 3
89
90 # Python keywords
91 KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
92                         'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
93                         'break', 'except', 'import', 'print', 'class', 'exec', 'in',
94                         'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
95                         'lambda', 'try' ]
96
97 # Module file extensions
98 MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd']
99
100 ModuleType = type(__builtin__)
101 NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
102
103 _modules = {}
104 _modules_updated = 0
105 _parse_cache = dict()
106
107 def _load_module_names():
108         """Searches the sys.path for module files and lists them, along with
109         sys.builtin_module_names, in the global dict _modules.
110         """
111         
112         global _modules
113         
114         for n in sys.builtin_module_names:
115                 _modules[n] = None
116         for p in sys.path:
117                 if p == '': p = os.curdir
118                 if not os.path.isdir(p): continue
119                 for f in os.listdir(p):
120                         for ext in MODULE_EXTS:
121                                 if f.endswith(ext):
122                                         _modules[f[:-len(ext)]] = None
123                                         break
124
125 _load_module_names()
126
127 def _trim_doc(doc):
128         """Trims the quotes from a quoted STRING token (eg. "'''text'''" -> "text")
129         """
130         
131         l = len(doc)
132         i = 0
133         while i < l/2 and (doc[i] == "'" or doc[i] == '"'):
134                 i += 1
135         return doc[i:-i]
136
137 def resolve_targets(txt, targets):
138         """Attempts to return a useful object for the locally or externally defined
139         entity described by targets. If the object is local (defined in txt), a
140         Definition instance is returned. If the object is external (imported or
141         built in), the object itself is returned. If no object can be found, None is
142         returned.
143         """
144         
145         count = len(targets)
146         if count==0: return None
147         
148         obj = None
149         local = None
150         i = 1
151         
152         desc = get_cached_descriptor(txt)
153         if desc.classes.has_key(targets[0]):
154                 local = desc.classes[targets[0]]
155         elif desc.defs.has_key(targets[0]):
156                 local = desc.defs[targets[0]]
157         elif desc.vars.has_key(targets[0]):
158                 obj = desc.vars[targets[0]].type
159         
160         if local:
161                 while i < count:
162                         if hasattr(local, 'classes') and local.classes.has_key(targets[i]):
163                                 local = local.classes[targets[i]]
164                         elif hasattr(local, 'defs') and local.defs.has_key(targets[i]):
165                                 local = local.defs[targets[i]]
166                         elif hasattr(local, 'vars') and local.vars.has_key(targets[i]):
167                                 obj = local.vars[targets[i]].type
168                                 local = None
169                                 i += 1
170                                 break
171                         else:
172                                 local = None
173                                 break
174                         i += 1
175         
176         if local: return local
177         
178         if not obj:
179                 if desc.imports.has_key(targets[0]):
180                         obj = desc.imports[targets[0]]
181                 else:
182                         builtins = get_builtins()
183                         if builtins.has_key(targets[0]):
184                                 obj = builtins[targets[0]]
185         
186         while obj and i < count:
187                 if hasattr(obj, targets[i]):
188                         obj = getattr(obj, targets[i])
189                 else:
190                         obj = None
191                         break
192                 i += 1
193         
194         return obj
195
196 def get_cached_descriptor(txt, force_parse=0):
197         """Returns the cached ScriptDesc for the specified Text object 'txt'. If the
198         script has not been parsed in the last 'period' seconds it will be reparsed
199         to obtain this descriptor.
200         
201         Specifying TP_AUTO for the period (default) will choose a period based on the
202         size of the Text object. Larger texts are parsed less often.
203         """
204         
205         global _parse_cache
206         
207         parse = True
208         key = hash(txt)
209         if not force_parse and _parse_cache.has_key(key):
210                 desc = _parse_cache[key]
211                 if desc.parse_due > time():
212                         parse = desc.incomplete
213         
214         if parse:
215                 desc = parse_text(txt)
216         
217         return desc
218
219 def parse_text(txt):
220         """Parses an entire script's text and returns a ScriptDesc instance
221         containing information about the script.
222         
223         If the text is not a valid Python script (for example if brackets are left
224         open), parsing may fail to complete. However, if this occurs, no exception
225         is thrown. Instead the returned ScriptDesc instance will have its incomplete
226         flag set and information processed up to this point will still be accessible.
227         """
228         
229         start_time = time()
230         txt.reset()
231         tokens = generate_tokens(txt.readline) # Throws TokenError
232         
233         curl, cursor = txt.getCursorPos()
234         linen = curl + 1 # Token line numbers are one-based
235         
236         imports = dict()
237         imp_step = 0
238         
239         classes = dict()
240         cls_step = 0
241         
242         defs = dict()
243         def_step = 0
244         
245         vars = dict()
246         var1_step = 0
247         var2_step = 0
248         var3_step = 0
249         var_accum = dict()
250         var_forflag = False
251         
252         indent = 0
253         prev_type = -1
254         prev_text = ''
255         incomplete = False
256         
257         while True:
258                 try:
259                         type, text, start, end, line = tokens.next()
260                 except StopIteration:
261                         break
262                 except TokenError, IndentationError:
263                         incomplete = True
264                         break
265                 
266                 # Skip all comments and line joining characters
267                 if type == COMMENT or type == NL:
268                         continue
269                 
270                 #################
271                 ## Indentation ##
272                 #################
273                 
274                 if type == INDENT:
275                         indent += 1
276                 elif type == DEDENT:
277                         indent -= 1
278                 
279                 #########################
280                 ## Module importing... ##
281                 #########################
282                 
283                 imp_store = False
284                 
285                 # Default, look for 'from' or 'import' to start
286                 if imp_step == 0:
287                         if text == 'from':
288                                 imp_tmp = []
289                                 imp_step = 1
290                         elif text == 'import':
291                                 imp_from = None
292                                 imp_tmp = []
293                                 imp_step = 2
294                 
295                 # Found a 'from', create imp_from in form '???.???...'
296                 elif imp_step == 1:
297                         if text == 'import':
298                                 imp_from = '.'.join(imp_tmp)
299                                 imp_tmp = []
300                                 imp_step = 2
301                         elif type == NAME:
302                                 imp_tmp.append(text)
303                         elif text != '.':
304                                 imp_step = 0 # Invalid syntax
305                 
306                 # Found 'import', imp_from is populated or None, create imp_name
307                 elif imp_step == 2:
308                         if text == 'as':
309                                 imp_name = '.'.join(imp_tmp)
310                                 imp_step = 3
311                         elif type == NAME or text == '*':
312                                 imp_tmp.append(text)
313                         elif text != '.':
314                                 imp_name = '.'.join(imp_tmp)
315                                 imp_symb = imp_name
316                                 imp_store = True
317                 
318                 # Found 'as', change imp_symb to this value and go back to step 2
319                 elif imp_step == 3:
320                         if type == NAME:
321                                 imp_symb = text
322                         else:
323                                 imp_store = True
324                 
325                 # Both imp_name and imp_symb have now been populated so we can import
326                 if imp_store:
327                         
328                         # Handle special case of 'import *'
329                         if imp_name == '*':
330                                 parent = get_module(imp_from)
331                                 imports.update(parent.__dict__)
332                                 
333                         else:
334                                 # Try importing the name as a module
335                                 try:
336                                         if imp_from:
337                                                 module = get_module(imp_from +'.'+ imp_name)
338                                         else:
339                                                 module = get_module(imp_name)
340                                 except (ImportError, ValueError, AttributeError, TypeError):
341                                         # Try importing name as an attribute of the parent
342                                         try:
343                                                 module = __import__(imp_from, globals(), locals(), [imp_name])
344                                                 imports[imp_symb] = getattr(module, imp_name)
345                                         except (ImportError, ValueError, AttributeError, TypeError):
346                                                 pass
347                                 else:
348                                         imports[imp_symb] = module
349                         
350                         # More to import from the same module?
351                         if text == ',':
352                                 imp_tmp = []
353                                 imp_step = 2
354                         else:
355                                 imp_step = 0
356                 
357                 ###################
358                 ## Class parsing ##
359                 ###################
360                 
361                 # If we are inside a class then def and variable parsing should be done
362                 # for the class. Otherwise the definitions are considered global
363                 
364                 # Look for 'class'
365                 if cls_step == 0:
366                         if text == 'class':
367                                 cls_name = None
368                                 cls_lineno = start[0]
369                                 cls_indent = indent
370                                 cls_step = 1
371                 
372                 # Found 'class', look for cls_name followed by '('
373                 elif cls_step == 1:
374                         if not cls_name:
375                                 if type == NAME:
376                                         cls_name = text
377                                         cls_sline = False
378                                         cls_defs = dict()
379                                         cls_vars = dict()
380                         elif text == ':':
381                                 cls_step = 2
382                 
383                 # Found 'class' name ... ':', now check if it's a single line statement
384                 elif cls_step == 2:
385                         if type == NEWLINE:
386                                 cls_sline = False
387                         else:
388                                 cls_sline = True
389                         cls_doc = ''
390                         cls_step = 3
391                 
392                 elif cls_step == 3:
393                         if not cls_doc and type == STRING:
394                                 cls_doc = _trim_doc(text)
395                         if cls_sline:
396                                 if type == NEWLINE:
397                                         classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno, cls_doc)
398                                         cls_step = 0
399                         else:
400                                 if type == DEDENT and indent <= cls_indent:
401                                         classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno, cls_doc)
402                                         cls_step = 0
403                 
404                 #################
405                 ## Def parsing ##
406                 #################
407                 
408                 # Look for 'def'
409                 if def_step == 0:
410                         if text == 'def':
411                                 def_name = None
412                                 def_lineno = start[0]
413                                 def_step = 1
414                 
415                 # Found 'def', look for def_name followed by '('
416                 elif def_step == 1:
417                         if type == NAME:
418                                 def_name = text
419                                 def_params = []
420                         elif def_name and text == '(':
421                                 def_step = 2
422                 
423                 # Found 'def' name '(', now identify the parameters upto ')'
424                 # TODO: Handle ellipsis '...'
425                 elif def_step == 2:
426                         if type == NAME:
427                                 def_params.append(text)
428                         elif text == ':':
429                                 def_step = 3
430                 
431                 # Found 'def' ... ':', now check if it's a single line statement
432                 elif def_step == 3:
433                         if type == NEWLINE:
434                                 def_sline = False
435                         else:
436                                 def_sline = True
437                         def_doc = ''
438                         def_step = 4
439                 
440                 elif def_step == 4:
441                         if type == STRING:
442                                 def_doc = _trim_doc(text)
443                         newdef = None
444                         if def_sline:
445                                 if type == NEWLINE:
446                                         newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
447                         else:
448                                 if type == NAME:
449                                         newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
450                         if newdef:
451                                 if cls_step > 0: # Parsing a class
452                                         cls_defs[def_name] = newdef
453                                 else:
454                                         defs[def_name] = newdef
455                                 def_step = 0
456                 
457                 ##########################
458                 ## Variable assignation ##
459                 ##########################
460                 
461                 if cls_step > 0: # Parsing a class
462                         # Look for 'self.???'
463                         if var1_step == 0:
464                                 if text == 'self':
465                                         var1_step = 1
466                         elif var1_step == 1:
467                                 if text == '.':
468                                         var_name = None
469                                         var1_step = 2
470                                 else:
471                                         var1_step = 0
472                         elif var1_step == 2:
473                                 if type == NAME:
474                                         var_name = text
475                                         if cls_vars.has_key(var_name):
476                                                 var_step = 0
477                                         else:
478                                                 var1_step = 3
479                         elif var1_step == 3:
480                                 if text == '=':
481                                         var1_step = 4
482                         elif var1_step == 4:
483                                 var_type = None
484                                 if type == NUMBER:
485                                         if text.find('.') != -1: var_type = float
486                                         else: var_type = int
487                                 elif type == STRING:
488                                         close = end[1]
489                                         var_type = str
490                                 elif text == '[':
491                                         close = line.find(']', end[1])
492                                         var_type = list
493                                 elif text == '(':
494                                         close = line.find(')', end[1])
495                                         var_type = tuple
496                                 elif text == '{':
497                                         close = line.find('}', end[1])
498                                         var_type = dict
499                                 elif text == 'dict':
500                                         close = line.find(')', end[1])
501                                         var_type = dict
502                                 if var_type and close+1 < len(line):
503                                         if line[close+1] != ' ' and line[close+1] != '\t':
504                                                 var_type = None
505                                 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
506                                 var1_step = 0
507                 
508                 elif def_step > 0: # Parsing a def
509                         # Look for 'global ???[,???]'
510                         if var2_step == 0:
511                                 if text == 'global':
512                                         var2_step = 1
513                         elif var2_step == 1:
514                                 if type == NAME:
515                                         if not vars.has_key(text):
516                                                 vars[text] = VarDesc(text, None, start[0])
517                                 elif text != ',' and type != NL:
518                                         var2_step == 0
519                 
520                 else: # In global scope
521                         if var3_step == 0:
522                                 # Look for names
523                                 if text == 'for':
524                                         var_accum = dict()
525                                         var_forflag = True
526                                 elif text == '=' or (var_forflag and text == 'in'):
527                                         var_forflag = False
528                                         var3_step = 1
529                                 elif type == NAME:
530                                         if prev_text != '.' and not vars.has_key(text):
531                                                 var_accum[text] = VarDesc(text, None, start[0])
532                                 elif not text in [',', '(', ')', '[', ']']:
533                                         var_accum = dict()
534                                         var_forflag = False
535                         elif var3_step == 1:
536                                 if len(var_accum) != 1:
537                                         var_type = None
538                                         vars.update(var_accum)
539                                 else:
540                                         var_name = var_accum.keys()[0]
541                                         var_type = None
542                                         if type == NUMBER:
543                                                 if text.find('.') != -1: var_type = float
544                                                 else: var_type = int
545                                         elif type == STRING: var_type = str
546                                         elif text == '[': var_type = list
547                                         elif text == '(': var_type = tuple
548                                         elif text == '{': var_type = dict
549                                         vars[var_name] = VarDesc(var_name, var_type, start[0])
550                                 var3_step = 0
551                 
552                 #######################
553                 ## General utilities ##
554                 #######################
555                 
556                 prev_type = type
557                 prev_text = text
558         
559         desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
560         desc.set_delay(10 * (time()-start_time) + 0.05)
561         
562         global _parse_cache
563         _parse_cache[hash(txt)] = desc
564         return desc
565
566 def get_modules(since=1):
567         """Returns the set of built-in modules and any modules that have been
568         imported into the system upto 'since' seconds ago.
569         """
570         
571         global _modules, _modules_updated
572         
573         t = time()
574         if _modules_updated < t - since:
575                 _modules.update(sys.modules)
576                 _modules_updated = t
577         return _modules.keys()
578
579 def suggest_cmp(x, y):
580         """Use this method when sorting a list of suggestions.
581         """
582         
583         return cmp(x[0].upper(), y[0].upper())
584
585 def get_module(name):
586         """Returns the module specified by its name. The module itself is imported
587         by this method and, as such, any initialization code will be executed.
588         """
589         
590         mod = __import__(name)
591         components = name.split('.')
592         for comp in components[1:]:
593                 mod = getattr(mod, comp)
594         return mod
595
596 def type_char(v):
597         """Returns the character used to signify the type of a variable. Use this
598         method to identify the type character for an item in a suggestion list.
599         
600         The following values are returned:
601           'm' if the parameter is a module
602           'f' if the parameter is callable
603           'v' if the parameter is variable or otherwise indeterminable
604         
605         """
606         
607         if isinstance(v, ModuleType):
608                 return 'm'
609         elif callable(v):
610                 return 'f'
611         else: 
612                 return 'v'
613
614 def get_context(txt):
615         """Establishes the context of the cursor in the given Blender Text object
616         
617         Returns one of:
618           CTX_NORMAL - Cursor is in a normal context
619           CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
620           CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
621           CTX_COMMENT - Cursor is inside a comment
622         
623         """
624         
625         l, cursor = txt.getCursorPos()
626         lines = txt.asLines(0, l+1)
627         
628         # FIXME: This method is too slow in large files for it to be called as often
629         # as it is. So for lines below the 1000th line we do this... (quorn)
630         if l > 1000: return CTX_NORMAL
631         
632         # Detect context (in string or comment)
633         in_str = CTX_NORMAL
634         for line in lines:
635                 if l == 0:
636                         end = cursor
637                 else:
638                         end = len(line)
639                         l -= 1
640                 
641                 # Comments end at new lines
642                 if in_str == CTX_COMMENT:
643                         in_str = CTX_NORMAL
644                 
645                 for i in range(end):
646                         if in_str == 0:
647                                 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
648                                 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
649                                 elif line[i] == '#': in_str = CTX_COMMENT
650                         else:
651                                 if in_str == CTX_SINGLE_QUOTE:
652                                         if line[i] == "'":
653                                                 in_str = CTX_NORMAL
654                                                 # In again if ' escaped, out again if \ escaped, and so on
655                                                 for a in range(i-1, -1, -1):
656                                                         if line[a] == '\\': in_str = 1-in_str
657                                                         else: break
658                                 elif in_str == CTX_DOUBLE_QUOTE:
659                                         if line[i] == '"':
660                                                 in_str = CTX_NORMAL
661                                                 # In again if " escaped, out again if \ escaped, and so on
662                                                 for a in range(i-1, -1, -1):
663                                                         if line[i-a] == '\\': in_str = 2-in_str
664                                                         else: break
665                 
666         return in_str
667
668 def current_line(txt):
669         """Extracts the Python script line at the cursor in the Blender Text object
670         provided and cursor position within this line as the tuple pair (line,
671         cursor).
672         """
673         
674         lineindex, cursor = txt.getCursorPos()
675         lines = txt.asLines()
676         line = lines[lineindex]
677         
678         # Join previous lines to this line if spanning
679         i = lineindex - 1
680         while i > 0:
681                 earlier = lines[i].rstrip()
682                 if earlier.endswith('\\'):
683                         line = earlier[:-1] + ' ' + line
684                         cursor += len(earlier)
685                 i -= 1
686         
687         # Join later lines while there is an explicit joining character
688         i = lineindex
689         while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
690                 later = lines[i+1].strip()
691                 line = line + ' ' + later[:-1]
692                 i += 1
693         
694         return line, cursor
695
696 def get_targets(line, cursor):
697         """Parses a period separated string of valid names preceding the cursor and
698         returns them as a list in the same order.
699         """
700         
701         i = cursor - 1
702         while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
703                 i -= 1
704         
705         return line[i+1:cursor].split('.')
706
707 def get_defs(txt):
708         """Returns a dictionary which maps definition names in the source code to
709         a list of their parameter names.
710         
711         The line 'def doit(one, two, three): print one' for example, results in the
712         mapping 'doit' : [ 'one', 'two', 'three' ]
713         """
714         
715         return get_cached_descriptor(txt).defs
716
717 def get_vars(txt):
718         """Returns a dictionary of variable names found in the specified Text
719         object. This method locates all names followed directly by an equal sign:
720         'a = ???' or indirectly as part of a tuple/list assignment or inside a
721         'for ??? in ???:' block.
722         """
723         
724         return get_cached_descriptor(txt).vars
725
726 def get_imports(txt):
727         """Returns a dictionary which maps symbol names in the source code to their
728         respective modules.
729         
730         The line 'from Blender import Text as BText' for example, results in the
731         mapping 'BText' : <module 'Blender.Text' (built-in)>
732         
733         Note that this method imports the modules to provide this mapping as as such
734         will execute any initilization code found within.
735         """
736         
737         return get_cached_descriptor(txt).imports
738
739 def get_builtins():
740         """Returns a dictionary of built-in modules, functions and variables."""
741         
742         return __builtin__.__dict__
743
744
745 #################################
746 ## Debugging utility functions ##
747 #################################
748
749 def print_cache_for(txt, period=sys.maxint):
750         """Prints out the data cached for a given Text object. If no period is
751         given the text will not be reparsed and the cached version will be returned.
752         Otherwise if the period has expired the text will be reparsed.
753         """
754         
755         desc = get_cached_descriptor(txt, period)
756         print '================================================'
757         print 'Name:', desc.name, '('+str(hash(txt))+')'
758         print '------------------------------------------------'
759         print 'Defs:'
760         for name, ddesc in desc.defs.items():
761                 print ' ', name, ddesc.params, ddesc.lineno
762                 print '   ', ddesc.doc
763         print '------------------------------------------------'
764         print 'Vars:'
765         for name, vdesc in desc.vars.items():
766                 print ' ', name, vdesc.type, vdesc.lineno
767         print '------------------------------------------------'
768         print 'Imports:'
769         for name, item in desc.imports.items():
770                 print ' ', name.ljust(15), item
771         print '------------------------------------------------'
772         print 'Classes:'
773         for clsnme, clsdsc in desc.classes.items():
774                 print '  *********************************'
775                 print '  Name:', clsnme
776                 print ' ', clsdsc.doc
777                 print '  ---------------------------------'
778                 print '  Defs:'
779                 for name, ddesc in clsdsc.defs.items():
780                         print '   ', name, ddesc.params, ddesc.lineno
781                         print '     ', ddesc.doc
782                 print '  ---------------------------------'
783                 print '  Vars:'
784                 for name, vdesc in clsdsc.vars.items():
785                         print '   ', name, vdesc.type, vdesc.lineno
786                 print '  *********************************'
787         print '================================================'