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