Skip to content

Commit 7f116d2

Browse files
refactor(client): simplify cleanup (#222)
This removes Client.__del__, but users are not expected to call this directly.
1 parent 1c946dd commit 7f116d2

File tree

4 files changed

+27
-54
lines changed

4 files changed

+27
-54
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ typecheck = { chain = [
8181
]}
8282
"typecheck:pyright" = "pyright"
8383
"typecheck:verify-types" = "pyright --verifytypes finch --ignoreexternal"
84-
"typecheck:mypy" = "mypy --enable-incomplete-feature=Unpack ."
84+
"typecheck:mypy" = "mypy ."
8585

8686
[build-system]
8787
requires = ["hatchling"]

src/finch/_base_client.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import uuid
77
import email
8+
import asyncio
89
import inspect
910
import logging
1011
import platform
@@ -672,9 +673,16 @@ def _idempotency_key(self) -> str:
672673
return f"stainless-python-retry-{uuid.uuid4()}"
673674

674675

676+
class SyncHttpxClientWrapper(httpx.Client):
677+
def __del__(self) -> None:
678+
try:
679+
self.close()
680+
except Exception:
681+
pass
682+
683+
675684
class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
676685
_client: httpx.Client
677-
_has_custom_http_client: bool
678686
_default_stream_cls: type[Stream[Any]] | None = None
679687

680688
def __init__(
@@ -747,15 +755,14 @@ def __init__(
747755
custom_headers=custom_headers,
748756
_strict_response_validation=_strict_response_validation,
749757
)
750-
self._client = http_client or httpx.Client(
758+
self._client = http_client or SyncHttpxClientWrapper(
751759
base_url=base_url,
752760
# cast to a valid type because mypy doesn't understand our type narrowing
753761
timeout=cast(Timeout, timeout),
754762
proxies=proxies,
755763
transport=transport,
756764
limits=limits,
757765
)
758-
self._has_custom_http_client = bool(http_client)
759766

760767
def is_closed(self) -> bool:
761768
return self._client.is_closed
@@ -1135,9 +1142,17 @@ def get_api_list(
11351142
return self._request_api_list(model, page, opts)
11361143

11371144

1145+
class AsyncHttpxClientWrapper(httpx.AsyncClient):
1146+
def __del__(self) -> None:
1147+
try:
1148+
# TODO(someday): support non asyncio runtimes here
1149+
asyncio.get_running_loop().create_task(self.aclose())
1150+
except Exception:
1151+
pass
1152+
1153+
11381154
class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
11391155
_client: httpx.AsyncClient
1140-
_has_custom_http_client: bool
11411156
_default_stream_cls: type[AsyncStream[Any]] | None = None
11421157

11431158
def __init__(
@@ -1210,15 +1225,14 @@ def __init__(
12101225
custom_headers=custom_headers,
12111226
_strict_response_validation=_strict_response_validation,
12121227
)
1213-
self._client = http_client or httpx.AsyncClient(
1228+
self._client = http_client or AsyncHttpxClientWrapper(
12141229
base_url=base_url,
12151230
# cast to a valid type because mypy doesn't understand our type narrowing
12161231
timeout=cast(Timeout, timeout),
12171232
proxies=proxies,
12181233
transport=transport,
12191234
limits=limits,
12201235
)
1221-
self._has_custom_http_client = bool(http_client)
12221236

12231237
def is_closed(self) -> bool:
12241238
return self._client.is_closed

src/finch/_client.py

+4-26
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import os
6-
import asyncio
76
from typing import Any, Union, Mapping
87
from typing_extensions import Self, override
98

@@ -32,6 +31,8 @@
3231
DEFAULT_MAX_RETRIES,
3332
SyncAPIClient,
3433
AsyncAPIClient,
34+
SyncHttpxClientWrapper,
35+
AsyncHttpxClientWrapper,
3536
)
3637

3738
__all__ = [
@@ -217,7 +218,7 @@ def copy(
217218
if http_client is not None:
218219
raise ValueError("The 'http_client' argument is mutually exclusive with 'connection_pool_limits'")
219220

220-
if self._has_custom_http_client:
221+
if not isinstance(self._client, SyncHttpxClientWrapper):
221222
raise ValueError(
222223
"A custom HTTP client has been set and is mutually exclusive with the 'connection_pool_limits' argument"
223224
)
@@ -250,16 +251,6 @@ def copy(
250251
# client.with_options(timeout=10).foo.create(...)
251252
with_options = copy
252253

253-
def __del__(self) -> None:
254-
if not hasattr(self, "_has_custom_http_client") or not hasattr(self, "close"):
255-
# this can happen if the '__init__' method raised an error
256-
return
257-
258-
if self._has_custom_http_client:
259-
return
260-
261-
self.close()
262-
263254
def get_access_token(
264255
self,
265256
code: str,
@@ -523,7 +514,7 @@ def copy(
523514
if http_client is not None:
524515
raise ValueError("The 'http_client' argument is mutually exclusive with 'connection_pool_limits'")
525516

526-
if self._has_custom_http_client:
517+
if not isinstance(self._client, AsyncHttpxClientWrapper):
527518
raise ValueError(
528519
"A custom HTTP client has been set and is mutually exclusive with the 'connection_pool_limits' argument"
529520
)
@@ -556,19 +547,6 @@ def copy(
556547
# client.with_options(timeout=10).foo.create(...)
557548
with_options = copy
558549

559-
def __del__(self) -> None:
560-
if not hasattr(self, "_has_custom_http_client") or not hasattr(self, "close"):
561-
# this can happen if the '__init__' method raised an error
562-
return
563-
564-
if self._has_custom_http_client:
565-
return
566-
567-
try:
568-
asyncio.get_running_loop().create_task(self.close())
569-
except Exception:
570-
pass
571-
572550
async def get_access_token(
573551
self,
574552
code: str,

tests/test_client.py

+2-21
Original file line numberDiff line numberDiff line change
@@ -695,24 +695,15 @@ def test_proxies_option_mutually_exclusive_with_http_client(self) -> None:
695695
http_client=http_client,
696696
)
697697

698-
def test_client_del(self) -> None:
699-
client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
700-
assert not client.is_closed()
701-
702-
client.__del__()
703-
704-
assert client.is_closed()
705-
706698
def test_copied_client_does_not_close_http(self) -> None:
707699
client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
708700
assert not client.is_closed()
709701

710702
copied = client.copy()
711703
assert copied is not client
712704

713-
copied.__del__()
705+
del copied
714706

715-
assert not copied.is_closed()
716707
assert not client.is_closed()
717708

718709
def test_client_context_manager(self) -> None:
@@ -1464,26 +1455,16 @@ async def test_proxies_option_mutually_exclusive_with_http_client(self) -> None:
14641455
http_client=http_client,
14651456
)
14661457

1467-
async def test_client_del(self) -> None:
1468-
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
1469-
assert not client.is_closed()
1470-
1471-
client.__del__()
1472-
1473-
await asyncio.sleep(0.2)
1474-
assert client.is_closed()
1475-
14761458
async def test_copied_client_does_not_close_http(self) -> None:
14771459
client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True)
14781460
assert not client.is_closed()
14791461

14801462
copied = client.copy()
14811463
assert copied is not client
14821464

1483-
copied.__del__()
1465+
del copied
14841466

14851467
await asyncio.sleep(0.2)
1486-
assert not copied.is_closed()
14871468
assert not client.is_closed()
14881469

14891470
async def test_client_context_manager(self) -> None:

0 commit comments

Comments
 (0)