From 6e435450b2ddcfb1644a1311fcd4c0a0c3c1187c Mon Sep 17 00:00:00 2001 From: Shivanath Devinarayanan Date: Mon, 19 Oct 2026 12:44:03 +0530 Subject: [PATCH 1/2] fix(backend): report why the _lbug extension failed to load When the bundled _lbug extension fails to import, get_pybind_module() swallows the ImportError and ladybug falls back to the C API backend. If the C API shared library is not present either (the published wheels do not ship it), the user only sees "Could not find lbug C API shared library", which hides the real cause. On macOS the usual cause is a missing OpenSSL 3 runtime: the wheels link /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib and exclude it on purpose. Keep the ImportError and include it in the RuntimeError, with an OpenSSL hint when libssl or libcrypto is the missing library. Co-Authored-By: Claude Opus 5.5 --- src_py/_backend.py | 26 ++++++++++++++++++++++++-- src_py/_lbug_capi.py | 9 +++------ test/test_backend_errors.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 test/test_backend_errors.py diff --git a/src_py/_backend.py b/src_py/_backend.py index 1109e62..c382370 100644 --- a/src_py/_backend.py +++ b/src_py/_backend.py @@ -8,6 +8,7 @@ _CAPI_MODULE: Any | None = None _PYBIND_MODULE: Any | None = None _PYBIND_IMPORT_ATTEMPTED = False +_PYBIND_IMPORT_ERROR: ImportError | None = None def _import_pybind_module() -> Any: @@ -32,12 +33,33 @@ def get_capi_module() -> Any: def get_pybind_module() -> Any | None: - global _PYBIND_MODULE, _PYBIND_IMPORT_ATTEMPTED + global _PYBIND_MODULE, _PYBIND_IMPORT_ATTEMPTED, _PYBIND_IMPORT_ERROR if _PYBIND_IMPORT_ATTEMPTED: return _PYBIND_MODULE _PYBIND_IMPORT_ATTEMPTED = True try: _PYBIND_MODULE = _import_pybind_module() - except ImportError: + except ImportError as exc: _PYBIND_MODULE = None + _PYBIND_IMPORT_ERROR = exc return _PYBIND_MODULE + + +def missing_capi_library_message() -> str: + """ + Explain why neither backend could load. + + The C API library is the fallback when the bundled ``_lbug`` extension fails to import, so a missing C API + library usually hides the real failure. Include that failure, such as a missing OpenSSL 3 runtime. + """ + msg = ( + "Could not find lbug C API shared library. " + "Set LBUG_C_API_LIB_PATH or download a shared lib (e.g. run " + "LBUG_LIB_KIND=shared bash scripts/download_lbug.sh)." + ) + if _PYBIND_IMPORT_ERROR is None: + return msg + msg += f" The bundled _lbug extension, which is tried first, failed to load: {_PYBIND_IMPORT_ERROR}" + if "libssl" in str(_PYBIND_IMPORT_ERROR) or "libcrypto" in str(_PYBIND_IMPORT_ERROR): + msg += " Ladybug needs an OpenSSL 3 runtime; on macOS, install it with `brew install openssl@3`." + return msg diff --git a/src_py/_lbug_capi.py b/src_py/_lbug_capi.py index 332249d..95806a6 100644 --- a/src_py/_lbug_capi.py +++ b/src_py/_lbug_capi.py @@ -178,12 +178,9 @@ def _resolve_library_path() -> str: if found: return found - msg = ( - "Could not find lbug C API shared library. " - "Set LBUG_C_API_LIB_PATH or download a shared lib (e.g. run " - "LBUG_LIB_KIND=shared bash scripts/download_lbug.sh)." - ) - raise RuntimeError(msg) + from ._backend import missing_capi_library_message + + raise RuntimeError(missing_capi_library_message()) _dlopen_mode = getattr(ctypes, "RTLD_GLOBAL", 0) | getattr(ctypes, "RTLD_NOW", 0) diff --git a/test/test_backend_errors.py b/test/test_backend_errors.py new file mode 100644 index 0000000..1934051 --- /dev/null +++ b/test/test_backend_errors.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import ladybug._backend as backend + + +def test_missing_capi_message_without_pybind_error(monkeypatch) -> None: + monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", None) + msg = backend.missing_capi_library_message() + assert msg.startswith("Could not find lbug C API shared library.") + assert "_lbug extension" not in msg + + +def test_missing_capi_message_reports_pybind_error(monkeypatch) -> None: + error = ImportError( + "dlopen(_lbug.cpython-312-darwin.so, 0x0002): Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib" + ) + monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", error) + msg = backend.missing_capi_library_message() + assert "Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib" in msg + assert "brew install openssl@3" in msg + + +def test_failed_pybind_import_is_recorded(monkeypatch) -> None: + error = ImportError("Library not loaded: libcrypto.3.dylib") + + def fail() -> None: + raise error + + monkeypatch.setattr(backend, "_PYBIND_IMPORT_ATTEMPTED", False) + monkeypatch.setattr(backend, "_PYBIND_MODULE", None) + monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", None) + monkeypatch.setattr(backend, "_import_pybind_module", fail) + assert backend.get_pybind_module() is None + assert "libcrypto.3.dylib" in str(backend._PYBIND_IMPORT_ERROR) From d16f178e7f52e57c9ce01ee1994ae193f8070e12 Mon Sep 17 00:00:00 2001 From: adsharma Date: Mon, 19 Oct 2026 17:56:24 -0700 Subject: [PATCH 2/2] style: apply black formatting to _backend.py --- src_py/_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src_py/_backend.py b/src_py/_backend.py index c382370..9b1ff49 100644 --- a/src_py/_backend.py +++ b/src_py/_backend.py @@ -60,6 +60,8 @@ def missing_capi_library_message() -> str: if _PYBIND_IMPORT_ERROR is None: return msg msg += f" The bundled _lbug extension, which is tried first, failed to load: {_PYBIND_IMPORT_ERROR}" - if "libssl" in str(_PYBIND_IMPORT_ERROR) or "libcrypto" in str(_PYBIND_IMPORT_ERROR): + if "libssl" in str(_PYBIND_IMPORT_ERROR) or "libcrypto" in str( + _PYBIND_IMPORT_ERROR + ): msg += " Ladybug needs an OpenSSL 3 runtime; on macOS, install it with `brew install openssl@3`." return msg