Previously relying on import to run scripts didn't work every time and was not the...
[blender.git] / release / scripts / bpymodules / BPyTextPlugin.py
1 import bpy, sys
2 import __builtin__, tokenize
3 from Blender.sys import time
4 from tokenize import generate_tokens, TokenError
5
6 # TODO: Remove the dependency for a full Python installation.
7
8 # Context types
9 NORMAL = 0
10 SINGLE_QUOTE = 1
11 DOUBLE_QUOTE = 2
12 COMMENT = 3
13
14 # Python keywords
15 KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
16                         'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
17                         'break', 'except', 'import', 'print', 'class', 'exec', 'in',
18                         'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
19                         'lambda', 'try' ]
20
21 # Used to cache the return value of generate_tokens
22 _token_cache = None
23 _cache_update = 0
24
25 ModuleType = type(__builtin__)
26 _modules = dict([(n, None) for n in sys.builtin_module_names])
27 _modules_updated = 0
28
29 def get_modules(since=1):
30         """Returns the set of built-in modules and any modules that have been
31         imported into the system upto 'since' seconds ago.
32         """
33         
34         global _modules, _modules_updated
35         
36         t = time()
37         if _modules_updated < t - since:
38                 _modules.update(sys.modules)
39                 _modules_updated = t
40         return _modules.keys()
41
42 def suggest_cmp(x, y):
43         """Use this method when sorting a list of suggestions.
44         """
45         
46         return cmp(x[0].upper(), y[0].upper())
47
48 def cached_generate_tokens(txt, since=1):
49         """A caching version of generate tokens for multiple parsing of the same
50         document within a given timescale.
51         """
52         
53         global _token_cache, _cache_update
54         
55         t = time()
56         if _cache_update < t - since:
57                 txt.reset()
58                 _token_cache = [g for g in generate_tokens(txt.readline)]
59                 _cache_update = t
60         return _token_cache
61
62 def get_module(name):
63         """Returns the module specified by its name. The module itself is imported
64         by this method and, as such, any initialization code will be executed.
65         """
66         
67         mod = __import__(name)
68         components = name.split('.')
69         for comp in components[1:]:
70                 mod = getattr(mod, comp)
71         return mod
72
73 def type_char(v):
74         """Returns the character used to signify the type of a variable. Use this
75         method to identify the type character for an item in a suggestion list.
76         
77         The following values are returned:
78           'm' if the parameter is a module
79           'f' if the parameter is callable
80           'v' if the parameter is variable or otherwise indeterminable
81         """
82         
83         if isinstance(v, ModuleType):
84                 return 'm'
85         elif callable(v):
86                 return 'f'
87         else: 
88                 return 'v'
89
90 def get_context(txt):
91         """Establishes the context of the cursor in the given Blender Text object
92         
93         Returns one of:
94           NORMAL - Cursor is in a normal context
95           SINGLE_QUOTE - Cursor is inside a single quoted string
96           DOUBLE_QUOTE - Cursor is inside a double quoted string
97           COMMENT - Cursor is inside a comment
98         
99         """
100         
101         l, cursor = txt.getCursorPos()
102         lines = txt.asLines()[:l+1]
103         
104         # Detect context (in string or comment)
105         in_str = 0                      # 1-single quotes, 2-double quotes
106         for line in lines:
107                 if l == 0:
108                         end = cursor
109                 else:
110                         end = len(line)
111                         l -= 1
112                 
113                 # Comments end at new lines
114                 if in_str == 3:
115                         in_str = 0
116                 
117                 for i in range(end):
118                         if in_str == 0:
119                                 if line[i] == "'": in_str = 1
120                                 elif line[i] == '"': in_str = 2
121                                 elif line[i] == '#': in_str = 3
122                         else:
123                                 if in_str == 1:
124                                         if line[i] == "'":
125                                                 in_str = 0
126                                                 # In again if ' escaped, out again if \ escaped, and so on
127                                                 for a in range(i-1, -1, -1):
128                                                         if line[a] == '\\': in_str = 1-in_str
129                                                         else: break
130                                 elif in_str == 2:
131                                         if line[i] == '"':
132                                                 in_str = 0
133                                                 # In again if " escaped, out again if \ escaped, and so on
134                                                 for a in range(i-1, -1, -1):
135                                                         if line[i-a] == '\\': in_str = 2-in_str
136                                                         else: break
137                 
138         return in_str
139
140 def current_line(txt):
141         """Extracts the Python script line at the cursor in the Blender Text object
142         provided and cursor position within this line as the tuple pair (line,
143         cursor)"""
144         
145         (lineindex, cursor) = txt.getCursorPos()
146         lines = txt.asLines()
147         line = lines[lineindex]
148         
149         # Join previous lines to this line if spanning
150         i = lineindex - 1
151         while i > 0:
152                 earlier = lines[i].rstrip()
153                 if earlier.endswith('\\'):
154                         line = earlier[:-1] + ' ' + line
155                         cursor += len(earlier)
156                 i -= 1
157         
158         # Join later lines while there is an explicit joining character
159         i = lineindex
160         while i < len(lines)-1 and lines[i].rstrip().endswith('\\'):
161                 later = lines[i+1].strip()
162                 line = line + ' ' + later[:-1]
163                 i += 1
164         
165         return line, cursor
166
167 def get_targets(line, cursor):
168         """Parses a period separated string of valid names preceding the cursor and
169         returns them as a list in the same order."""
170         
171         targets = []
172         i = cursor - 1
173         while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
174                 i -= 1
175         
176         pre = line[i+1:cursor]
177         return pre.split('.')
178
179 def get_imports(txt):
180         """Returns a dictionary which maps symbol names in the source code to their
181         respective modules.
182         
183         The line 'from Blender import Text as BText' for example, results in the
184         mapping 'BText' : <module 'Blender.Text' (built-in)>
185         
186         Note that this method imports the modules to provide this mapping as as such
187         will execute any initilization code found within.
188         """
189         
190         # Unfortunately, generate_tokens may fail if the script leaves brackets or
191         # strings open or there are other syntax errors. For now we return an empty
192         # dictionary until an alternative parse method is implemented.
193         try:
194                 tokens = cached_generate_tokens(txt)
195         except TokenError:
196                 return dict()
197         
198         imports = dict()
199         step = 0
200         
201         for type, string, start, end, line in tokens:
202                 store = False
203                 
204                 # Default, look for 'from' or 'import' to start
205                 if step == 0:
206                         if string == 'from':
207                                 tmp = []
208                                 step = 1
209                         elif string == 'import':
210                                 fromname = None
211                                 tmp = []
212                                 step = 2
213                 
214                 # Found a 'from', create fromname in form '???.???...'
215                 elif step == 1:
216                         if string == 'import':
217                                 fromname = '.'.join(tmp)
218                                 tmp = []
219                                 step = 2
220                         elif type == tokenize.NAME:
221                                 tmp.append(string)
222                         elif string != '.':
223                                 step = 0 # Invalid syntax
224                 
225                 # Found 'import', fromname is populated or None, create impname
226                 elif step == 2:
227                         if string == 'as':
228                                 impname = '.'.join(tmp)
229                                 step = 3
230                         elif type == tokenize.NAME or string == '*':
231                                 tmp.append(string)
232                         elif string != '.':
233                                 impname = '.'.join(tmp)
234                                 symbol = impname
235                                 store = True
236                 
237                 # Found 'as', change symbol to this value and go back to step 2
238                 elif step == 3:
239                         if type == tokenize.NAME:
240                                 symbol = string
241                         else:
242                                 store = True
243                 
244                 # Both impname and symbol have now been populated so we can import
245                 if store:
246                         
247                         # Handle special case of 'import *'
248                         if impname == '*':
249                                 parent = get_module(fromname)
250                                 imports.update(parent.__dict__)
251                                 
252                         else:
253                                 # Try importing the name as a module
254                                 try:
255                                         if fromname:
256                                                 module = get_module(fromname +'.'+ impname)
257                                         else:
258                                                 module = get_module(impname)
259                                         imports[symbol] = module
260                                 except (ImportError, ValueError, AttributeError, TypeError):
261                                         # Try importing name as an attribute of the parent
262                                         try:
263                                                 module = __import__(fromname, globals(), locals(), [impname])
264                                                 imports[symbol] = getattr(module, impname)
265                                         except (ImportError, ValueError, AttributeError, TypeError):
266                                                 pass
267                         
268                         # More to import from the same module?
269                         if string == ',':
270                                 tmp = []
271                                 step = 2
272                         else:
273                                 step = 0
274         
275         return imports
276
277 def get_builtins():
278         """Returns a dictionary of built-in modules, functions and variables."""
279         
280         return __builtin__.__dict__
281
282 def get_defs(txt):
283         """Returns a dictionary which maps definition names in the source code to
284         a list of their parameter names.
285         
286         The line 'def doit(one, two, three): print one' for example, results in the
287         mapping 'doit' : [ 'one', 'two', 'three' ]
288         """
289         
290         # See above for problems with generate_tokens
291         try:
292                 tokens = cached_generate_tokens(txt)
293         except TokenError:
294                 return dict()
295         
296         defs = dict()
297         step = 0
298         
299         for type, string, start, end, line in tokens:
300                 
301                 # Look for 'def'
302                 if step == 0:
303                         if string == 'def':
304                                 name = None
305                                 step = 1
306                 
307                 # Found 'def', look for name followed by '('
308                 elif step == 1:
309                         if type == tokenize.NAME:
310                                 name = string
311                                 params = []
312                         elif name and string == '(':
313                                 step = 2
314                 
315                 # Found 'def' name '(', now identify the parameters upto ')'
316                 # TODO: Handle ellipsis '...'
317                 elif step == 2:
318                         if type == tokenize.NAME:
319                                 params.append(string)
320                         elif string == ')':
321                                 defs[name] = params
322                                 step = 0
323                 
324         return defs
325
326 def get_vars(txt):
327         """Returns a dictionary of variable names found in the specified Text
328         object. This method locates all names followed directly by an equal sign:
329         'a = ???' or indirectly as part of a tuple/list assignment or inside a
330         'for ??? in ???:' block.
331         """
332         
333         # See above for problems with generate_tokens
334         try:
335                 tokens = cached_generate_tokens(txt)
336         except TokenError:
337                 return []
338         
339         vars = []
340         accum = [] # Used for tuple/list assignment
341         foring = False
342         
343         for type, string, start, end, line in tokens:
344                 
345                 # Look for names
346                 if string == 'for':
347                         foring = True
348                 if string == '=' or (foring and string == 'in'):
349                         vars.extend(accum)
350                         accum = []
351                         foring = False
352                 elif type == tokenize.NAME:
353                         accum.append(string)
354                 elif not string in [',', '(', ')', '[', ']']:
355                         accum = []
356                         foring = False
357                 
358         return vars