"""
script_watcher.py: Reload watched script upon changes.
Copyright (C) 2015 Isaac Weaver
Author: Isaac Weaver <[email protected]>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
bl_info = {
"name": "Script Watcher",
"author": "Isaac Weaver",
"version": (0, 7),
"blender": (2, 75, 0),
"location": "Properties > Scene > Script Watcher",
"description": "Reloads an external script on edits.",
"warning": "Still in beta stage.",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Development/Script_Watcher",
"tracker_url": "https://github.com/wisaac407/blender-script-watcher/issues/new",
"category": "Development",
}
import os
import sys
import io
import traceback
import types
import subprocess
import bpy
from bpy.app.handlers import persistent
@persistent
def load_handler(dummy):
running = bpy.context.scene.sw_settings.running
# First of all, make sure script watcher is off on all the scenes.
for scene in bpy.data.scenes:
bpy.ops.wm.sw_watch_end({'scene': scene})
# Startup script watcher on the current scene if needed.
if running and bpy.context.scene.sw_settings.auto_watch_on_startup:
bpy.ops.wm.sw_watch_start()
def isnum(s):
return s[1:].isnumeric() and s[0] in '-+1234567890'
# Define the script watching operator.
class WatchScriptOperator(bpy.types.Operator):
"""Watches the script for changes, reloads the script if any changes occur."""
bl_idname = "wm.sw_watch_start"
bl_label = "Watch Script"
_timer = None
_running = False
_times = None
filepath = None
def get_paths(self):
"""Find all the python paths surrounding the given filepath."""
dirname = os.path.dirname(self.filepath)
paths = []
filepaths = []
for root, dirs, files in os.walk(dirname, topdown=True):
if '__init__.py' in files:
paths.append(root)
for f in files:
filepaths.append(os.path.join(root, f))
else:
dirs[:] = [] # No __init__ so we stop walking this dir.
# If we just have one (non __init__) file then return just that file.
return paths, filepaths or [self.filepath]
def get_mod_name(self):
"""Return the module name and the root path of the givin python file path."""
dir, mod = os.path.split(self.filepath)
# Module is a package.
if mod == '__init__.py':
mod = os.path.basename(dir)
dir = os.path.dirname(dir)
# Module is a single file.
else:
mod = os.path.splitext(mod)[0]
return mod, dir
def remove_cached_mods(self):
"""Remove all the script modules from the system cache."""
paths, files = self.get_paths()
for mod_name, mod in list(sys.modules.items()):
if hasattr(mod, '__file__') and os.path.dirname(mod.__file__) in paths:
del sys.modules[mod_name]
def _reload_script_module(self):
print('Reloading script:', self.filepath)
self.remove_cached_mods()
try:
f = open(self.filepath)
paths, files = self.get_paths()
# Get the module name and the root module path.
mod_name, mod_root = self.get_mod_name()
# Create the module and setup the basic properties.
mod = types.ModuleType('__main__')
mod.__file__ = self.filepath
mod.__path__ = paths
mod.__package__ = mod_name
# Add the module to the system module cache.
sys.modules[mod_name] = mod
# Fianally, execute the module.
exec (compile(f.read(), self.filepath, 'exec'), mod.__dict__)
except IOError:
print('Could not open script file.')
except:
sys.stderr.write("There was an error when running the script:\n" + traceback.format_exc())
else:
f.close()
def reload_script(self, context):
"""Reload this script while printing the output to blenders python console."""
# Run the script.
self._reload_script_module()
def modal(self, context, event):
if not context.scene.sw_settings.running:
self.cancel(context)
return {'CANCELLED'}
if context.scene.sw_settings.reload:
context.scene.sw_settings.reload = False
self.reload_script(context)
return {'PASS_THROUGH'}
if event.type == 'TIMER':
for path in self._times:
cur_time = os.stat(path).st_mtime
if cur_time != self._times[path]:
self._times[path] = cur_time
self.reload_script(context)
return {'PASS_THROUGH'}
def execute(self, context):
if context.scene.sw_settings.running:
return {'CANCELLED'}
# Grab the settings and store them as local variables.
self.filepath = bpy.path.abspath(context.scene.sw_settings.filepath)
# If it's not a file, doesn't exist or permistion is denied we don't precede.
if not os.path.isfile(self.filepath):
self.report({'ERROR'}, 'Unable to open script.')
return {'CANCELLED'}
# Setup the times dict to keep track of when all the files where last edited.
dirs, files = self.get_paths()
self._times = dict(
(path, os.stat(path).st_mtime) for path in files) # Where we store the times of all the paths.
self._times[files[0]] = 0 # We set one of the times to 0 so the script will be loaded on startup.
# Setup the event timer.
wm = context.window_manager
self._timer = wm.event_timer_add(0.1, context.window)
wm.modal_handler_add(self)
context.scene.sw_settings.running = True
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self.remove_cached_mods()
context.scene.sw_settings.running = False
class CancelScriptWatcher(bpy.types.Operator):
"""Stop watching the current script."""
bl_idname = "wm.sw_watch_end"
bl_label = "Stop Watching"
def execute(self, context):
# Setting the running flag to false will cause the modal to cancel itself.
context.scene.sw_settings.running = False
return {'FINISHED'}
class ReloadScriptWatcher(bpy.types.Operator):
"""Reload the current script."""
bl_idname = "wm.sw_reload"
bl_label = "Reload Script"
def execute(self, context):
# Setting the reload flag to true will cause the modal to cancel itself.
context.scene.sw_settings.reload = True
return {'FINISHED'}
# Create the UI for the operator. NEEDS FINISHING!!
class ScriptWatcherPanel(bpy.types.Panel):
"""UI for the script watcher."""
bl_label = "Script Watcher"
bl_idname = "SCENE_PT_script_watcher"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
def draw(self, context):
layout = self.layout
running = context.scene.sw_settings.running
col = layout.column()
col.prop(context.scene.sw_settings, 'filepath')
col.prop(context.scene.sw_settings, 'auto_watch_on_startup')
col.operator('wm.sw_watch_start', icon='VISIBLE_IPO_ON')
col.enabled = not running
if running:
row = layout.row(align=True)
row.operator('wm.sw_watch_end', icon='CANCEL')
row.operator('wm.sw_reload', icon='FILE_REFRESH')
class ScriptWatcherSettings(bpy.types.PropertyGroup):
"""All the script watcher settings."""
running = bpy.props.BoolProperty(default=False)
reload = bpy.props.BoolProperty(default=False)
filepath = bpy.props.StringProperty(
name='Script',
description='Script file to watch for changes.',
subtype='FILE_PATH'
)
auto_watch_on_startup = bpy.props.BoolProperty(
name='Watch on startup',
description='Watch script automatically on new .blend load',
default=False
)
def register():
bpy.utils.register_module(__name__)
bpy.types.Scene.sw_settings = \
bpy.props.PointerProperty(type=ScriptWatcherSettings)
bpy.app.handlers.load_post.append(load_handler)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.app.handlers.load_post.remove(load_handler)
del bpy.types.Scene.sw_settings
if __name__ == "__main__":
register()