From d42e8af9c145a8f2a9c1fc1bf9b7054cac49503d Mon Sep 17 00:00:00 2001 From: gyomh Date: Wed, 8 Jul 2026 16:06:28 +0200 Subject: [PATCH] Add version-aware dependency validation and fix registration masking check_module()/store_package_show() previously marked any importable module as satisfying its requirement regardless of installed version, so a stale SpoutGL/syphon-python/cyndilib install was silently reported as fine. Add version comparison (_version_satisfies) shared by both detection paths so outdated installs are correctly rejected. If a dependency then raised anything other than ModuleNotFoundError during startup (e.g. AttributeError from a missing method on an old version), the exception escaped uncaught after pip_importer's classes were already registered. Blender disabled the addon but left those classes registered, so the next enable attempt failed on "already registered as a subclass" instead of showing the real error. Catch the broader exception and roll back pip_importer's registration so the actual failure is reported. Also surface "Outdated (installed -> required)" in the preferences panel instead of the generic "Not installed" label when a stale version is detected, using the version captured by the checks above. --- __init__.py | 10 ++++++ pip_importer.py | 83 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/__init__.py b/__init__.py index 20c2149..8b90b86 100644 --- a/__init__.py +++ b/__init__.py @@ -65,6 +65,16 @@ def register(): print( "Addon isn't available, install required module via Properties > Addons > TextureSharing" ) + except Exception as e: + # Any other failure here (e.g. an outdated dependency raising + # AttributeError instead of ModuleNotFoundError) must not leave the + # pip_importer classes registered: otherwise the next enable attempt + # crashes on "already registered as a subclass" instead of showing + # the real underlying error. + pip_importer.unregister() + print( + "TextureSharing failed to start due to an unexpected error: {}".format(e) + ) def unregister(): try: diff --git a/pip_importer.py b/pip_importer.py index 943fdda..b056fdf 100644 --- a/pip_importer.py +++ b/pip_importer.py @@ -30,13 +30,74 @@ class Package: _author: str = "" _license: str = "" _location: str = "" - + _installed_version: str = "" + @property def module(self) -> str: if self.custom_module is None: return self.name return self.custom_module +# comparison operators accepted in a package's version spec (e.g. "==0.1.1"), +# ordered so that two-character operators are matched before their prefix +_VERSION_OPERATORS = ("==", "!=", ">=", "<=", "~=", ">", "<") + +def _parse_version_spec(spec): + spec = spec.strip() + for op in _VERSION_OPERATORS: + if spec.startswith(op): + return op, spec[len(op):].strip() + return "==", spec + +def _version_key(v): + # best-effort numeric parse of a dotted version string, ignoring any + # pre/post-release suffixes (e.g. "0.1.1rc1" -> (0, 1, 1)) + parts = [] + for chunk in v.split("."): + digits = "" + for ch in chunk: + if ch.isdigit(): + digits += ch + else: + break + parts.append(int(digits) if digits else 0) + return tuple(parts) + +def _version_satisfies(installed, spec): + # returns whether the installed version string satisfies the package's + # required version spec (e.g. "==0.1.1", ">=0.1.0"). An empty spec means + # any installed version is acceptable. + if not spec: + return True + if not installed: + return False + + op, required = _parse_version_spec(spec) + iv, rv = _version_key(installed), _version_key(required) + + if op == "==": + return iv == rv + if op == "!=": + return iv != rv + if op == ">=": + return iv >= rv + if op == "<=": + return iv <= rv + if op == ">": + return iv > rv + if op == "<": + return iv < rv + if op == "~=": + return iv >= rv and iv[:-1] == rv[:-1] + return iv == rv + +def _get_installed_version(package): + try: + from importlib.metadata import version as pkg_version + return pkg_version(package.name) + except Exception: + return "" + def add_package(package): pip_packages.append(package) @@ -61,7 +122,8 @@ def check_module(package): module = sys.modules[package.module] if hasattr(module, '__path__'): package._location = module.__path__[0] - package._registered = True + package._installed_version = _get_installed_version(package) + package._registered = _version_satisfies(package._installed_version, package.version) except KeyError: package._registered = False @@ -123,11 +185,12 @@ def store_package_show(package, result): package._home_page = data.get('Home-page') package._author = data.get('Author') package._license = data.get('License') - package._location = data.get('Location') - package._registered = True + package._location = data.get('Location') + package._installed_version = data.get('Version', '') + package._registered = _version_satisfies(package._installed_version, package.version) + + return package._registered - return True - return False def check_modules(): @@ -238,7 +301,13 @@ def draw(self, context): ).package_path=package.name else: allInstalled = False - row.label(text="Not installed", icon="CANCEL") + if package._installed_version: + row.label( + text="Outdated ({} -> {})".format(package._installed_version, package.version), + icon="CANCEL", + ) + else: + row.label(text="Not installed", icon="CANCEL") if package.install_manualy: row.prop(spout_addon_props, 'my_file_path') row.operator(