2 import __builtin__, tokenize
3 from Blender.sys import time
4 from tokenize import generate_tokens, TokenError, \
5 COMMENT, DEDENT, INDENT, NAME, NEWLINE, NL
9 def __init__(self, name, imports, classes, defs, vars, incomplete=False):
11 self.imports = imports
12 self.classes = classes
15 self.incomplete = incomplete
23 def __init__(self, name, defs, vars, lineno):
31 def __init__(self, name, params, lineno):
38 def __init__(self, name, type, lineno):
40 self.type = type # None for unknown (supports: dict/list/str)
50 # Special period constants
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',
60 ModuleType = type(__builtin__)
61 NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
63 _modules = dict([(n, None) for n in sys.builtin_module_names])
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.
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.
76 global _parse_cache, NoneScriptDesc, AUTO
89 if _parse_cache.has_key(key):
90 desc = _parse_cache[key]
91 if desc.time >= time() - period:
92 parse = desc.incomplete
96 desc = parse_text(txt)
98 if _parse_cache.has_key(key):
100 desc = NoneScriptDesc
105 """Parses an entire script's text and returns a ScriptDesc instance
106 containing information about the script.
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.
115 tokens = generate_tokens(txt.readline) # Throws TokenError
117 curl, cursor = txt.getCursorPos()
118 linen = curl + 1 # Token line numbers are one-based
142 for type, string, start, end, line in tokens:
144 # Skip all comments and line joining characters
145 if type == COMMENT or type == NL:
157 #########################
158 ## Module importing... ##
159 #########################
163 # Default, look for 'from' or 'import' to start
168 elif string == 'import':
173 # Found a 'from', create imp_from in form '???.???...'
175 if string == 'import':
176 imp_from = '.'.join(imp_tmp)
180 imp_tmp.append(string)
182 imp_step = 0 # Invalid syntax
184 # Found 'import', imp_from is populated or None, create imp_name
187 imp_name = '.'.join(imp_tmp)
189 elif type == NAME or string == '*':
190 imp_tmp.append(string)
192 imp_name = '.'.join(imp_tmp)
196 # Found 'as', change imp_symb to this value and go back to step 2
203 # Both imp_name and imp_symb have now been populated so we can import
206 # Handle special case of 'import *'
208 parent = get_module(imp_from)
209 imports.update(parent.__dict__)
212 # Try importing the name as a module
215 module = get_module(imp_from +'.'+ imp_name)
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
222 module = __import__(imp_from, globals(), locals(), [imp_name])
223 imports[imp_symb] = getattr(module, imp_name)
224 except (ImportError, ValueError, AttributeError, TypeError):
227 # More to import from the same module?
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
243 if string == 'class':
245 cls_lineno = start[0]
249 # Found 'class', look for cls_name followed by '('
260 # Found 'class' name ... ':', now check if it's a single line statement
272 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
275 if type == DEDENT and indent <= cls_indent:
276 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
287 def_lineno = start[0]
290 # Found 'def', look for def_name followed by '('
295 elif def_name and string == '(':
298 # Found 'def' name '(', now identify the parameters upto ')'
299 # TODO: Handle ellipsis '...'
302 def_params.append(string)
304 if cls_step > 0: # Parsing a class
305 cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
307 defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
310 ##########################
311 ## Variable assignation ##
312 ##########################
314 if cls_step > 0: # Parsing a class
315 # Look for 'self.???'
328 if cls_vars.has_key(var_name):
338 close = line.find(']', end[1])
340 elif string == '"' or string == '"':
341 close = line.find(string, end[1])
344 close = line.find(')', end[1])
347 close = line.find('}', end[1])
349 elif string == 'dict':
350 close = line.find(')', end[1])
352 if var_type and close+1 < len(line):
353 if line[close+1] != ' ' and line[close+1] != '\t':
355 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
358 elif def_step > 0: # Parsing a def
359 # Look for 'global ???[,???]'
361 if string == 'global':
366 elif string != ',' and type != NL:
369 else: # In global scope
375 elif string == '=' or (var_forflag and string == 'in'):
379 if prev_string != '.' and not vars.has_key(string):
380 var_accum[string] = VarDesc(string, None, start[0])
381 elif not string in [',', '(', ')', '[', ']']:
385 if len(var_accum) != 1:
387 vars.update(var_accum)
389 var_name = var_accum.keys()[0]
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])
398 #######################
399 ## General utilities ##
400 #######################
411 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
415 _parse_cache[hash(txt.name)] = desc
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.
423 global _modules, _modules_updated
426 if _modules_updated < t - since:
427 _modules.update(sys.modules)
429 return _modules.keys()
431 def suggest_cmp(x, y):
432 """Use this method when sorting a list of suggestions.
435 return cmp(x[0].upper(), y[0].upper())
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.
442 mod = __import__(name)
443 components = name.split('.')
444 for comp in components[1:]:
445 mod = getattr(mod, comp)
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.
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
459 if isinstance(v, ModuleType):
466 def get_context(txt):
467 """Establishes the context of the cursor in the given Blender Text object
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
477 global CTX_NORMAL, CTX_SINGLE_QUOTE, CTX_DOUBLE_QUOTE, CTX_COMMENT
478 l, cursor = txt.getCursorPos()
479 lines = txt.asLines()[:l+1]
481 # Detect context (in string or comment)
490 # Comments end at new lines
491 if in_str == CTX_COMMENT:
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
500 if in_str == CTX_SINGLE_QUOTE:
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
507 elif in_str == CTX_DOUBLE_QUOTE:
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
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,
523 lineindex, cursor = txt.getCursorPos()
524 lines = txt.asLines()
525 line = lines[lineindex]
527 # Join previous lines to this line if spanning
530 earlier = lines[i].rstrip()
531 if earlier.endswith('\\'):
532 line = earlier[:-1] + ' ' + line
533 cursor += len(earlier)
536 # Join later lines while there is an explicit joining character
538 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
539 later = lines[i+1].strip()
540 line = line + ' ' + later[:-1]
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.
552 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
555 pre = line[i+1:cursor]
556 return pre.split('.')
559 """Returns a dictionary which maps definition names in the source code to
560 a list of their parameter names.
562 The line 'def doit(one, two, three): print one' for example, results in the
563 mapping 'doit' : [ 'one', 'two', 'three' ]
566 return get_cached_descriptor(txt).defs
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.
575 return get_cached_descriptor(txt).vars
577 def get_imports(txt):
578 """Returns a dictionary which maps symbol names in the source code to their
581 The line 'from Blender import Text as BText' for example, results in the
582 mapping 'BText' : <module 'Blender.Text' (built-in)>
584 Note that this method imports the modules to provide this mapping as as such
585 will execute any initilization code found within.
588 return get_cached_descriptor(txt).imports
591 """Returns a dictionary of built-in modules, functions and variables."""
593 return __builtin__.__dict__
596 #################################
597 ## Debugging utility functions ##
598 #################################
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.
606 desc = get_cached_descriptor(txt, period)
607 print '================================================'
608 print 'Name:', desc.name, '('+str(hash(txt))+')'
609 print '------------------------------------------------'
611 for name, ddesc in desc.defs.items():
612 print ' ', name, ddesc.params, ddesc.lineno
613 print '------------------------------------------------'
615 for name, vdesc in desc.vars.items():
616 print ' ', name, vdesc.type, vdesc.lineno
617 print '------------------------------------------------'
619 for name, item in desc.imports.items():
620 print ' ', name.ljust(15), item
621 print '------------------------------------------------'
623 for clsnme, clsdsc in desc.classes.items():
624 print ' *********************************'
625 print ' Name:', clsnme
626 print ' ---------------------------------'
628 for name, ddesc in clsdsc.defs.items():
629 print ' ', name, ddesc.params, ddesc.lineno
630 print ' ---------------------------------'
632 for name, vdesc in clsdsc.vars.items():
633 print ' ', name, vdesc.type, vdesc.lineno
634 print ' *********************************'
635 print '================================================'