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 # Module file extensions
78 MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd']
80 ModuleType = type(__builtin__)
81 NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
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.
94 for n in sys.builtin_module_names:
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:
102 _modules[f[:-len(ext)]] = None
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.
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.
119 if period == TP_AUTO:
130 if _parse_cache.has_key(key):
131 desc = _parse_cache[key]
132 if desc.time >= time() - period:
133 parse = desc.incomplete
136 desc = parse_text(txt)
141 """Parses an entire script's text and returns a ScriptDesc instance
142 containing information about the script.
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.
151 tokens = generate_tokens(txt.readline) # Throws TokenError
153 curl, cursor = txt.getCursorPos()
154 linen = curl + 1 # Token line numbers are one-based
179 type, string, start, end, line = tokens.next()
180 except StopIteration:
182 except TokenError, IndentationError:
186 # Skip all comments and line joining characters
187 if type == COMMENT or type == NL:
199 #########################
200 ## Module importing... ##
201 #########################
205 # Default, look for 'from' or 'import' to start
210 elif string == 'import':
215 # Found a 'from', create imp_from in form '???.???...'
217 if string == 'import':
218 imp_from = '.'.join(imp_tmp)
222 imp_tmp.append(string)
224 imp_step = 0 # Invalid syntax
226 # Found 'import', imp_from is populated or None, create imp_name
229 imp_name = '.'.join(imp_tmp)
231 elif type == NAME or string == '*':
232 imp_tmp.append(string)
234 imp_name = '.'.join(imp_tmp)
238 # Found 'as', change imp_symb to this value and go back to step 2
245 # Both imp_name and imp_symb have now been populated so we can import
248 # Handle special case of 'import *'
250 parent = get_module(imp_from)
251 imports.update(parent.__dict__)
254 # Try importing the name as a module
257 module = get_module(imp_from +'.'+ imp_name)
259 module = get_module(imp_name)
260 except (ImportError, ValueError, AttributeError, TypeError):
261 # Try importing name as an attribute of the parent
263 module = __import__(imp_from, globals(), locals(), [imp_name])
264 imports[imp_symb] = getattr(module, imp_name)
265 except (ImportError, ValueError, AttributeError, TypeError):
268 imports[imp_symb] = module
270 # More to import from the same module?
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
286 if string == 'class':
288 cls_lineno = start[0]
292 # Found 'class', look for cls_name followed by '('
303 # Found 'class' name ... ':', now check if it's a single line statement
315 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
318 if type == DEDENT and indent <= cls_indent:
319 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno)
330 def_lineno = start[0]
333 # Found 'def', look for def_name followed by '('
338 elif def_name and string == '(':
341 # Found 'def' name '(', now identify the parameters upto ')'
342 # TODO: Handle ellipsis '...'
345 def_params.append(string)
347 if cls_step > 0: # Parsing a class
348 cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
350 defs[def_name] = FunctionDesc(def_name, def_params, def_lineno)
353 ##########################
354 ## Variable assignation ##
355 ##########################
357 if cls_step > 0: # Parsing a class
358 # Look for 'self.???'
371 if cls_vars.has_key(var_name):
381 close = line.find(']', end[1])
387 close = line.find(')', end[1])
390 close = line.find('}', end[1])
392 elif string == 'dict':
393 close = line.find(')', end[1])
395 if var_type and close+1 < len(line):
396 if line[close+1] != ' ' and line[close+1] != '\t':
398 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
401 elif def_step > 0: # Parsing a def
402 # Look for 'global ???[,???]'
404 if string == 'global':
409 elif string != ',' and type != NL:
412 else: # In global scope
418 elif string == '=' or (var_forflag and string == 'in'):
422 if prev_string != '.' and not vars.has_key(string):
423 var_accum[string] = VarDesc(string, None, start[0])
424 elif not string in [',', '(', ')', '[', ']']:
428 if len(var_accum) != 1:
430 vars.update(var_accum)
432 var_name = var_accum.keys()[0]
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])
441 #######################
442 ## General utilities ##
443 #######################
448 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
452 _parse_cache[hash(txt)] = desc
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.
460 global _modules, _modules_updated
463 if _modules_updated < t - since:
464 _modules.update(sys.modules)
466 return _modules.keys()
468 def suggest_cmp(x, y):
469 """Use this method when sorting a list of suggestions.
472 return cmp(x[0].upper(), y[0].upper())
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.
479 mod = __import__(name)
480 components = name.split('.')
481 for comp in components[1:]:
482 mod = getattr(mod, comp)
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.
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
496 if isinstance(v, ModuleType):
503 def get_context(txt):
504 """Establishes the context of the cursor in the given Blender Text object
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
514 l, cursor = txt.getCursorPos()
515 lines = txt.asLines(0, l+1)
517 # FIXME: This method is too slow in large files for it to be called as often
518 # as it is. So for lines below the 1000th line we do this... (quorn)
519 if l > 1000: return CTX_NORMAL
521 # Detect context (in string or comment)
530 # Comments end at new lines
531 if in_str == CTX_COMMENT:
536 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
537 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
538 elif line[i] == '#': in_str = CTX_COMMENT
540 if in_str == CTX_SINGLE_QUOTE:
543 # In again if ' escaped, out again if \ escaped, and so on
544 for a in range(i-1, -1, -1):
545 if line[a] == '\\': in_str = 1-in_str
547 elif in_str == CTX_DOUBLE_QUOTE:
550 # In again if " escaped, out again if \ escaped, and so on
551 for a in range(i-1, -1, -1):
552 if line[i-a] == '\\': in_str = 2-in_str
557 def current_line(txt):
558 """Extracts the Python script line at the cursor in the Blender Text object
559 provided and cursor position within this line as the tuple pair (line,
563 lineindex, cursor = txt.getCursorPos()
564 lines = txt.asLines()
565 line = lines[lineindex]
567 # Join previous lines to this line if spanning
570 earlier = lines[i].rstrip()
571 if earlier.endswith('\\'):
572 line = earlier[:-1] + ' ' + line
573 cursor += len(earlier)
576 # Join later lines while there is an explicit joining character
578 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
579 later = lines[i+1].strip()
580 line = line + ' ' + later[:-1]
585 def get_targets(line, cursor):
586 """Parses a period separated string of valid names preceding the cursor and
587 returns them as a list in the same order.
592 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
595 pre = line[i+1:cursor]
596 return pre.split('.')
599 """Returns a dictionary which maps definition names in the source code to
600 a list of their parameter names.
602 The line 'def doit(one, two, three): print one' for example, results in the
603 mapping 'doit' : [ 'one', 'two', 'three' ]
606 return get_cached_descriptor(txt).defs
609 """Returns a dictionary of variable names found in the specified Text
610 object. This method locates all names followed directly by an equal sign:
611 'a = ???' or indirectly as part of a tuple/list assignment or inside a
612 'for ??? in ???:' block.
615 return get_cached_descriptor(txt).vars
617 def get_imports(txt):
618 """Returns a dictionary which maps symbol names in the source code to their
621 The line 'from Blender import Text as BText' for example, results in the
622 mapping 'BText' : <module 'Blender.Text' (built-in)>
624 Note that this method imports the modules to provide this mapping as as such
625 will execute any initilization code found within.
628 return get_cached_descriptor(txt).imports
631 """Returns a dictionary of built-in modules, functions and variables."""
633 return __builtin__.__dict__
636 #################################
637 ## Debugging utility functions ##
638 #################################
640 def print_cache_for(txt, period=sys.maxint):
641 """Prints out the data cached for a given Text object. If no period is
642 given the text will not be reparsed and the cached version will be returned.
643 Otherwise if the period has expired the text will be reparsed.
646 desc = get_cached_descriptor(txt, period)
647 print '================================================'
648 print 'Name:', desc.name, '('+str(hash(txt))+')'
649 print '------------------------------------------------'
651 for name, ddesc in desc.defs.items():
652 print ' ', name, ddesc.params, ddesc.lineno
653 print '------------------------------------------------'
655 for name, vdesc in desc.vars.items():
656 print ' ', name, vdesc.type, vdesc.lineno
657 print '------------------------------------------------'
659 for name, item in desc.imports.items():
660 print ' ', name.ljust(15), item
661 print '------------------------------------------------'
663 for clsnme, clsdsc in desc.classes.items():
664 print ' *********************************'
665 print ' Name:', clsnme
666 print ' ---------------------------------'
668 for name, ddesc in clsdsc.defs.items():
669 print ' ', name, ddesc.params, ddesc.lineno
670 print ' ---------------------------------'
672 for name, vdesc in clsdsc.vars.items():
673 print ' ', name, vdesc.type, vdesc.lineno
674 print ' *********************************'
675 print '================================================'