Extensions: forward any errors setting up wheels to reports

When actions on extensions failed setup extensions, errors weren't
forwarded to the user and were only visible in the terminal.
This commit is contained in:
Campbell Barton
2025-02-07 20:14:26 +11:00
parent 466eecf909
commit fb3fe458bf
3 changed files with 106 additions and 29 deletions
+55 -18
View File
@@ -1116,7 +1116,12 @@ def _extensions_enabled_from_repo_directory_and_pkg_id_sequence(repo_directory_a
return extensions_enabled_pending
def _extensions_repo_sync_wheels(repo_cache_store, extensions_enabled):
def _extensions_repo_sync_wheels(
repo_cache_store, # `bl_extension_utils.RepoCacheStore`
extensions_enabled, # `set[tuple[str, str]]`
*,
error_fn, # `Callable[[Exception], None]`
): # `-> None`
"""
This function collects all wheels from all packages and ensures the packages are either extracted or removed
when they are no longer used.
@@ -1128,7 +1133,7 @@ def _extensions_repo_sync_wheels(repo_cache_store, extensions_enabled):
wheel_list = []
for repo_index, pkg_manifest_local in enumerate(repo_cache_store.pkg_manifest_from_local_ensure(
error_fn=print,
error_fn=error_fn,
ignore_missing=True,
)):
repo = repos_all[repo_index]
@@ -1157,13 +1162,25 @@ def _extensions_repo_sync_wheels(repo_cache_store, extensions_enabled):
local_dir=local_dir,
wheel_list=wheel_list,
debug=bpy.app.debug_python,
error_fn=error_fn,
)
def _extensions_repo_refresh_on_change(repo_cache_store, *, extensions_enabled, compat_calc, stats_calc):
def _extensions_repo_refresh_on_change(
repo_cache_store, # `bl_extension_utils.RepoCacheStore`
*,
extensions_enabled, # `set[tuple[str, str]] | None`
compat_calc, # `bool`
stats_calc, # `bool`
error_fn, # `Callable[[Exception], None]`
): # `-> None`
import addon_utils
if extensions_enabled is not None:
_extensions_repo_sync_wheels(repo_cache_store, extensions_enabled)
_extensions_repo_sync_wheels(
repo_cache_store,
extensions_enabled,
error_fn=error_fn,
)
# Wheel sync handled above.
if compat_calc:
@@ -1181,6 +1198,7 @@ def _extensions_repo_refresh_on_change(repo_cache_store, *, extensions_enabled,
addon_utils.extensions_refresh(
ensure_wheels=False,
addon_modules_pending=addon_modules_pending,
handle_error=error_fn,
)
if stats_calc:
@@ -1773,7 +1791,10 @@ class EXTENSIONS_OT_repo_refresh_all(Operator):
# In-line `bpy.ops.preferences.addon_refresh`.
addon_utils.modules_refresh()
# Ensure compatibility info and wheels is up to date.
addon_utils.extensions_refresh(ensure_wheels=True)
addon_utils.extensions_refresh(
ensure_wheels=True,
handle_error=lambda ex: self.report({'ERROR'}, str(ex)),
)
_preferences_ui_redraw()
_preferences_ui_refresh_addons()
@@ -2128,6 +2149,7 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
),
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
_preferences_ensure_enabled_all(
@@ -2240,6 +2262,10 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
del self.repo_lock
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
# Refresh installed packages for repositories that were operated on.
repo_cache_store = repo_cache_store_ensure()
for directory in self._repo_directories:
@@ -2263,12 +2289,9 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled,
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
for directory, pkg_id_sequence in self._repo_map_packages_addon_only:
pkg_manifest_local = repo_cache_store.refresh_local_from_directory(
@@ -2297,6 +2320,7 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled_test,
compat_calc=False,
stats_calc=False,
error_fn=handle_error,
)
_preferences_ui_redraw()
@@ -2400,6 +2424,10 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
del self.repo_lock
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
for directory, pkg_id_sequence in self._pkg_id_sequence_from_directory.items():
_extensions_repo_temp_files_make_stale(repo_directory=directory)
_extensions_repo_uninstall_stale_package_fallback(
@@ -2420,6 +2448,7 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
extensions_enabled=_extensions_enabled(),
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
_preferences_theme_state_restore(self._theme_restore)
@@ -2622,6 +2651,10 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
del self.repo_lock
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
pkg_manifest_local = repo_cache_store.refresh_local_from_directory(
directory=self.repo_directory,
error_fn=self.error_fn_from_exception,
@@ -2642,13 +2675,9 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled,
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
_preferences_ensure_enabled_all(
addon_restore=self._addon_restore,
handle_error=handle_error,
@@ -2678,6 +2707,7 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled_test,
compat_calc=False,
stats_calc=False,
error_fn=handle_error,
)
_extensions_repo_temp_files_make_stale(self.repo_directory)
@@ -2994,6 +3024,10 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
def exec_command_finish(self, canceled):
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
# Unlock repositories.
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
del self.repo_lock
@@ -3021,12 +3055,9 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled,
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
_preferences_ensure_enabled_all(
addon_restore=self._addon_restore,
handle_error=handle_error,
@@ -3056,6 +3087,7 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
extensions_enabled=extensions_enabled_test,
compat_calc=False,
stats_calc=False,
error_fn=handle_error,
)
_extensions_repo_temp_files_make_stale(self.repo_directory)
@@ -3459,6 +3491,10 @@ class EXTENSIONS_OT_package_uninstall(Operator, _ExtCmdMixIn):
def exec_command_finish(self, canceled):
# TODO: it would be nice to include this message in the banner.
def handle_error(ex):
self.report({'ERROR'}, str(ex))
_extensions_repo_temp_files_make_stale(repo_directory=self.repo_directory)
_extensions_repo_uninstall_stale_package_fallback(
repo_directory=self.repo_directory,
@@ -3494,6 +3530,7 @@ class EXTENSIONS_OT_package_uninstall(Operator, _ExtCmdMixIn):
extensions_enabled=_extensions_enabled(),
compat_calc=True,
stats_calc=True,
error_fn=handle_error,
)
_preferences_theme_state_restore(self._theme_restore)
@@ -13,6 +13,7 @@ __all__ = (
"apply_action",
)
import contextlib
import os
import re
import shutil
@@ -311,6 +312,7 @@ def apply_action(
local_dir: str,
local_dir_site_packages: str,
wheel_list: list[WheelSource],
error_fn: Callable[[Exception], None],
remove_error_fn: Callable[[str, Exception], None],
debug: bool,
) -> None:
@@ -419,5 +421,18 @@ def apply_action(
filepath = wheels_dir_info_to_filepath_map[dir_info]
# `ZipFile.extractall` is needed because some wheels contain paths that point to parent directories.
# Handle this *safely* by allowing extracting to parent directories but limit this to the `local_dir`.
with zipfile.ZipFile(filepath, mode="r") as zip_fh:
_zipfile_extractall_safe(zip_fh, local_dir_site_packages, local_dir)
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(filepath, mode="r")
except Exception as ex:
print("Error ({:s}) opening zip-file: {:s}".format(str(ex), filepath))
error_fn(ex)
continue
with contextlib.closing(zip_fh_context) as zip_fh:
_zipfile_extractall_safe(
zip_fh,
local_dir_site_packages,
local_dir,
)
+34 -9
View File
@@ -348,7 +348,10 @@ def enable(module_name, *, default_set=False, persistent=False, refresh_handled=
if (is_extension := module_name.startswith(_ext_base_pkg_idname_with_dot)):
if not refresh_handled:
extensions_refresh(addon_modules_pending=[module_name])
extensions_refresh(
addon_modules_pending=[module_name],
handle_error=handle_error,
)
# Ensure the extensions are compatible.
if _extensions_incompatible:
@@ -390,7 +393,7 @@ def enable(module_name, *, default_set=False, persistent=False, refresh_handled=
print("Exception in module unregister():", (mod_file or module_name))
handle_error(ex)
if is_extension and not refresh_handled:
extensions_refresh()
extensions_refresh(handle_error=handle_error)
return None
mod.__addon_enabled__ = False
@@ -406,7 +409,7 @@ def enable(module_name, *, default_set=False, persistent=False, refresh_handled=
del sys.modules[module_name]
if is_extension and not refresh_handled:
extensions_refresh()
extensions_refresh(handle_error=handle_error)
return None
mod.__addon_enabled__ = False
@@ -478,7 +481,7 @@ def enable(module_name, *, default_set=False, persistent=False, refresh_handled=
if default_set:
_addon_remove(module_name)
if is_extension and not refresh_handled:
extensions_refresh()
extensions_refresh(handle_error=handle_error)
return None
if is_extension:
@@ -517,7 +520,7 @@ def enable(module_name, *, default_set=False, persistent=False, refresh_handled=
if default_set:
_addon_remove(module_name)
if is_extension and not refresh_handled:
extensions_refresh()
extensions_refresh(handle_error=handle_error)
return None
finally:
_bl_owner_id_set(owner_id_prev)
@@ -579,7 +582,7 @@ def disable(module_name, *, default_set=False, refresh_handled=False, handle_err
_addon_remove(module_name)
if not refresh_handled:
extensions_refresh()
extensions_refresh(handle_error=handle_error)
if _bpy.app.debug_python:
print("\taddon_utils.disable", module_name)
@@ -1145,12 +1148,13 @@ def _initialize_extensions_compat_ensure_up_to_date(extensions_directory, extens
return updated, wheel_list
def _initialize_extensions_compat_ensure_up_to_date_wheels(extensions_directory, wheel_list, debug):
def _initialize_extensions_compat_ensure_up_to_date_wheels(extensions_directory, wheel_list, debug, error_fn):
import os
_extension_sync_wheels(
local_dir=os.path.join(extensions_directory, ".local"),
wheel_list=wheel_list,
debug=debug,
error_fn=error_fn,
)
@@ -1160,6 +1164,7 @@ def _initialize_extensions_compat_data(
ensure_wheels, # `bool`
addon_modules_pending, # `Sequence[str] | None`
use_startup_fastpath, # `bool`
error_fn, # `Callable[[Exception], None] | None`
):
# WARNING: this function must *never* raise an exception because it would interfere with low level initialization.
# As the function deals with file IO, use what are typically over zealous exception checks so as to rule out
@@ -1213,8 +1218,17 @@ def _initialize_extensions_compat_data(
if ensure_wheels:
if updated:
if error_fn is None:
def error_fn(ex):
print("Error:", str(ex))
try:
_initialize_extensions_compat_ensure_up_to_date_wheels(extensions_directory, wheel_list, debug)
_initialize_extensions_compat_ensure_up_to_date_wheels(
extensions_directory,
wheel_list,
debug,
error_fn=error_fn,
)
except Exception:
print("Extension: unexpected error updating wheels, this is is a bug!")
import traceback
@@ -1337,6 +1351,7 @@ def _extension_sync_wheels(
local_dir, # `str`
wheel_list, # `list[WheelSource]`
debug, # `bool`
error_fn, # `Callable[[Exception], None]`
): # `-> None`
import os
import sys
@@ -1358,6 +1373,7 @@ def _extension_sync_wheels(
local_dir=local_dir,
local_dir_site_packages=local_dir_site_packages,
wheel_list=wheel_list,
error_fn=error_fn,
remove_error_fn=remove_error_fn,
debug=debug,
)
@@ -1804,6 +1820,8 @@ def _initialize_extensions_repos_once():
ensure_wheels=True,
addon_modules_pending=None,
use_startup_fastpath=True,
# Runs on startup, fall back to printing.
error_fn=None,
)
# Setup repositories for the first time.
@@ -1819,7 +1837,11 @@ def _initialize_extensions_repos_once():
# -----------------------------------------------------------------------------
# Extension Public API
def extensions_refresh(ensure_wheels=True, addon_modules_pending=None):
def extensions_refresh(
ensure_wheels=True,
addon_modules_pending=None,
handle_error=None,
):
"""
Ensure data relating to extensions is up to date.
This should be called after extensions on the file-system have changed.
@@ -1829,6 +1851,8 @@ def extensions_refresh(ensure_wheels=True, addon_modules_pending=None):
:arg addon_modules_pending: Refresh these add-ons by listing their package names, as if they are enabled.
This is needed so wheels can be setup before the add-on is enabled.
:type addon_modules_pending: Sequence[str] | None
:arg handle_error: Called in the case of an error, taking an exception argument.
:type handle_error: Callable[[Exception], None] | None
"""
# Ensure any changes to extensions refresh `_extensions_incompatible`.
@@ -1837,6 +1861,7 @@ def extensions_refresh(ensure_wheels=True, addon_modules_pending=None):
ensure_wheels=ensure_wheels,
addon_modules_pending=addon_modules_pending,
use_startup_fastpath=False,
error_fn=handle_error,
)