fix: client close graceful (#22)

Co-authored-by: zhengweijun <weijun.zheng@aminer.cn>
This commit is contained in:
wellenzheng 2025-08-04 11:18:57 +08:00 committed by GitHub
parent 42d6cc34d1
commit d98e87cb36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 942 additions and 4 deletions

65
examples/example_types.py Normal file
View 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
View 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)

View file

@ -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"

View file

@ -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):

View file

@ -1,2 +1,2 @@
__title__ = 'Z.ai'
__version__ = '0.0.1'
__version__ = '0.0.2'

View file

@ -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

View 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()

View 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()

View 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()