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
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.
15 def __init__(self, name, imports, classes, defs, vars, incomplete=False):
17 self.imports = imports
18 self.classes = classes
21 self.incomplete = incomplete
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.
33 def __init__(self, name, defs, vars, lineno):
40 """Describes a function through its name and list of parameters (name,
41 params) and the line on which it is defined (lineno).
44 def __init__(self, name, params, lineno):
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
55 def __init__(self, name, type, lineno):
57 self.type = type # None for unknown (supports: dict/list/str)
67 # Special time period constants
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',
77 ModuleType = type(__builtin__)
78 NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
80 _modules = dict([(n, None) for n in sys.builtin_module_names])
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.
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.
93 global _parse_cache, NoneScriptDesc, AUTO
106 if _parse_cache.has_key(key):
107 desc = _parse_cache[key]
108 if desc.time >= time() - period:
109 parse = desc.incomplete
112 desc = parse_text(txt)
117 """Parses an entire script's text and returns a ScriptDesc instance
118 containing information about the script.
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.
127 tokens = generate_tokens(txt.readline) # Throws TokenError
129 curl, cursor = txt.getCursorPos()
130 linen = curl + 1 # Token line numbers are one-based
155 type, string, start, end, line = tokens.next()
156 except StopIteration:
162 # Skip all comments and line joining characters
163 if type == COMMENT or type == NL:
175 #########################
176 ## Module importing... ##
177 #########################
181 # Default, look for 'from' or 'import' to start
186 elif string == 'import':
191 # Found a 'from', create imp_from in form '???.???...'
193 if string == 'import':
194 imp_from = '.'.join(imp_tmp)
198 imp_tmp.append(string)
200 imp_step = 0 # Invalid syntax
202 # Found 'import', imp_from is populated or None, create imp_name
205 imp_name = '.'.join(imp_tmp)
207 elif type == NAME or string == '*':
208 imp_tmp.append(string)
210 imp_name = '.'.join(imp_tmp)
214 # Found 'as', change imp_symb to this value and go back to step 2
221 # Both imp_name and imp_symb have now been populated so we can import
224 # Handle special case of 'import *'
226 parent = get_module(imp_from)
227 imports.update(parent.__dict__)
230 # Try importing the name as a module
233 module = get_module(imp_from +'.'+ imp_name)
235 module = get_module(imp_name)
236 except (ImportError, ValueError, AttributeError, TypeError):
237 # Try importing name as an attribute of the parent
239 module = __import__(imp_from, globals(), locals(), [imp_name])
240 except (ImportError, ValueError, AttributeError, TypeError):
243 imports[imp_symb] = getattr(module, imp_name)
245 imports[imp_symb] = module
247 # More to import from the same module?
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
263 if string == 'class':
265 cls_lineno = start[0]
269 # Found 'class', look for cls_name followed by '('
280 # Found 'class' name ... ':', now check if it's a single line statement
292 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
295 if type == DEDENT and indent <= cls_indent:
296 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
307 def_lineno = start[0]
310 # Found 'def', look for def_name followed by '('
315 elif def_name and string == '(':
318 # Found 'def' name '(', now identify the parameters upto ')'
319 # TODO: Handle ellipsis '...'
322 def_params.append(string)
324 if cls_step > 0: # Parsing a class
325 cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
327 defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
330 ##########################
331 ## Variable assignation ##
332 ##########################
334 if cls_step > 0: # Parsing a class
335 # Look for 'self.???'
348 if cls_vars.has_key(var_name):
358 close = line.find(']', end[1])
364 close = line.find(')', end[1])
367 close = line.find('}', end[1])
369 elif string == 'dict':
370 close = line.find(')', end[1])
372 if var_type and close+1 < len(line):
373 if line[close+1] != ' ' and line[close+1] != '\t':
375 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
378 elif def_step > 0: # Parsing a def
379 # Look for 'global ???[,???]'
381 if string == 'global':
386 elif string != ',' and type != NL:
389 else: # In global scope
395 elif string == '=' or (var_forflag and string == 'in'):
399 if prev_string != '.' and not vars.has_key(string):
400 var_accum[string] = VarDesc(string, None, start[0])
401 elif not string in [',', '(', ')', '[', ']']:
405 if len(var_accum) != 1:
407 vars.update(var_accum)
409 var_name = var_accum.keys()[0]
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])
418 #######################
419 ## General utilities ##
420 #######################
425 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
429 _parse_cache[hash(txt.name)] = desc
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.
437 global _modules, _modules_updated
440 if _modules_updated < t - since:
441 _modules.update(sys.modules)
443 return _modules.keys()
445 def suggest_cmp(x, y):
446 """Use this method when sorting a list of suggestions.
449 return cmp(x[0].upper(), y[0].upper())
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.
456 mod = __import__(name)
457 components = name.split('.')
458 for comp in components[1:]:
459 mod = getattr(mod, comp)
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.
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
473 if isinstance(v, ModuleType):
480 def get_context(txt):
481 """Establishes the context of the cursor in the given Blender Text object
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
491 global CTX_NORMAL, CTX_SINGLE_QUOTE, CTX_DOUBLE_QUOTE, CTX_COMMENT
492 l, cursor = txt.getCursorPos()
493 lines = txt.asLines()[:l+1]
495 # Detect context (in string or comment)
504 # Comments end at new lines
505 if in_str == CTX_COMMENT:
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
514 if in_str == CTX_SINGLE_QUOTE:
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
521 elif in_str == CTX_DOUBLE_QUOTE:
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
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,
537 lineindex, cursor = txt.getCursorPos()
538 lines = txt.asLines()
539 line = lines[lineindex]
541 # Join previous lines to this line if spanning
544 earlier = lines[i].rstrip()
545 if earlier.endswith('\\'):
546 line = earlier[:-1] + ' ' + line
547 cursor += len(earlier)
550 # Join later lines while there is an explicit joining character
552 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
553 later = lines[i+1].strip()
554 line = line + ' ' + later[:-1]
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.
566 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
569 pre = line[i+1:cursor]
570 return pre.split('.')
573 """Returns a dictionary which maps definition names in the source code to
574 a list of their parameter names.
576 The line 'def doit(one, two, three): print one' for example, results in the
577 mapping 'doit' : [ 'one', 'two', 'three' ]
580 return get_cached_descriptor(txt).defs
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.
589 return get_cached_descriptor(txt).vars
591 def get_imports(txt):
592 """Returns a dictionary which maps symbol names in the source code to their
595 The line 'from Blender import Text as BText' for example, results in the
596 mapping 'BText' : <module 'Blender.Text' (built-in)>
598 Note that this method imports the modules to provide this mapping as as such
599 will execute any initilization code found within.
602 return get_cached_descriptor(txt).imports
605 """Returns a dictionary of built-in modules, functions and variables."""
607 return __builtin__.__dict__
610 #################################
611 ## Debugging utility functions ##
612 #################################
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.
620 desc = get_cached_descriptor(txt, period)
621 print '================================================'
622 print 'Name:', desc.name, '('+str(hash(txt))+')'
623 print '------------------------------------------------'
625 for name, ddesc in desc.defs.items():
626 print ' ', name, ddesc.params, ddesc.lineno
627 print '------------------------------------------------'
629 for name, vdesc in desc.vars.items():
630 print ' ', name, vdesc.type, vdesc.lineno
631 print '------------------------------------------------'
633 for name, item in desc.imports.items():
634 print ' ', name.ljust(15), item
635 print '------------------------------------------------'
637 for clsnme, clsdsc in desc.classes.items():
638 print ' *********************************'
639 print ' Name:', clsnme
640 print ' ---------------------------------'
642 for name, ddesc in clsdsc.defs.items():
643 print ' ', name, ddesc.params, ddesc.lineno
644 print ' ---------------------------------'
646 for name, vdesc in clsdsc.vars.items():
647 print ' ', name, vdesc.type, vdesc.lineno
648 print ' *********************************'
649 print '================================================'