Hi, After talking with Campbell at length this morning (my morning), here's what was agreed upon.
- Keep using a metaclass to build a list of extension type per module - The automatic registration system should support both runtime and define time registration. [1] - Turning on or off automatic registration should require *at most* a single function call at the end of a module. [2] The only last decision to take, as far as I understood (and I hope there wasn't a misunderstanding) is whether or not the automatic method should be opt in or opt out. I've made some changes to the automatic system today (patch attached) that would make it easy to support opt in or opt out painlessly. It also demonstrates how the metaclass can be used to gather more information about the types (file and line where they are defined). Regards, Martin Don't read the following unless you want boring details on stuff most people don't care about (and shouldn't) =============================================================== [0] Currently, IDPropGroups are treated separately than other types. That was a work around for previous coding practices and needs to be removed. [1] Define time is the usual case: built in modules and addons. Runtime is both for definitions during a call to a module and for text window execution (although the later one could probably work like built-in modules). [2] Something like bpy.utils.manual_registration() to turn off or bpy.utils.register(__name__) to register all types in a module. This removes the need of doing shenanigans like calling the module's register method if __name__ == "main" to support text window execution and whatnot, which, IMHO is a good thing. [3] I also suggested we support optional register and unregister class methods in RNA types. This would help remove code entropy and make the definitions more self contained (for example, a class defining OBJ import could also contain the code to add and remove itself from menus instead of having it in the module's function). The jury is still out on whether that's a good idea or not. [4] We both agreed that IDPropGroups properties should be defined using the same syntax as operator properties (if possible) instead of having to add them post registration. Campbell said he'd try his hand at that later this week (IIRC).
Index: release/scripts/modules/bpy_types.py =================================================================== --- release/scripts/modules/bpy_types.py (revision 34509) +++ release/scripts/modules/bpy_types.py (working copy) @@ -553,82 +553,104 @@ import collections TypeMap = {} -# Properties (IDPropertyGroup) are different from types because they need to be registered -# before adding sub properties to them, so they are registered on definition -# and unregistered on unload -PropertiesMap = {} # Using our own loading function we set this to false # so when running a script directly in the text editor -# registers moduals instantly. +# registers modules instantly. _register_immediate = True +class DelayedRegistration: + def __enter__(self): + global _register_immediate + _register_immediate = False -def _unregister_module(module, free=True): - for t in TypeMap.get(module, ()): + def __exit__(self, *args): + global _register_immediate + _register_immediate = True + +class TypeEntry: + def __init__(self, cls, path, line): + self.cls = cls + self.path = path + self.line = line + self.registered = False + + def register(self): + if self.registered: + return + + import bpy + t = self.cls try: - bpy_types.unregister(t) + bpy_types.register(t) + if hasattr(t, "register"): + t.register() + self.registered = True + + if bpy.app.debug: + print("Registered class '%s.%s'" % (t.__module__, t.__name__)) + print("\t", self.path, "line", self.line) except: import traceback - print("bpy.utils._unregister_module(): Module '%s' failed to unregister class '%s.%s'" % (module, t.__module__, t.__name__)) - traceback.print_exc() + print("bpy.utils._register_module(): failed to register class '%s.%s'" % (t.__module__, t.__name__)) + print("\t", self.path, "line", self.line) + traceback.print_last() + + def unregister(self): + if not self.registered: + return - if free == True and module in TypeMap: - del TypeMap[module] - - for t in PropertiesMap.get(module, ()): + t = self.cls try: + if hasattr(t, "unregister"): + t.unregister() bpy_types.unregister(t) + self.registered = False except: import traceback - print("bpy.utils._unload_module(): Module '%s' failed to unregister class '%s.%s'" % (module, t.__module__, t.__name__)) - traceback.print_exc() + print("bpy.utils._unregister_module(): failed to unregister class '%s.%s'" % (t.__module__, t.__name__)) + print("\t", self.path, "line", self.line) + print("\t", self.text) + traceback.print_last() - if free == True and module in PropertiesMap: - del PropertiesMap[module] +def _unregister_module(module, free=True): + for entry in reversed(TypeMap.get(module, ())): + entry.unregister() + if free == True and module in TypeMap: + del TypeMap[module] def _register_module(module): - for t in TypeMap.get(module, ()): - try: - bpy_types.register(t) - except: - import traceback - import sys - print("bpy.utils._register_module(): '%s' failed to register class '%s.%s'" % (sys.modules[module].__file__, t.__module__, t.__name__)) - traceback.print_exc() + for entry in TypeMap.get(module, ()): + entry.register() class RNAMeta(type): - @classmethod - def _register_immediate(cls): - return _register_immediate - def __new__(cls, name, bases, classdict, **args): + global TypeMap + import traceback result = type.__new__(cls, name, bases, classdict) if bases and bases[0] != StructRNA: module = result.__module__ - ClassMap = TypeMap + sf = traceback.extract_stack(limit=2)[0] + entry = TypeEntry(result, sf[0], sf[1]) # Register right away if needed - if cls._register_immediate(): - bpy_types.register(result) - ClassMap = PropertiesMap + if _register_immediate: + entry.register() # first part of packages only if "." in module: module = module[:module.index(".")] + + TypeMap.setdefault(module, []).append(entry) - ClassMap.setdefault(module, []).append(result) - return result -class RNAMetaRegister(RNAMeta, StructMetaIDProp): - @classmethod - def _register_immediate(cls): - return True +class RNAMetaIDProp(RNAMeta, StructMetaIDProp): + pass class OrderedMeta(RNAMeta): @@ -685,10 +707,9 @@ return ops.macro_define(self, opname) -class IDPropertyGroup(StructRNA, metaclass=RNAMetaRegister): +class IDPropertyGroup(StructRNA, metaclass=RNAMetaIDProp): __slots__ = () - class RenderEngine(StructRNA, metaclass=RNAMeta): __slots__ = () Index: release/scripts/modules/bpy/utils.py =================================================================== --- release/scripts/modules/bpy/utils.py (revision 34509) +++ release/scripts/modules/bpy/utils.py (working copy) @@ -100,107 +100,103 @@ import traceback import time - # must be set back to True on exits - _bpy_types._register_immediate = False - - t_main = time.time() - - loaded_modules = set() - - if refresh_scripts: - original_modules = _sys.modules.values() - - if reload_scripts: - _bpy_types.TypeMap.clear() - _bpy_types.PropertiesMap.clear() - - # just unload, dont change user defaults, this means we can sync to reload. - # note that they will only actually reload of the modification time changes. - # this `wont` work for packages so... its not perfect. - for module_name in [ext.module for ext in _bpy.context.user_preferences.addons]: - addon_disable(module_name, default_set=False) - - def register_module_call(mod): - _bpy_types._register_module(mod.__name__) - register = getattr(mod, "register", None) - if register: + with _bpy_types.DelayedRegistration(): + t_main = time.time() + + loaded_modules = set() + + if refresh_scripts: + original_modules = _sys.modules.values() + + if reload_scripts: + _bpy_types.TypeMap.clear() + _bpy_types.PropertiesMap.clear() + + # just unload, dont change user defaults, this means we can sync to reload. + # note that they will only actually reload of the modification time changes. + # this `wont` work for packages so... its not perfect. + for module_name in [ext.module for ext in _bpy.context.user_preferences.addons]: + addon_disable(module_name, default_set=False) + + def register_module_call(mod): + _bpy_types._register_module(mod.__name__) + register = getattr(mod, "register", None) + if register: + try: + register() + except: + traceback.print_exc() + else: + print("\nWarning! '%s' has no register function, this is now a requirement for registerable scripts." % mod.__file__) + + def unregister_module_call(mod): + _bpy_types._unregister_module(mod.__name__) + unregister = getattr(mod, "unregister", None) + if unregister: + try: + unregister() + except: + traceback.print_exc() + + def test_reload(mod): + import imp + # reloading this causes internal errors + # because the classes from this module are stored internally + # possibly to refresh internal references too but for now, best not to. + if mod == _bpy_types: + return mod + try: - register() + return imp.reload(mod) except: traceback.print_exc() - else: - print("\nWarning! '%s' has no register function, this is now a requirement for registerable scripts." % mod.__file__) + + def test_register(mod): + + if refresh_scripts and mod in original_modules: + return + + if reload_scripts and mod: + print("Reloading:", mod) + mod = test_reload(mod) + + if mod: + register_module_call(mod) + _global_loaded_modules.append(mod.__name__) + + if reload_scripts: + + # module names -> modules + _global_loaded_modules[:] = [_sys.modules[mod_name] for mod_name in _global_loaded_modules] + + # loop over and unload all scripts + _global_loaded_modules.reverse() + for mod in _global_loaded_modules: + unregister_module_call(mod) + + for mod in _global_loaded_modules: + test_reload(mod) + + _global_loaded_modules[:] = [] + + user_path = user_script_path() + + for base_path in script_paths(): + for path_subdir in ("", "ui", "op", "io", "keyingsets", "modules"): + path = _os.path.join(base_path, path_subdir) + if _os.path.isdir(path): + _sys_path_ensure(path) + + # only add this to sys.modules, dont run + if path_subdir == "modules": + continue + + if user_path != base_path and path_subdir == "": + continue # avoid loading 2.4x scripts + + for mod in modules_from_path(path, loaded_modules): + test_register(mod) - def unregister_module_call(mod): - _bpy_types._unregister_module(mod.__name__) - unregister = getattr(mod, "unregister", None) - if unregister: - try: - unregister() - except: - traceback.print_exc() - - def test_reload(mod): - import imp - # reloading this causes internal errors - # because the classes from this module are stored internally - # possibly to refresh internal references too but for now, best not to. - if mod == _bpy_types: - return mod - - try: - return imp.reload(mod) - except: - traceback.print_exc() - - def test_register(mod): - - if refresh_scripts and mod in original_modules: - return - - if reload_scripts and mod: - print("Reloading:", mod) - mod = test_reload(mod) - - if mod: - register_module_call(mod) - _global_loaded_modules.append(mod.__name__) - - if reload_scripts: - - # module names -> modules - _global_loaded_modules[:] = [_sys.modules[mod_name] for mod_name in _global_loaded_modules] - - # loop over and unload all scripts - _global_loaded_modules.reverse() - for mod in _global_loaded_modules: - unregister_module_call(mod) - - for mod in _global_loaded_modules: - test_reload(mod) - - _global_loaded_modules[:] = [] - - user_path = user_script_path() - - for base_path in script_paths(): - for path_subdir in ("", "ui", "op", "io", "keyingsets", "modules"): - path = _os.path.join(base_path, path_subdir) - if _os.path.isdir(path): - _sys_path_ensure(path) - - # only add this to sys.modules, dont run - if path_subdir == "modules": - continue - - if user_path != base_path and path_subdir == "": - continue # avoid loading 2.4x scripts - - for mod in modules_from_path(path, loaded_modules): - test_register(mod) - - _bpy_types._register_immediate = True - # deal with addons seperately addon_reset_all(reload_scripts) @@ -366,76 +362,73 @@ import sys import bpy_types as _bpy_types import imp - - _bpy_types._register_immediate = False - - def handle_error(): - import traceback - traceback.print_exc() - _bpy_types._register_immediate = True - - # reload if the mtime changes - mod = sys.modules.get(module_name) - if mod: - mod.__addon_enabled__ = False - mtime_orig = getattr(mod, "__time__", 0) - mtime_new = os.path.getmtime(mod.__file__) - if mtime_orig != mtime_new: - print("module changed on disk:", mod.__file__, "reloading...") - - try: - imp.reload(mod) - except: - handle_error() - del sys.modules[module_name] - return None + + with _bpy_types.DelayedRegistration(): + + def handle_error(): + import traceback + traceback.print_exc() + + # reload if the mtime changes + mod = sys.modules.get(module_name) + if mod: mod.__addon_enabled__ = False + mtime_orig = getattr(mod, "__time__", 0) + mtime_new = os.path.getmtime(mod.__file__) + if mtime_orig != mtime_new: + 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 + # 1) try import + try: + mod = __import__(module_name) + mod.__time__ = os.path.getmtime(mod.__file__) + mod.__addon_enabled__ = False + except: + handle_error() + return None + + # 2) try register collected modules + try: + _bpy_types._register_module(module_name) + except: + handle_error() + del sys.modules[module_name] + return None + + # 3) try run the modules register function + try: + mod.register() + except: + handle_error() + _bpy_types._unregister_module(module_name) + del sys.modules[module_name] + return None + + # * OK loaded successfully! * + if default_set: + # just incase its enabled alredy + ext = _bpy.context.user_preferences.addons.get(module_name) + if not ext: + ext = _bpy.context.user_preferences.addons.new() + ext.module = module_name + + mod.__addon_enabled__ = True + + if _bpy.app.debug: + print("\tbpy.utils.addon_enable", mod.__name__) + + return mod - # Split registering up into 3 steps so we can undo if it fails par way through - # 1) try import - try: - mod = __import__(module_name) - mod.__time__ = os.path.getmtime(mod.__file__) - mod.__addon_enabled__ = False - except: - handle_error() - return None - # 2) try register collected modules - try: - _bpy_types._register_module(module_name) - except: - handle_error() - del sys.modules[module_name] - return None - - # 3) try run the modules register function - try: - mod.register() - except: - handle_error() - _bpy_types._unregister_module(module_name) - del sys.modules[module_name] - return None - - # * OK loaded successfully! * - if default_set: - # just incase its enabled alredy - ext = _bpy.context.user_preferences.addons.get(module_name) - if not ext: - ext = _bpy.context.user_preferences.addons.new() - ext.module = module_name - - _bpy_types._register_immediate = True - - mod.__addon_enabled__ = True - - if _bpy.app.debug: - print("\tbpy.utils.addon_enable", mod.__name__) - - return mod - - def addon_disable(module_name, default_set=True): """ Disables an addon by name.
_______________________________________________ Bf-committers mailing list Bf-committers@blender.org http://lists.blender.org/mailman/listinfo/bf-committers