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, parents, defs, vars, lineno, doc=''):
60
Definition.__init__(self, name, lineno, doc)
61
self.parents = parents
65
class FunctionDesc(Definition):
66
"""Describes a function through its name and list of parameters (name,
67
params) and the line on which it is defined (lineno).
70
def __init__(self, name, params, lineno, doc=''):
71
Definition.__init__(self, name, lineno, doc)
74
class VarDesc(Definition):
75
"""Describes a variable through its name and type (if ascertainable) and the
76
line on which it is defined (lineno). If no type can be determined, type
80
def __init__(self, name, type, lineno):
81
Definition.__init__(self, name, lineno)
82
self.type = type # None for unknown (supports: dict/list/str)
92
KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
93
'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
94
'break', 'except', 'import', 'print', 'class', 'exec', 'in',
95
'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
98
# Module file extensions
99
MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd']
101
ModuleType = type(__builtin__)
102
NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True)
106
_parse_cache = dict()
108
def _load_module_names():
109
"""Searches the sys.path for module files and lists them, along with
110
sys.builtin_module_names, in the global dict _modules.
115
for n in sys.builtin_module_names:
118
if p == '': p = os.curdir
119
if not os.path.isdir(p): continue
120
for f in os.listdir(p):
121
for ext in MODULE_EXTS:
123
_modules[f[:-len(ext)]] = None
129
"""Trims the quotes from a quoted STRING token (eg. "'''text'''" -> "text")
134
while i < l/2 and (doc[i] == "'" or doc[i] == '"'):
138
def resolve_targets(txt, targets):
139
"""Attempts to return a useful object for the locally or externally defined
140
entity described by targets. If the object is local (defined in txt), a
141
Definition instance is returned. If the object is external (imported or
142
built in), the object itself is returned. If no object can be found, None is
147
if count==0: return None
153
desc = get_cached_descriptor(txt)
154
b = targets[0].find('(')
155
if b==-1: b = None # Trick to let us use [:b] and get the whole string
157
if desc.classes.has_key(targets[0][:b]):
158
local = desc.classes[targets[0][:b]]
159
elif desc.defs.has_key(targets[0]):
160
local = desc.defs[targets[0]]
161
elif desc.vars.has_key(targets[0]):
162
obj = desc.vars[targets[0]].type
166
b = targets[i].find('(')
168
if hasattr(local, 'classes') and local.classes.has_key(targets[i][:b]):
169
local = local.classes[targets[i][:b]]
170
elif hasattr(local, 'defs') and local.defs.has_key(targets[i]):
171
local = local.defs[targets[i]]
172
elif hasattr(local, 'vars') and local.vars.has_key(targets[i]):
173
obj = local.vars[targets[i]].type
182
if local: return local
185
if desc.imports.has_key(targets[0]):
186
obj = desc.imports[targets[0]]
188
builtins = get_builtins()
189
if builtins.has_key(targets[0]):
190
obj = builtins[targets[0]]
192
while obj and i < count:
193
if hasattr(obj, targets[i]):
194
obj = getattr(obj, targets[i])
202
def get_cached_descriptor(txt, force_parse=0):
203
"""Returns the cached ScriptDesc for the specified Text object 'txt'. If the
204
script has not been parsed in the last 'period' seconds it will be reparsed
205
to obtain this descriptor.
207
Specifying TP_AUTO for the period (default) will choose a period based on the
208
size of the Text object. Larger texts are parsed less often.
215
if not force_parse and _parse_cache.has_key(key):
216
desc = _parse_cache[key]
217
if desc.parse_due > time():
218
parse = desc.incomplete
221
desc = parse_text(txt)
226
"""Parses an entire script's text and returns a ScriptDesc instance
227
containing information about the script.
229
If the text is not a valid Python script (for example if brackets are left
230
open), parsing may fail to complete. However, if this occurs, no exception
231
is thrown. Instead the returned ScriptDesc instance will have its incomplete
232
flag set and information processed up to this point will still be accessible.
237
tokens = generate_tokens(txt.readline) # Throws TokenError
239
curl, cursor = txt.getCursorPos()
240
linen = curl + 1 # Token line numbers are one-based
265
type, text, start, end, line = tokens.next()
266
except StopIteration:
268
except (TokenError, IndentationError):
272
# Skip all comments and line joining characters
273
if type == COMMENT or type == NL:
285
#########################
286
## Module importing... ##
287
#########################
291
# Default, look for 'from' or 'import' to start
296
elif text == 'import':
301
# Found a 'from', create imp_from in form '???.???...'
304
imp_from = '.'.join(imp_tmp)
310
imp_step = 0 # Invalid syntax
312
# Found 'import', imp_from is populated or None, create imp_name
315
imp_name = '.'.join(imp_tmp)
317
elif type == NAME or text == '*':
320
imp_name = '.'.join(imp_tmp)
324
# Found 'as', change imp_symb to this value and go back to step 2
331
# Both imp_name and imp_symb have now been populated so we can import
334
# Handle special case of 'import *'
336
parent = get_module(imp_from)
337
imports.update(parent.__dict__)
340
# Try importing the name as a module
343
module = get_module(imp_from +'.'+ imp_name)
345
module = get_module(imp_name)
346
except (ImportError, ValueError, AttributeError, TypeError):
347
# Try importing name as an attribute of the parent
349
module = __import__(imp_from, globals(), locals(), [imp_name])
350
imports[imp_symb] = getattr(module, imp_name)
351
except (ImportError, ValueError, AttributeError, TypeError):
354
imports[imp_symb] = module
356
# More to import from the same module?
367
# If we are inside a class then def and variable parsing should be done
368
# for the class. Otherwise the definitions are considered global
374
cls_lineno = start[0]
378
# Found 'class', look for cls_name followed by '(' parents ')'
388
if classes.has_key(text):
389
parent = classes[text]
390
cls_parents[text] = parent
391
cls_defs.update(parent.defs)
392
cls_vars.update(parent.vars)
396
# Found 'class' name ... ':', now check if it's a single line statement
406
if not cls_doc and type == STRING:
407
cls_doc = _trim_doc(text)
410
classes[cls_name] = ClassDesc(cls_name, cls_parents, cls_defs, cls_vars, cls_lineno, cls_doc)
413
if type == DEDENT and indent <= cls_indent:
414
classes[cls_name] = ClassDesc(cls_name, cls_parents, cls_defs, cls_vars, cls_lineno, cls_doc)
425
def_lineno = start[0]
428
# Found 'def', look for def_name followed by '('
433
elif def_name and text == '(':
436
# Found 'def' name '(', now identify the parameters upto ')'
437
# TODO: Handle ellipsis '...'
440
def_params.append(text)
444
# Found 'def' ... ':', now check if it's a single line statement
455
def_doc = _trim_doc(text)
459
newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
462
newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc)
464
if cls_step > 0: # Parsing a class
465
cls_defs[def_name] = newdef
467
defs[def_name] = newdef
470
##########################
471
## Variable assignation ##
472
##########################
474
if cls_step > 0: # Parsing a class
475
# Look for 'self.???'
488
if cls_vars.has_key(var_name):
501
if text.find('.') != -1: var_type = float
507
close = line.find(']', end[1])
510
close = line.find(')', end[1])
513
close = line.find('}', end[1])
516
close = line.find(')', end[1])
518
if var_type and close+1 < len(line):
519
if line[close+1] != ' ' and line[close+1] != '\t':
521
cls_vars[var_name] = VarDesc(var_name, var_type, start[0])
524
elif def_step > 0: # Parsing a def
525
# Look for 'global ???[,???]'
531
if not vars.has_key(text):
532
vars[text] = VarDesc(text, None, start[0])
533
elif text != ',' and type != NL:
536
else: # In global scope
542
elif text == '=' or (var_forflag and text == 'in'):
546
if prev_text != '.' and not vars.has_key(text):
547
var_accum[text] = VarDesc(text, None, start[0])
548
elif not text in [',', '(', ')', '[', ']']:
552
if len(var_accum) != 1:
554
vars.update(var_accum)
556
var_name = var_accum.keys()[0]
559
if text.find('.') != -1: var_type = float
561
elif type == STRING: var_type = str
562
elif text == '[': var_type = list
563
elif text == '(': var_type = tuple
564
elif text == '{': var_type = dict
565
vars[var_name] = VarDesc(var_name, var_type, start[0])
568
#######################
569
## General utilities ##
570
#######################
575
desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete)
576
desc.set_delay(10 * (time()-start_time) + 0.05)
579
_parse_cache[hash(txt)] = desc
582
def get_modules(since=1):
583
"""Returns the set of built-in modules and any modules that have been
584
imported into the system upto 'since' seconds ago.
587
global _modules, _modules_updated
590
if _modules_updated < t - since:
591
_modules.update(sys.modules)
593
return _modules.keys()
595
def suggest_cmp(x, y):
596
"""Use this method when sorting a list of suggestions.
599
return cmp(x[0].upper(), y[0].upper())
601
def get_module(name):
602
"""Returns the module specified by its name. The module itself is imported
603
by this method and, as such, any initialization code will be executed.
606
mod = __import__(name)
607
components = name.split('.')
608
for comp in components[1:]:
609
mod = getattr(mod, comp)
613
"""Returns the character used to signify the type of a variable. Use this
614
method to identify the type character for an item in a suggestion list.
616
The following values are returned:
617
'm' if the parameter is a module
618
'f' if the parameter is callable
619
'v' if the parameter is variable or otherwise indeterminable
623
if isinstance(v, ModuleType):
630
def get_context(txt):
631
"""Establishes the context of the cursor in the given Blender Text object
634
CTX_NORMAL - Cursor is in a normal context
635
CTX_SINGLE_QUOTE - Cursor is inside a single quoted string
636
CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string
637
CTX_COMMENT - Cursor is inside a comment
641
l, cursor = txt.getCursorPos()
642
lines = txt.asLines(0, l+1)
644
# FIXME: This method is too slow in large files for it to be called as often
645
# as it is. So for lines below the 1000th line we do this... (quorn)
646
if l > 1000: return CTX_NORMAL
648
# Detect context (in string or comment)
657
# Comments end at new lines
658
if in_str == CTX_COMMENT:
663
if line[i] == "'": in_str = CTX_SINGLE_QUOTE
664
elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE
665
elif line[i] == '#': in_str = CTX_COMMENT
667
if in_str == CTX_SINGLE_QUOTE:
670
# In again if ' escaped, out again if \ escaped, and so on
671
for a in range(i-1, -1, -1):
672
if line[a] == '\\': in_str = 1-in_str
674
elif in_str == CTX_DOUBLE_QUOTE:
677
# In again if " escaped, out again if \ escaped, and so on
678
for a in range(i-1, -1, -1):
679
if line[i-a] == '\\': in_str = 2-in_str
684
def current_line(txt):
685
"""Extracts the Python script line at the cursor in the Blender Text object
686
provided and cursor position within this line as the tuple pair (line,
690
lineindex, cursor = txt.getCursorPos()
691
lines = txt.asLines()
692
line = lines[lineindex]
694
# Join previous lines to this line if spanning
697
earlier = lines[i].rstrip()
698
if earlier.endswith('\\'):
699
line = earlier[:-1] + ' ' + line
700
cursor += len(earlier)
703
# Join later lines while there is an explicit joining character
705
while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
706
later = lines[i+1].strip()
707
line = line + ' ' + later[:-1]
712
def get_targets(line, cursor):
713
"""Parses a period separated string of valid names preceding the cursor and
714
returns them as a list in the same order.
722
if line[i] == ')': brk += 1
724
if line[i] == '(': brk -= 1
727
targets.insert(0, line[i+1:j]); j=i
728
elif not (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
731
targets.insert(0, line[i+1:j])
735
"""Returns a dictionary which maps definition names in the source code to
736
a list of their parameter names.
738
The line 'def doit(one, two, three): print one' for example, results in the
739
mapping 'doit' : [ 'one', 'two', 'three' ]
742
return get_cached_descriptor(txt).defs
745
"""Returns a dictionary of variable names found in the specified Text
746
object. This method locates all names followed directly by an equal sign:
747
'a = ???' or indirectly as part of a tuple/list assignment or inside a
748
'for ??? in ???:' block.
751
return get_cached_descriptor(txt).vars
753
def get_imports(txt):
754
"""Returns a dictionary which maps symbol names in the source code to their
757
The line 'from Blender import Text as BText' for example, results in the
758
mapping 'BText' : <module 'Blender.Text' (built-in)>
760
Note that this method imports the modules to provide this mapping as as such
761
will execute any initilization code found within.
764
return get_cached_descriptor(txt).imports
767
"""Returns a dictionary of built-in modules, functions and variables."""
769
return __builtin__.__dict__
772
#################################
773
## Debugging utility functions ##
774
#################################
776
def print_cache_for(txt, period=sys.maxint):
777
"""Prints out the data cached for a given Text object. If no period is
778
given the text will not be reparsed and the cached version will be returned.
779
Otherwise if the period has expired the text will be reparsed.
782
desc = get_cached_descriptor(txt, period)
783
print '================================================'
784
print 'Name:', desc.name, '('+str(hash(txt))+')'
785
print '------------------------------------------------'
787
for name, ddesc in desc.defs.items():
788
print ' ', name, ddesc.params, ddesc.lineno
790
print '------------------------------------------------'
792
for name, vdesc in desc.vars.items():
793
print ' ', name, vdesc.type, vdesc.lineno
794
print '------------------------------------------------'
796
for name, item in desc.imports.items():
797
print ' ', name.ljust(15), item
798
print '------------------------------------------------'
800
for clsnme, clsdsc in desc.classes.items():
801
print ' *********************************'
802
print ' Name:', clsnme
803
print ' ', clsdsc.doc
804
print ' ---------------------------------'
806
for name, ddesc in clsdsc.defs.items():
807
print ' ', name, ddesc.params, ddesc.lineno
809
print ' ---------------------------------'
811
for name, vdesc in clsdsc.vars.items():
812
print ' ', name, vdesc.type, vdesc.lineno
813
print ' *********************************'
814
print '================================================'