Skip to content

Commit 7bcc788

Browse files
shivanathdclaude
andauthored
fix(backend): report why the _lbug extension failed to load (#72)
* 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 <noreply@anthropic.com>
1 parent c852805 commit 7bcc788

3 files changed

Lines changed: 63 additions & 8 deletions

File tree

‎src_py/_backend.py‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_CAPI_MODULE: Any | None = None
99
_PYBIND_MODULE: Any | None = None
1010
_PYBIND_IMPORT_ATTEMPTED = False
11+
_PYBIND_IMPORT_ERROR: ImportError | None = None
1112

1213

1314
def _import_pybind_module() -> Any:
@@ -32,12 +33,35 @@ def get_capi_module() -> Any:
3233

3334

3435
def get_pybind_module() -> Any | None:
35-
global _PYBIND_MODULE, _PYBIND_IMPORT_ATTEMPTED
36+
global _PYBIND_MODULE, _PYBIND_IMPORT_ATTEMPTED, _PYBIND_IMPORT_ERROR
3637
if _PYBIND_IMPORT_ATTEMPTED:
3738
return _PYBIND_MODULE
3839
_PYBIND_IMPORT_ATTEMPTED = True
3940
try:
4041
_PYBIND_MODULE = _import_pybind_module()
41-
except ImportError:
42+
except ImportError as exc:
4243
_PYBIND_MODULE = None
44+
_PYBIND_IMPORT_ERROR = exc
4345
return _PYBIND_MODULE
46+
47+
48+
def missing_capi_library_message() -> str:
49+
"""
50+
Explain why neither backend could load.
51+
52+
The C API library is the fallback when the bundled ``_lbug`` extension fails to import, so a missing C API
53+
library usually hides the real failure. Include that failure, such as a missing OpenSSL 3 runtime.
54+
"""
55+
msg = (
56+
"Could not find lbug C API shared library. "
57+
"Set LBUG_C_API_LIB_PATH or download a shared lib (e.g. run "
58+
"LBUG_LIB_KIND=shared bash scripts/download_lbug.sh)."
59+
)
60+
if _PYBIND_IMPORT_ERROR is None:
61+
return msg
62+
msg += f" The bundled _lbug extension, which is tried first, failed to load: {_PYBIND_IMPORT_ERROR}"
63+
if "libssl" in str(_PYBIND_IMPORT_ERROR) or "libcrypto" in str(
64+
_PYBIND_IMPORT_ERROR
65+
):
66+
msg += " Ladybug needs an OpenSSL 3 runtime; on macOS, install it with `brew install openssl@3`."
67+
return msg

‎src_py/_lbug_capi.py‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,9 @@ def _resolve_library_path() -> str:
178178
if found:
179179
return found
180180

181-
msg = (
182-
"Could not find lbug C API shared library. "
183-
"Set LBUG_C_API_LIB_PATH or download a shared lib (e.g. run "
184-
"LBUG_LIB_KIND=shared bash scripts/download_lbug.sh)."
185-
)
186-
raise RuntimeError(msg)
181+
from ._backend import missing_capi_library_message
182+
183+
raise RuntimeError(missing_capi_library_message())
187184

188185

189186
_dlopen_mode = getattr(ctypes, "RTLD_GLOBAL", 0) | getattr(ctypes, "RTLD_NOW", 0)

‎test/test_backend_errors.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
import ladybug._backend as backend
4+
5+
6+
def test_missing_capi_message_without_pybind_error(monkeypatch) -> None:
7+
monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", None)
8+
msg = backend.missing_capi_library_message()
9+
assert msg.startswith("Could not find lbug C API shared library.")
10+
assert "_lbug extension" not in msg
11+
12+
13+
def test_missing_capi_message_reports_pybind_error(monkeypatch) -> None:
14+
error = ImportError(
15+
"dlopen(_lbug.cpython-312-darwin.so, 0x0002): Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib"
16+
)
17+
monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", error)
18+
msg = backend.missing_capi_library_message()
19+
assert "Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib" in msg
20+
assert "brew install openssl@3" in msg
21+
22+
23+
def test_failed_pybind_import_is_recorded(monkeypatch) -> None:
24+
error = ImportError("Library not loaded: libcrypto.3.dylib")
25+
26+
def fail() -> None:
27+
raise error
28+
29+
monkeypatch.setattr(backend, "_PYBIND_IMPORT_ATTEMPTED", False)
30+
monkeypatch.setattr(backend, "_PYBIND_MODULE", None)
31+
monkeypatch.setattr(backend, "_PYBIND_IMPORT_ERROR", None)
32+
monkeypatch.setattr(backend, "_import_pybind_module", fail)
33+
assert backend.get_pybind_module() is None
34+
assert "libcrypto.3.dylib" in str(backend._PYBIND_IMPORT_ERROR)

0 commit comments

Comments
 (0)