Compare commits

..

12 Commits

Author SHA1 Message Date
Bastien Montagne
4195e9e1a7 MEM_guarded: Improve error reporting usefulness.
The main change from this commit is the usage of ASAN poisoning (if
available) to trigger an ASAN report on the erroring memory block.

The main benefit is the report of the allocation backtrace of that
faulty memory block.

Pull Request: https://projects.blender.org/blender/blender/pulls/124231
2024-07-08 12:10:37 +02:00
Campbell Barton
cdb61375fe Merge branch 'blender-v4.2-release' 2024-07-08 18:27:52 +10:00
Campbell Barton
386d6f098a AUTHORS: correct invalid de-duplication 2024-07-08 18:20:53 +10:00
Campbell Barton
e52783033d Merge branch 'blender-v4.2-release' 2024-07-08 15:31:44 +10:00
Campbell Barton
9bbf269fe3 Extensions: suppress all pylint warnings & handle add-on errors
- Suppress pylint warnings via comments or be minor changes.
- Any errors disabling add-ons before upgrading or uninstalling are now
  reported to the operators.
- Disable cyclic import warning is it's impractical to resolve.
- Enable useless-suppression warning.
2024-07-08 15:29:27 +10:00
YimingWu
0fbb83ef85 Fix #124296: Lower case name in Multistroke modifier
Just a typo in Grease Pencil Multistroke modifier.

Pull Request: https://projects.blender.org/blender/blender/pulls/124327
2024-07-08 06:09:50 +02:00
Campbell Barton
817efe4aef Merge branch 'blender-v4.2-release' 2024-07-08 13:33:34 +10:00
Campbell Barton
05feac04fa Extensions: minor changes to error handling & uninstalling
- Any change that cases validate to fail reports a fatal_error.
- If removing a package fails, attempt to the remove cache.
2024-07-08 13:27:58 +10:00
Campbell Barton
bae1d3311f Merge branch 'blender-v4.2-release' 2024-07-08 13:05:49 +10:00
Campbell Barton
ac6e216b26 Merge branch 'blender-v4.2-release' 2024-07-08 13:05:45 +10:00
Campbell Barton
74f8ac15f5 Extensions: "build --split-platform" now includes it's own wheels
The `[build.generated]` section now includes a filtered list of wheels
to simplify checking existing wheels on the server.

Also disallow quotes & control characters in wheel paths because they
already shouldn't be used and doing so would cause escaping issues in
the generated TOML.

Implements #124242.
2024-07-08 12:50:43 +10:00
Campbell Barton
f3630777f6 Extensions: show add-on errors in the add-ons panel
Restore the UI from 4.1. Also correct an error when extensions are
disabled.
2024-07-08 11:36:52 +10:00
17 changed files with 396 additions and 163 deletions

