blender/release/scripts/modules/addon_utils.py
Campbell Barton ef665b3d18 dissallow access to the context while addons import and register.
Since the window manager is needed for keymaps this is kept as an exception.

some addons will need updating, but in every case I've seen addons should not be accessing the context while registering.
(bad stuff! - declaring the scene as a global variable - which crashes when the users loads a new file, manipulating the active object or scene... tsk tsk)
2012-12-19 07:27:23 +00:00

422 lines
13 KiB
Python

# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
__all__ = (
"paths",
"modules",
"check",
"enable",
"disable",
"reset_all",
"module_bl_info",
)
import bpy as _bpy
_user_preferences = _bpy.context.user_preferences
class _RestrictedContext():
__slots__ = ()
@property
def window_manager(self):
return _bpy.data.window_managers[0]
_ctx_restricted = _RestrictedContext()
error_duplicates = False
error_encoding = False
addons_fake_modules = {}
def paths():
# RELEASE SCRIPTS: official scripts distributed in Blender releases
addon_paths = _bpy.utils.script_paths("addons")
# CONTRIB SCRIPTS: good for testing but not official scripts yet
# if folder addons_contrib/ exists, scripts in there will be loaded too
addon_paths += _bpy.utils.script_paths("addons_contrib")
# EXTERN SCRIPTS: external projects scripts
# if folder addons_extern/ exists, scripts in there will be loaded too
addon_paths += _bpy.utils.script_paths("addons_extern")
return addon_paths
def modules(module_cache):
global error_duplicates
global error_encoding
import os
error_duplicates = False
error_encoding = False
path_list = paths()
# fake module importing
def fake_module(mod_name, mod_path, speedy=True, force_support=None):
global error_encoding
if _bpy.app.debug_python:
print("fake_module", mod_path, mod_name)
import ast
ModuleType = type(ast)
file_mod = open(mod_path, "r", encoding='UTF-8')
if speedy:
lines = []
line_iter = iter(file_mod)
l = ""
while not l.startswith("bl_info"):
try:
l = line_iter.readline()
except UnicodeDecodeError as e:
if not error_encoding:
error_encoding = True
print("Error reading file as UTF-8:", mod_path, e)
file_mod.close()
return None
if len(l) == 0:
break
while l.rstrip():
lines.append(l)
try:
l = line_iter.readline()
except UnicodeDecodeError as e:
if not error_encoding:
error_encoding = True
print("Error reading file as UTF-8:", mod_path, e)
file_mod.close()
return None
data = "".join(lines)
else:
data = file_mod.read()
file_mod.close()
try:
ast_data = ast.parse(data, filename=mod_path)
except:
print("Syntax error 'ast.parse' can't read %r" % mod_path)
import traceback
traceback.print_exc()
ast_data = None
body_info = None
if ast_data:
for body in ast_data.body:
if body.__class__ == ast.Assign:
if len(body.targets) == 1:
if getattr(body.targets[0], "id", "") == "bl_info":
body_info = body
break
if body_info:
try:
mod = ModuleType(mod_name)
mod.bl_info = ast.literal_eval(body.value)
mod.__file__ = mod_path
mod.__time__ = os.path.getmtime(mod_path)
except:
print("AST error in module %s" % mod_name)
import traceback
traceback.print_exc()
raise
if force_support is not None:
mod.bl_info["support"] = force_support
return mod
else:
print("fake_module: addon missing 'bl_info' "
"gives bad performance!: %r" % mod_path)
return None
modules_stale = set(module_cache.keys())
for path in path_list:
# force all contrib addons to be 'TESTING'
if path.endswith(("addons_contrib", "addons_extern")):
force_support = 'TESTING'
else:
force_support = None
for mod_name, mod_path in _bpy.path.module_names(path):
modules_stale -= {mod_name}
mod = module_cache.get(mod_name)
if mod:
if mod.__file__ != mod_path:
print("multiple addons with the same name:\n %r\n %r" %
(mod.__file__, mod_path))
error_duplicates = True
elif mod.__time__ != os.path.getmtime(mod_path):
print("reloading addon:",
mod_name,
mod.__time__,
os.path.getmtime(mod_path),
mod_path,
)
del module_cache[mod_name]
mod = None
if mod is None:
mod = fake_module(mod_name,
mod_path,
force_support=force_support)
if mod:
module_cache[mod_name] = mod
# just in case we get stale modules, not likely
for mod_stale in modules_stale:
del module_cache[mod_stale]
del modules_stale
mod_list = list(module_cache.values())
mod_list.sort(key=lambda mod: (mod.bl_info["category"],
mod.bl_info["name"],
))
return mod_list
def check(module_name):
"""
Returns the loaded state of the addon.
:arg module_name: The name of the addon and module.
:type module_name: string
:return: (loaded_default, loaded_state)
:rtype: tuple of booleans
"""
import sys
loaded_default = module_name in _user_preferences.addons
mod = sys.modules.get(module_name)
loaded_state = mod and getattr(mod, "__addon_enabled__", Ellipsis)
if loaded_state is Ellipsis:
print("Warning: addon-module %r found module "
"but without __addon_enabled__ field, "
"possible name collision from file: %r" %
(module_name, getattr(mod, "__file__", "<unknown>")))
loaded_state = False
if mod and getattr(mod, "__addon_persistent__", False):
loaded_default = True
return loaded_default, loaded_state
def enable(module_name, default_set=True, persistent=False):
"""
Enables an addon by name.
:arg module_name: The name of the addon and module.
:type module_name: string
:return: the loaded module or None on failure.
:rtype: module
"""
import os
import sys
def handle_error():
import traceback
traceback.print_exc()
# reload if the mtime changes
mod = sys.modules.get(module_name)
# chances of the file _not_ existing are low, but it could be removed
if mod and os.path.exists(mod.__file__):
mod.__addon_enabled__ = False
mtime_orig = getattr(mod, "__time__", 0)
mtime_new = os.path.getmtime(mod.__file__)
if mtime_orig != mtime_new:
import imp
print("module changed on disk:", mod.__file__, "reloading...")
try:
imp.reload(mod)
except:
handle_error()
del sys.modules[module_name]
return None
mod.__addon_enabled__ = False
# Split registering up into 3 steps so we can undo
# if it fails par way through.
# first disable the context, using the context at all is
# really bad while loading an addon, don't do it!
ctx = _bpy.context
_bpy.context = _ctx_restricted
# 1) try import
try:
mod = __import__(module_name)
mod.__time__ = os.path.getmtime(mod.__file__)
mod.__addon_enabled__ = False
except:
handle_error()
_bpy.context = ctx
return None
# 2) try register collected modules
# removed, addons need to handle own registration now.
# 3) try run the modules register function
try:
mod.register()
except:
print("Exception in module register(): %r" %
getattr(mod, "__file__", module_name))
handle_error()
del sys.modules[module_name]
_bpy.context = ctx
return None
# finally restore the context
_bpy.context = ctx
# * OK loaded successfully! *
if default_set:
# just in case its enabled already
ext = _user_preferences.addons.get(module_name)
if not ext:
ext = _user_preferences.addons.new()
ext.module = module_name
mod.__addon_enabled__ = True
mod.__addon_persistent__ = persistent
if _bpy.app.debug_python:
print("\taddon_utils.enable", mod.__name__)
return mod
def disable(module_name, default_set=True):
"""
Disables an addon by name.
:arg module_name: The name of the addon and module.
:type module_name: string
"""
import sys
mod = sys.modules.get(module_name)
# possible this addon is from a previous session and didn't load a
# module this time. So even if the module is not found, still disable
# the addon in the user prefs.
if mod and getattr(mod, "__addon_enabled__", False) is not False:
mod.__addon_enabled__ = False
mod.__addon_persistent = False
try:
mod.unregister()
except:
print("Exception in module unregister(): %r" %
getattr(mod, "__file__", module_name))
import traceback
traceback.print_exc()
else:
print("addon_utils.disable: %s not %s." %
(module_name, "disabled" if mod is None else "loaded"))
# could be in more then once, unlikely but better do this just in case.
addons = _user_preferences.addons
if default_set:
while module_name in addons:
addon = addons.get(module_name)
if addon:
addons.remove(addon)
if _bpy.app.debug_python:
print("\taddon_utils.disable", module_name)
def reset_all(reload_scripts=False):
"""
Sets the addon state based on the user preferences.
"""
import sys
import imp
# RELEASE SCRIPTS: official scripts distributed in Blender releases
paths_list = paths()
for path in paths_list:
_bpy.utils._sys_path_ensure(path)
for mod_name, mod_path in _bpy.path.module_names(path):
is_enabled, is_loaded = check(mod_name)
# first check if reload is needed before changing state.
if reload_scripts:
mod = sys.modules.get(mod_name)
if mod:
imp.reload(mod)
if is_enabled == is_loaded:
pass
elif is_enabled:
enable(mod_name)
elif is_loaded:
print("\taddon_utils.reset_all unloading", mod_name)
disable(mod_name)
def module_bl_info(mod, info_basis={"name": "",
"author": "",
"version": (),
"blender": (),
"location": "",
"description": "",
"wiki_url": "",
"tracker_url": "",
"support": 'COMMUNITY',
"category": "",
"warning": "",
"show_expanded": False,
}
):
addon_info = getattr(mod, "bl_info", {})
# avoid re-initializing
if "_init" in addon_info:
return addon_info
if not addon_info:
mod.bl_info = addon_info
for key, value in info_basis.items():
addon_info.setdefault(key, value)
if not addon_info["name"]:
addon_info["name"] = mod.__name__
addon_info["_init"] = None
return addon_info