Typing 'from a import b' threw an error if b was not found. Accidentally moved this...
[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                                                 imports[imp_symb] = getattr(module, imp_name)
241                                         except (ImportError, ValueError, AttributeError, TypeError):
242                                                 pass
243                                 else:
244                                         imports[imp_symb] = module
245                         
246                         # More to import from the same module?
247                         if string == ',':
248                                 imp_tmp = []
249                                 imp_step = 2
250                         else:
251                                 imp_step = 0
252                 
253                 ###################
254                 ## Class parsing ##
255                 ###################
256                 
257                 # If we are inside a class then def and variable parsing should be done
258                 # for the class. Otherwise the definitions are considered global
259                 
260                 # Look for 'class'
261                 if cls_step == 0:
262                         if string == 'class':
263                                 cls_name = None
264                                 cls_lineno = start[0]
265                                 cls_indent = indent
266                                 cls_step = 1
267                 
268                 # Found 'class', look for cls_name followed by '('
269                 elif cls_step == 1:
270                         if not cls_name:
271                                 if type == NAME:
272                                         cls_name = string
273                                         cls_sline = False
274                                         cls_defs = dict()
275                                         cls_vars = dict()
276                         elif string == ':':
277                                 cls_step = 2
278                 
279                 # Found 'class' name ... ':', now check if it's a single line statement
280                 elif cls_step == 2:
281                         if type == NEWLINE:
282                                 cls_sline = False
283                                 cls_step = 3
284                         else:
285                                 cls_sline = True
286                                 cls_step = 3
287                 
288                 elif cls_step == 3:
289                         if cls_sline:
290                                 if type == NEWLINE:
291                                         classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
292                                         cls_step = 0
293                         else:
294                                 if type == DEDENT and indent <= cls_indent:
295                                         classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
296                                         cls_step = 0
297                 
298                 #################
299                 ## Def parsing ##
300                 #################
301                 
302                 # Look for 'def'
303                 if def_step == 0:
304                         if string == 'def':
305                                 def_name = None
306                                 def_lineno = start[0]
307                                 def_step = 1
308                 
309                 # Found 'def', look for def_name followed by '('
310                 elif def_step == 1:
311                         if type == NAME:
312                                 def_name = string
313                                 def_params = []
314                         elif def_name and string == '(':
315                                 def_step = 2
316                 
317                 # Found 'def' name '(', now identify the parameters upto ')'
318                 # TODO: Handle ellipsis '...'
319                 elif def_step == 2:
320                         if type == NAME:
321                                 def_params.append(string)
322                         elif string == ')':
323                                 if cls_step > 0: # Parsing a class
324                                         cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
325                                 else:
326                                         defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
327                                 def_step = 0
328                 
329                 ##########################
330                 ## Variable assignation ##
331                 ##########################
332                 
333                 if cls_step > 0: # Parsing a class
334                         # Look for 'self.???'
335                         if var1_step == 0:
336                                 if string == 'self':
337                                         var1_step = 1
338                         elif var1_step == 1:
339                                 if string == '.':
340                                         var_name = None
341                                         var1_step = 2
342                                 else:
343                                         var1_step = 0
344                         elif var1_step == 2:
345                                 if type == NAME:
346                                         var_name = string
347                                         if cls_vars.has_key(var_name):
348                                                 var_step = 0
349                                         else:
350                                                 var1_step = 3
351                         elif var1_step == 3:
352                                 if string == '=':
353                                         var1_step = 4
354                         elif var1_step == 4:
355                                 var_type = None
356                                 if string == '[':
357                                         close = line.find(']', end[1])
358                                         var_type = list
359                                 elif type == STRING:
360                                         close = end[1]
361                                         var_type = str
362                                 elif string == '(':
363                                         close = line.find(')', end[1])
364                                         var_type = tuple
365                                 elif string == '{':
366                                         close = line.find('}', end[1])
367                                         var_type = dict
368                                 elif string == 'dict':
369                                         close = line.find(')', end[1])
370                                         var_type = dict
371                                 if var_type and close+1 < len(line):
372                                         if line[close+1] != ' ' and line[close+1] != '\t':
373                                                 var_type = None
374                                 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
375                                 var1_step = 0
376                 
377                 elif def_step > 0: # Parsing a def
378                         # Look for 'global ???[,???]'
379                         if var2_step == 0:
380                                 if string == 'global':
381                                         var2_step = 1
382                         elif var2_step == 1:
383                                 if type == NAME:
384                                         vars[string] = True
385                                 elif string != ',' and type != NL:
386                                         var2_step == 0
387                 
388                 else: # In global scope
389                         if var3_step == 0:
390                                 # Look for names
391                                 if string == 'for':
392                                         var_accum = dict()
393                                         var_forflag = True
394                                 elif string == '=' or (var_forflag and string == 'in'):
395                                         var_forflag = False
396                                         var3_step = 1
397                                 elif type == NAME:
398                                         if prev_string != '.' and not vars.has_key(string):
399                                                 var_accum[string] = VarDesc(string, None, start[0])
400                                 elif not string in [',', '(', ')', '[', ']']:
401                                         var_accum = dict()
402                                         var_forflag = False
403                         elif var3_step == 1:
404                                 if len(var_accum) != 1:
405                                         var_type = None
406                                         vars.update(var_accum)
407                                 else:
408                                         var_name = var_accum.keys()[0]
409                                         var_type = None
410                                         if string == '[': var_type = list
411                                         elif type == STRING: var_type = str
412                                         elif string == '(': var_type = tuple
413                                         elif string == '{': var_type = dict
414                                         vars[var_name] = VarDesc(var_name, var_type, start[0])
415                                 var3_step = 0
416                 
417                 #######################
418                 ## General utilities ##
419                 #######################
420                 
421                 prev_type = type
422                 prev_string = string
423         
424         desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
425         desc.set_time()
426         
427         global _parse_cache
428         _parse_cache[hash(txt.name)] = desc
429         return desc
430
431 def get_modules(since=1):
432         """Returns the set of built-in modules and any modules that have been
433         imported into the system upto 'since' seconds ago.
434         """
435         
436         global _modules, _modules_updated
437         
438         t = time()
439         if _modules_updated < t - since:
440                 _modules.update(sys.modules)
441                 _modules_updated = t
442         return _modules.keys()
443
444 def suggest_cmp(x, y):
445         """Use this method when sorting a list of suggestions.
446         """
447         
448         return cmp(x[0].upper(), y[0].upper())
449
450 def get_module(name):
451         """Returns the module specified by its name. The module itself is imported
452         by this method and, as such, any initialization code will be executed.
453         """
454         
455         mod = __import__(name)
456         components = name.split('.')
457         for comp in components[1:]:
458                 mod = getattr(mod, comp)
459         return mod
460
461 def type_char(v):
462         """Returns the character used to signify the type of a variable. Use this
463         method to identify the type character for an item in a suggestion list.
464         
465         The following values are returned:
466           'm' if the parameter is a module
467           'f' if the parameter is callable
468           'v' if the parameter is variable or otherwise indeterminable
469         
470         """
471         
472         if isinstance(v, ModuleType):
473                 return 'm'
474         elif callable(v):
475                 return 'f'
476         else: 
477                 return 'v'
478
479 def get_context(txt):
480         """Establishes the context of the cursor in the given Blender Text object
481         
482         Returns one of:
483           CTX_NORMAL - Cursor is in a normal context
484           CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
485           CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
486           CTX_COMMENT - Cursor is inside a comment
487         
488         """
489         
490         global CTX_NORMAL, CTX_SINGLE_QUOTE, CTX_DOUBLE_QUOTE, CTX_COMMENT
491         l, cursor = txt.getCursorPos()
492         lines = txt.asLines()[:l+1]
493         
494         # Detect context (in string or comment)
495         in_str = CTX_NORMAL
496         for line in lines:
497                 if l == 0:
498                         end = cursor
499                 else:
500                         end = len(line)
501                         l -= 1
502                 
503                 # Comments end at new lines
504                 if in_str == CTX_COMMENT:
505                         in_str = CTX_NORMAL
506                 
507                 for i in range(end):
508                         if in_str == 0:
509                                 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
510                                 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
511                                 elif line[i] == '#': in_str = CTX_COMMENT
512                         else:
513                                 if in_str == CTX_SINGLE_QUOTE:
514                                         if line[i] == "'":
515                                                 in_str = CTX_NORMAL
516                                                 # In again if ' escaped, out again if \ escaped, and so on
517                                                 for a in range(i-1, -1, -1):
518                                                         if line[a] == '\\': in_str = 1-in_str
519                                                         else: break
520                                 elif in_str == CTX_DOUBLE_QUOTE:
521                                         if line[i] == '"':
522                                                 in_str = CTX_NORMAL
523                                                 # In again if " escaped, out again if \ escaped, and so on
524                                                 for a in range(i-1, -1, -1):
525                                                         if line[i-a] == '\\': in_str = 2-in_str
526                                                         else: break
527                 
528         return in_str
529
530 def current_line(txt):
531         """Extracts the Python script line at the cursor in the Blender Text object
532         provided and cursor position within this line as the tuple pair (line,
533         cursor).
534         """
535         
536         lineindex, cursor = txt.getCursorPos()
537         lines = txt.asLines()
538         line = lines[lineindex]
539         
540         # Join previous lines to this line if spanning
541         i = lineindex - 1
542         while i > 0:
543                 earlier = lines[i].rstrip()
544                 if earlier.endswith('\\'):
545                         line = earlier[:-1] + ' ' + line
546                         cursor += len(earlier)
547                 i -= 1
548         
549         # Join later lines while there is an explicit joining character
550         i = lineindex
551         while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
552                 later = lines[i+1].strip()
553                 line = line + ' ' + later[:-1]
554                 i += 1
555         
556         return line, cursor
557
558 def get_targets(line, cursor):
559         """Parses a period separated string of valid names preceding the cursor and
560         returns them as a list in the same order.
561         """
562         
563         targets = []
564         i = cursor - 1
565         while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
566                 i -= 1
567         
568         pre = line[i+1:cursor]
569         return pre.split('.')
570
571 def get_defs(txt):
572         """Returns a dictionary which maps definition names in the source code to
573         a list of their parameter names.
574         
575         The line 'def doit(one, two, three): print one' for example, results in the
576         mapping 'doit' : [ 'one', 'two', 'three' ]
577         """
578         
579         return get_cached_descriptor(txt).defs
580
581 def get_vars(txt):
582         """Returns a dictionary of variable names found in the specified Text
583         object. This method locates all names followed directly by an equal sign:
584         'a = ???' or indirectly as part of a tuple/list assignment or inside a
585         'for ??? in ???:' block.
586         """
587         
588         return get_cached_descriptor(txt).vars
589
590 def get_imports(txt):
591         """Returns a dictionary which maps symbol names in the source code to their
592         respective modules.
593         
594         The line 'from Blender import Text as BText' for example, results in the
595         mapping 'BText' : <module 'Blender.Text' (built-in)>
596         
597         Note that this method imports the modules to provide this mapping as as such
598         will execute any initilization code found within.
599         """
600         
601         return get_cached_descriptor(txt).imports
602
603 def get_builtins():
604         """Returns a dictionary of built-in modules, functions and variables."""
605         
606         return __builtin__.__dict__
607
608
609 #################################
610 ## Debugging utility functions ##
611 #################################
612
613 def print_cache_for(txt, period=sys.maxint):
614         """Prints out the data cached for a given Text object. If no period is
615         given the text will not be reparsed and the cached version will be returned.
616         Otherwise if the period has expired the text will be reparsed.
617         """
618         
619         desc = get_cached_descriptor(txt, period)
620         print '================================================'
621         print 'Name:', desc.name, '('+str(hash(txt))+')'
622         print '------------------------------------------------'
623         print 'Defs:'
624         for name, ddesc in desc.defs.items():
625                 print ' ', name, ddesc.params, ddesc.lineno
626         print '------------------------------------------------'
627         print 'Vars:'
628         for name, vdesc in desc.vars.items():
629                 print ' ', name, vdesc.type, vdesc.lineno
630         print '------------------------------------------------'
631         print 'Imports:'
632         for name, item in desc.imports.items():
633                 print ' ', name.ljust(15), item
634         print '------------------------------------------------'
635         print 'Classes:'
636         for clsnme, clsdsc in desc.classes.items():
637                 print '  *********************************'
638                 print '  Name:', clsnme
639                 print '  ---------------------------------'
640                 print '  Defs:'
641                 for name, ddesc in clsdsc.defs.items():
642                         print '   ', name, ddesc.params, ddesc.lineno
643                 print '  ---------------------------------'
644                 print '  Vars:'
645                 for name, vdesc in clsdsc.vars.items():
646                         print '   ', name, vdesc.type, vdesc.lineno
647                 print '  *********************************'
648         print '================================================'