Fix for numeric var types creating an error.
[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                                         close = end[1]
486                                         if text.find('.') != -1: var_type = float
487                                         else: var_type = int
488                                 elif type == STRING:
489                                         close = end[1]
490                                         var_type = str
491                                 elif text == '[':
492                                         close = line.find(']', end[1])
493                                         var_type = list
494                                 elif text == '(':
495                                         close = line.find(')', end[1])
496                                         var_type = tuple
497                                 elif text == '{':
498                                         close = line.find('}', end[1])
499                                         var_type = dict
500                                 elif text == 'dict':
501                                         close = line.find(')', end[1])
502                                         var_type = dict
503                                 if var_type and close+1 < len(line):
504                                         if line[close+1] != ' ' and line[close+1] != '\t':
505                                                 var_type = None
506                                 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
507                                 var1_step = 0
508                 
509                 elif def_step > 0: # Parsing a def
510                         # Look for 'global ???[,???]'
511                         if var2_step == 0:
512                                 if text == 'global':
513                                         var2_step = 1
514                         elif var2_step == 1:
515                                 if type == NAME:
516                                         if not vars.has_key(text):
517                                                 vars[text] = VarDesc(text, None, start[0])
518                                 elif text != ',' and type != NL:
519                                         var2_step == 0
520                 
521                 else: # In global scope
522                         if var3_step == 0:
523                                 # Look for names
524                                 if text == 'for':
525                                         var_accum = dict()
526                                         var_forflag = True
527                                 elif text == '=' or (var_forflag and text == 'in'):
528                                         var_forflag = False
529                                         var3_step = 1
530                                 elif type == NAME:
531                                         if prev_text != '.' and not vars.has_key(text):
532                                                 var_accum[text] = VarDesc(text, None, start[0])
533                                 elif not text in [',', '(', ')', '[', ']']:
534                                         var_accum = dict()
535                                         var_forflag = False
536                         elif var3_step == 1:
537                                 if len(var_accum) != 1:
538                                         var_type = None
539                                         vars.update(var_accum)
540                                 else:
541                                         var_name = var_accum.keys()[0]
542                                         var_type = None
543                                         if type == NUMBER:
544                                                 if text.find('.') != -1: var_type = float
545                                                 else: var_type = int
546                                         elif type == STRING: var_type = str
547                                         elif text == '[': var_type = list
548                                         elif text == '(': var_type = tuple
549                                         elif text == '{': var_type = dict
550                                         vars[var_name] = VarDesc(var_name, var_type, start[0])
551                                 var3_step = 0
552                 
553                 #######################
554                 ## General utilities ##
555                 #######################
556                 
557                 prev_type = type
558                 prev_text = text
559         
560         desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
561         desc.set_delay(10 * (time()-start_time) + 0.05)
562         
563         global _parse_cache
564         _parse_cache[hash(txt)] = desc
565         return desc
566
567 def get_modules(since=1):
568         """Returns the set of built-in modules and any modules that have been
569         imported into the system upto 'since' seconds ago.
570         """
571         
572         global _modules, _modules_updated
573         
574         t = time()
575         if _modules_updated < t - since:
576                 _modules.update(sys.modules)
577                 _modules_updated = t
578         return _modules.keys()
579
580 def suggest_cmp(x, y):
581         """Use this method when sorting a list of suggestions.
582         """
583         
584         return cmp(x[0].upper(), y[0].upper())
585
586 def get_module(name):
587         """Returns the module specified by its name. The module itself is imported
588         by this method and, as such, any initialization code will be executed.
589         """
590         
591         mod = __import__(name)
592         components = name.split('.')
593         for comp in components[1:]:
594                 mod = getattr(mod, comp)
595         return mod
596
597 def type_char(v):
598         """Returns the character used to signify the type of a variable. Use this
599         method to identify the type character for an item in a suggestion list.
600         
601         The following values are returned:
602           'm' if the parameter is a module
603           'f' if the parameter is callable
604           'v' if the parameter is variable or otherwise indeterminable
605         
606         """
607         
608         if isinstance(v, ModuleType):
609                 return 'm'
610         elif callable(v):
611                 return 'f'
612         else: 
613                 return 'v'
614
615 def get_context(txt):
616         """Establishes the context of the cursor in the given Blender Text object
617         
618         Returns one of:
619           CTX_NORMAL - Cursor is in a normal context
620           CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
621           CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
622           CTX_COMMENT - Cursor is inside a comment
623         
624         """
625         
626         l, cursor = txt.getCursorPos()
627         lines = txt.asLines(0, l+1)
628         
629         # FIXME: This method is too slow in large files for it to be called as often
630         # as it is. So for lines below the 1000th line we do this... (quorn)
631         if l > 1000: return CTX_NORMAL
632         
633         # Detect context (in string or comment)
634         in_str = CTX_NORMAL
635         for line in lines:
636                 if l == 0:
637                         end = cursor
638                 else:
639                         end = len(line)
640                         l -= 1
641                 
642                 # Comments end at new lines
643                 if in_str == CTX_COMMENT:
644                         in_str = CTX_NORMAL
645                 
646                 for i in range(end):
647                         if in_str == 0:
648                                 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
649                                 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
650                                 elif line[i] == '#': in_str = CTX_COMMENT
651                         else:
652                                 if in_str == CTX_SINGLE_QUOTE:
653                                         if line[i] == "'":
654                                                 in_str = CTX_NORMAL
655                                                 # In again if ' escaped, out again if \ escaped, and so on
656                                                 for a in range(i-1, -1, -1):
657                                                         if line[a] == '\\': in_str = 1-in_str
658                                                         else: break
659                                 elif in_str == CTX_DOUBLE_QUOTE:
660                                         if line[i] == '"':
661                                                 in_str = CTX_NORMAL
662                                                 # In again if " escaped, out again if \ escaped, and so on
663                                                 for a in range(i-1, -1, -1):
664                                                         if line[i-a] == '\\': in_str = 2-in_str
665                                                         else: break
666                 
667         return in_str
668
669 def current_line(txt):
670         """Extracts the Python script line at the cursor in the Blender Text object
671         provided and cursor position within this line as the tuple pair (line,
672         cursor).
673         """
674         
675         lineindex, cursor = txt.getCursorPos()
676         lines = txt.asLines()
677         line = lines[lineindex]
678         
679         # Join previous lines to this line if spanning
680         i = lineindex - 1
681         while i > 0:
682                 earlier = lines[i].rstrip()
683                 if earlier.endswith('\\'):
684                         line = earlier[:-1] + ' ' + line
685                         cursor += len(earlier)
686                 i -= 1
687         
688         # Join later lines while there is an explicit joining character
689         i = lineindex
690         while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
691                 later = lines[i+1].strip()
692                 line = line + ' ' + later[:-1]
693                 i += 1
694         
695         return line, cursor
696
697 def get_targets(line, cursor):
698         """Parses a period separated string of valid names preceding the cursor and
699         returns them as a list in the same order.
700         """
701         
702         i = cursor - 1
703         while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
704                 i -= 1
705         
706         return line[i+1:cursor].split('.')
707
708 def get_defs(txt):
709         """Returns a dictionary which maps definition names in the source code to
710         a list of their parameter names.
711         
712         The line 'def doit(one, two, three): print one' for example, results in the
713         mapping 'doit' : [ 'one', 'two', 'three' ]
714         """
715         
716         return get_cached_descriptor(txt).defs
717
718 def get_vars(txt):
719         """Returns a dictionary of variable names found in the specified Text
720         object. This method locates all names followed directly by an equal sign:
721         'a = ???' or indirectly as part of a tuple/list assignment or inside a
722         'for ??? in ???:' block.
723         """
724         
725         return get_cached_descriptor(txt).vars
726
727 def get_imports(txt):
728         """Returns a dictionary which maps symbol names in the source code to their
729         respective modules.
730         
731         The line 'from Blender import Text as BText' for example, results in the
732         mapping 'BText' : <module 'Blender.Text' (built-in)>
733         
734         Note that this method imports the modules to provide this mapping as as such
735         will execute any initilization code found within.
736         """
737         
738         return get_cached_descriptor(txt).imports
739
740 def get_builtins():
741         """Returns a dictionary of built-in modules, functions and variables."""
742         
743         return __builtin__.__dict__
744
745
746 #################################
747 ## Debugging utility functions ##
748 #################################
749
750 def print_cache_for(txt, period=sys.maxint):
751         """Prints out the data cached for a given Text object. If no period is
752         given the text will not be reparsed and the cached version will be returned.
753         Otherwise if the period has expired the text will be reparsed.
754         """
755         
756         desc = get_cached_descriptor(txt, period)
757         print '================================================'
758         print 'Name:', desc.name, '('+str(hash(txt))+')'
759         print '------------------------------------------------'
760         print 'Defs:'
761         for name, ddesc in desc.defs.items():
762                 print ' ', name, ddesc.params, ddesc.lineno
763                 print '   ', ddesc.doc
764         print '------------------------------------------------'
765         print 'Vars:'
766         for name, vdesc in desc.vars.items():
767                 print ' ', name, vdesc.type, vdesc.lineno
768         print '------------------------------------------------'
769         print 'Imports:'
770         for name, item in desc.imports.items():
771                 print ' ', name.ljust(15), item
772         print '------------------------------------------------'
773         print 'Classes:'
774         for clsnme, clsdsc in desc.classes.items():
775                 print '  *********************************'
776                 print '  Name:', clsnme
777                 print ' ', clsdsc.doc
778                 print '  ---------------------------------'
779                 print '  Defs:'
780                 for name, ddesc in clsdsc.defs.items():
781                         print '   ', name, ddesc.params, ddesc.lineno
782                         print '     ', ddesc.doc
783                 print '  ---------------------------------'
784                 print '  Vars:'
785                 for name, vdesc in clsdsc.vars.items():
786                         print '   ', name, vdesc.type, vdesc.lineno
787                 print '  *********************************'
788         print '================================================'