Created a BPy module BPyTextPlugin to centralize functions used across the text plugi...
[blender.git] / release / scripts / bpymodules / BPyTextPlugin.py
1 import bpy, sys
2 import __builtin__, tokenize
3 from tokenize import generate_tokens
4 # TODO: Remove the dependency for a full Python installation. Currently only the
5 # tokenize module is required 
6
7 # Context types
8 NORMAL = 0
9 SINGLE_QUOTE = 1
10 DOUBLE_QUOTE = 2
11 COMMENT = 3
12
13 # Python keywords
14 KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
15                         'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
16                         'break', 'except', 'import', 'print', 'class', 'exec', 'in',
17                         'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
18                         'lambda', 'try' ]
19
20
21 def suggest_cmp(x, y):
22         """Use this method when sorting a list for suggestions"""
23         
24         return cmp(x[0], y[0])
25
26 def get_module(name):
27         """Returns the module specified by its name. This module is imported and as
28         such will run any initialization code specified within the module."""
29         
30         mod = __import__(name)
31         components = name.split('.')
32         for comp in components[1:]:
33                 mod = getattr(mod, comp)
34         return mod
35
36 def is_module(m):
37         """Taken from the inspect module of the standard Python installation"""
38         
39         return isinstance(m, type(bpy))
40
41 def type_char(v):
42         if is_module(v):
43                 return 'm'
44         elif callable(v):
45                 return 'f'
46         else: 
47                 return 'v'
48
49 def get_context(line, cursor):
50         """Establishes the context of the cursor in the given line
51         
52         Returns one of:
53           NORMAL - Cursor is in a normal context
54           SINGLE_QUOTE - Cursor is inside a single quoted string
55           DOUBLE_QUOTE - Cursor is inside a double quoted string
56           COMMENT - Cursor is inside a comment
57         
58         """
59         
60         # Detect context (in string or comment)
61         in_str = 0                      # 1-single quotes, 2-double quotes
62         for i in range(cursor):
63                 if not in_str:
64                         if line[i] == "'": in_str = 1
65                         elif line[i] == '"': in_str = 2
66                         elif line[i] == '#': return 3 # In a comment so quit
67                 else:
68                         if in_str == 1:
69                                 if line[i] == "'":
70                                         in_str = 0
71                                         # In again if ' escaped, out again if \ escaped, and so on
72                                         for a in range(1, i+1):
73                                                 if line[i-a] == '\\': in_str = 1-in_str
74                                                 else: break
75                         elif in_str == 2:
76                                 if line[i] == '"':
77                                         in_str = 0
78                                         # In again if " escaped, out again if \ escaped, and so on
79                                         for a in range(1, i+1):
80                                                 if line[i-a] == '\\': in_str = 2-in_str
81                                                 else: break
82         return in_str
83
84 def current_line(txt):
85         """Extracts the Python script line at the cursor in the Blender Text object
86         provided and cursor position within this line as the tuple pair (line,
87         cursor)"""
88         
89         (lineindex, cursor) = txt.getCursorPos()
90         lines = txt.asLines()
91         line = lines[lineindex]
92         
93         # Join previous lines to this line if spanning
94         i = lineindex - 1
95         while i > 0:
96                 earlier = lines[i].rstrip()
97                 if earlier.endswith('\\'):
98                         line = earlier[:-1] + ' ' + line
99                         cursor += len(earlier)
100                 i -= 1
101         
102         # Join later lines while there is an explicit joining character
103         i = lineindex
104         while i < len(lines)-1 and line[i].rstrip().endswith('\\'):
105                 later = lines[i+1].strip()
106                 line = line + ' ' + later[:-1]
107         
108         return line, cursor
109
110 def get_targets(line, cursor):
111         """Parses a period separated string of valid names preceding the cursor and
112         returns them as a list in the same order."""
113         
114         targets = []
115         i = cursor - 1
116         while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'):
117                 i -= 1
118         
119         pre = line[i+1:cursor]
120         return pre.split('.')
121
122 def get_imports(txt):
123         """Returns a dictionary which maps symbol names in the source code to their
124         respective modules.
125         
126         The line 'from Blender import Text as BText' for example, results in the
127         mapping 'BText' : <module 'Blender.Text' (built-in)>
128         
129         Note that this method imports the modules to provide this mapping as as such
130         will execute any initilization code found within.
131         """
132         
133         # Unfortunately, generate_tokens may fail if the script leaves brackets or
134         # strings open or there are other syntax errors. For now we return an empty
135         # dictionary until an alternative parse method is implemented.
136         try:
137                 txt.reset()
138                 tokens = generate_tokens(txt.readline)
139         except:
140                 return dict()
141         
142         imports = dict()
143         step = 0
144         
145         for type, string, start, end, line in tokens:
146                 store = False
147                 
148                 # Default, look for 'from' or 'import' to start
149                 if step == 0:
150                         if string == 'from':
151                                 tmp = []
152                                 step = 1
153                         elif string == 'import':
154                                 fromname = None
155                                 tmp = []
156                                 step = 2
157                 
158                 # Found a 'from', create fromname in form '???.???...'
159                 elif step == 1:
160                         if string == 'import':
161                                 fromname = '.'.join(tmp)
162                                 tmp = []
163                                 step = 2
164                         elif type == tokenize.NAME:
165                                 tmp.append(string)
166                         elif string != '.':
167                                 step = 0 # Invalid syntax
168                 
169                 # Found 'import', fromname is populated or None, create impname
170                 elif step == 2:
171                         if string == 'as':
172                                 impname = '.'.join(tmp)
173                                 step = 3
174                         elif type == tokenize.NAME:
175                                 tmp.append(string)
176                         elif string != '.':
177                                 impname = '.'.join(tmp)
178                                 symbol = impname
179                                 store = True
180                 
181                 # Found 'as', change symbol to this value and go back to step 2
182                 elif step == 3:
183                         if type == tokenize.NAME:
184                                 symbol = string
185                         else:
186                                 store = True
187                 
188                 # Both impname and symbol have now been populated so we can import
189                 if store:
190                         
191                         # Handle special case of 'import *'
192                         if impname == '*':
193                                 parent = get_module(fromname)
194                                 for symbol, attr in parent.__dict__.items():
195                                         imports[symbol] = attr
196                                 
197                         else:
198                                 # Try importing the name as a module
199                                 try:
200                                         if fromname:
201                                                 module = get_module(fromname +'.'+ impname)
202                                         else:
203                                                 module = get_module(impname)
204                                         imports[symbol] = module
205                                 except:
206                                         # Try importing name as an attribute of the parent
207                                         try:
208                                                 module = __import__(fromname, globals(), locals(), [impname])
209                                                 imports[symbol] = getattr(module, impname)
210                                         except:
211                                                 pass
212                         
213                         # More to import from the same module?
214                         if string == ',':
215                                 tmp = []
216                                 step = 2
217                         else:
218                                 step = 0
219         
220         return imports
221
222
223 def get_builtins():
224         """Returns a dictionary of built-in modules, functions and variables."""
225         
226         return __builtin__.__dict__
227
228 def get_defs(txt):
229         """Returns a dictionary which maps definition names in the source code to
230         a list of their parameter names.
231         
232         The line 'def doit(one, two, three): print one' for example, results in the
233         mapping 'doit' : [ 'one', 'two', 'three' ]
234         """
235         
236         # See above for problems with generate_tokens
237         try:
238                 txt.reset()
239                 tokens = generate_tokens(txt.readline)
240         except:
241                 return dict()
242         
243         defs = dict()
244         step = 0
245         
246         for type, string, start, end, line in tokens:
247                 
248                 # Look for 'def'
249                 if step == 0:
250                         if string == 'def':
251                                 name = None
252                                 step = 1
253                 
254                 # Found 'def', look for name followed by '('
255                 elif step == 1:
256                         if type == tokenize.NAME:
257                                 name = string
258                                 params = []
259                         elif name and string == '(':
260                                 step = 2
261                 
262                 # Found 'def' name '(', now identify the parameters upto ')'
263                 # TODO: Handle ellipsis '...'
264                 elif step == 2:
265                         if type == tokenize.NAME:
266                                 params.append(string)
267                         elif string == ')':
268                                 defs[name] = params
269                                 step = 0
270                 
271         return defs