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 imports[imp_symb] = getattr(module, imp_name)
241 except (ImportError, ValueError, AttributeError, TypeError):
244 imports[imp_symb] = module
246 # More to import from the same module?
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
262 if string == 'class':
264 cls_lineno = start[0]
268 # Found 'class', look for cls_name followed by '('
279 # Found 'class' name ... ':', now check if it's a single line statement
291 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
294 if type == DEDENT and indent <= cls_indent:
295 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
306 def_lineno = start[0]
309 # Found 'def', look for def_name followed by '('
314 elif def_name and string == '(':
317 # Found 'def' name '(', now identify the parameters upto ')'
318 # TODO: Handle ellipsis '...'
321 def_params.append(string)
323 if cls_step > 0: # Parsing a class
324 cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
326 defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
329 ##########################
330 ## Variable assignation ##
331 ##########################
333 if cls_step > 0: # Parsing a class
334 # Look for 'self.???'
347 if cls_vars.has_key(var_name):
357 close = line.find(']', end[1])
363 close = line.find(')', end[1])
366 close = line.find('}', end[1])
368 elif string == 'dict':
369 close = line.find(')', end[1])
371 if var_type and close+1 < len(line):
372 if line[close+1] != ' ' and line[close+1] != '\t':
374 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
377 elif def_step > 0: # Parsing a def
378 # Look for 'global ???[,???]'
380 if string == 'global':
385 elif string != ',' and type != NL:
388 else: # In global scope
394 elif string == '=' or (var_forflag and string == 'in'):
398 if prev_string != '.' and not vars.has_key(string):
399 var_accum[string] = VarDesc(string, None, start[0])
400 elif not string in [',', '(', ')', '[', ']']:
404 if len(var_accum) != 1:
406 vars.update(var_accum)
408 var_name = var_accum.keys()[0]
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])
417 #######################
418 ## General utilities ##
419 #######################
424 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
428 _parse_cache[hash(txt.name)] = desc
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.
436 global _modules, _modules_updated
439 if _modules_updated < t - since:
440 _modules.update(sys.modules)
442 return _modules.keys()
444 def suggest_cmp(x, y):
445 """Use this method when sorting a list of suggestions.
448 return cmp(x[0].upper(), y[0].upper())
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.
455 mod = __import__(name)
456 components = name.split('.')
457 for comp in components[1:]:
458 mod = getattr(mod, comp)
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.
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
472 if isinstance(v, ModuleType):
479 def get_context(txt):
480 """Establishes the context of the cursor in the given Blender Text object
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
490 global CTX_NORMAL, CTX_SINGLE_QUOTE, CTX_DOUBLE_QUOTE, CTX_COMMENT
491 l, cursor = txt.getCursorPos()
492 lines = txt.asLines()[:l+1]
494 # Detect context (in string or comment)
503 # Comments end at new lines
504 if in_str == CTX_COMMENT:
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
513 if in_str == CTX_SINGLE_QUOTE:
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
520 elif in_str == CTX_DOUBLE_QUOTE:
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
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,
536 lineindex, cursor = txt.getCursorPos()
537 lines = txt.asLines()
538 line = lines[lineindex]
540 # Join previous lines to this line if spanning
543 earlier = lines[i].rstrip()
544 if earlier.endswith('\\'):
545 line = earlier[:-1] + ' ' + line
546 cursor += len(earlier)
549 # Join later lines while there is an explicit joining character
551 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
552 later = lines[i+1].strip()
553 line = line + ' ' + later[:-1]
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.
565 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
568 pre = line[i+1:cursor]
569 return pre.split('.')
572 """Returns a dictionary which maps definition names in the source code to
573 a list of their parameter names.
575 The line 'def doit(one, two, three): print one' for example, results in the
576 mapping 'doit' : [ 'one', 'two', 'three' ]
579 return get_cached_descriptor(txt).defs
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.
588 return get_cached_descriptor(txt).vars
590 def get_imports(txt):
591 """Returns a dictionary which maps symbol names in the source code to their
594 The line 'from Blender import Text as BText' for example, results in the
595 mapping 'BText' : <module 'Blender.Text' (built-in)>
597 Note that this method imports the modules to provide this mapping as as such
598 will execute any initilization code found within.
601 return get_cached_descriptor(txt).imports
604 """Returns a dictionary of built-in modules, functions and variables."""
606 return __builtin__.__dict__
609 #################################
610 ## Debugging utility functions ##
611 #################################
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.
619 desc = get_cached_descriptor(txt, period)
620 print '================================================'
621 print 'Name:', desc.name, '('+str(hash(txt))+')'
622 print '------------------------------------------------'
624 for name, ddesc in desc.defs.items():
625 print ' ', name, ddesc.params, ddesc.lineno
626 print '------------------------------------------------'
628 for name, vdesc in desc.vars.items():
629 print ' ', name, vdesc.type, vdesc.lineno
630 print '------------------------------------------------'
632 for name, item in desc.imports.items():
633 print ' ', name.ljust(15), item
634 print '------------------------------------------------'
636 for clsnme, clsdsc in desc.classes.items():
637 print ' *********************************'
638 print ' Name:', clsnme
639 print ' ---------------------------------'
641 for name, ddesc in clsdsc.defs.items():
642 print ' ', name, ddesc.params, ddesc.lineno
643 print ' ---------------------------------'
645 for name, vdesc in clsdsc.vars.items():
646 print ' ', name, vdesc.type, vdesc.lineno
647 print ' *********************************'
648 print '================================================'