mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
fix(openai): guard httpx finalizers (#37570)
Same shape as the merged anthropic patch in #37064, ported to `libs/partners/openai`. `_SyncHttpxClientWrapper.__del__` / `_AsyncHttpxClientWrapper.__del__` check `self.is_closed`, which reads `self._state`. When a wrapper is created without `__init__` running to completion — `copy.deepcopy` via `__new__` + `__setstate__`, or a constructor that raised partway through — `_state` is missing and the finalizer prints ``` Exception ignored in: <function _SyncHttpxClientWrapper.__del__ at 0x...> Traceback (most recent call last): File ".../langchain_openai/chat_models/_client_utils.py", line 366, in __del__ if self.is_closed: File ".../httpx/_client.py", line 228, in is_closed return self._state == ClientState.CLOSED AttributeError: '_SyncHttpxClientWrapper' object has no attribute '_state' ``` at GC time. Same noise pattern that #37064 fixed for the anthropic partner. Hoist the `is_closed` access inside the existing `try/except` so the `AttributeError` is swallowed alongside the `close()` / `aclose()` exceptions that block already handles. Tests: two new unit tests build the wrappers via `__new__` (no `__init__` → no `_state`) and call `__del__` directly, mirroring the tests added in #37064. Verified: - `cd libs/partners/openai && make format` -> all checks passed - `cd libs/partners/openai && make test TEST_FILE=tests/unit_tests/chat_models/test_client_utils.py` -> 37 passed, 1 skipped (linux-only) - `cd libs/partners/openai && make lint` -> all checks passed, mypy clean
This commit is contained in:
@@ -363,10 +363,9 @@ class _SyncHttpxClientWrapper(openai.DefaultHttpxClient):
|
|||||||
"""Borrowed from openai._base_client."""
|
"""Borrowed from openai._base_client."""
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
|
try:
|
||||||
if self.is_closed:
|
if self.is_closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
self.close()
|
self.close()
|
||||||
except Exception: # noqa: S110
|
except Exception: # noqa: S110
|
||||||
pass
|
pass
|
||||||
@@ -376,10 +375,9 @@ class _AsyncHttpxClientWrapper(openai.DefaultAsyncHttpxClient):
|
|||||||
"""Borrowed from openai._base_client."""
|
"""Borrowed from openai._base_client."""
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
|
try:
|
||||||
if self.is_closed:
|
if self.is_closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
# TODO(someday): support non asyncio runtimes here
|
# TODO(someday): support non asyncio runtimes here
|
||||||
asyncio.get_running_loop().create_task(self.aclose())
|
asyncio.get_running_loop().create_task(self.aclose())
|
||||||
except Exception: # noqa: S110
|
except Exception: # noqa: S110
|
||||||
|
|||||||
@@ -810,3 +810,21 @@ def test_client_build_applies_socket_options_when_user_opts_in(
|
|||||||
assert all(tuple(opts) == tuple(explicit) for opts in recorded), (
|
assert all(tuple(opts) == tuple(explicit) for opts in recorded), (
|
||||||
f"expected user-supplied opts, got {recorded!r}"
|
f"expected user-supplied opts, got {recorded!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_client_wrapper_del_handles_uninitialized_client() -> None:
|
||||||
|
"""Test sync wrapper finalizer handles clients without initialized state."""
|
||||||
|
client = _client_utils._SyncHttpxClientWrapper.__new__(
|
||||||
|
_client_utils._SyncHttpxClientWrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
client.__del__()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_client_wrapper_del_handles_uninitialized_client() -> None:
|
||||||
|
"""Test async wrapper finalizer handles clients without initialized state."""
|
||||||
|
client = _client_utils._AsyncHttpxClientWrapper.__new__(
|
||||||
|
_client_utils._AsyncHttpxClientWrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
client.__del__()
|
||||||
|
|||||||
Reference in New Issue
Block a user