@ -212,18 +212,10 @@ static bool malloc_debug_memset = false;
/* implementation */
/* --------------------------------------------------------------------- */
#ifdef __GNUC__
__attribute__((format(printf, 1, 2)))
#endif
static void
print_error(const char *str, ...)
static void print_error(const char *message, va_list str_format_args)
{
char buf[1024];
va_list ap;
va_start(ap, str);
vsnprintf(buf, sizeof(buf), str, ap);
va_end(ap);
char buf[512];
vsnprintf(buf, sizeof(buf), message, str_format_args);
buf[sizeof(buf) - 1] = '\0';
if (error_callback) {
@ -234,6 +226,50 @@ print_error(const char *str, ...)
}
}
#ifdef __GNUC__
__attribute__((format(printf, 1, 2)))
#endif
static void
print_error(const char *message, ...)
{
va_list str_format_args;
va_start(str_format_args, message);
print_error(message, str_format_args);
va_end(str_format_args);
}
#ifdef __GNUC__
__attribute__((format(printf, 2, 3)))
#endif
static void
report_error_on_address(const void *vmemh, const char *message, ...)
{
va_list str_format_args;
va_start(str_format_args, message);
print_error(message, str_format_args);
va_end(str_format_args);
if (vmemh == nullptr) {
MEM_trigger_error_on_memory_block(nullptr, 0);
return;
}
const MemHead *memh = static_cast<const MemHead *>(vmemh);
memh--;
size_t len = memh->len;
const void *address = memh;
size_t size = len + sizeof(*memh) + sizeof(MemTail);
if (UNLIKELY(memh->alignment > 0)) {
const MemHeadAligned *memh_aligned = memh;
address = MEMHEAD_REAL_PTR(memh_aligned);
size = len + sizeof(*memh_aligned) + MEMHEAD_ALIGN_PADDING(memh_aligned->alignment) +
sizeof(MemTail);
}
MEM_trigger_error_on_memory_block(address, size);
}
static pthread_mutex_t thread_lock = PTHREAD_MUTEX_INITIALIZER;
static void mem_lock_thread()
@ -290,12 +326,9 @@ void *MEM_guarded_dupallocN(const void *vmemh)
memh--;
if ((memh->flag & MEMHEAD_FLAG_FROM_CPP_NEW) != 0) {
print_error(
"Attempt to use C-style MEM_dupallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_dupallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
#ifndef DEBUG_MEMDUPLINAME
@ -353,12 +386,9 @@ void *MEM_guarded_reallocN_id(void *vmemh, size_t len, const char *str)
memh--;
if ((memh->flag & MEMHEAD_FLAG_FROM_CPP_NEW) != 0) {
print_error(
"Attempt to use C-style MEM_reallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_reallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
if (LIKELY(memh->alignment == 0)) {
@ -398,12 +428,9 @@ void *MEM_guarded_recallocN_id(void *vmemh, size_t len, const char *str)
memh--;
if ((memh->flag & MEMHEAD_FLAG_FROM_CPP_NEW) != 0) {
print_error(
"Attempt to use C-style MEM_recallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_recallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
if (LIKELY(memh->alignment == 0)) {
@ -1012,11 +1039,9 @@ void MEM_guarded_freeN(void *vmemh, const AllocationType allocation_type)
if (allocation_type != AllocationType::NEW_DELETE &&
(memh->flag & MEMHEAD_FLAG_FROM_CPP_NEW) != 0)
{
print_error(
report_error_on_address(
vmemh,
"Attempt to use C-style MEM_freeN on a pointer created with CPP-style MEM_new or new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
}
if (memh->tag1 == MEMFREE && memh->tag2 == MEMFREE) {

@ -9,6 +9,27 @@
* \ingroup intern_mem
*/
#include <cstdlib>
/* BEGIN copied from BLI_asan.h */
/* Clang defines this. */
#ifndef __has_feature
# define __has_feature(x) 0
#endif
#if (defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)) && \
(!defined(_MSC_VER) || _MSC_VER > 1929) /* MSVC 2019 and below doesn't ship ASAN headers. */
# include "sanitizer/asan_interface.h"
# define WITH_ASAN
#else
/* Ensure return value is used. Just using UNUSED_VARS results in a warning. */
# define ASAN_POISON_MEMORY_REGION(addr, size) (void)(0 && ((size) != 0 && (addr) != NULL))
# define ASAN_UNPOISON_MEMORY_REGION(addr, size) (void)(0 && ((size) != 0 && (addr) != NULL))
#endif
/* END copied from BLI_asan.h */
MEM_INLINE bool MEM_size_safe_multiply(size_t a, size_t b, size_t *result)
{
/* A size_t with its high-half bits all set to 1. */
@ -27,3 +48,42 @@ MEM_INLINE bool MEM_size_safe_multiply(size_t a, size_t b, size_t *result)
*/
return ((high_bits & (a | b)) == 0 || (*result / b == a));
}
/**
* Util to trigger an error for the given memory block.
*
* When ASAN is available, it will poison the memory block before accessing it, to trigger a
* detailed ASAN report. Otherwise, it will abort if aborting on assert is set.
*/
#ifdef WITH_ASAN
MEM_INLINE void MEM_trigger_error_on_memory_block(const void *address, const size_t size)
{
if (address == nullptr) {
# ifdef WITH_ASSERT_ABORT
abort();
# endif
return;
}
/* Trigger ASAN error by poisoning the memory and accessing it. */
ASAN_POISON_MEMORY_REGION(address, size);
char *buffer = const_cast<char *>(static_cast<const char *>(address));
const char c = *buffer;
*buffer &= 255;
*buffer = c;
/* In case ASAN is set to not terminate on error, but abort on assert is requested. */
# ifdef WITH_ASSERT_ABORT
abort();
# endif
ASAN_UNPOISON_MEMORY_REGION(address, size);
}
#else
MEM_INLINE void MEM_trigger_error_on_memory_block(const void * /* address */,
const size_t /* size */)
{
# ifdef WITH_ASSERT_ABORT
abort();
# endif
}
#endif

@ -69,21 +69,6 @@ size_t malloc_usable_size(void *ptr);
# define MEM_INLINE static inline
#endif
/* BEGIN copied from BLI_asan.h */
/* Clang defines this. */
#ifndef __has_feature
# define __has_feature(x) 0
#endif
#if (defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)) && \
(!defined(_MSC_VER) || _MSC_VER > 1929) /* MSVC 2019 and below doesn't ship ASAN headers. */
# include "sanitizer/asan_interface.h"
# define WITH_ASAN
#endif
/* END copied from BLI_asan.h */
#define IS_POW2(a) (((a) & ((a)-1)) == 0)
/* Extra padding which needs to be applied on MemHead to make it aligned. */

@ -74,18 +74,10 @@ enum {
#define MEMHEAD_IS_FROM_CPP_NEW(memhead) ((memhead)->len & size_t(MEMHEAD_FLAG_FROM_CPP_NEW))
#define MEMHEAD_LEN(memhead) ((memhead)->len & ~size_t(MEMHEAD_FLAG_MASK))
#ifdef __GNUC__
__attribute__((format(printf, 1, 2)))
#endif
static void
print_error(const char *str, ...)
static void print_error(const char *message, va_list str_format_args)
{
char buf[512];
va_list ap;
va_start(ap, str);
vsnprintf(buf, sizeof(buf), str, ap);
va_end(ap);
vsnprintf(buf, sizeof(buf), message, str_format_args);
buf[sizeof(buf) - 1] = '\0';
if (error_callback) {
@ -93,6 +85,48 @@ print_error(const char *str, ...)
}
}
#ifdef __GNUC__
__attribute__((format(printf, 1, 2)))
#endif
static void
print_error(const char *message, ...)
{
va_list str_format_args;
va_start(str_format_args, message);
print_error(message, str_format_args);
va_end(str_format_args);
}
#ifdef __GNUC__
__attribute__((format(printf, 2, 3)))
#endif
static void
report_error_on_address(const void *vmemh, const char *message, ...)
{
va_list str_format_args;
va_start(str_format_args, message);
print_error(message, str_format_args);
va_end(str_format_args);
if (vmemh == nullptr) {
MEM_trigger_error_on_memory_block(nullptr, 0);
return;
}
MemHead *memh = MEMHEAD_FROM_PTR(vmemh);
size_t len = MEMHEAD_LEN(memh);
void *address = memh;
size_t size = len + sizeof(*memh);
if (UNLIKELY(MEMHEAD_IS_ALIGNED(memh))) {
MemHeadAligned *memh_aligned = MEMHEAD_ALIGNED_FROM_PTR(vmemh);
address = MEMHEAD_REAL_PTR(memh_aligned);
size = len + sizeof(*memh_aligned) + MEMHEAD_ALIGN_PADDING(memh_aligned->alignment);
}
MEM_trigger_error_on_memory_block(address, size);
}
size_t MEM_lockfree_allocN_len(const void *vmemh)
{
if (LIKELY(vmemh)) {
@ -109,10 +143,7 @@ void MEM_lockfree_freeN(void *vmemh, AllocationType allocation_type)
}
if (UNLIKELY(vmemh == nullptr)) {
print_error("Attempt to free nullptr pointer\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh, "Attempt to free nullptr pointer\n");
return;
}
@ -120,11 +151,9 @@ void MEM_lockfree_freeN(void *vmemh, AllocationType allocation_type)
size_t len = MEMHEAD_LEN(memh);
if (allocation_type != AllocationType::NEW_DELETE && MEMHEAD_IS_FROM_CPP_NEW(memh)) {
print_error(
report_error_on_address(
vmemh,
"Attempt to use C-style MEM_freeN on a pointer created with CPP-style MEM_new or new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
}
memory_usage_block_free(len);
@ -149,12 +178,9 @@ void *MEM_lockfree_dupallocN(const void *vmemh)
const size_t prev_size = MEM_lockfree_allocN_len(vmemh);
if (MEMHEAD_IS_FROM_CPP_NEW(memh)) {
print_error(
"Attempt to use C-style MEM_dupallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_dupallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
if (UNLIKELY(MEMHEAD_IS_ALIGNED(memh))) {
@ -179,12 +205,9 @@ void *MEM_lockfree_reallocN_id(void *vmemh, size_t len, const char *str)
const size_t old_len = MEM_lockfree_allocN_len(vmemh);
if (MEMHEAD_IS_FROM_CPP_NEW(memh)) {
print_error(
"Attempt to use C-style MEM_reallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_reallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
if (LIKELY(!MEMHEAD_IS_ALIGNED(memh))) {
@ -225,12 +248,9 @@ void *MEM_lockfree_recallocN_id(void *vmemh, size_t len, const char *str)
const size_t old_len = MEM_lockfree_allocN_len(vmemh);
if (MEMHEAD_IS_FROM_CPP_NEW(memh)) {
print_error(
"Attempt to use C-style MEM_recallocN on a pointer created with CPP-style MEM_new or "
"new\n");
#ifdef WITH_ASSERT_ABORT
abort();
#endif
report_error_on_address(vmemh,
"Attempt to use C-style MEM_recallocN on a pointer created with "
"CPP-style MEM_new or new\n");
}
if (LIKELY(!MEMHEAD_IS_ALIGNED(memh))) {

@ -72,7 +72,8 @@ watch_check_ruff:
check_pylint:
@cd "$(BASE_DIR)" && \
pylint $(PY_FILES) \
--disable=C0103,C0111,C0201,C0301,C0302,C0415,R1702,R1705,R0902,R0903,R0913,E0611,E0401,I1101,R0801,C0209,W0511,W0718,W0719,C0413,R0911,R0912,R0914,R0915 \
--enable=useless-suppression \
--disable=C0103,C0111,C0201,C0301,C0302,C0415,R0401,R1702,R1705,R0902,R0903,R0913,E0611,E0401,I1101,R0801,C0209,W0511,W0718,W0719,C0413,R0911,R0912,R0914,R0915 \
--msg-template='{abspath}:{line}:{column}: {msg_id}: {msg} ({symbol})'
watch_check_pylint:
@cd "$(BASE_DIR)" && \

@ -400,7 +400,6 @@ def monkeypatch_extenions_repos_update_pre_impl():
def monkeypatch_extenions_repos_update_post_impl():
import os
# pylint: disable-next=redefined-outer-name
from . import bl_extension_ops
repo_cache_store = repo_cache_store_ensure()
@ -437,7 +436,7 @@ def monkeypatch_extensions_repos_update_pre(*_):
except Exception as ex:
print_debug("ERROR", str(ex))
try:
monkeypatch_extensions_repos_update_pre._fn_orig()
monkeypatch_extensions_repos_update_pre.fn_orig()
except Exception as ex:
print_debug("ERROR", str(ex))
@ -446,7 +445,7 @@ def monkeypatch_extensions_repos_update_pre(*_):
def monkeypatch_extenions_repos_update_post(*_):
print_debug("POST:")
try:
monkeypatch_extenions_repos_update_post._fn_orig()
monkeypatch_extenions_repos_update_post.fn_orig()
except Exception as ex:
print_debug("ERROR", str(ex))
try:
@ -458,40 +457,50 @@ def monkeypatch_extenions_repos_update_post(*_):
def monkeypatch_install():
import addon_utils
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_update_pre
# pylint: disable-next=protected-access
fn_orig = addon_utils._initialize_extension_repos_pre
fn_override = monkeypatch_extensions_repos_update_pre
for i, fn in enumerate(handlers):
if fn is fn_orig:
handlers[i] = fn_override
fn_override._fn_orig = fn_orig
fn_override.fn_orig = fn_orig
break
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_update_post
# pylint: disable-next=protected-access
fn_orig = addon_utils._initialize_extension_repos_post
fn_override = monkeypatch_extenions_repos_update_post
for i, fn in enumerate(handlers):
if fn is fn_orig:
handlers[i] = fn_override
fn_override._fn_orig = fn_orig
fn_override.fn_orig = fn_orig
break
def monkeypatch_uninstall():
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_update_pre
fn_override = monkeypatch_extensions_repos_update_pre
for i, fn in enumerate(handlers):
if fn is fn_override:
handlers[i] = fn_override._fn_orig
del fn_override._fn_orig
handlers[i] = fn_override.fn_orig
del fn_override.fn_orig
break
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_update_post
fn_override = monkeypatch_extenions_repos_update_post
for i, fn in enumerate(handlers):
if fn is fn_override:
handlers[i] = fn_override._fn_orig
del fn_override._fn_orig
handlers[i] = fn_override.fn_orig
del fn_override.fn_orig
break
@ -636,9 +645,11 @@ def register():
from bl_ui.space_userpref import USERPREF_MT_interface_theme_presets
USERPREF_MT_interface_theme_presets.append(theme_preset_draw)
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_sync
handlers.append(extenion_repos_sync)
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_files_clear
handlers.append(extenion_repos_files_clear)
@ -676,10 +687,12 @@ def unregister():
from bl_ui.space_userpref import USERPREF_MT_interface_theme_presets
USERPREF_MT_interface_theme_presets.remove(theme_preset_draw)
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_sync
if extenion_repos_sync in handlers:
handlers.remove(extenion_repos_sync)
# pylint: disable-next=protected-access
handlers = bpy.app.handlers._extension_repos_files_clear
if extenion_repos_files_clear in handlers:
handlers.remove(extenion_repos_files_clear)

@ -404,7 +404,7 @@ def repo_iter_valid_only(context, *, exclude_remote, exclude_system):
if (not repo_item.use_remote_url) and (repo_item.source == 'SYSTEM'):
continue
# Ignore repositories that have invalid settings.
directory, remote_url = repo_paths_or_none(repo_item)
directory, _remote_url = repo_paths_or_none(repo_item)
if directory is None:
continue
yield repo_item
@ -494,16 +494,17 @@ def repo_cache_store_refresh_from_prefs(repo_cache_store, include_disabled=False
return repos
def _preferences_ensure_disabled(*, repo_item, pkg_id_sequence, default_set):
def _preferences_ensure_disabled(
*,
repo_item, # `RepoItem`
pkg_id_sequence, # `List[str]`
default_set, # `bool`
error_fn, # `Callable[[Exception], None]`
): # `-> Dict[str, Tuple[boo, bool]]`
import sys
import addon_utils
result = {}
errors = []
def handle_error(ex):
print("Error:", ex)
errors.append(str(ex))
modules_clear = []
@ -530,7 +531,7 @@ def _preferences_ensure_disabled(*, repo_item, pkg_id_sequence, default_set):
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=handle_error)
addon_utils.disable(addon_module_name, default_set=default_set, handle_error=error_fn)
modules_clear.append(pkg_id)
@ -569,11 +570,12 @@ def _preferences_ensure_disabled(*, repo_item, pkg_id_sequence, default_set):
continue
delattr(repo_module, pkg_id)
return result, errors
return result
def _preferences_ensure_enabled(*, repo_item, pkg_id_sequence, result, handle_error):
import addon_utils
_ = repo_item, pkg_id_sequence
for addon_module_name, (loaded_default, loaded_state) in result.items():
# The module was not loaded, so no need to restore it.
if not loaded_state:
@ -639,6 +641,7 @@ def _preferences_ui_redraw():
def _preferences_ui_refresh_addons():
import addon_utils
# TODO: make a public method.
# pylint: disable-next=protected-access
addon_utils.modules._is_first = True
@ -928,6 +931,7 @@ def _extensions_repo_sync_wheels(repo_cache_store, extensions_enabled):
local_dir = os.path.join(extensions, ".local")
# WARNING: bad level call, avoid making this a public function just now.
# pylint: disable-next=protected-access
addon_utils._extension_sync_wheels(
local_dir=local_dir,
wheel_list=wheel_list,
@ -1347,6 +1351,7 @@ class EXTENSIONS_OT_repo_sync(Operator, _ExtCmdMixIn):
repos_lock.append(repo_item.directory)
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=repos_lock,
cookie=cookie_from_session(),
@ -1444,6 +1449,7 @@ class EXTENSIONS_OT_repo_sync_all(Operator, _ExtCmdMixIn):
repos_lock.append(repo_item.directory)
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=repos_lock,
cookie=cookie_from_session(),
@ -1496,11 +1502,15 @@ class EXTENSIONS_OT_repo_refresh_all(Operator):
# Re-generate JSON meta-data from TOML files (needed for offline repository).
repo_cache_store.refresh_remote_from_directory(
directory=repo_item.directory,
# NOTE: this isn't a problem as the callback isn't stored.
# pylint: disable-next=cell-var-from-loop
error_fn=lambda ex: self._exceptions_as_report(repo_item.name, ex),
force=True,
)
repo_cache_store.refresh_local_from_directory(
directory=repo_item.directory,
# NOTE: this isn't a problem as the callback isn't stored.
# pylint: disable-next=cell-var-from-loop
error_fn=lambda ex: self._exceptions_as_report(repo_item.name, ex),
)
@ -1532,7 +1542,9 @@ class EXTENSIONS_OT_repo_enable_from_drop(Operator):
print(self.repo_index)
if (repo := repo_lookup_by_index_or_none_with_report(self.repo_index, self.report)) is None:
return {'CANCELLED'}
# pylint: disable-next=attribute-defined-outside-init
self._repo_name = repo.name
# pylint: disable-next=attribute-defined-outside-init
self._repo_remote_url = repo.remote_url
wm = context.window_manager
@ -1599,8 +1611,11 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
def exec_command_iter(self, is_modal):
from . import bl_extension_utils
# pylint: disable-next=attribute-defined-outside-init
self._repo_directories = set()
# pylint: disable-next=attribute-defined-outside-init
self._addon_restore = []
# pylint: disable-next=attribute-defined-outside-init
self._theme_restore = _preferences_theme_state_create()
use_active_only = self.use_active_only
@ -1690,6 +1705,7 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
return None
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=list(self._repo_directories),
cookie=cookie_from_session(),
@ -1698,10 +1714,11 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
return None
for repo_item, pkg_id_sequence in handle_addons_info:
result, errors = _preferences_ensure_disabled(
result = _preferences_ensure_disabled(
repo_item=repo_item,
pkg_id_sequence=pkg_id_sequence,
default_set=False,
error_fn=lambda ex: self.report({'ERROR'}, str(ex)),
)
self._addon_restore.append((repo_item, pkg_id_sequence, result))
@ -1765,7 +1782,9 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
error_fn=self.error_fn_from_exception,
))
repo_pkg_map = _pkg_marked_by_repo(repo_cache_store, pkg_manifest_remote_all)
# pylint: disable-next=attribute-defined-outside-init
self._repo_directories = set()
# pylint: disable-next=attribute-defined-outside-init
self._repo_map_packages_addon_only = []
package_count = 0
@ -1819,6 +1838,7 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
return None
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=list(self._repo_directories),
cookie=cookie_from_session(),
@ -1919,7 +1939,9 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
repo_pkg_map = _pkg_marked_by_repo(repo_cache_store, pkg_manifest_local_all)
package_count = 0
# pylint: disable-next=attribute-defined-outside-init
self._repo_directories = set()
# pylint: disable-next=attribute-defined-outside-init
self._theme_restore = _preferences_theme_state_create()
# Track add-ons to disable before uninstalling.
@ -1958,6 +1980,7 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
return None
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=list(self._repo_directories),
cookie=cookie_from_session(),
@ -1966,11 +1989,12 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
return None
for repo_item, pkg_id_sequence in handle_addons_info:
# No need to store the result (`_`) because the add-ons aren't going to be enabled again.
_, errors = _preferences_ensure_disabled(
# No need to store the result because the add-ons aren't going to be enabled again.
_preferences_ensure_disabled(
repo_item=repo_item,
pkg_id_sequence=pkg_id_sequence,
default_set=True,
error_fn=lambda ex: self.report({'ERROR'}, str(ex)),
)
return bl_extension_utils.CommandBatch(
@ -2066,7 +2090,9 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
pkg_is_legacy_addon,
)
# pylint: disable-next=attribute-defined-outside-init
self._addon_restore = []
# pylint: disable-next=attribute-defined-outside-init
self._theme_restore = _preferences_theme_state_create()
# Happens when run from scripts and this argument isn't passed in.
@ -2138,7 +2164,9 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
return None
# Collect package ID's.
# pylint: disable-next=attribute-defined-outside-init
self.repo_directory = directory
# pylint: disable-next=attribute-defined-outside-init
self.pkg_id_sequence = pkg_id_sequence
# Detect upgrade.
@ -2151,15 +2179,17 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
if pkg_manifest_local is not None:
pkg_id_sequence_upgrade = [pkg_id for pkg_id in pkg_id_sequence if pkg_id in pkg_manifest_local]
if pkg_id_sequence_upgrade:
result, errors = _preferences_ensure_disabled(
result = _preferences_ensure_disabled(
repo_item=repo_item,
pkg_id_sequence=pkg_id_sequence_upgrade,
default_set=False,
error_fn=lambda ex: self.report({'ERROR'}, str(ex)),
)
self._addon_restore.append((repo_item, pkg_id_sequence_upgrade, result))
del repo_cache_store, pkg_manifest_local
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=[repo_item.directory],
cookie=cookie_from_session(),
@ -2503,7 +2533,9 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
if not self._is_ready_to_execute():
return None
# pylint: disable-next=attribute-defined-outside-init
self._addon_restore = []
# pylint: disable-next=attribute-defined-outside-init
self._theme_restore = _preferences_theme_state_create()
directory = _repo_dir_and_index_get(self.repo_index, self.repo_directory, self.report)
@ -2531,15 +2563,17 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
if is_installed:
pkg_id_sequence = (pkg_id,)
result, errors = _preferences_ensure_disabled(
result = _preferences_ensure_disabled(
repo_item=repo_item,
pkg_id_sequence=pkg_id_sequence,
default_set=False,
error_fn=lambda ex: self.report({'ERROR'}, str(ex)),
)
self._addon_restore.append((repo_item, pkg_id_sequence, result))
del pkg_id_sequence
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=[repo_item.directory],
cookie=cookie_from_session(),
@ -2729,7 +2763,7 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
)
layout = self.layout
_repo_index, repo_name, pkg_id, item_remote = self._drop_variables
_repo_index, repo_name, _pkg_id, item_remote = self._drop_variables
layout.label(text="Do you want to install the following {:s}?".format(item_remote.type))
@ -2809,7 +2843,6 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
repo_from_url_name, # `str`
url, # `str`
):
import string
from .bl_extension_utils import (
platform_from_this_system,
)
@ -2945,6 +2978,7 @@ class EXTENSIONS_OT_package_uninstall(Operator, _ExtCmdMixIn):
def exec_command_iter(self, is_modal):
from . import bl_extension_utils
# pylint: disable-next=attribute-defined-outside-init
self._theme_restore = _preferences_theme_state_create()
directory = _repo_dir_and_index_get(self.repo_index, self.repo_directory, self.report)
@ -2959,13 +2993,16 @@ class EXTENSIONS_OT_package_uninstall(Operator, _ExtCmdMixIn):
self.report({'ERROR'}, "Package ID not set")
return None
_, errors = _preferences_ensure_disabled(
# No need to store the result because the add-ons aren't going to be enabled again.
_preferences_ensure_disabled(
repo_item=repo_item,
pkg_id_sequence=(pkg_id,),
default_set=True,
error_fn=lambda ex: self.report({'ERROR'}, str(ex)),
)
# Lock repositories.
# pylint: disable-next=attribute-defined-outside-init
self.repo_lock = bl_extension_utils.RepoLock(
repo_directories=[repo_item.directory],
cookie=cookie_from_session(),
@ -3388,7 +3425,7 @@ class EXTENSIONS_OT_userpref_tags_set(Operator):
return {'CANCELLED'}
tags_clear(wm, tags_attr)
if self.value is False:
if value is False:
tags_refresh(wm, tags_attr, default_value=False)
_preferences_ui_redraw()

@ -561,6 +561,30 @@ def addons_panel_draw_items(
return module_names
def addons_panel_draw_error_duplicates(layout):
import addon_utils
box = layout.box()
row = box.row()
row.label(text="Multiple add-ons with the same name found!")
row.label(icon='ERROR')
box.label(text="Delete one of each pair to resolve:")
for (addon_name, addon_file, addon_path) in addon_utils.error_duplicates:
box.separator()
sub_col = box.column(align=True)
sub_col.label(text=addon_name + ":")
sub_col.label(text=" " + addon_file)
sub_col.label(text=" " + addon_path)
def addons_panel_draw_error_generic(layout, lines):
box = layout.box()
sub = box.row()
sub.label(text=lines[0])
sub.label(icon='ERROR')
for l in lines[1:]:
box.label(text=l)
def addons_panel_draw_impl(
self,
context, # `bpy.types.Context`
@ -580,14 +604,26 @@ def addons_panel_draw_impl(
from . import repo_cache_store_ensure
layout = self.layout
# First show any errors, this should be an exceptional situation that should be resolved,
# otherwise add-ons may not behave correctly.
if addon_utils.error_duplicates:
addons_panel_draw_error_duplicates(layout)
if addon_utils.error_encoding:
addons_panel_draw_error_generic(
layout, (
"One or more add-ons do not have UTF-8 encoding",
"(see console for details)",
),
)
repo_cache_store = repo_cache_store_ensure()
# This isn't elegant, but the preferences aren't available on registration.
if not repo_cache_store.is_init():
repo_cache_store_refresh_from_prefs(repo_cache_store)
layout = self.layout
prefs = context.preferences
# Define a top-most column to place warnings (if-any).
@ -716,6 +752,7 @@ def addons_panel_draw(panel, context):
# Light weight wrapper for extension local and remote extension manifest data.
# Used for display purposes. Includes some information for filtering.
# pylint: disable-next=wrong-import-order
from collections import namedtuple
ExtensionUI = namedtuple(
@ -863,15 +900,14 @@ class ExtensionUI_FilterParams:
if is_addon:
if is_installed:
# Currently we only need to know the module name once installed.
addon_module_name = repo_module_prefix + pkg_id
# pylint: disable-next=possibly-used-before-assignment
addon_module_name = repo_module_prefix + pkg_id
is_enabled = addon_module_name in self.addons_enabled
else:
is_enabled = False
addon_module_name = None
elif is_theme:
# pylint: disable-next=possibly-used-before-assignment
is_enabled = (repo_index, pkg_id) == self.active_theme_info
addon_module_name = None
else:

@ -292,8 +292,9 @@ def command_output_from_json_0(
# Internal Functions.
#
# pylint: disable-next=useless-return
def repositories_validate_or_errors(repos: Sequence[str]) -> Optional[InfoItemSeq]:
_ = repos
return None
@ -853,6 +854,8 @@ class CommandBatch:
def _exec_blocking_single(
self,
report_fn: Callable[[str, str], None],
# TODO: investigate using this or removing it.
# pylint: disable-next=unused-argument
request_exit_fn: Callable[[], bool],
) -> bool:
for cmd in self._batch:
@ -1298,6 +1301,7 @@ def pkg_manifest_params_compatible_or_error(
item=item,
filter_blender_version=this_blender_version,
filter_platform=this_platform,
# pylint: disable-next=unnecessary-lambda
skip_message_fn=lambda msg: result_report.append(msg),
error_fn=error_fn,
)
@ -1891,6 +1895,7 @@ class RepoCacheStore:
) -> Optional[Dict[str, PkgManifest_Normalized]]:
for repo_entry in self._repos:
if directory == repo_entry.directory:
# pylint: disable-next=protected-access
return repo_entry._json_data_refresh(force=force, error_fn=error_fn)
raise ValueError("Directory {:s} not a known repo".format(directory))
@ -1927,6 +1932,7 @@ class RepoCacheStore:
# While we could yield a valid manifest here,
# leave it to the caller to skip "remote" data for local-only repositories.
if repo_entry.remote_url:
# pylint: disable-next=protected-access
yield repo_entry._json_data_ensure(
check_files=check_files,
ignore_missing=ignore_missing,

@ -232,6 +232,7 @@ def force_exit_ok_enable() -> None:
def execfile(filepath: str) -> Dict[str, Any]:
global_namespace = {"__file__": filepath, "__name__": "__main__"}
with open(filepath, "rb") as fh:
# pylint: disable-next=exec-used
exec(compile(fh.read(), filepath, 'exec'), global_namespace)
return global_namespace
@ -446,6 +447,7 @@ def sha256_from_file_or_error(
(exact hashing method may change).
"""
try:
# pylint: disable-next=consider-using-with
fh_context = open(filepath, 'rb')
except Exception as ex:
return "error opening file: {:s}".format(str(ex))
@ -531,6 +533,8 @@ def rmtree_with_fallback_or_error(
if sys.version_info >= (3, 12):
shutil.rmtree(path, onexc=lambda *args: errors.append(args))
else:
# Ignore as the deprecated logic is only used for older Python versions.
# pylint: disable-next=deprecated-argument
shutil.rmtree(path, onerror=lambda *args: errors.append((args[0], args[1], args[2][1])))
# Happy path (for practically all cases).
@ -642,7 +646,7 @@ def pkg_manifest_from_dict_and_validate_impl(
for key in PkgManifest._fields:
val = data.get(key, ...)
if val is ...:
# pylint: disable-next=no-member
# pylint: disable-next=no-member,protected-access
val = PkgManifest._field_defaults.get(key, ...)
# `pkg_manifest_is_valid_or_error{_all}` will have caught this, assert all the same.
assert val is not ...
@ -820,6 +824,7 @@ def pkg_manifest_from_archive_and_validate(
strict: bool,
) -> Union[PkgManifest, str]:
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(filepath, mode="r")
except Exception as ex:
return "Error extracting archive \"{:s}\"".format(str(ex))
@ -836,6 +841,7 @@ def pkg_is_legacy_addon(filepath: str) -> bool:
return True
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(filepath, mode="r")
except Exception:
return False
@ -1450,12 +1456,12 @@ def pkg_manifest_tags_valid_or_error(
#
# However manifests from severs that don't adhere to strict rules are not prevented from loading.
# pylint: disable-next=useless-return
def pkg_manifest_validate_field_nop(
value: Any,
strict: bool,
) -> Optional[str]:
_ = strict, value
# pylint: disable-next=useless-return
return None
@ -1698,9 +1704,16 @@ def pkg_manifest_validate_field_wheels(
filename_spec = "{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl"
for wheel in value:
if "\"" in wheel:
return "wheel paths most not contain quotes, found {!r}".format(wheel)
if "\\" in wheel:
return "wheel paths must use forward slashes, found {!r}".format(wheel)
if (error := pkg_manifest_validate_field_any_non_empty_string_stripped_no_control_chars(
wheel, True,
)) is not None:
return "wheel paths detected: {:s}, found {!r}".format(error, wheel)
wheel_filename = os.path.basename(wheel)
if not wheel_filename.lower().endswith(".whl"):
return "wheel paths must end with \".whl\", found {!r}".format(wheel)
@ -1809,11 +1822,11 @@ def pkg_manifest_is_valid_or_error_impl(
is_default_value = False
x_val = data.get(x_key, ...)
if x_val is ...:
# pylint: disable-next=no-member
# pylint: disable-next=no-member, protected-access
x_val = PkgManifest._field_defaults.get(x_key, ...)
if from_repo:
if x_val is ...:
# pylint: disable-next=no-member
# pylint: disable-next=no-member, protected-access
x_val = PkgManifest_Archive._field_defaults.get(x_key, ...)
if x_val is ...:
error_list.append("missing \"{:s}\"".format(x_key))
@ -1898,6 +1911,9 @@ def pkg_manifest_dict_apply_build_generated_table(manifest_dict: Dict[str, Any])
if (platforms := build_generated.get("platforms")) is not None:
manifest_dict["platforms"] = platforms
if (wheels := build_generated.get("wheels")) is not None:
manifest_dict["wheels"] = wheels
# -----------------------------------------------------------------------------
# Standalone Utilities
@ -1986,6 +2002,35 @@ def blender_platform_compatible_with_wheel_platform(platform: str, wheel_platfor
return platform == platform_blender
def blender_platform_compatible_with_wheel_platform_from_filepath(platform: str, wheel_filepath: str) -> bool:
wheel_filename = os.path.splitext(os.path.basename(wheel_filepath))[0]
wheel_filename_split = wheel_filename.split("-")
# This should be unreachable because the manifest has been validated, add assert.
assert len(wheel_filename_split) >= 5, "Internal error, manifest validation disallows this"
wheel_platform = wheel_filename_split[-1]
return blender_platform_compatible_with_wheel_platform(platform, wheel_platform)
def paths_filter_wheels_by_platform(
wheels: List[str],
platform: str,
) -> List[str]:
"""
All paths are wheels with filenames that follow the wheel spec.
Return wheels which are compatible with the ``platform``.
"""
wheels_result: List[str] = []
for wheel_filepath in wheels:
if blender_platform_compatible_with_wheel_platform_from_filepath(platform, wheel_filepath):
wheels_result.append(wheel_filepath)
return wheels_result
def build_paths_filter_wheels_by_platform(
build_paths: List[Tuple[str, str]],
platform: str,
@ -1997,17 +2042,7 @@ def build_paths_filter_wheels_by_platform(
build_paths_for_platform: List[Tuple[str, str]] = []
for item in build_paths:
# Both the absolute/relative path can be used to get the filename.
# Use the relative since it's likely to be shorter.
wheel_filename = os.path.splitext(os.path.basename(item[1]))[0]
wheel_filename_split = wheel_filename.split("-")
# This should be unreachable because the manifest has been validated, add assert.
assert len(wheel_filename_split) >= 5, "Internal error, manifest validation disallows this"
wheel_platform = wheel_filename_split[-1]
if blender_platform_compatible_with_wheel_platform(platform, wheel_platform):
if blender_platform_compatible_with_wheel_platform_from_filepath(platform, item[1]):
build_paths_for_platform.append(item)
return build_paths_for_platform
@ -2221,7 +2256,7 @@ def pkg_manifest_toml_is_valid_or_error(filepath: str, strict: bool) -> Tuple[Op
return None, result
def pkg_manifest_detect_duplicates(pkg_idname: str, pkg_items: List[PkgManifest]) -> Optional[str]:
def pkg_manifest_detect_duplicates(pkg_items: List[PkgManifest]) -> Optional[str]:
"""
When a repository includes multiple packages with the same ID, ensure they don't conflict.
@ -3177,7 +3212,7 @@ class subcmd_server:
for pkg_idname, pkg_items in repo_data_idname_map.items():
if len(pkg_items) == 1:
continue
if (error := pkg_manifest_detect_duplicates(pkg_idname, pkg_items)) is not None:
if (error := pkg_manifest_detect_duplicates(pkg_items)) is not None:
msglog.warn("archive found with duplicates for id {:s}: {:s}".format(pkg_idname, error))
if html:
@ -3325,6 +3360,7 @@ class subcmd_client:
directories_to_clean: List[str] = []
with CleanupPathsContext(files=(), directories=directories_to_clean):
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(filepath_archive, mode="r")
except Exception as ex:
msglog.error("Error extracting archive: {:s}".format(str(ex)))
@ -3527,9 +3563,6 @@ class subcmd_client:
has_fatal_error = True
continue
def error_handle(ex: Exception) -> None:
msglog.error("{:s}: {:s}".format(pkg_idname, str(ex)))
pkg_info_list = [
pkg_info for pkg_info in pkg_info_list
if not repository_filter_skip(
@ -3537,7 +3570,10 @@ class subcmd_client:
filter_blender_version=blender_version_tuple,
filter_platform=platform_this,
skip_message_fn=None,
error_fn=error_handle,
error_fn=lambda ex: any_as_none(
# pylint: disable-next=cell-var-from-loop
msglog.error("{:s}: {:s}".format(pkg_idname, str(ex))),
),
)
]
@ -3653,7 +3689,7 @@ class subcmd_client:
# Validate:
if filename_archive_size_test != archive_size_expected:
msglog.error("Archive size mismatch \"{:s}\", expected {:d}, was {:d}".format(
msglog.fatal_error("Archive size mismatch \"{:s}\", expected {:d}, was {:d}".format(
pkg_idname,
archive_size_expected,
filename_archive_size_test,
@ -3661,7 +3697,7 @@ class subcmd_client:
return False
filename_archive_hash_test = "sha256:" + sha256.hexdigest()
if filename_archive_hash_test != archive_hash_expected:
msglog.error("Archive checksum mismatch \"{:s}\", expected {:s}, was {:s}".format(
msglog.fatal_error("Archive checksum mismatch \"{:s}\", expected {:s}, was {:s}".format(
pkg_idname,
archive_hash_expected,
filename_archive_hash_test,
@ -3747,9 +3783,8 @@ class subcmd_client:
if (error := rmtree_with_fallback_or_error(filepath_local_pkg)) is not None:
msglog.error("Failure to remove \"{:s}\" with error ({:s})".format(pkg_idname, error))
continue
msglog.status("Removed \"{:s}\"".format(pkg_idname))
else:
msglog.status("Removed \"{:s}\"".format(pkg_idname))
filepath_local_cache_archive = os.path.join(local_cache_dir, pkg_idname + PKG_EXT)
if os.path.exists(filepath_local_cache_archive):
@ -3757,12 +3792,13 @@ class subcmd_client:
if user_dir:
filepath_user_pkg = os.path.join(user_dir, pkg_idname)
if os.path.isdir(filepath_user_pkg):
if os.path.exists(filepath_user_pkg):
if (error := rmtree_with_fallback_or_error(filepath_user_pkg)) is not None:
msglog.error(
"Failure to remove \"{:s}\" user files with error ({:s})".format(pkg_idname, error),
)
continue
else:
msglog.status("Removed cache \"{:s}\"".format(pkg_idname))
return True
@ -3942,7 +3978,7 @@ class subcmd_author:
del build_paths_extra_canonical
except Exception as ex:
msglog.status("Error building path list \"{:s}\"".format(str(ex)))
msglog.fatal_error("Error building path list \"{:s}\"".format(str(ex)))
return False
request_exit = False
@ -3994,9 +4030,10 @@ class subcmd_author:
with CleanupPathsContext(files=(outfile_temp,), directories=()):
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(outfile_temp, 'w', zipfile.ZIP_DEFLATED, compresslevel=9)
except Exception as ex:
msglog.status("Error creating archive \"{:s}\"".format(str(ex)))
msglog.fatal_error("Error creating archive \"{:s}\"".format(str(ex)))
return False
with contextlib.closing(zip_fh_context) as zip_fh:
@ -4011,13 +4048,25 @@ class subcmd_author:
b"# This must not be included in source manifests.\n",
b"[build.generated]\n",
"platforms = [\"{:s}\"]\n".format(platform).encode("utf-8"),
# Including wheels simplifies server side check as this list can be tested
# without the server having to filter by platform too.
b"wheels = [",
", ".join([
# NOTE: accept no string escaping as the rules for wheel paths
# are already strict so strings don't require quoting.
"\"{:s}\"".format(wheel) for wheel in paths_filter_wheels_by_platform(
manifest.wheels or [],
platform,
)
]).encode("utf-8"),
b"]\n"
b"# END GENERATED CONTENT.\n",
))
try:
with open(filepath_abs, "rb") as temp_fh:
zip_data_override = temp_fh.read() + zip_data_override
except Exception as ex:
msglog.status("Error overriding manifest \"{:s}\"".format(str(ex)))
msglog.fatal_error("Error overriding manifest \"{:s}\"".format(str(ex)))
return False
# Handy for testing that sub-directories:
@ -4029,7 +4078,7 @@ class subcmd_author:
else:
zip_fh.write(filepath_abs, filepath_rel, compress_type=compress_type)
except Exception as ex:
msglog.status("Error adding to archive \"{:s}\"".format(str(ex)))
msglog.fatal_error("Error adding to archive \"{:s}\"".format(str(ex)))
return False
if verbose:
@ -4140,6 +4189,7 @@ class subcmd_author:
# extract the archive into a temporary directory and run validation there.
try:
# pylint: disable-next=consider-using-with
zip_fh_context = zipfile.ZipFile(pkg_source_archive, mode="r")
except Exception as ex:
msglog.status("Error extracting archive \"{:s}\"".format(str(ex)))
@ -4147,14 +4197,14 @@ class subcmd_author:
with contextlib.closing(zip_fh_context) as zip_fh:
if (archive_subdir := pkg_zipfile_detect_subdir_or_none(zip_fh)) is None:
msglog.status("Error, archive has no manifest: \"{:s}\"".format(PKG_MANIFEST_FILENAME_TOML))
msglog.fatal_error("Error, archive has no manifest: \"{:s}\"".format(PKG_MANIFEST_FILENAME_TOML))
return False
# Demote errors to status as the function of this action is to check the manifest is stable.
manifest = pkg_manifest_from_zipfile_and_validate_all_errors(zip_fh, archive_subdir, strict=True)
if isinstance(manifest, list):
msglog.status("Error parsing TOML in \"{:s}\"".format(pkg_source_archive))
msglog.fatal_error("Error parsing TOML in \"{:s}\"".format(pkg_source_archive))
for error_msg in manifest:
msglog.status(error_msg)
msglog.fatal_error(error_msg)
return False
if valid_tags_filepath:
@ -4182,7 +4232,7 @@ class subcmd_author:
ok = True
for filepath in expected_files:
if zip_fh.NameToInfo.get(filepath) is None:
msglog.status("Error, file missing from {:s}: \"{:s}\"".format(
msglog.fatal_error("Error, file missing from {:s}: \"{:s}\"".format(
manifest.type,
filepath,
))
@ -4785,6 +4835,7 @@ def main(
# While this is typically the case, is only guaranteed to be `TextIO` so check `reconfigure` is available.
if not isinstance(fh, io.TextIOWrapper):
continue
# pylint: disable-next=no-member; False positive.
if fh.encoding.lower().partition(":")[0] == "utf-8":
continue
fh.reconfigure(encoding="utf-8")

@ -91,7 +91,9 @@ class HTTPServerContext:
http_thread.daemon = True
http_thread.start()
# pylint: disable-next=attribute-defined-outside-init
self._http_thread = http_thread
# pylint: disable-next=attribute-defined-outside-init
self._http_server = http_server
def __exit__(self, _type: Any, _value: Any, traceback: Any) -> None:

@ -111,6 +111,7 @@ classifiers = [
cwd=temp_dir,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
check=True,
)
result = search(temp_dir, lambda entry: entry.name.endswith(".whl"))

@ -15,9 +15,8 @@ import sys
import tempfile
import tomllib
import unittest
import zipfile
import unittest.util
import zipfile
from typing import (
Any,

@ -291,6 +291,8 @@ def run_blender(
},
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
# Allow the caller to read a non-zero return-code.
check=False,
)
stdout = output.stdout.decode("utf-8")
stderr = output.stderr.decode("utf-8")
@ -754,11 +756,8 @@ class TestPlatform(TestWithTempBlenderUser_MixIn, unittest.TestCase):
def main() -> None:
global TEMP_DIR_BLENDER_USER
global TEMP_DIR_REMOTE
global TEMP_DIR_LOCAL
global TEMP_DIR_TMPDIR
global TEMP_DIR_REMOTE_AS_URL
# pylint: disable-next=global-statement
global TEMP_DIR_BLENDER_USER, TEMP_DIR_REMOTE, TEMP_DIR_LOCAL, TEMP_DIR_TMPDIR, TEMP_DIR_REMOTE_AS_URL
with tempfile.TemporaryDirectory() as temp_prefix:
TEMP_DIR_BLENDER_USER = os.path.join(temp_prefix, "bl_ext_blender")

@ -2617,7 +2617,6 @@ class USERPREF_PT_addons(AddOnPanel, Panel):
if filter in {"All", "Enabled"}:
# Append missing scripts
# First collect scripts that are used but have no script file.
module_names = {mod.__name__ for mod in addon_modules}
missing_modules = {
addon_module_name for addon_module_name in used_addon_module_name_map
if addon_module_name not in module_names

@ -9853,7 +9853,7 @@ static void rna_def_modifier_grease_pencil_multiply(BlenderRNA *brna)
RNA_def_property_int_sdna(prop, nullptr, "duplications");
RNA_def_property_range(prop, 0, 999);
RNA_def_property_ui_range(prop, 1, 10, 1, 1);
RNA_def_property_ui_text(prop, "duplicates", "How many copies of strokes be displayed");
RNA_def_property_ui_text(prop, "Duplicates", "How many copies of strokes be displayed");
RNA_def_property_update(prop, 0, "rna_Modifier_update");
prop = RNA_def_property(srna, "distance", PROP_FLOAT, PROP_DISTANCE);

@ -195,7 +195,6 @@ def canonical_author_map() -> Dict[str, str]:
"Daniel Salazar <zanqdo@gmail.com>": (
"Daniel Salazar <zanqdo>",
"Daniel Salazar <zanqdo@noreply.localhost>",
"Daniel Santana <dgsantana>",
"ZanQdo <zanqdo@gmail.com>",
"zanqdo <zanqdo@gmail.com>",
),