fix: client close graceful (#22)
Co-authored-by: zhengweijun <weijun.zheng@aminer.cn>
This commit is contained in:
parent
42d6cc34d1
commit
d98e87cb36
9 changed files with 942 additions and 4 deletions
65
examples/example_types.py
Normal file
65
examples/example_types.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict
|
||||
|
||||
Message = dict[str, str] # keys role, content
|
||||
MessageList = list[Message]
|
||||
|
||||
__all__ = ['Message', 'MessageList', 'Templates', 'SamplerBase', 'EvalResult', 'SingleEvalResult', 'Eval']
|
||||
|
||||
Templates = {
|
||||
'base': "{task_template}",
|
||||
|
||||
'meta-chat': "[INST] {task_template} [/INST]",
|
||||
|
||||
'vicuna-chat': "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions. USER: {task_template} ASSISTANT:",
|
||||
|
||||
'lwm-chat': "You are a helpful assistant. USER: {task_template} ASSISTANT: ",
|
||||
|
||||
'command-r-chat': "<BOS_TOKEN><|START_OF_TURN_TOKEN|><|USER_TOKEN|>{task_template}<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>",
|
||||
|
||||
'chatglm-chat': "[gMASK]sop<|user|> \n {task_template}<|assistant|> \n ",
|
||||
|
||||
'glm-4-chat': "[gMASK]<sop><|user|>\n{task_template}<|assistant|>",
|
||||
|
||||
'tgi-glm-4-chat': "<|user|>\n{task_template}<|assistant|>",
|
||||
|
||||
'RWKV': "User: hi\n\nAssistant: Hi. I am your assistant and I will provide expert full response in full details. Please feel free to ask any question and I will always answer it\n\nUser: {task_template}\n\nAssistant:",
|
||||
}
|
||||
|
||||
class SamplerBase:
|
||||
"""
|
||||
Base class for defining a sampling model, which can be evaluated,
|
||||
or used as part of the grading process.
|
||||
"""
|
||||
|
||||
def __call__(self, message_list: MessageList) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvalResult:
|
||||
"""
|
||||
Result of running an evaluation (usually consisting of many samples)
|
||||
"""
|
||||
|
||||
score: Optional[float] = None # top-line metric
|
||||
metrics: Optional[Dict[str, float]] = None # other metrics
|
||||
|
||||
|
||||
@dataclass
|
||||
class SingleEvalResult:
|
||||
"""
|
||||
Result of evaluating a single sample
|
||||
"""
|
||||
|
||||
score: Optional[float] = None # top-line metric
|
||||
metrics: Dict[str, float] = field(default_factory=dict) # other metrics with default empty dict
|
||||
|
||||
|
||||
class Eval:
|
||||
"""
|
||||
Base class for defining an evaluation.
|
||||
"""
|
||||
|
||||
def __call__(self, sampler: SamplerBase) -> EvalResult:
|
||||
raise NotImplementedError
|
||||
113
examples/glm4_5_thinking.py
Normal file
113
examples/glm4_5_thinking.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from example_types import MessageList, SamplerBase
|
||||
from zai import ZhipuAiClient
|
||||
|
||||
|
||||
class ZaiSampler(SamplerBase):
|
||||
"""
|
||||
Sample from TGI's completion API
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "glm-4.5",
|
||||
api_key: str = '',
|
||||
system_message: Optional[str] = None,
|
||||
temperature: float = 0.0,
|
||||
max_tokens: int = 4096,
|
||||
stream: bool = False,
|
||||
):
|
||||
self.system_message = system_message
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.model = model
|
||||
self.client = ZhipuAiClient(api_key=api_key)
|
||||
self.stream = stream
|
||||
|
||||
def get_resp(self, message_list):
|
||||
for _ in range(3):
|
||||
try:
|
||||
chat_completion = self.client.chat.completions.create(
|
||||
messages=message_list,
|
||||
model=self.model,
|
||||
temperature=self.temperature,
|
||||
top_p=self.top_p,
|
||||
max_tokens=self.max_tokens
|
||||
)
|
||||
output = chat_completion.choices[0].message.content
|
||||
return output
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}\nTraceback: {traceback.format_exc()}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
print(f"failed, last exception: {e if 'e' in locals() else ''}")
|
||||
return ''
|
||||
|
||||
|
||||
def get_resp_stream(self, message_list, top_p=-1, temperature=-1):
|
||||
temperature = temperature if temperature > 0 else self.temperature
|
||||
top_p = top_p if top_p > 0 else 0.95
|
||||
final = ''
|
||||
for _ in range(200):
|
||||
try:
|
||||
chat_completion_res = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=message_list,
|
||||
thinking={
|
||||
"type": "enabled",
|
||||
},
|
||||
stream=True,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
for chunk in chat_completion_res:
|
||||
if chunk.choices[0].delta.content:
|
||||
final += chunk.choices[0].delta.content
|
||||
break
|
||||
except Exception as e:
|
||||
final = ""
|
||||
print(f"Exception: {e}\nTraceback: {traceback.format_exc()}")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
if final == '':
|
||||
print(f"failed in get_resp for 50 times, last exception: {e if 'e' in locals() else ''}")
|
||||
return ''
|
||||
|
||||
content = ''
|
||||
if '</think>' in final:
|
||||
content = final.split("</think>")[-1].strip()
|
||||
if not content:
|
||||
content = final[-512:].strip()
|
||||
else:
|
||||
content = final[-512:].strip()
|
||||
|
||||
return content
|
||||
|
||||
def __call__(self, message_list: MessageList, top_p=0.95, temperature=0.6) -> str:
|
||||
if self.system_message:
|
||||
message_list = [
|
||||
{
|
||||
"role": "system", "content": self.system_message
|
||||
}
|
||||
] + message_list
|
||||
|
||||
if not self.stream:
|
||||
return self.get_resp(message_list, top_p, temperature)
|
||||
else:
|
||||
return self.get_resp_stream(message_list, top_p, temperature)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = ZaiSampler(model="glm-4.5", api_key=os.getenv("ZAI_API_KEY"), stream=True)
|
||||
messages = [
|
||||
{"role": "user", "content": "Hi?"},
|
||||
]
|
||||
response = client(messages)
|
||||
print(response)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "zai-sdk"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
description = "A SDK library for accessing big model apis from Z.ai"
|
||||
authors = ["Z.ai"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -204,7 +204,13 @@ class BaseClient(HttpClient):
|
|||
if self._has_custom_http_client:
|
||||
return
|
||||
|
||||
self.close()
|
||||
try:
|
||||
# Check if client is still valid before closing
|
||||
if hasattr(self, '_client') and self._client is not None:
|
||||
self.close()
|
||||
except Exception:
|
||||
# Ignore any exceptions during cleanup to avoid masking the original error
|
||||
pass
|
||||
|
||||
|
||||
class ZaiClient(BaseClient):
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
__title__ = 'Z.ai'
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '0.0.2'
|
||||
|
|
|
|||
|
|
@ -481,7 +481,12 @@ class HttpClient:
|
|||
return self._client.is_closed
|
||||
|
||||
def close(self):
|
||||
self._client.close()
|
||||
try:
|
||||
if hasattr(self, '_client') and self._client is not None and not self._client.is_closed:
|
||||
self._client.close()
|
||||
except Exception:
|
||||
# Ignore any exceptions during cleanup to avoid masking the original error
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
|
|
|||
267
tests/integration_tests/test_client_cleanup_integration.py
Normal file
267
tests/integration_tests/test_client_cleanup_integration.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
"""
|
||||
Integration tests for client cleanup functionality
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add src to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from zai import ZaiClient, ZhipuAiClient
|
||||
|
||||
|
||||
class TestClientCleanupIntegration(unittest.TestCase):
|
||||
"""Integration tests for client cleanup functionality"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
# Set a dummy API key for testing
|
||||
os.environ['ZAI_API_KEY'] = 'test-api-key'
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
if 'ZAI_API_KEY' in os.environ:
|
||||
del os.environ['ZAI_API_KEY']
|
||||
|
||||
def test_context_manager_with_api_calls(self):
|
||||
"""Test context manager with actual API calls (mocked)"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
with ZaiClient() as client:
|
||||
# Simulate API call
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate some API operations
|
||||
# (In real usage, these would be actual API calls)
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
# After our fix, the client should still be open because we check is_closed
|
||||
# and don't close if it's already closed
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_explicit_close_with_api_calls(self):
|
||||
"""Test explicit close with actual API calls (mocked)"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
client = ZaiClient()
|
||||
try:
|
||||
# Simulate API call
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate some API operations
|
||||
# (In real usage, these would be actual API calls)
|
||||
pass
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
# Client should be closed
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_multiple_clients_in_sequence(self):
|
||||
"""Test multiple clients created and closed in sequence"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
# Create and close multiple clients
|
||||
for i in range(3):
|
||||
with ZaiClient() as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Verify that close was called for each client
|
||||
# Our fix allows multiple close calls, so the count might be higher
|
||||
self.assertGreaterEqual(mock_client.close.call_count, 3)
|
||||
|
||||
def test_client_reuse_after_close(self):
|
||||
"""Test reusing a client after it has been closed"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
client = ZaiClient()
|
||||
client.close()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Try to close again - should not raise exception
|
||||
client.close()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
def test_zhipu_client_cleanup_integration(self):
|
||||
"""Test ZhipuAiClient cleanup integration"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
with ZhipuAiClient() as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_client_with_custom_http_client_integration(self):
|
||||
"""Test client with custom HTTP client integration"""
|
||||
import httpx
|
||||
|
||||
# Create a real httpx client for testing
|
||||
custom_client = httpx.Client()
|
||||
|
||||
with ZaiClient(http_client=custom_client) as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
# The client is actually closed in this case
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
# Custom client should be closed by our client
|
||||
self.assertTrue(custom_client.is_closed)
|
||||
|
||||
# No need to clean up custom client as it's already closed
|
||||
|
||||
def test_client_cleanup_with_exception_handling(self):
|
||||
"""Test client cleanup when exceptions occur during API calls"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
try:
|
||||
with ZaiClient() as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate an exception during API call
|
||||
raise Exception("Simulated API error")
|
||||
except Exception:
|
||||
# Exception should be caught and client should still be closed
|
||||
pass
|
||||
|
||||
# Client should be closed even after exception
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_client_cleanup_with_timeout(self):
|
||||
"""Test client cleanup with timeout scenarios"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
# Create client with custom timeout
|
||||
import httpx
|
||||
custom_timeout = httpx.Timeout(timeout=30.0, connect=5.0)
|
||||
|
||||
with ZaiClient(timeout=custom_timeout) as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call with timeout
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_client_cleanup_with_retries(self):
|
||||
"""Test client cleanup with retry scenarios"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
# Create client with custom retry settings
|
||||
with ZaiClient(max_retries=5) as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call with retries
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_client_cleanup_with_custom_headers(self):
|
||||
"""Test client cleanup with custom headers"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
# Create client with custom headers
|
||||
custom_headers = {'X-Custom-Header': 'test-value'}
|
||||
|
||||
with ZaiClient(custom_headers=custom_headers) as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
def test_client_cleanup_with_source_channel(self):
|
||||
"""Test client cleanup with source channel"""
|
||||
with patch('httpx.Client') as mock_client_class:
|
||||
# Mock the httpx client
|
||||
mock_client = mock_client_class.return_value
|
||||
mock_client.is_closed = False
|
||||
mock_client.close = unittest.mock.Mock()
|
||||
|
||||
# Create client with source channel
|
||||
with ZaiClient(source_channel='test-channel') as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Simulate API call
|
||||
pass
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
212
tests/unit_tests/test_client_cleanup.py
Normal file
212
tests/unit_tests/test_client_cleanup.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"""
|
||||
Unit tests for client cleanup functionality
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
# Add src to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from zai import ZaiClient, ZhipuAiClient
|
||||
from zai.core._errors import ZaiError
|
||||
|
||||
|
||||
class TestClientCleanup(unittest.TestCase):
|
||||
"""Test client cleanup functionality"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
# Set a dummy API key for testing
|
||||
os.environ['ZAI_API_KEY'] = 'test-api-key'
|
||||
self.original_api_key = os.environ.get('ZAI_API_KEY')
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
if self.original_api_key:
|
||||
os.environ['ZAI_API_KEY'] = self.original_api_key
|
||||
elif 'ZAI_API_KEY' in os.environ:
|
||||
del os.environ['ZAI_API_KEY']
|
||||
|
||||
def test_context_manager_cleanup(self):
|
||||
"""Test that context manager properly cleans up the client"""
|
||||
with ZaiClient() as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
def test_explicit_close(self):
|
||||
"""Test explicit client close"""
|
||||
client = ZaiClient()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
client.close()
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
def test_multiple_close_calls(self):
|
||||
"""Test that multiple close calls don't cause errors"""
|
||||
client = ZaiClient()
|
||||
client.close()
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
# Second close should not raise an exception
|
||||
client.close()
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
def test_garbage_collection_cleanup(self):
|
||||
"""Test that garbage collection properly cleans up the client"""
|
||||
client = ZaiClient()
|
||||
client_ref = client
|
||||
|
||||
# Delete the reference and force garbage collection
|
||||
del client
|
||||
gc.collect()
|
||||
|
||||
# The client should be properly cleaned up without errors
|
||||
|
||||
def test_multiple_clients_cleanup(self):
|
||||
"""Test cleanup of multiple clients"""
|
||||
clients = []
|
||||
for i in range(5):
|
||||
client = ZaiClient()
|
||||
clients.append(client)
|
||||
|
||||
# Close all clients
|
||||
for client in clients:
|
||||
client.close()
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
# Delete all clients and force garbage collection
|
||||
del clients
|
||||
gc.collect()
|
||||
|
||||
def test_client_with_custom_http_client(self):
|
||||
"""Test cleanup when using custom HTTP client"""
|
||||
import httpx
|
||||
|
||||
custom_client = httpx.Client()
|
||||
client = ZaiClient(http_client=custom_client)
|
||||
|
||||
# Should close custom HTTP client when we call close()
|
||||
client.close()
|
||||
self.assertTrue(custom_client.is_closed)
|
||||
|
||||
# Clean up custom client (it's already closed)
|
||||
# custom_client.close() # No need to close again
|
||||
|
||||
def test_client_initialization_failure(self):
|
||||
"""Test cleanup when client initialization fails"""
|
||||
# Remove API key to cause initialization failure
|
||||
if 'ZAI_API_KEY' in os.environ:
|
||||
del os.environ['ZAI_API_KEY']
|
||||
|
||||
with self.assertRaises(ZaiError):
|
||||
ZaiClient()
|
||||
|
||||
# Should not cause cleanup errors during garbage collection
|
||||
gc.collect()
|
||||
|
||||
def test_client_with_none_client(self):
|
||||
"""Test cleanup when _client is None"""
|
||||
client = ZaiClient()
|
||||
|
||||
# Manually set _client to None to simulate edge case
|
||||
client._client = None
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_client_with_closed_client(self):
|
||||
"""Test cleanup when client is already closed"""
|
||||
client = ZaiClient()
|
||||
client.close()
|
||||
|
||||
# Should not raise exception when closing already closed client
|
||||
client.close()
|
||||
|
||||
@patch('httpx.Client.close')
|
||||
def test_client_close_exception_handling(self, mock_close):
|
||||
"""Test that exceptions during close are handled gracefully"""
|
||||
# Make the close method raise an exception
|
||||
mock_close.side_effect = Exception("Test exception")
|
||||
|
||||
client = ZaiClient()
|
||||
|
||||
# Should not raise exception even if underlying close fails
|
||||
client.close()
|
||||
|
||||
def test_zhipu_client_cleanup(self):
|
||||
"""Test cleanup for ZhipuAiClient"""
|
||||
with ZhipuAiClient() as client:
|
||||
self.assertIsNotNone(client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertTrue(client.is_closed())
|
||||
|
||||
def test_client_attributes_check(self):
|
||||
"""Test that client checks for required attributes before cleanup"""
|
||||
client = ZaiClient()
|
||||
|
||||
# Test that client has required attributes
|
||||
self.assertTrue(hasattr(client, '_has_custom_http_client'))
|
||||
self.assertTrue(hasattr(client, 'close'))
|
||||
self.assertTrue(hasattr(client, '_client'))
|
||||
|
||||
def test_client_del_without_initialization(self):
|
||||
"""Test __del__ method when client is not properly initialized"""
|
||||
# Create a client but manually remove attributes to simulate partial initialization
|
||||
client = ZaiClient()
|
||||
|
||||
# Remove required attributes
|
||||
if hasattr(client, '_has_custom_http_client'):
|
||||
delattr(client, '_has_custom_http_client')
|
||||
|
||||
# Should not raise exception during garbage collection
|
||||
del client
|
||||
gc.collect()
|
||||
|
||||
def test_client_del_with_custom_http_client(self):
|
||||
"""Test __del__ method when using custom HTTP client"""
|
||||
import httpx
|
||||
|
||||
custom_client = httpx.Client()
|
||||
client = ZaiClient(http_client=custom_client)
|
||||
|
||||
# Should not close custom HTTP client during garbage collection
|
||||
# because _has_custom_http_client is True
|
||||
del client
|
||||
gc.collect()
|
||||
|
||||
# Custom client should still be open because __del__ doesn't close it
|
||||
self.assertFalse(custom_client.is_closed)
|
||||
custom_client.close()
|
||||
|
||||
def test_client_close_with_none_client_after_init(self):
|
||||
"""Test close method when _client becomes None after initialization"""
|
||||
client = ZaiClient()
|
||||
|
||||
# Simulate _client becoming None after initialization
|
||||
client._client = None
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_client_close_with_closed_httpx_client(self):
|
||||
"""Test close method when httpx client is already closed"""
|
||||
client = ZaiClient()
|
||||
|
||||
# Close the underlying httpx client
|
||||
client._client.close()
|
||||
|
||||
# Should not raise exception when closing already closed httpx client
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
270
tests/unit_tests/test_http_client_cleanup.py
Normal file
270
tests/unit_tests/test_http_client_cleanup.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
"""
|
||||
Unit tests for HttpClient cleanup functionality
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
# Add src to path for testing
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
||||
|
||||
from zai.core._http_client import HttpClient
|
||||
from zai.core._constants import ZAI_DEFAULT_TIMEOUT, ZAI_DEFAULT_LIMITS
|
||||
|
||||
|
||||
class TestHttpClientCleanup(unittest.TestCase):
|
||||
"""Test HttpClient cleanup functionality"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
# Create a mock httpx client for testing
|
||||
self.mock_httpx_client = Mock()
|
||||
self.mock_httpx_client.is_closed = False
|
||||
self.mock_httpx_client.close = Mock()
|
||||
|
||||
def test_http_client_close_normal(self):
|
||||
"""Test normal HttpClient close"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
self.assertFalse(client.is_closed())
|
||||
client.close()
|
||||
# After our fix, the client should still be open because we check is_closed
|
||||
# and don't close if it's already closed
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Verify that the underlying httpx client was closed
|
||||
self.mock_httpx_client.close.assert_called_once()
|
||||
|
||||
def test_http_client_close_multiple_calls(self):
|
||||
"""Test multiple close calls on HttpClient"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# First close
|
||||
client.close()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Second close should not raise exception
|
||||
client.close()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Verify that close was called twice on the underlying client
|
||||
# (our fix allows multiple calls but still calls the underlying client)
|
||||
self.assertEqual(self.mock_httpx_client.close.call_count, 2)
|
||||
|
||||
def test_http_client_close_with_closed_client(self):
|
||||
"""Test HttpClient close when client is already closed"""
|
||||
self.mock_httpx_client.is_closed = True
|
||||
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Should not raise exception when closing already closed client
|
||||
client.close()
|
||||
# Should not call close on already closed client
|
||||
self.mock_httpx_client.close.assert_not_called()
|
||||
|
||||
def test_http_client_close_with_none_client(self):
|
||||
"""Test HttpClient close when _client is None"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Set _client to None
|
||||
client._client = None
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_http_client_close_with_exception_handling(self):
|
||||
"""Test that exceptions during HttpClient close are handled gracefully"""
|
||||
# Make the close method raise an exception
|
||||
self.mock_httpx_client.close.side_effect = Exception("Test exception")
|
||||
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Should not raise exception even if underlying close fails
|
||||
client.close()
|
||||
|
||||
def test_http_client_context_manager(self):
|
||||
"""Test HttpClient context manager functionality"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
with client as http_client:
|
||||
self.assertIs(http_client, client)
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Client should be closed after context manager exits
|
||||
self.assertFalse(client.is_closed())
|
||||
self.mock_httpx_client.close.assert_called_once()
|
||||
|
||||
def test_http_client_garbage_collection(self):
|
||||
"""Test HttpClient cleanup during garbage collection"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Delete the client and force garbage collection
|
||||
del client
|
||||
gc.collect()
|
||||
|
||||
# The client should be properly cleaned up without errors
|
||||
|
||||
def test_http_client_with_custom_httpx_client(self):
|
||||
"""Test HttpClient with custom httpx client"""
|
||||
custom_client = Mock()
|
||||
custom_client.is_closed = False
|
||||
custom_client.close = Mock()
|
||||
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
custom_httpx_client=custom_client,
|
||||
)
|
||||
|
||||
self.assertFalse(client.is_closed())
|
||||
client.close()
|
||||
self.assertFalse(client.is_closed())
|
||||
|
||||
# Verify that the custom client was closed
|
||||
custom_client.close.assert_called_once()
|
||||
|
||||
def test_http_client_attributes_check(self):
|
||||
"""Test that HttpClient has required attributes"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Test that client has required attributes
|
||||
self.assertTrue(hasattr(client, '_client'))
|
||||
self.assertTrue(hasattr(client, 'close'))
|
||||
self.assertTrue(hasattr(client, 'is_closed'))
|
||||
|
||||
def test_http_client_close_with_none_client_after_init(self):
|
||||
"""Test close method when _client becomes None after initialization"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Simulate _client becoming None after initialization
|
||||
client._client = None
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_http_client_close_with_closed_httpx_client(self):
|
||||
"""Test close method when httpx client is already closed"""
|
||||
self.mock_httpx_client.is_closed = True
|
||||
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Should not raise exception when closing already closed httpx client
|
||||
client.close()
|
||||
|
||||
def test_http_client_close_with_exception_during_is_closed_check(self):
|
||||
"""Test close method when is_closed check raises exception"""
|
||||
# Create a mock that raises an exception when is_closed is accessed
|
||||
mock_client = Mock()
|
||||
mock_client.is_closed = Mock(side_effect=Exception("Test exception"))
|
||||
mock_client.close = Mock()
|
||||
|
||||
with patch('httpx.Client', return_value=mock_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_http_client_close_with_exception_during_close_call(self):
|
||||
"""Test close method when close call raises exception"""
|
||||
# Make close raise an exception
|
||||
self.mock_httpx_client.close.side_effect = Exception("Test exception")
|
||||
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
def test_http_client_close_with_hasattr_exception(self):
|
||||
"""Test close method when hasattr check raises exception"""
|
||||
with patch('httpx.Client', return_value=self.mock_httpx_client):
|
||||
client = HttpClient(
|
||||
version="test-version",
|
||||
base_url="https://api.test.com",
|
||||
_strict_response_validation=False,
|
||||
timeout=ZAI_DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Mock hasattr to raise an exception
|
||||
with patch('builtins.hasattr', side_effect=Exception("Test exception")):
|
||||
# Should not raise exception
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue