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.name)] = 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 global CTX_NORMAL, CTX_SINGLE_QUOTE, CTX_DOUBLE_QUOTE, CTX_COMMENT
515 l, cursor = txt.getCursorPos()
516 lines = txt.asLines()[:l+1]
518 # Detect context (in string or comment)
527 # Comments end at new lines
528 if in_str == CTX_COMMENT:
533 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
534 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
535 elif line[i] == '#': in_str = CTX_COMMENT
537 if in_str == CTX_SINGLE_QUOTE:
540 # In again if ' escaped, out again if \ escaped, and so on
541 for a in range(i-1, -1, -1):
542 if line[a] == '\\': in_str = 1-in_str
544 elif in_str == CTX_DOUBLE_QUOTE:
547 # In again if " escaped, out again if \ escaped, and so on
548 for a in range(i-1, -1, -1):
549 if line[i-a] == '\\': in_str = 2-in_str
554 def current_line(txt):
555 """Extracts the Python script line at the cursor in the Blender Text object
556 provided and cursor position within this line as the tuple pair (line,
560 lineindex, cursor = txt.getCursorPos()
561 lines = txt.asLines()
562 line = lines[lineindex]
564 # Join previous lines to this line if spanning
567 earlier = lines[i].rstrip()
568 if earlier.endswith('\\'):
569 line = earlier[:-1] + ' ' + line
570 cursor += len(earlier)
573 # Join later lines while there is an explicit joining character
575 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
576 later = lines[i+1].strip()
577 line = line + ' ' + later[:-1]
582 def get_targets(line, cursor):
583 """Parses a period separated string of valid names preceding the cursor and
584 returns them as a list in the same order.
589 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
592 pre = line[i+1:cursor]
593 return pre.split('.')
596 """Returns a dictionary which maps definition names in the source code to
597 a list of their parameter names.
599 The line 'def doit(one, two, three): print one' for example, results in the
600 mapping 'doit' : [ 'one', 'two', 'three' ]
603 return get_cached_descriptor(txt).defs
606 """Returns a dictionary of variable names found in the specified Text
607 object. This method locates all names followed directly by an equal sign:
608 'a = ???' or indirectly as part of a tuple/list assignment or inside a
609 'for ??? in ???:' block.
612 return get_cached_descriptor(txt).vars
614 def get_imports(txt):
615 """Returns a dictionary which maps symbol names in the source code to their
618 The line 'from Blender import Text as BText' for example, results in the
619 mapping 'BText' : <module 'Blender.Text' (built-in)>
621 Note that this method imports the modules to provide this mapping as as such
622 will execute any initilization code found within.
625 return get_cached_descriptor(txt).imports
628 """Returns a dictionary of built-in modules, functions and variables."""
630 return __builtin__.__dict__
633 #################################
634 ## Debugging utility functions ##
635 #################################
637 def print_cache_for(txt, period=sys.maxint):
638 """Prints out the data cached for a given Text object. If no period is
639 given the text will not be reparsed and the cached version will be returned.
640 Otherwise if the period has expired the text will be reparsed.
643 desc = get_cached_descriptor(txt, period)
644 print '================================================'
645 print 'Name:', desc.name, '('+str(hash(txt))+')'
646 print '------------------------------------------------'
648 for name, ddesc in desc.defs.items():
649 print ' ', name, ddesc.params, ddesc.lineno
650 print '------------------------------------------------'
652 for name, vdesc in desc.vars.items():
653 print ' ', name, vdesc.type, vdesc.lineno
654 print '------------------------------------------------'
656 for name, item in desc.imports.items():
657 print ' ', name.ljust(15), item
658 print '------------------------------------------------'
660 for clsnme, clsdsc in desc.classes.items():
661 print ' *********************************'
662 print ' Name:', clsnme
663 print ' ---------------------------------'
665 for name, ddesc in clsdsc.defs.items():
666 print ' ', name, ddesc.params, ddesc.lineno
667 print ' ---------------------------------'
669 for name, vdesc in clsdsc.vars.items():
670 print ' ', name, vdesc.type, vdesc.lineno
671 print ' *********************************'
672 print '================================================'