1 """The BPyTextPlugin Module
3 Use get_cached_descriptor(txt) to retrieve information about the script held in
6 Use print_cache_for(txt) to print the information to the console.
8 Use line, cursor = current_line(txt) to get the logical line and cursor position
10 Use get_targets(line, cursor) to find out what precedes the cursor:
11 aaa.bbb.cc|c.ddd -> ['aaa', 'bbb', 'cc']
13 Use resolve_targets(txt, targets) to turn a target list into a usable object if
14 one is found to match.
18 import __builtin__, tokenize
19 from Blender.sys import time
20 from tokenize import generate_tokens, TokenError, \
21 COMMENT, DEDENT, INDENT, NAME, NEWLINE, NL, STRING, NUMBER
24 """Describes a definition or defined object through its name, line number
25 and docstring. This is the base class for definition based descriptors.
28 def __init__(self, name, lineno, doc=''):
34 """Describes a script through lists of further descriptor objects (classes,
35 defs, vars) and dictionaries to built-in types (imports). If a script has
36 not been fully parsed, its incomplete flag will be set. The time of the last
37 parse is held by the time field and the name of the text object from which
38 it was parsed, the name field.
41 def __init__(self, name, imports, classes, defs, vars, incomplete=False):
43 self.imports = imports
44 self.classes = classes
47 self.incomplete = incomplete
50 def set_delay(self, delay):
51 self.parse_due = time() + delay
53 class ClassDesc(Definition):
54 """Describes a class through lists of further descriptor objects (defs and
55 vars). The name of the class is held by the name field and the line on
56 which it is defined is held in lineno.
59 def __init__(self, name, defs, vars, lineno, doc=''):
60 Definition.__init__(self, name, lineno, doc)
64 class FunctionDesc(Definition):
65 """Describes a function through its name and list of parameters (name,
66 params) and the line on which it is defined (lineno).
69 def __init__(self, name, params, lineno, doc=''):
70 Definition.__init__(self, name, lineno, doc)
73 class VarDesc(Definition):
74 """Describes a variable through its name and type (if ascertainable) and the
75 line on which it is defined (lineno). If no type can be determined, type
79 def __init__(self, name, type, lineno):
80 Definition.__init__(self, name, lineno)
81 self.type = type # None for unknown (supports: dict/list/str)
91 KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
92 'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
93 'break', 'except', 'import', 'print', 'class', 'exec', 'in',
94 'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
97 # Module file extensions
98 MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd']
100 ModuleType = type(__builtin__)
101 NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
105 _parse_cache = dict()
107 def _load_module_names():
108 """Searches the sys.path for module files and lists them, along with
109 sys.builtin_module_names, in the global dict _modules.
114 for n in sys.builtin_module_names:
117 if p == '': p = os.curdir
118 if not os.path.isdir(p): continue
119 for f in os.listdir(p):
120 for ext in MODULE_EXTS:
122 _modules[f[:-len(ext)]] = None
128 """Trims the quotes from a quoted STRING token (eg. "'''text'''" -> "text")
133 while i < l/2 and (doc[i] == "'" or doc[i] == '"'):
137 def resolve_targets(txt, targets):
138 """Attempts to return a useful object for the locally or externally defined
139 entity described by targets. If the object is local (defined in txt), a
140 Definition instance is returned. If the object is external (imported or
141 built in), the object itself is returned. If no object can be found, None is
146 if count==0: return None
152 desc = get_cached_descriptor(txt)
153 if desc.classes.has_key(targets[0]):
154 local = desc.classes[targets[0]]
155 elif desc.defs.has_key(targets[0]):
156 local = desc.defs[targets[0]]
157 elif desc.vars.has_key(targets[0]):
158 obj = desc.vars[targets[0]].type
162 if hasattr(local, 'classes') and local.classes.has_key(targets[i]):
163 local = local.classes[targets[i]]
164 elif hasattr(local, 'defs') and local.defs.has_key(targets[i]):
165 local = local.defs[targets[i]]
166 elif hasattr(local, 'vars') and local.vars.has_key(targets[i]):
167 obj = local.vars[targets[i]].type
176 if local: return local
179 if desc.imports.has_key(targets[0]):
180 obj = desc.imports[targets[0]]
182 builtins = get_builtins()
183 if builtins.has_key(targets[0]):
184 obj = builtins[targets[0]]
186 while obj and i < count:
187 if hasattr(obj, targets[i]):
188 obj = getattr(obj, targets[i])
196 def get_cached_descriptor(txt, force_parse=0):
197 """Returns the cached ScriptDesc for the specified Text object 'txt'. If the
198 script has not been parsed in the last 'period' seconds it will be reparsed
199 to obtain this descriptor.
201 Specifying TP_AUTO for the period (default) will choose a period based on the
202 size of the Text object. Larger texts are parsed less often.
209 if not force_parse and _parse_cache.has_key(key):
210 desc = _parse_cache[key]
211 if desc.parse_due > time():
212 parse = desc.incomplete
215 desc = parse_text(txt)
220 """Parses an entire script's text and returns a ScriptDesc instance
221 containing information about the script.
223 If the text is not a valid Python script (for example if brackets are left
224 open), parsing may fail to complete. However, if this occurs, no exception
225 is thrown. Instead the returned ScriptDesc instance will have its incomplete
226 flag set and information processed up to this point will still be accessible.
231 tokens = generate_tokens(txt.readline) # Throws TokenError
233 curl, cursor = txt.getCursorPos()
234 linen = curl + 1 # Token line numbers are one-based
259 type, text, start, end, line = tokens.next()
260 except StopIteration:
262 except TokenError, IndentationError:
266 # Skip all comments and line joining characters
267 if type == COMMENT or type == NL:
279 #########################
280 ## Module importing... ##
281 #########################
285 # Default, look for 'from' or 'import' to start
290 elif text == 'import':
295 # Found a 'from', create imp_from in form '???.???...'
298 imp_from = '.'.join(imp_tmp)
304 imp_step = 0 # Invalid syntax
306 # Found 'import', imp_from is populated or None, create imp_name
309 imp_name = '.'.join(imp_tmp)
311 elif type == NAME or text == '*':
314 imp_name = '.'.join(imp_tmp)
318 # Found 'as', change imp_symb to this value and go back to step 2
325 # Both imp_name and imp_symb have now been populated so we can import
328 # Handle special case of 'import *'
330 parent = get_module(imp_from)
331 imports.update(parent.__dict__)
334 # Try importing the name as a module
337 module = get_module(imp_from +'.'+ imp_name)
339 module = get_module(imp_name)
340 except (ImportError, ValueError, AttributeError, TypeError):
341 # Try importing name as an attribute of the parent
343 module = __import__(imp_from, globals(), locals(), [imp_name])
344 imports[imp_symb] = getattr(module, imp_name)
345 except (ImportError, ValueError, AttributeError, TypeError):
348 imports[imp_symb] = module
350 # More to import from the same module?
361 # If we are inside a class then def and variable parsing should be done
362 # for the class. Otherwise the definitions are considered global
368 cls_lineno = start[0]
372 # Found 'class', look for cls_name followed by '('
383 # Found 'class' name ... ':', now check if it's a single line statement
393 if not cls_doc and type == STRING:
394 cls_doc = _trim_doc(text)
397 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno, cls_doc)
400 if type == DEDENT and indent <= cls_indent:
401 classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno, cls_doc)
412 def_lineno = start[0]
415 # Found 'def', look for def_name followed by '('
420 elif def_name and text == '(':
423 # Found 'def' name '(', now identify the parameters upto ')'
424 # TODO: Handle ellipsis '...'
427 def_params.append(text)
431 # Found 'def' ... ':', now check if it's a single line statement
442 def_doc = _trim_doc(text)
446 newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
449 newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
451 if cls_step > 0: # Parsing a class
452 cls_defs[def_name] = newdef
454 defs[def_name] = newdef
457 ##########################
458 ## Variable assignation ##
459 ##########################
461 if cls_step > 0: # Parsing a class
462 # Look for 'self.???'
475 if cls_vars.has_key(var_name):
486 if text.find('.') != -1: var_type = float
492 close = line.find(']', end[1])
495 close = line.find(')', end[1])
498 close = line.find('}', end[1])
501 close = line.find(')', end[1])
503 if var_type and close+1 < len(line):
504 if line[close+1] != ' ' and line[close+1] != '\t':
506 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
509 elif def_step > 0: # Parsing a def
510 # Look for 'global ???[,???]'
516 if not vars.has_key(text):
517 vars[text] = VarDesc(text, None, start[0])
518 elif text != ',' and type != NL:
521 else: # In global scope
527 elif text == '=' or (var_forflag and text == 'in'):
531 if prev_text != '.' and not vars.has_key(text):
532 var_accum[text] = VarDesc(text, None, start[0])
533 elif not text in [',', '(', ')', '[', ']']:
537 if len(var_accum) != 1:
539 vars.update(var_accum)
541 var_name = var_accum.keys()[0]
544 if text.find('.') != -1: var_type = float
546 elif type == STRING: var_type = str
547 elif text == '[': var_type = list
548 elif text == '(': var_type = tuple
549 elif text == '{': var_type = dict
550 vars[var_name] = VarDesc(var_name, var_type, start[0])
553 #######################
554 ## General utilities ##
555 #######################
560 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
561 desc.set_delay(10 * (time()-start_time) + 0.05)
564 _parse_cache[hash(txt)] = desc
567 def get_modules(since=1):
568 """Returns the set of built-in modules and any modules that have been
569 imported into the system upto 'since' seconds ago.
572 global _modules, _modules_updated
575 if _modules_updated < t - since:
576 _modules.update(sys.modules)
578 return _modules.keys()
580 def suggest_cmp(x, y):
581 """Use this method when sorting a list of suggestions.
584 return cmp(x[0].upper(), y[0].upper())
586 def get_module(name):
587 """Returns the module specified by its name. The module itself is imported
588 by this method and, as such, any initialization code will be executed.
591 mod = __import__(name)
592 components = name.split('.')
593 for comp in components[1:]:
594 mod = getattr(mod, comp)
598 """Returns the character used to signify the type of a variable. Use this
599 method to identify the type character for an item in a suggestion list.
601 The following values are returned:
602 'm' if the parameter is a module
603 'f' if the parameter is callable
604 'v' if the parameter is variable or otherwise indeterminable
608 if isinstance(v, ModuleType):
615 def get_context(txt):
616 """Establishes the context of the cursor in the given Blender Text object
619 CTX_NORMAL - Cursor is in a normal context
620 CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
621 CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
622 CTX_COMMENT - Cursor is inside a comment
626 l, cursor = txt.getCursorPos()
627 lines = txt.asLines(0, l+1)
629 # FIXME: This method is too slow in large files for it to be called as often
630 # as it is. So for lines below the 1000th line we do this... (quorn)
631 if l > 1000: return CTX_NORMAL
633 # Detect context (in string or comment)
642 # Comments end at new lines
643 if in_str == CTX_COMMENT:
648 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
649 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
650 elif line[i] == '#': in_str = CTX_COMMENT
652 if in_str == CTX_SINGLE_QUOTE:
655 # In again if ' escaped, out again if \ escaped, and so on
656 for a in range(i-1, -1, -1):
657 if line[a] == '\\': in_str = 1-in_str
659 elif in_str == CTX_DOUBLE_QUOTE:
662 # In again if " escaped, out again if \ escaped, and so on
663 for a in range(i-1, -1, -1):
664 if line[i-a] == '\\': in_str = 2-in_str
669 def current_line(txt):
670 """Extracts the Python script line at the cursor in the Blender Text object
671 provided and cursor position within this line as the tuple pair (line,
675 lineindex, cursor = txt.getCursorPos()
676 lines = txt.asLines()
677 line = lines[lineindex]
679 # Join previous lines to this line if spanning
682 earlier = lines[i].rstrip()
683 if earlier.endswith('\\'):
684 line = earlier[:-1] + ' ' + line
685 cursor += len(earlier)
688 # Join later lines while there is an explicit joining character
690 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
691 later = lines[i+1].strip()
692 line = line + ' ' + later[:-1]
697 def get_targets(line, cursor):
698 """Parses a period separated string of valid names preceding the cursor and
699 returns them as a list in the same order.
703 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
706 return line[i+1:cursor].split('.')
709 """Returns a dictionary which maps definition names in the source code to
710 a list of their parameter names.
712 The line 'def doit(one, two, three): print one' for example, results in the
713 mapping 'doit' : [ 'one', 'two', 'three' ]
716 return get_cached_descriptor(txt).defs
719 """Returns a dictionary of variable names found in the specified Text
720 object. This method locates all names followed directly by an equal sign:
721 'a = ???' or indirectly as part of a tuple/list assignment or inside a
722 'for ??? in ???:' block.
725 return get_cached_descriptor(txt).vars
727 def get_imports(txt):
728 """Returns a dictionary which maps symbol names in the source code to their
731 The line 'from Blender import Text as BText' for example, results in the
732 mapping 'BText' : <module 'Blender.Text' (built-in)>
734 Note that this method imports the modules to provide this mapping as as such
735 will execute any initilization code found within.
738 return get_cached_descriptor(txt).imports
741 """Returns a dictionary of built-in modules, functions and variables."""
743 return __builtin__.__dict__
746 #################################
747 ## Debugging utility functions ##
748 #################################
750 def print_cache_for(txt, period=sys.maxint):
751 """Prints out the data cached for a given Text object. If no period is
752 given the text will not be reparsed and the cached version will be returned.
753 Otherwise if the period has expired the text will be reparsed.
756 desc = get_cached_descriptor(txt, period)
757 print '================================================'
758 print 'Name:', desc.name, '('+str(hash(txt))+')'
759 print '------------------------------------------------'
761 for name, ddesc in desc.defs.items():
762 print ' ', name, ddesc.params, ddesc.lineno
764 print '------------------------------------------------'
766 for name, vdesc in desc.vars.items():
767 print ' ', name, vdesc.type, vdesc.lineno
768 print '------------------------------------------------'
770 for name, item in desc.imports.items():
771 print ' ', name.ljust(15), item
772 print '------------------------------------------------'
774 for clsnme, clsdsc in desc.classes.items():
775 print ' *********************************'
776 print ' Name:', clsnme
777 print ' ', clsdsc.doc
778 print ' ---------------------------------'
780 for name, ddesc in clsdsc.defs.items():
781 print ' ', name, ddesc.params, ddesc.lineno
783 print ' ---------------------------------'
785 for name, vdesc in clsdsc.vars.items():
786 print ' ', name, vdesc.type, vdesc.lineno
787 print ' *********************************'
788 print '================================================'