Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src_py/_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -32,12 +33,35 @@ 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
9 changes: 3 additions & 6 deletions src_py/_lbug_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions test/test_backend_errors.py
Original file line number Diff line number Diff line change
@@ -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)
Loading