· 6 years ago · Sep 18, 2019, 09:30 PM
1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software Foundation,
15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8-80 compliant>
20
21
22
23#====================Version History====================
24# 1.0 Initial release.
25# 1.1 Added error message handling and processing.
26# 1.2 Added seamless handling of modules/addons using '__name__'
27# 1.3 Added option to set the number of scriipts from the panels.
28# Quieted down the 'Cancel' icon.
29#====================================================
30
31
32
33bl_info = {
34 "name": "Script Runner",
35 "author": "Christopher Barrett (www.goodspiritgraphics.com)",
36 "version": (1, 4),
37 "blender": (2, 80, 0),
38 "api": 58537,
39 "location": "File > Import > Run Script (.py), & '3D View Tool Shelf', & 'Text Editor Tool Shelf'.",
40 "description": "Run a python script from any file directory.",
41 "warning": "",
42 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Script_Runner",
43 "tracker_url": "",
44 "category": "Import-Export"
45 }
46
47
48from bpy_extras.io_utils import ImportHelper
49import bpy
50from bpy.app.handlers import persistent
51import os, sys, platform
52import shutil
53import fnmatch
54import errno
55import ntpath
56import traceback
57import importlib
58import string
59import random
60
61from bpy.types import Operator, AddonPreferences
62from bpy.props import EnumProperty, StringProperty,BoolProperty, IntProperty, FloatVectorProperty
63
64
65class cp(bpy.types.PropertyGroup):
66 script_path : bpy.props.StringProperty()
67
68
69#------------------------------Preferences
70
71
72def update_num_scripts(self,context):
73 #context.user_preferences.addons[__name__].preferences.script_path.add()
74 us = context.preferences.addons[__name__].preferences
75 while self.num_scripts > len(us.fpath):
76 us.fpath.add()
77
78
79class ScriptRunnerAddonPreferences(AddonPreferences):
80 bl_idname = __name__
81
82 error_msg_verbose : BoolProperty(name="error_msg_verbose", default = False)
83 display_num_scripts : BoolProperty(name="display_num_scripts", default = True)
84 display_3d : BoolProperty(name="display_3d", default = True)
85 display_text_editor : BoolProperty(name="display_text_editor", default = True)
86 num_scripts : IntProperty(name="Number of Scripts", description = "Set to the number of scripts to display in the panel", min = 1, max = 50, default = 3, update=update_num_scripts)
87 fpath : bpy.props.CollectionProperty(type=cp)
88 items_per_row : IntProperty(name = "Per row", description = "Items per row", min = 1, max = 10, default = 1)
89 buttons_only : BoolProperty(name="Show buttons only", default = False)
90
91
92 def draw(self, context):
93 user_settings = bpy.context.preferences.addons[__name__].preferences
94
95 layout = self.layout
96 split = layout.split()
97
98 #-----Column 1
99 col1 = split.column()
100 row = col1.row()
101 row.label(text ="Number of Script Slots to Display:")
102 row = col1.row()
103 row.label(text="Number of items in a row:")
104
105 #-----Column 2
106 col2 = split.column()
107 row = col2.row()
108 row.prop(self, "num_scripts", text="Scripts", slider=False)
109 row = col2.row()
110 row.prop(self, "items_per_row", slider=False)
111 row = col2.row()
112
113 #-----Column 3
114 col3 = split.column()
115 row = col3.row()
116 row.prop(self, "display_num_scripts", text="Display scripts slider on panel", toggle=False)
117 row = col3.row()
118 row.prop(self, "error_msg_verbose", text="Verbose Error Messages", toggle = False)
119
120
121def draw_scripts_slider(self, context, row):
122 row.prop(context.preferences.addons[__name__].preferences, "num_scripts", text="Scripts")
123
124#-------------------------------Panels
125
126
127class SCRIPTRUNNERPANEL_PT_2(bpy.types.Panel):
128 bl_label = "Script Runner"
129 bl_space_type = "VIEW_3D"
130 bl_region_type = "UI"
131
132
133 def draw(self, context):
134 user_settings = bpy.context.preferences.addons[__name__].preferences
135
136 layout = self.layout
137 row = layout.row(align=True)
138 if user_settings.display_num_scripts:
139 #Copy the slider from the addon user preferences.
140 draw_scripts_slider(self, context, row)
141
142 row.prop(user_settings, "items_per_row")
143 row = layout.row()
144 row.prop(user_settings, "buttons_only")
145
146 #Script 1
147 row = layout.row(align = True)
148
149 sce = context.scene
150 for i in range(0, user_settings.num_scripts):
151
152 if i%user_settings.items_per_row == 0:
153 row = layout.row(align=True)
154 if not user_settings.buttons_only:
155
156 slot_load = row.operator("object.script_load", text = "", icon='FILEBROWSER')
157 slot_load.script_slot = i
158
159 slot_clear = row.operator("object.script_clear", text = "", icon='X')
160 slot_clear.script_slot = i
161
162 if user_settings.fpath[i].script_path != "":
163 label = path_leaf(user_settings.fpath[i].script_path)
164 else:
165 label = ""
166
167 slot_run = row.operator("object.script_run", text = label)
168 slot_run.script_slot = i
169
170
171#----------------------------------Operators
172
173
174#-------Menu (main program operation)
175
176
177
178class ScriptRunner(bpy.types.Operator, ImportHelper):
179 bl_idname = "file.run_script";
180 bl_label = "Run Script";
181
182 filter_glob : StringProperty(
183 default="*.py",
184 options={'HIDDEN'},
185 )
186
187 filename_ext = ".py";
188
189
190 def execute(self, context):
191
192 if (self.filepath == ""):
193 pass
194
195 else:
196 #Check if file exists, could be a path.
197 if os.path.isfile(self.filepath):
198 ScriptRunner.checkDir(self.filepath)
199
200 return {'FINISHED'}
201
202
203
204 def checkDir(file_path):
205
206 try:
207 error = 0
208 script_path = os.path.join( bpy.utils.script_path_user() , "modules")
209 error = 1
210 cache_path = os.path.join( bpy.utils.script_path_user() , "modules" )
211 cache_path = os.path.join( cache_path, "__pycache__" )
212 error = 2
213 old_name = path_leaf(file_path).split(".")[0]
214 new_name = "script_runner_" + ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) + "_" + old_name
215 error = 3
216 new_file_path = os.path.join( script_path, new_name + ".py")
217 error = 4
218 val = os.makedirs(script_path, exist_ok=True)
219 error = 5
220
221 #No creation or mode error with 'makedirs'.
222 ScriptRunner.runIt(file_path, new_name, new_file_path, cache_path)
223
224
225 except OSError as exception:
226 #errno.EEXIST = 17
227 if exception.errno != errno.EEXIST:
228 msg = "ScriptRunner error in 'checkDir': "
229 print(msg, str(error) + ", sysError: " + str(exception.errno) )
230
231 else:
232 #Shouldn't get here, but could with directory exists with mode error?
233 ScriptRunner.runIt(file_path, new_name, new_file_path, cache_path)
234
235
236 return
237
238
239
240 def runIt(file_path, new_name, new_file_path, cache_path):
241
242 try:
243 error = 0
244
245 #Allow seamless 'addon' usage by replacing '__main__' with the modules temp name.
246 f = open(ScriptRunner.check_escape_string(file_path), encoding="utf8")
247 lines = f.readlines()
248 f.close()
249
250 found = False
251 for each in lines:
252 if each.find("__main__"):
253 found = True
254 break
255
256 if found:
257 f = open(ScriptRunner.check_escape_string(new_file_path),'w')
258 for each in lines:
259 line = each.replace("__main__", new_name)
260 f.writelines(line)
261 f.close()
262
263 else:
264 shutil.copy(file_path, new_file_path)
265
266
267 error = 1
268 bpy.utils.load_scripts(refresh_scripts = True)
269 error = 2
270 file_name = path_leaf(file_path)
271 error = 3
272 print("\n")
273 importlib.import_module(new_name)
274
275 print("\n" + "Script Runner - running file: " + '"' + file_name + '"' + "......Success!")
276
277 ScriptRunner.cleanUp(new_name, new_file_path, cache_path)
278
279 except:
280
281 if error == 3:
282 user_settings = bpy.context.preferences.addons[__name__].preferences
283
284 if user_settings.error_msg_verbose:
285 msg = "\n" + "Script Runner - script error in file: " + '"' + file_name + '"'
286 print(msg)
287
288 formatted_lines = traceback.format_exc().splitlines()
289 for each in formatted_lines:
290 #Ignore the errors created by 'Script Runner'.
291 if each.find("frozen importlib") == -1:
292 if each.find(new_file_path) != -1:
293 each = each.replace(new_file_path, file_path)
294
295 print(each)
296
297 else:
298 msg = "\n" + "Script Runner - script error in file: " + '"' + file_name + '" '
299 formatted_lines = traceback.format_exc().splitlines()
300
301 for i in range(len(formatted_lines) - 4, len(formatted_lines)):
302 if formatted_lines[i].find(new_file_path) != -1:
303 formatted_lines[i] = formatted_lines[i].replace(new_file_path, file_path)
304
305 pos = formatted_lines[i].rfind('line ')
306 if pos != -1:
307 line_num = formatted_lines[i][pos::]
308
309
310 print(msg + line_num)
311 print(formatted_lines[-3])
312 print(formatted_lines[-2])
313 print(formatted_lines[-1])
314
315 else:
316 msg = "Script Runner error in 'runIt': "
317 print(msg, error)
318
319
320 ScriptRunner.cleanUp(new_name, new_file_path, cache_path)
321
322 return
323
324
325 def check_escape_string(s):
326 backslash_map = { '\a': r'\a', '\b': r'\b', '\f': r'\f', '\n': r'\n', '\r': r'\r', '\t': r'\t', '\v': r'\v' }
327 for key, value in backslash_map.items():
328 s = s.replace(key, value)
329 return s
330
331
332 def cleanUp(new_name, new_file_path, cache_path):
333 try:
334 error = 4
335 #Clean up the files.
336 os.remove(new_file_path)
337 error = 5
338 #print(ScriptRunner.findFile(new_name + ".*", cache_path))
339 cache_file = ScriptRunner.findFile(new_name + ".*", cache_path)[0]
340 error = 6
341 os.remove(cache_file)
342
343 except:
344 #If import fails then there is no 'cache_file' to clean up so error 4 gets reported.
345 if error != 5:
346 msg = "ScriptRunner error in 'cleanUp': "
347 print(msg, error)
348
349
350 return
351
352
353
354 def findFile(pattern, path):
355 result = []
356 for root, dirs, files in os.walk(path):
357 for name in files:
358 if fnmatch.fnmatch(name, pattern):
359 result.append(os.path.join(root, name))
360 return result
361
362
363#----------Script buttons
364
365
366class ScriptLoad_OT_(bpy.types.Operator, ImportHelper):
367 bl_idname = "object.script_load"
368 bl_label = "Load Script"
369 bl_description = "Assign a script to this slot."
370
371 script_slot : IntProperty(options={'HIDDEN'})
372
373 filter_glob : StringProperty(
374 default="*.py",
375 options={'HIDDEN'},)
376
377 filename_ext = ".py";
378
379
380 def execute(self, context):
381
382 if (self.filepath == ""):
383 pass
384 else:
385 #Check if file exists, could be a path.
386 if os.path.isfile(self.filepath):
387 user_settings = bpy.context.preferences.addons[__name__].preferences
388
389 user_settings.fpath[self.script_slot].script_path = self.filepath
390
391 return {'FINISHED'}
392
393
394class ScriptClear_OT_(bpy.types.Operator):
395 bl_idname = "object.script_clear"
396 bl_label = ""
397 bl_description = "Clear the script from this slot."
398
399
400 script_slot : IntProperty()
401
402
403 def execute(self, context):
404 clearSlot(self.script_slot)
405
406 return {'FINISHED'}
407
408
409class SCRIPTRUN_OT_(bpy.types.Operator):
410 bl_idname = "object.script_run"
411 bl_label = "Script 1"
412 bl_description = "Run the script in this slot."
413
414 script_slot : IntProperty()
415
416
417 def execute(self, context):
418
419 user_settings = context.preferences.addons[__name__].preferences
420 file_path = user_settings.fpath[self.script_slot].script_path
421
422 if os.path.isfile(file_path):
423 ScriptRunner.checkDir(file_path)
424 else:
425 clearSlot(self.script_slot)
426
427 return {'FINISHED'}
428
429
430#------------------------Functions
431
432
433def clearSlot(script_slot):
434 user_settings = bpy.context.preferences.addons[__name__].preferences
435 user_settings.fpath[script_slot].script_path = ""
436
437 return
438
439
440#If the file ends with a slash, the basename will be empty.
441def path_leaf(path):
442 head, tail = ntpath.split(path)
443
444 return tail or ntpath.basename(head)
445
446
447#Stored paths in user prefs may no longer be valid.
448def checkFiles():
449 user_settings = bpy.context.preferences.addons[__name__].preferences
450 sce = bpy.context.scene
451
452 for i in range(0, user_settings.num_scripts):
453 user_settings.fpath.add()
454 if not os.path.isfile(user_settings.fpath[i].script_path): user_settings.fpath[i].script_path= ""
455
456 #print ("checkfiles")
457#----------------Register/Unregister
458
459
460def create_menu(self, context):
461 self.layout.operator(ScriptRunner.bl_idname,text="Run Script (.py)");
462
463
464@persistent
465def init_properties():
466
467 print ("Script Runner:init_properties")
468 checkFiles()
469 return None
470
471
472classes = (
473 cp, ScriptRunnerAddonPreferences,
474 ScriptRunner, SCRIPTRUNNERPANEL_PT_2,
475 ScriptLoad_OT_, ScriptClear_OT_,
476 SCRIPTRUN_OT_,
477)
478
479def register():
480
481 from bpy.utils import register_class
482 for cls in classes:
483 register_class(cls)
484
485 #user_settings = bpy.context.user_preferences.addons[__name__].preferences
486
487 #if user_settings.display_3d:
488 # register_class(ScriptRunnerPanel2)
489
490 bpy.types.TOPBAR_MT_file_export.append(create_menu)
491 #bpy.app.handlers.depsgraph_update_post.append(init_properties)
492 bpy.app.timers.register(init_properties, first_interval=0, persistent=True)
493 #checkFiles()
494
495
496def unregister():
497
498 from bpy.utils import unregister_class
499 for cls in reversed(classes):
500 unregister_class(cls)
501
502 bpy.types.TOPBAR_MT_file_export.remove(create_menu)
503
504
505if (__name__ == "__main__"):
506 register()
507 print ("register")