1 Star 0 Fork 0

sephirothalucard/myblendercontrib

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
script_watcher.py 16.73 KB
一键复制 编辑 原始数据 按行查看 历史
sambler 提交于 2019-07-16 19:15 . updates from external repos
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
"""
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, 80, 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 console_python # Blender module giving us access to the blender python console.
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()
# Reset the consoles list to remove all the consoles that don't exist anymore.
for screen in bpy.data.screens:
screen.sw_consoles.clear()
def add_scrollback(ctx, text, text_type):
for line in text:
bpy.ops.console.scrollback_append(ctx, text=line.replace('\t', ' '),
type=text_type)
def get_console_id(area):
"""Return the console id of the given region."""
if area.type == 'CONSOLE': # Only continue if we have a console area.
for region in area.regions:
if region.type == 'WINDOW':
return hash(region) # The id is the hash of the window region.
return False
def isnum(s):
return s[1:].isnumeric() and s[0] in '-+1234567890'
def make_annotations(cls):
"""Converts class fields to annotations if running with Blender 2.8"""
if bpy.app.version < (2, 80):
return cls
bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)}
if bl_props:
if '__annotations__' not in cls.__dict__:
setattr(cls, '__annotations__', {})
annotations = cls.__dict__['__annotations__']
for k, v in bl_props.items():
annotations[k] = v
delattr(cls, k)
return cls
class SplitIO(io.StringIO):
"""Feed the input stream into another stream."""
PREFIX = '[Script Watcher]: '
_can_prefix = True
def __init__(self, stream):
io.StringIO.__init__(self)
self.stream = stream
def write(self, s):
# Make sure we prefix our string before we do anything else with it.
if self._can_prefix:
s = self.PREFIX + s
# only add the prefix if the last stream ended with a newline.
self._can_prefix = s.endswith('\n')
# Make sure to call the super classes write method.
io.StringIO.write(self, s)
# When we are written to, we also write to the secondary stream.
self.stream.write(s)
class ScriptWatcherLoader:
"""Load the script"""
filepath = None
mod_name = None
run_main = None
def __init__(self, filepath, run_main=False):
self.filepath = filepath
self.mod_name = self.get_mod_name()
self.run_main = run_main
def load_module(self):
"""Load the module"""
try:
f = open(self.filepath)
paths, files = self.get_paths()
# Create the module and setup the basic properties.
mod = types.ModuleType(self.mod_name if self.run_main else '__main__')
mod.__file__ = self.filepath
mod.__path__ = paths
mod.__package__ = self.mod_name
mod.__loader__ = self
# Add the module to the system module cache.
sys.modules[self.mod_name] = mod
# Finally, execute the module.
exec(compile(f.read(), self.filepath, 'exec'), mod.__dict__)
if self.run_main and 'main' in mod.__dict__:
mod.main()
except IOError:
print('Could not open script file.')
except:
sys.stderr.write("There was an error when loading the module:\n" + traceback.format_exc())
else:
f.close()
def reload(self):
"""Reload the module clearing any cached sub-modules"""
print('Reloading script:', self.filepath)
self.remove_cached_mods()
self.load_module()
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."""
dir, mod = os.path.split(self.filepath)
# Module is a package.
if mod == '__init__.py':
mod = os.path.basename(dir)
# Module is a single file.
else:
mod = os.path.splitext(mod)[0]
return mod
def remove_cached_mods(self):
"""Remove all the script modules from the system cache."""
paths = self.get_paths()
for mod_name, mod in list(sys.modules.items()):
if hasattr(mod, '__file__') and mod.__file__ and os.path.dirname(mod.__file__) in paths:
del sys.modules[mod_name]
# Addon preferences.
@make_annotations
class ScriptWatcherPreferences(bpy.types.AddonPreferences):
bl_idname = __name__
editor_path = bpy.props.StringProperty(
name='Editor Path',
description='Path to external editor.',
subtype='FILE_PATH'
)
def draw(self, context):
layout = self.layout
layout.prop(self, 'editor_path')
# 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
use_py_console = None
loader = None
def reload_script(self, context):
"""Reload this script while printing the output to blenders python console."""
# Setup stdout and stderr.
stdout = SplitIO(sys.stdout)
stderr = SplitIO(sys.stderr)
sys.stdout = stdout
sys.stderr = stderr
# Run the script.
self.loader.reload()
# Go back to the begining so we can read the streams.
stdout.seek(0)
stderr.seek(0)
# Don't use readlines because that leaves trailing new lines.
output = stdout.read().split('\n')
output_err = stderr.read().split('\n')
for console in context.screen.sw_consoles:
if console.active and isnum(console.name): # Make sure it's not some random string.
console, _, _ = console_python.get_console(int(console.name))
# Set the locals to the modules dict.
console.locals = sys.modules[self.loader.mod_name].__dict__
if self.use_py_console:
# Print the output to the consoles.
for area in context.screen.areas:
if area.type == "CONSOLE":
ctx = context.copy()
ctx.update({"area": area})
# Actually print the output.
if output:
add_scrollback(ctx, output, 'OUTPUT')
if output_err:
add_scrollback(ctx, output_err, 'ERROR')
# Cleanup
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
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.use_py_console = context.scene.sw_settings.use_py_console
filepath = bpy.path.abspath(context.scene.sw_settings.filepath)
# If it's not a file, doesn't exist or permission is denied we don't proceed.
if not os.path.isfile(filepath):
self.report({'ERROR'}, 'Unable to open script.')
return {'CANCELLED'}
self.loader = ScriptWatcherLoader(filepath, context.scene.sw_settings.run_main)
# Setup the times dict to keep track of when all the files where last edited.
dirs, files = self.loader.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, window=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)
# Should we call a separate close function on the loader?
self.loader.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'}
class OpenExternalEditor(bpy.types.Operator):
"""Edit script in an external text editor."""
bl_idname = "wm.sw_edit_externally"
bl_label = "Edit Externally"
def execute(self, context):
addon_prefs = context.user_preferences.addons[__name__].preferences
filepath = bpy.path.abspath(context.scene.sw_settings.filepath)
subprocess.Popen((addon_prefs.editor_path, filepath))
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, 'use_py_console')
col.prop(context.scene.sw_settings, 'auto_watch_on_startup')
col.prop(context.scene.sw_settings, 'run_main')
if bpy.app.version < (2, 80, 0):
col.operator('wm.sw_watch_start', icon='VISIBLE_IPO_ON')
else:
col.operator('wm.sw_watch_start', icon='HIDE_OFF')
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')
layout.separator()
layout.operator('wm.sw_edit_externally', icon='TEXT')
@make_annotations
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'
)
use_py_console = bpy.props.BoolProperty(
name='Use py console',
description='Use blenders built-in python console for program output (e.g. print statements and error messages)',
default=False
)
auto_watch_on_startup = bpy.props.BoolProperty(
name='Watch on startup',
description='Watch script automatically on new .blend load',
default=False
)
run_main = bpy.props.BoolProperty(
name='Run Main',
description='Instead of running the module with the name __main__ execute the module and call main()',
default=False,
)
def update_debug(self, context):
console_id = get_console_id(context.area)
console, _, _ = console_python.get_console(console_id)
if self.active:
console.globals = console.locals
if context.scene.sw_settings.running:
dir, mod = os.path.split(bpy.path.abspath(context.scene.sw_settings.filepath))
# XXX This is almost the same as get_mod_name so it should become a global function.
if mod == '__init__.py':
mod = os.path.basename(dir)
else:
mod = os.path.splitext(mod)[0]
console.locals = sys.modules[mod].__dict__
else:
console.locals = console.globals
# ctx = context.copy() # Operators only take dicts.
# bpy.ops.console.update_console(ctx, debug_mode=self.active, script='test-script.py')
@make_annotations
class SWConsoleSettings(bpy.types.PropertyGroup):
active = bpy.props.BoolProperty(
name="Debug Mode",
update=update_debug,
description="Enter Script Watcher debugging mode (when in debug mode you can access the script variables).",
default=False
)
class SWConsoleHeader(bpy.types.Header):
bl_idname = "CONSOLE_HT_script_watcher"
bl_space_type = 'CONSOLE'
def draw(self, context):
layout = self.layout
cs = context.screen.sw_consoles
console_id = str(get_console_id(context.area))
# Make sure this console is in the consoles collection.
if console_id not in cs:
console = cs.add()
console.name = console_id
row = layout.row()
row.scale_x = 1.8
row.prop(cs[console_id], 'active', toggle=True)
classes = (
ScriptWatcherPreferences,
WatchScriptOperator,
CancelScriptWatcher,
ReloadScriptWatcher,
OpenExternalEditor,
ScriptWatcherPanel,
ScriptWatcherSettings,
SWConsoleSettings,
SWConsoleHeader,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.sw_settings = \
bpy.props.PointerProperty(type=ScriptWatcherSettings)
bpy.app.handlers.load_post.append(load_handler)
bpy.types.Screen.sw_consoles = bpy.props.CollectionProperty(
type=SWConsoleSettings
)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
bpy.app.handlers.load_post.remove(load_handler)
del bpy.types.Scene.sw_settings
del bpy.types.Screen.sw_consoles
if __name__ == "__main__":
register()
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/sephirothalucard/myblendercontrib.git
[email protected]:sephirothalucard/myblendercontrib.git
sephirothalucard
myblendercontrib
myblendercontrib
master

搜索帮助