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):
485 if text.find('.') != -1: var_type = float
491 close = line.find(']', end[1])
494 close = line.find(')', end[1])
497 close = line.find('}', end[1])
500 close = line.find(')', end[1])
502 if var_type and close+1 < len(line):
503 if line[close+1] != ' ' and line[close+1] != '\t':
505 cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
508 elif def_step > 0: # Parsing a def
509 # Look for 'global ???[,???]'
515 if not vars.has_key(text):
516 vars[text] = VarDesc(text, None, start[0])
517 elif text != ',' and type != NL:
520 else: # In global scope
526 elif text == '=' or (var_forflag and text == 'in'):
530 if prev_text != '.' and not vars.has_key(text):
531 var_accum[text] = VarDesc(text, None, start[0])
532 elif not text in [',', '(', ')', '[', ']']:
536 if len(var_accum) != 1:
538 vars.update(var_accum)
540 var_name = var_accum.keys()[0]
543 if text.find('.') != -1: var_type = float
545 elif type == STRING: var_type = str
546 elif text == '[': var_type = list
547 elif text == '(': var_type = tuple
548 elif text == '{': var_type = dict
549 vars[var_name] = VarDesc(var_name, var_type, start[0])
552 #######################
553 ## General utilities ##
554 #######################
559 desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
560 desc.set_delay(10 * (time()-start_time) + 0.05)
563 _parse_cache[hash(txt)] = desc
566 def get_modules(since=1):
567 """Returns the set of built-in modules and any modules that have been
568 imported into the system upto 'since' seconds ago.
571 global _modules, _modules_updated
574 if _modules_updated < t - since:
575 _modules.update(sys.modules)
577 return _modules.keys()
579 def suggest_cmp(x, y):
580 """Use this method when sorting a list of suggestions.
583 return cmp(x[0].upper(), y[0].upper())
585 def get_module(name):
586 """Returns the module specified by its name. The module itself is imported
587 by this method and, as such, any initialization code will be executed.
590 mod = __import__(name)
591 components = name.split('.')
592 for comp in components[1:]:
593 mod = getattr(mod, comp)
597 """Returns the character used to signify the type of a variable. Use this
598 method to identify the type character for an item in a suggestion list.
600 The following values are returned:
601 'm' if the parameter is a module
602 'f' if the parameter is callable
603 'v' if the parameter is variable or otherwise indeterminable
607 if isinstance(v, ModuleType):
614 def get_context(txt):
615 """Establishes the context of the cursor in the given Blender Text object
618 CTX_NORMAL - Cursor is in a normal context
619 CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
620 CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
621 CTX_COMMENT - Cursor is inside a comment
625 l, cursor = txt.getCursorPos()
626 lines = txt.asLines(0, l+1)
628 # FIXME: This method is too slow in large files for it to be called as often
629 # as it is. So for lines below the 1000th line we do this... (quorn)
630 if l > 1000: return CTX_NORMAL
632 # Detect context (in string or comment)
641 # Comments end at new lines
642 if in_str == CTX_COMMENT:
647 if line[i] == "'": in_str = CTX_SINGLE_QUOTE
648 elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
649 elif line[i] == '#': in_str = CTX_COMMENT
651 if in_str == CTX_SINGLE_QUOTE:
654 # In again if ' escaped, out again if \ escaped, and so on
655 for a in range(i-1, -1, -1):
656 if line[a] == '\\': in_str = 1-in_str
658 elif in_str == CTX_DOUBLE_QUOTE:
661 # In again if " escaped, out again if \ escaped, and so on
662 for a in range(i-1, -1, -1):
663 if line[i-a] == '\\': in_str = 2-in_str
668 def current_line(txt):
669 """Extracts the Python script line at the cursor in the Blender Text object
670 provided and cursor position within this line as the tuple pair (line,
674 lineindex, cursor = txt.getCursorPos()
675 lines = txt.asLines()
676 line = lines[lineindex]
678 # Join previous lines to this line if spanning
681 earlier = lines[i].rstrip()
682 if earlier.endswith('\\'):
683 line = earlier[:-1] + ' ' + line
684 cursor += len(earlier)
687 # Join later lines while there is an explicit joining character
689 while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
690 later = lines[i+1].strip()
691 line = line + ' ' + later[:-1]
696 def get_targets(line, cursor):
697 """Parses a period separated string of valid names preceding the cursor and
698 returns them as a list in the same order.
702 while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
705 return line[i+1:cursor].split('.')
708 """Returns a dictionary which maps definition names in the source code to
709 a list of their parameter names.
711 The line 'def doit(one, two, three): print one' for example, results in the
712 mapping 'doit' : [ 'one', 'two', 'three' ]
715 return get_cached_descriptor(txt).defs
718 """Returns a dictionary of variable names found in the specified Text
719 object. This method locates all names followed directly by an equal sign:
720 'a = ???' or indirectly as part of a tuple/list assignment or inside a
721 'for ??? in ???:' block.
724 return get_cached_descriptor(txt).vars
726 def get_imports(txt):
727 """Returns a dictionary which maps symbol names in the source code to their
730 The line 'from Blender import Text as BText' for example, results in the
731 mapping 'BText' : <module 'Blender.Text' (built-in)>
733 Note that this method imports the modules to provide this mapping as as such
734 will execute any initilization code found within.
737 return get_cached_descriptor(txt).imports
740 """Returns a dictionary of built-in modules, functions and variables."""
742 return __builtin__.__dict__
745 #################################
746 ## Debugging utility functions ##
747 #################################
749 def print_cache_for(txt, period=sys.maxint):
750 """Prints out the data cached for a given Text object. If no period is
751 given the text will not be reparsed and the cached version will be returned.
752 Otherwise if the period has expired the text will be reparsed.
755 desc = get_cached_descriptor(txt, period)
756 print '================================================'
757 print 'Name:', desc.name, '('+str(hash(txt))+')'
758 print '------------------------------------------------'
760 for name, ddesc in desc.defs.items():
761 print ' ', name, ddesc.params, ddesc.lineno
763 print '------------------------------------------------'
765 for name, vdesc in desc.vars.items():
766 print ' ', name, vdesc.type, vdesc.lineno
767 print '------------------------------------------------'
769 for name, item in desc.imports.items():
770 print ' ', name.ljust(15), item
771 print '------------------------------------------------'
773 for clsnme, clsdsc in desc.classes.items():
774 print ' *********************************'
775 print ' Name:', clsnme
776 print ' ', clsdsc.doc
777 print ' ---------------------------------'
779 for name, ddesc in clsdsc.defs.items():
780 print ' ', name, ddesc.params, ddesc.lineno
782 print ' ---------------------------------'
784 for name, vdesc in clsdsc.vars.items():
785 print ' ', name, vdesc.type, vdesc.lineno
786 print ' *********************************'
787 print '================================================'