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