2008-07-15 12:55:20 +00:00
|
|
|
import bpy
|
2008-07-15 07:34:46 +00:00
|
|
|
import __builtin__, tokenize
|
2008-07-15 12:55:20 +00:00
|
|
|
from Blender.sys import time
|
|
|
|
from tokenize import generate_tokens, TokenError
|
2008-07-15 07:34:46 +00:00
|
|
|
# TODO: Remove the dependency for a full Python installation. Currently only the
|
|
|
|
# tokenize module is required
|
|
|
|
|
|
|
|
# Context types
|
|
|
|
NORMAL = 0
|
|
|
|
SINGLE_QUOTE = 1
|
|
|
|
DOUBLE_QUOTE = 2
|
|
|
|
COMMENT = 3
|
|
|
|
|
|
|
|
# Python keywords
|
|
|
|
KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
|
|
|
|
'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
|
|
|
|
'break', 'except', 'import', 'print', 'class', 'exec', 'in',
|
|
|
|
'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
|
|
|
|
'lambda', 'try' ]
|
|
|
|
|
2008-07-15 12:55:20 +00:00
|
|
|
# Used to cache the return value of generate_tokens
|
|
|
|
_token_cache = None
|
|
|
|
_cache_update = 0
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
def suggest_cmp(x, y):
|
2008-07-15 12:55:20 +00:00
|
|
|
"""Use this method when sorting a list of suggestions.
|
|
|
|
"""
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
return cmp(x[0], y[0])
|
|
|
|
|
2008-07-15 12:55:20 +00:00
|
|
|
def cached_generate_tokens(txt, since=1):
|
|
|
|
"""A caching version of generate tokens for multiple parsing of the same
|
|
|
|
document within a given timescale.
|
|
|
|
"""
|
|
|
|
|
|
|
|
global _token_cache, _cache_update
|
|
|
|
|
|
|
|
if _cache_update < time() - since:
|
|
|
|
txt.reset()
|
|
|
|
_token_cache = [g for g in generate_tokens(txt.readline)]
|
|
|
|
_cache_update = time()
|
|
|
|
return _token_cache
|
|
|
|
|
2008-07-15 07:34:46 +00:00
|
|
|
def get_module(name):
|
2008-07-15 12:55:20 +00:00
|
|
|
"""Returns the module specified by its name. The module itself is imported
|
|
|
|
by this method and, as such, any initialization code will be executed.
|
|
|
|
"""
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
mod = __import__(name)
|
|
|
|
components = name.split('.')
|
|
|
|
for comp in components[1:]:
|
|
|
|
mod = getattr(mod, comp)
|
|
|
|
return mod
|
|
|
|
|
|
|
|
def is_module(m):
|
2008-07-15 12:55:20 +00:00
|
|
|
"""Taken from the inspect module of the standard Python installation.
|
|
|
|
"""
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
return isinstance(m, type(bpy))
|
|
|
|
|
|
|
|
def type_char(v):
|
2008-07-15 12:55:20 +00:00
|
|
|
"""Returns the character used to signify the type of a variable. Use this
|
|
|
|
method to identify the type character for an item in a suggestion list.
|
|
|
|
|
|
|
|
The following values are returned:
|
|
|
|
'm' if the parameter is a module
|
|
|
|
'f' if the parameter is callable
|
|
|
|
'v' if the parameter is variable or otherwise indeterminable
|
|
|
|
"""
|
|
|
|
|
2008-07-15 07:34:46 +00:00
|
|
|
if is_module(v):
|
|
|
|
return 'm'
|
|
|
|
elif callable(v):
|
|
|
|
return 'f'
|
|
|
|
else:
|
|
|
|
return 'v'
|
|
|
|
|
2008-07-15 12:55:20 +00:00
|
|
|
def get_context(txt):
|
|
|
|
"""Establishes the context of the cursor in the given Blender Text object
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
Returns one of:
|
|
|
|
NORMAL - Cursor is in a normal context
|
|
|
|
SINGLE_QUOTE - Cursor is inside a single quoted string
|
|
|
|
DOUBLE_QUOTE - Cursor is inside a double quoted string
|
|
|
|
COMMENT - Cursor is inside a comment
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2008-07-15 12:55:20 +00:00
|
|
|
l, cursor = txt.getCursorPos()
|
|
|
|
lines = txt.asLines()[:l+1]
|
|
|
|
|
2008-07-15 07:34:46 +00:00
|
|
|
# Detect context (in string or comment)
|
|
|
|
in_str = 0 # 1-single quotes, 2-double quotes
|
2008-07-15 12:55:20 +00:00
|
|
|
for line in lines:
|
|
|
|
if l == 0:
|
|
|
|
end = cursor
|
2008-07-15 07:34:46 +00:00
|
|
|
else:
|
2008-07-15 12:55:20 +00:00
|
|
|
end = len(line)
|
|
|
|
l -= 1
|
|
|
|
|
|
|
|
# Comments end at new lines
|
|
|
|
if in_str == 3:
|
|
|
|
in_str = 0
|
|
|
|
|
|
|
|
for i in range(end):
|
|
|
|
if in_str == 0:
|
|
|
|
if line[i] == "'": in_str = 1
|
|
|
|
elif line[i] == '"': in_str = 2
|
|
|
|
elif line[i] == '#': in_str = 3
|
|
|
|
else:
|
|
|
|
if in_str == 1:
|
|
|
|
if line[i] == "'":
|
|
|
|
in_str = 0
|
|
|
|
# In again if ' escaped, out again if \ escaped, and so on
|
|
|
|
for a in range(i-1, -1, -1):
|
|
|
|
if line[a] == '\\': in_str = 1-in_str
|
|
|
|
else: break
|
|
|
|
elif in_str == 2:
|
|
|
|
if line[i] == '"':
|
|
|
|
in_str = 0
|
|
|
|
# In again if " escaped, out again if \ escaped, and so on
|
|
|
|
for a in range(i-1, -1, -1):
|
|
|
|
if line[i-a] == '\\': in_str = 2-in_str
|
|
|
|
else: break
|
|
|
|
|
2008-07-15 07:34:46 +00:00
|
|
|
return in_str
|
|
|
|
|
|
|
|
def current_line(txt):
|
|
|
|
"""Extracts the Python script line at the cursor in the Blender Text object
|
|
|
|
provided and cursor position within this line as the tuple pair (line,
|
|
|
|
cursor)"""
|
|
|
|
|
|
|
|
(lineindex, cursor) = txt.getCursorPos()
|
|
|
|
lines = txt.asLines()
|
|
|
|
line = lines[lineindex]
|
|
|
|
|
|
|
|
# Join previous lines to this line if spanning
|
|
|
|
i = lineindex - 1
|
|
|
|
while i > 0:
|
|
|
|
earlier = lines[i].rstrip()
|
|
|
|
if earlier.endswith('\\'):
|
|
|
|
line = earlier[:-1] + ' ' + line
|
|
|
|
cursor += len(earlier)
|
|
|
|
i -= 1
|
|
|
|
|
|
|
|
# Join later lines while there is an explicit joining character
|
|
|
|
i = lineindex
|
2008-07-15 12:55:20 +00:00
|
|
|
while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
|
2008-07-15 07:34:46 +00:00
|
|
|
later = lines[i+1].strip()
|
|
|
|
line = line + ' ' + later[:-1]
|
2008-07-15 12:55:20 +00:00
|
|
|
i += 1
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
return line, cursor
|
|
|
|
|
|
|
|
def get_targets(line, cursor):
|
|
|
|
"""Parses a period separated string of valid names preceding the cursor and
|
|
|
|
returns them as a list in the same order."""
|
|
|
|
|
|
|
|
targets = []
|
|
|
|
i = cursor - 1
|
|
|
|
while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
|
|
|
|
i -= 1
|
|
|
|
|
|
|
|
pre = line[i+1:cursor]
|
|
|
|
return pre.split('.')
|
|
|
|
|
|
|
|
def get_imports(txt):
|
|
|
|
"""Returns a dictionary which maps symbol names in the source code to their
|
|
|
|
respective modules.
|
|
|
|
|
|
|
|
The line 'from Blender import Text as BText' for example, results in the
|
|
|
|
mapping 'BText' : <module 'Blender.Text' (built-in)>
|
|
|
|
|
|
|
|
Note that this method imports the modules to provide this mapping as as such
|
|
|
|
will execute any initilization code found within.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Unfortunately, generate_tokens may fail if the script leaves brackets or
|
|
|
|
# strings open or there are other syntax errors. For now we return an empty
|
|
|
|
# dictionary until an alternative parse method is implemented.
|
|
|
|
try:
|
2008-07-15 12:55:20 +00:00
|
|
|
tokens = cached_generate_tokens(txt)
|
|
|
|
except TokenError:
|
2008-07-15 07:34:46 +00:00
|
|
|
return dict()
|
|
|
|
|
|
|
|
imports = dict()
|
|
|
|
step = 0
|
|
|
|
|
|
|
|
for type, string, start, end, line in tokens:
|
|
|
|
store = False
|
|
|
|
|
|
|
|
# Default, look for 'from' or 'import' to start
|
|
|
|
if step == 0:
|
|
|
|
if string == 'from':
|
|
|
|
tmp = []
|
|
|
|
step = 1
|
|
|
|
elif string == 'import':
|
|
|
|
fromname = None
|
|
|
|
tmp = []
|
|
|
|
step = 2
|
|
|
|
|
|
|
|
# Found a 'from', create fromname in form '???.???...'
|
|
|
|
elif step == 1:
|
|
|
|
if string == 'import':
|
|
|
|
fromname = '.'.join(tmp)
|
|
|
|
tmp = []
|
|
|
|
step = 2
|
|
|
|
elif type == tokenize.NAME:
|
|
|
|
tmp.append(string)
|
|
|
|
elif string != '.':
|
|
|
|
step = 0 # Invalid syntax
|
|
|
|
|
|
|
|
# Found 'import', fromname is populated or None, create impname
|
|
|
|
elif step == 2:
|
|
|
|
if string == 'as':
|
|
|
|
impname = '.'.join(tmp)
|
|
|
|
step = 3
|
|
|
|
elif type == tokenize.NAME:
|
|
|
|
tmp.append(string)
|
|
|
|
elif string != '.':
|
|
|
|
impname = '.'.join(tmp)
|
|
|
|
symbol = impname
|
|
|
|
store = True
|
|
|
|
|
|
|
|
# Found 'as', change symbol to this value and go back to step 2
|
|
|
|
elif step == 3:
|
|
|
|
if type == tokenize.NAME:
|
|
|
|
symbol = string
|
|
|
|
else:
|
|
|
|
store = True
|
|
|
|
|
|
|
|
# Both impname and symbol have now been populated so we can import
|
|
|
|
if store:
|
|
|
|
|
|
|
|
# Handle special case of 'import *'
|
|
|
|
if impname == '*':
|
|
|
|
parent = get_module(fromname)
|
2008-07-15 12:55:20 +00:00
|
|
|
imports.update(parent.__dict__)
|
2008-07-15 07:34:46 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
# Try importing the name as a module
|
|
|
|
try:
|
|
|
|
if fromname:
|
|
|
|
module = get_module(fromname +'.'+ impname)
|
|
|
|
else:
|
|
|
|
module = get_module(impname)
|
|
|
|
imports[symbol] = module
|
2008-07-15 12:55:20 +00:00
|
|
|
except (ImportError, ValueError, AttributeError, TypeError):
|
2008-07-15 07:34:46 +00:00
|
|
|
# Try importing name as an attribute of the parent
|
|
|
|
try:
|
|
|
|
module = __import__(fromname, globals(), locals(), [impname])
|
|
|
|
imports[symbol] = getattr(module, impname)
|
2008-07-15 12:55:20 +00:00
|
|
|
except (ImportError, ValueError, AttributeError, TypeError):
|
2008-07-15 07:34:46 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
# More to import from the same module?
|
|
|
|
if string == ',':
|
|
|
|
tmp = []
|
|
|
|
step = 2
|
|
|
|
else:
|
|
|
|
step = 0
|
|
|
|
|
|
|
|
return imports
|
|
|
|
|
|
|
|
def get_builtins():
|
|
|
|
"""Returns a dictionary of built-in modules, functions and variables."""
|
|
|
|
|
|
|
|
return __builtin__.__dict__
|
|
|
|
|
|
|
|
def get_defs(txt):
|
|
|
|
"""Returns a dictionary which maps definition names in the source code to
|
|
|
|
a list of their parameter names.
|
|
|
|
|
|
|
|
The line 'def doit(one, two, three): print one' for example, results in the
|
|
|
|
mapping 'doit' : [ 'one', 'two', 'three' ]
|
|
|
|
"""
|
|
|
|
|
|
|
|
# See above for problems with generate_tokens
|
|
|
|
try:
|
2008-07-15 12:55:20 +00:00
|
|
|
tokens = cached_generate_tokens(txt)
|
|
|
|
except TokenError:
|
2008-07-15 07:34:46 +00:00
|
|
|
return dict()
|
|
|
|
|
|
|
|
defs = dict()
|
|
|
|
step = 0
|
|
|
|
|
|
|
|
for type, string, start, end, line in tokens:
|
|
|
|
|
|
|
|
# Look for 'def'
|
|
|
|
if step == 0:
|
|
|
|
if string == 'def':
|
|
|
|
name = None
|
|
|
|
step = 1
|
|
|
|
|
|
|
|
# Found 'def', look for name followed by '('
|
|
|
|
elif step == 1:
|
|
|
|
if type == tokenize.NAME:
|
|
|
|
name = string
|
|
|
|
params = []
|
|
|
|
elif name and string == '(':
|
|
|
|
step = 2
|
|
|
|
|
|
|
|
# Found 'def' name '(', now identify the parameters upto ')'
|
|
|
|
# TODO: Handle ellipsis '...'
|
|
|
|
elif step == 2:
|
|
|
|
if type == tokenize.NAME:
|
|
|
|
params.append(string)
|
|
|
|
elif string == ')':
|
|
|
|
defs[name] = params
|
|
|
|
step = 0
|
|
|
|
|
|
|
|
return defs
|
2008-07-15 12:55:20 +00:00
|
|
|
|
|
|
|
def get_vars(txt):
|
|
|
|
"""Returns a dictionary of variable names found in the specified Text
|
|
|
|
object. This method locates all names followed directly by an equal sign:
|
|
|
|
'a = ???' or indirectly as part of a tuple/list assignment or inside a
|
|
|
|
'for ??? in ???:' block.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# See above for problems with generate_tokens
|
|
|
|
try:
|
|
|
|
tokens = cached_generate_tokens(txt)
|
|
|
|
except TokenError:
|
|
|
|
return []
|
|
|
|
|
|
|
|
vars = []
|
|
|
|
accum = [] # Used for tuple/list assignment
|
|
|
|
foring = False
|
|
|
|
|
|
|
|
for type, string, start, end, line in tokens:
|
|
|
|
|
|
|
|
# Look for names
|
|
|
|
if string == 'for':
|
|
|
|
foring = True
|
|
|
|
if string == '=' or (foring and string == 'in'):
|
|
|
|
vars.extend(accum)
|
|
|
|
accum = []
|
|
|
|
foring = False
|
|
|
|
elif type == tokenize.NAME:
|
|
|
|
accum.append(string)
|
|
|
|
elif not string in [',', '(', ')', '[', ']']:
|
|
|
|
accum = []
|
|
|
|
foring = False
|
|
|
|
|
|
|
|
return vars
|