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