From 6943dcb03ad2acaa83c3472e3820cfeb4b4504c5 Mon Sep 17 00:00:00 2001 From: longhao Date: Sat, 13 Dec 2025 15:30:59 +0800 Subject: [PATCH 1/4] fix: resolve Blender 2.83+ PySide compatibility issues - Add generic bpy.app module wrapper for Shiboken compatibility - Patch both bpy.app.translations and bpy.app.handlers (fixes #127) - Fix logging.basicConfig encoding parameter for Python < 3.9 (Blender 2.83) - Use __slots__ for memory optimization in wrapper class Thanks to @Jerakin for the suggestions in PR #128 and #133 Fixes: https://github.com/techartorg/bqt/issues/127 Related: https://github.com/techartorg/bqt/pull/133 --- bqt/__init__.py | 128 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/bqt/__init__.py b/bqt/__init__.py index 284d117..6cb18e8 100644 --- a/bqt/__init__.py +++ b/bqt/__init__.py @@ -4,11 +4,127 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. """ -import os +# CRITICAL: Fix bpy.app submodules BEFORE importing any Qt/PySide modules +# This prevents Shiboken from failing when it tries to introspect bpy.app.translations/handlers +# See: https://github.com/techartorg/bqt/issues/127 import sys -from pathlib import Path + +# Modules that need patching for Shiboken compatibility +_BPY_APP_MODULES_TO_PATCH = ['translations', 'handlers'] + + +def _create_bpy_app_wrapper(original, module_name): + """Create a compatibility wrapper for bpy.app submodules. + + Args: + original: The original bpy.app submodule object + module_name: Name of the module (e.g., 'translations', 'handlers') + """ + class BpyAppModuleWrapper: + __slots__ = ('_orig',) + + def __init__(self, orig): + object.__setattr__(self, '_orig', orig) + + def __getattr__(self, name): + # Provide missing attributes that Shiboken expects + if name == '__name__': + return 'bpy.app.{}'.format(module_name) + elif name == '__module__': + return 'bpy.app' + elif name == '__qualname__': + return module_name + elif name == '__ne__': + return lambda self, other: not self.__eq__(other) + elif name == '__doc__': + return 'Blender {} compatibility wrapper'.format(module_name) + return getattr(object.__getattribute__(self, '_orig'), name) + + def __setattr__(self, name, value): + if name == '_orig': + object.__setattr__(self, name, value) + else: + setattr(object.__getattribute__(self, '_orig'), name, value) + + def __dir__(self): + orig = object.__getattribute__(self, '_orig') + orig_attrs = dir(orig) if hasattr(orig, '__dir__') else [] + extra_attrs = ['__name__', '__module__', '__qualname__', '__ne__', '__doc__'] + return list(set(orig_attrs + extra_attrs)) + + def __repr__(self): + return ''.format(module_name) + + return BpyAppModuleWrapper(original) + + +def _patch_bpy_module_in_sys_modules(module_name): + """Patch bpy.app.{module_name} in sys.modules if it exists and needs patching.""" + full_name = "bpy.app.{}".format(module_name) + if full_name not in sys.modules: + return False + + module = sys.modules[full_name] + required_attrs = ['__name__', '__module__', '__qualname__'] + + # Only patch if missing required attributes + if all(hasattr(module, attr) for attr in required_attrs): + return False + + wrapped = _create_bpy_app_wrapper(module, module_name) + sys.modules[full_name] = wrapped + return True + + +def _fix_existing_bpy_module(module_name): + """Fix bpy.app.{module_name} if it already exists and needs patching.""" + import bpy + if not hasattr(bpy.app, module_name): + return False + + module = getattr(bpy.app, module_name) + required_attrs = ['__name__', '__module__', '__qualname__'] + + # Only fix if missing required attributes + if all(hasattr(module, attr) for attr in required_attrs): + return False + + # Try direct attribute setting first + attrs_to_add = { + '__name__': 'bpy.app.{}'.format(module_name), + '__module__': 'bpy.app', + '__qualname__': module_name, + } + + try: + for attr_name, attr_value in attrs_to_add.items(): + if not hasattr(module, attr_name): + setattr(module, attr_name, attr_value) + return True + except (AttributeError, TypeError): + # If direct setting fails, use wrapper + wrapped = _create_bpy_app_wrapper(module, module_name) + setattr(bpy.app, module_name, wrapped) + return True +def _apply_bpy_app_patches(): + """Apply all bpy.app module patches. Order matters!""" + # First patch sys.modules + for module_name in _BPY_APP_MODULES_TO_PATCH: + _patch_bpy_module_in_sys_modules(module_name) + + # Then fix existing modules + for module_name in _BPY_APP_MODULES_TO_PATCH: + _fix_existing_bpy_module(module_name) + + +# Apply patches immediately before any Qt/PySide imports +_apply_bpy_app_patches() + +import os +from pathlib import Path + # add to sys path so we can import bqt current_dir = str(Path(__file__).parent.parent) if current_dir not in sys.path: @@ -92,12 +208,16 @@ def setup_logger(): logger.debug("BQT_LOG_LEVEL not set, using default 'WARNING'") log_level_name = "WARNING" if log_level_name not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: - logger.warning(f"BQT_LOG_LEVEL is set to invalid value '{log_level_name}', using default 'WARNING'") + logger.warning("BQT_LOG_LEVEL is set to invalid value '{}', using default 'WARNING'".format(log_level_name)) log_level_name = "WARNING" # print(f"BQT_LOG_LEVEL is set to '{log_level_name}'") log_level = logging.getLevelName(log_level_name) logger.setLevel(log_level) - logging.basicConfig(encoding='utf-8') + # encoding parameter was added in Python 3.9, use conditional for Blender 2.83 compatibility + if sys.version_info >= (3, 9): + logging.basicConfig(encoding='utf-8') + else: + logging.basicConfig() def register(): From 8969fc24b3587394150d1a9cbcdff5f84012019c Mon Sep 17 00:00:00 2001 From: hannes Date: Mon, 15 Dec 2025 13:56:01 +0000 Subject: [PATCH 2/4] move to separate file --- bqt/__init__.py | 4 ++++ bqt/pyside_compatibility.py | 0 2 files changed, 4 insertions(+) create mode 100644 bqt/pyside_compatibility.py diff --git a/bqt/__init__.py b/bqt/__init__.py index e057fc0..8cddf55 100644 --- a/bqt/__init__.py +++ b/bqt/__init__.py @@ -4,6 +4,10 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. """ +import bqt.pyside_compatibility +# Apply patches immediately before any Qt/PySide imports +bqt.pyside_compatibility._apply_bpy_app_patches() + import os import sys from pathlib import Path diff --git a/bqt/pyside_compatibility.py b/bqt/pyside_compatibility.py new file mode 100644 index 0000000..e69de29 From 74c796233ffbfe43557d68819b40d3eef67955f9 Mon Sep 17 00:00:00 2001 From: hannes Date: Mon, 15 Dec 2025 15:03:03 +0000 Subject: [PATCH 3/4] commit file --- bqt/pyside_compatibility.py | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/bqt/pyside_compatibility.py b/bqt/pyside_compatibility.py index e69de29..95c6a26 100644 --- a/bqt/pyside_compatibility.py +++ b/bqt/pyside_compatibility.py @@ -0,0 +1,114 @@ +# CRITICAL: Fix bpy.app submodules BEFORE importing any Qt/PySide modules +# This prevents Shiboken from failing when it tries to introspect bpy.app.translations/handlers +# See: https://github.com/techartorg/bqt/issues/127 + +import sys + + +# Modules that need patching for Shiboken compatibility +_BPY_APP_MODULES_TO_PATCH = ['translations', 'handlers'] + + +def _create_bpy_app_wrapper(original, module_name): + """Create a compatibility wrapper for bpy.app submodules. + Args: + original: The original bpy.app submodule object + module_name: Name of the module (e.g., 'translations', 'handlers') + """ + class BpyAppModuleWrapper: + __slots__ = ('_orig',) + + def __init__(self, orig): + object.__setattr__(self, '_orig', orig) + + def __getattr__(self, name): + # Provide missing attributes that Shiboken expects + if name == '__name__': + return 'bpy.app.{}'.format(module_name) + elif name == '__module__': + return 'bpy.app' + elif name == '__qualname__': + return module_name + elif name == '__ne__': + return lambda self, other: not self.__eq__(other) + elif name == '__doc__': + return 'Blender {} compatibility wrapper'.format(module_name) + return getattr(object.__getattribute__(self, '_orig'), name) + + def __setattr__(self, name, value): + if name == '_orig': + object.__setattr__(self, name, value) + else: + setattr(object.__getattribute__(self, '_orig'), name, value) + + def __dir__(self): + orig = object.__getattribute__(self, '_orig') + orig_attrs = dir(orig) if hasattr(orig, '__dir__') else [] + extra_attrs = ['__name__', '__module__', '__qualname__', '__ne__', '__doc__'] + return list(set(orig_attrs + extra_attrs)) + + def __repr__(self): + return ''.format(module_name) + + return BpyAppModuleWrapper(original) + + +def _patch_bpy_module_in_sys_modules(module_name): + """Patch bpy.app.{module_name} in sys.modules if it exists and needs patching.""" + full_name = "bpy.app.{}".format(module_name) + if full_name not in sys.modules: + return False + + module = sys.modules[full_name] + required_attrs = ['__name__', '__module__', '__qualname__'] + + # Only patch if missing required attributes + if all(hasattr(module, attr) for attr in required_attrs): + return False + + wrapped = _create_bpy_app_wrapper(module, module_name) + sys.modules[full_name] = wrapped + return True + + +def _fix_existing_bpy_module(module_name): + """Fix bpy.app.{module_name} if it already exists and needs patching.""" + import bpy + if not hasattr(bpy.app, module_name): + return False + + module = getattr(bpy.app, module_name) + required_attrs = ['__name__', '__module__', '__qualname__'] + + # Only fix if missing required attributes + if all(hasattr(module, attr) for attr in required_attrs): + return False + + # Try direct attribute setting first + attrs_to_add = { + '__name__': 'bpy.app.{}'.format(module_name), + '__module__': 'bpy.app', + '__qualname__': module_name, + } + + try: + for attr_name, attr_value in attrs_to_add.items(): + if not hasattr(module, attr_name): + setattr(module, attr_name, attr_value) + return True + except (AttributeError, TypeError): + # If direct setting fails, use wrapper + wrapped = _create_bpy_app_wrapper(module, module_name) + setattr(bpy.app, module_name, wrapped) + return True + + +def _apply_bpy_app_patches(): + """Apply all bpy.app module patches. Order matters!""" + # First patch sys.modules + for module_name in _BPY_APP_MODULES_TO_PATCH: + _patch_bpy_module_in_sys_modules(module_name) + + # Then fix existing modules + for module_name in _BPY_APP_MODULES_TO_PATCH: + _fix_existing_bpy_module(module_name) From b7349fa6ca7e9fa431e5189a6458ab4e173511df Mon Sep 17 00:00:00 2001 From: hannes Date: Mon, 15 Dec 2025 15:04:27 +0000 Subject: [PATCH 4/4] refactor --- bqt/__init__.py | 4 ++-- bqt/{pyside_compatibility.py => __pyside_compatibility.py} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename bqt/{pyside_compatibility.py => __pyside_compatibility.py} (99%) diff --git a/bqt/__init__.py b/bqt/__init__.py index 8cddf55..bef85f2 100644 --- a/bqt/__init__.py +++ b/bqt/__init__.py @@ -4,9 +4,9 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. """ -import bqt.pyside_compatibility +import bqt.__pyside_compatibility # Apply patches immediately before any Qt/PySide imports -bqt.pyside_compatibility._apply_bpy_app_patches() +bqt.__pyside_compatibility.apply_bpy_app_patches() import os import sys diff --git a/bqt/pyside_compatibility.py b/bqt/__pyside_compatibility.py similarity index 99% rename from bqt/pyside_compatibility.py rename to bqt/__pyside_compatibility.py index 95c6a26..89e7d5d 100644 --- a/bqt/pyside_compatibility.py +++ b/bqt/__pyside_compatibility.py @@ -103,7 +103,7 @@ def _fix_existing_bpy_module(module_name): return True -def _apply_bpy_app_patches(): +def apply_bpy_app_patches(): """Apply all bpy.app module patches. Order matters!""" # First patch sys.modules for module_name in _BPY_APP_MODULES_TO_PATCH: