From cd07e729a2161de769c76f2ad230f2d6fd7e6071 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 9 Nov 2024 11:44:31 +1100 Subject: [PATCH] Fix #130024: addon_utils.enable(..) doesn't setup extensions wheels addon_utils.enable/disable now handle wheels so that the functions can be used with extension add-ons. A new argument `refresh_handled` supports scripts handing refresh themselves which is needed to avoid refreshing many times in cases when there are multiple calls to enable/disable. This is mostly useful for internal operations. --- .../addons_core/bl_pkg/bl_extension_ops.py | 22 ++++++-- scripts/modules/_bpy_internal/addons/cli.py | 2 +- scripts/modules/addon_utils.py | 51 ++++++++++++++----- scripts/startup/bl_operators/userpref.py | 9 ---- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/scripts/addons_core/bl_pkg/bl_extension_ops.py b/scripts/addons_core/bl_pkg/bl_extension_ops.py index 0374b155747..568336ebc19 100644 --- a/scripts/addons_core/bl_pkg/bl_extension_ops.py +++ b/scripts/addons_core/bl_pkg/bl_extension_ops.py @@ -675,7 +675,12 @@ def _preferences_ensure_disabled( if not hasattr(repo_module, pkg_id): print("Repo module \"{:s}.{:s}\" not a sub-module!".format(".".join(module_base_elem), pkg_id)) - addon_utils.disable(addon_module_name, default_set=default_set, handle_error=error_fn) + addon_utils.disable( + addon_module_name, + default_set=default_set, + refresh_handled=True, + handle_error=error_fn, + ) modules_clear.append(pkg_id) @@ -725,7 +730,12 @@ def _preferences_ensure_enabled(*, repo_item, pkg_id_sequence, result, handle_er if not loaded_state: continue - addon_utils.enable(addon_module_name, default_set=loaded_default, handle_error=handle_error) + addon_utils.enable( + addon_module_name, + default_set=loaded_default, + refresh_handled=True, + handle_error=handle_error, + ) def _preferences_ensure_enabled_all(*, addon_restore, handle_error): @@ -766,7 +776,13 @@ def _preferences_install_post_enable_on_install( continue addon_module_name = "{:s}.{:s}.{:s}".format(_ext_base_pkg_idname, repo_item.module, pkg_id) - addon_utils.enable(addon_module_name, default_set=True, handle_error=handle_error) + addon_utils.enable( + addon_module_name, + default_set=True, + # Handled by `_extensions_repo_sync_wheels`. + refresh_handled=True, + handle_error=handle_error, + ) elif item_local.type == "theme": if has_theme: continue diff --git a/scripts/modules/_bpy_internal/addons/cli.py b/scripts/modules/_bpy_internal/addons/cli.py index 8500e4b7c50..60bb540653b 100644 --- a/scripts/modules/_bpy_internal/addons/cli.py +++ b/scripts/modules/_bpy_internal/addons/cli.py @@ -31,7 +31,7 @@ def set_from_cli(addons_as_string): for m in addon_modules: if check(m)[1] is False: - if enable(m, persistent=True) is None: + if enable(m, persistent=True, refresh_handled=True) is None: if check_extension(m): addon_modules_extensions_has_failure = True diff --git a/scripts/modules/addon_utils.py b/scripts/modules/addon_utils.py index 4fc3530903e..be90b034ab5 100644 --- a/scripts/modules/addon_utils.py +++ b/scripts/modules/addon_utils.py @@ -45,7 +45,11 @@ def _initialize_once(): _initialize_extensions_repos_once() for addon in _preferences.addons: - enable(addon.module) + enable( + addon.module, + # Ensured by `_initialize_extensions_repos_once`. + refresh_handled=True, + ) _initialize_ensure_extensions_addon() @@ -304,7 +308,7 @@ def _addon_remove(module_name): addons.remove(addon) -def enable(module_name, *, default_set=False, persistent=False, handle_error=None): +def enable(module_name, *, default_set=False, persistent=False, refresh_handled=False, handle_error=None): """ Enables an addon by name. @@ -314,6 +318,10 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non :type default_set: bool :arg persistent: Ensure the addon is enabled for the entire session (after loading new files). :type persistent: bool + :arg refresh_handled: When true, :func:`extensions_refresh` must have been called with ``module_name`` + included in ``addon_modules_pending``. + This should be used to avoid many calls to refresh extensions when enabling multiple add-ons at once. + :type refresh_handled: bool :arg handle_error: Called in the case of an error, taking an exception argument. :type handle_error: Callable[[Exception], None] | None :return: the loaded module or None on failure. @@ -338,6 +346,9 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non traceback.print_exc() if (is_extension := module_name.startswith(_ext_base_pkg_idname_with_dot)): + if not refresh_handled: + extensions_refresh(addon_modules_pending=[module_name]) + # Ensure the extensions are compatible. if _extensions_incompatible: if (error := _extensions_incompatible.get( @@ -347,8 +358,13 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non raise RuntimeError("Extension {:s} is incompatible ({:s})".format(module_name, error)) except RuntimeError as ex: handle_error(ex) + # No need to call `extensions_refresh` because incompatible extensions + # will not have their wheels installed. return None + # NOTE: from now on, before returning None, `extensions_refresh()` must be called + # to ensure wheels setup in anticipation for this extension being used are removed upon failure. + # reload if the mtime changes mod = sys.modules.get(module_name) # chances of the file _not_ existing are low, but it could be removed @@ -372,6 +388,8 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non except Exception as ex: print("Exception in module unregister():", (mod_file or module_name)) handle_error(ex) + if is_extension and not refresh_handled: + extensions_refresh() return None mod.__addon_enabled__ = False @@ -385,6 +403,9 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non except Exception as ex: handle_error(ex) del sys.modules[module_name] + + if is_extension and not refresh_handled: + extensions_refresh() return None mod.__addon_enabled__ = False @@ -455,6 +476,8 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non if default_set: _addon_remove(module_name) + if is_extension and not refresh_handled: + extensions_refresh() return None if is_extension: @@ -492,6 +515,8 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non del sys.modules[module_name] if default_set: _addon_remove(module_name) + if is_extension and not refresh_handled: + extensions_refresh() return None finally: _bl_owner_id_set(owner_id_prev) @@ -506,7 +531,7 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non return mod -def disable(module_name, *, default_set=False, handle_error=None): +def disable(module_name, *, default_set=False, refresh_handled=False, handle_error=None): """ Disables an addon by name. @@ -552,6 +577,9 @@ def disable(module_name, *, default_set=False, handle_error=None): if default_set: _addon_remove(module_name) + if not refresh_handled: + extensions_refresh() + if _bpy.app.debug_python: print("\taddon_utils.disable", module_name) @@ -568,12 +596,7 @@ def reset_all(*, reload_scripts=False): # Update extensions compatibility (after reloading preferences). # Potentially refreshing wheels too. - _initialize_extensions_compat_data( - _bpy.utils.user_resource('EXTENSIONS'), - ensure_wheels=True, - addon_modules_pending=None, - use_startup_fastpath=False, - ) + extensions_refresh() for path, pkg_id in _paths_with_extension_repos(): if not pkg_id: @@ -592,7 +615,7 @@ def reset_all(*, reload_scripts=False): if is_enabled == is_loaded: pass elif is_enabled: - enable(mod_name) + enable(mod_name, refresh_handled=True) elif is_loaded: print("\taddon_utils.reset_all unloading", mod_name) disable(mod_name) @@ -623,7 +646,7 @@ def disable_all(): # of one add-on disables others. for mod_name, mod in addon_modules: if getattr(mod, "__addon_enabled__", False): - disable(mod_name) + disable(mod_name, refresh_handled=True) def _blender_manual_url_prefix(): @@ -1508,7 +1531,7 @@ def _initialize_extension_repos_post_addons_prepare( continue module_name_prev = "{:s}.{:s}.{:s}".format(_ext_base_pkg_idname, module_id_prev, submodule_id) module_name_next = "{:s}.{:s}.{:s}".format(_ext_base_pkg_idname, module_id_next, submodule_id) - disable(module_name_prev, default_set=False) + disable(module_name_prev, default_set=False, refresh_handled=True) addon = repo_userdef.get(submodule_id) default_set = addon is not None persistent = getattr(mod, "__addon_persistent__", False) @@ -1541,7 +1564,7 @@ def _initialize_extension_repos_post_addons_prepare( for submodule_id, mod in repo_runtime.items(): module_name_prev = "{:s}.{:s}.{:s}".format(_ext_base_pkg_idname, module_id, submodule_id) - disable(module_name_prev, default_set=default_set) + disable(module_name_prev, default_set=default_set, refresh_handled=True) del repo del repo_module_map @@ -1550,7 +1573,7 @@ def _initialize_extension_repos_post_addons_prepare( repo_userdef = addon_userdef_info.get(module_id_prev, {}) for submodule_id in repo_userdef.keys(): module_name_prev = "{:s}.{:s}.{:s}".format(_ext_base_pkg_idname, module_id_prev, submodule_id) - disable(module_name_prev, default_set=True) + disable(module_name_prev, default_set=True, refresh_handled=True) return addons_to_enable diff --git a/scripts/startup/bl_operators/userpref.py b/scripts/startup/bl_operators/userpref.py index 956bfec1b9a..dcb0076f284 100644 --- a/scripts/startup/bl_operators/userpref.py +++ b/scripts/startup/bl_operators/userpref.py @@ -484,9 +484,6 @@ class PREFERENCES_OT_addon_enable(Operator): # Ensure any wheels are setup before enabling. module_name = self.module - is_extension = addon_utils.check_extension(module_name) - if is_extension: - addon_utils.extensions_refresh(ensure_wheels=True, addon_modules_pending=[module_name]) mod = addon_utils.enable(module_name, default_set=True, handle_error=err_cb) @@ -511,10 +508,6 @@ class PREFERENCES_OT_addon_enable(Operator): if err_str: self.report({'ERROR'}, err_str) - if is_extension: - # Since the add-on didn't work, remove any wheels it may have installed. - addon_utils.extensions_refresh(ensure_wheels=True) - result = {'CANCELLED'} if cursor_set: @@ -552,8 +545,6 @@ class PREFERENCES_OT_addon_disable(Operator): module_name = self.module is_extension = addon_utils.check_extension(module_name) addon_utils.disable(module_name, default_set=True, handle_error=err_cb) - if is_extension: - addon_utils.extensions_refresh(ensure_wheels=True) if err_str: self.report({'ERROR'}, err_str)