chore(internal): codegen related update

This commit is contained in:
stainless-app[bot] 2025-10-31 02:31:29 +00:00
parent 81dc9145e0
commit 33f3521d9b

View file

@ -59,47 +59,45 @@ def _get_open_connections(client: Opencode | AsyncOpencode) -> int:
class TestOpencode:
client = Opencode(base_url=base_url, _strict_response_validation=True)
@pytest.mark.respx(base_url=base_url)
def test_raw_response(self, respx_mock: MockRouter) -> None:
def test_raw_response(self, respx_mock: MockRouter, client: Opencode) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = self.client.post("/foo", cast_to=httpx.Response)
response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Opencode) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
response = self.client.post("/foo", cast_to=httpx.Response)
response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
def test_copy(self) -> None:
copied = self.client.copy()
assert id(copied) != id(self.client)
def test_copy(self, client: Opencode) -> None:
copied = client.copy()
assert id(copied) != id(client)
def test_copy_default_options(self) -> None:
def test_copy_default_options(self, client: Opencode) -> None:
# options that have a default are overridden correctly
copied = self.client.copy(max_retries=7)
copied = client.copy(max_retries=7)
assert copied.max_retries == 7
assert self.client.max_retries == 2
assert client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
assert isinstance(self.client.timeout, httpx.Timeout)
copied = self.client.copy(timeout=None)
assert isinstance(client.timeout, httpx.Timeout)
copied = client.copy(timeout=None)
assert copied.timeout is None
assert isinstance(self.client.timeout, httpx.Timeout)
assert isinstance(client.timeout, httpx.Timeout)
def test_copy_default_headers(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
@ -132,6 +130,7 @@ class TestOpencode:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
client.close()
def test_copy_default_query(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"})
@ -167,13 +166,15 @@ class TestOpencode:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
def test_copy_signature(self) -> None:
client.close()
def test_copy_signature(self, client: Opencode) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
self.client.__init__, # type: ignore[misc]
client.__init__, # type: ignore[misc]
)
copy_signature = inspect.signature(self.client.copy)
copy_signature = inspect.signature(client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@ -184,12 +185,12 @@ class TestOpencode:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
def test_copy_build_request(self, client: Opencode) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
client = self.client.copy()
client._build_request(options)
client_copy = client.copy()
client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@ -246,14 +247,12 @@ class TestOpencode:
print(frame)
raise AssertionError()
def test_request_timeout(self) -> None:
request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
def test_request_timeout(self, client: Opencode) -> None:
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
request = self.client._build_request(
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
)
request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(100.0)
@ -264,6 +263,8 @@ class TestOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
client.close()
def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
with httpx.Client(timeout=None) as http_client:
@ -273,6 +274,8 @@ class TestOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
client.close()
# no timeout given to the httpx client should not use the httpx default
with httpx.Client() as http_client:
client = Opencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
@ -281,6 +284,8 @@ class TestOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
client.close()
# explicitly passing the default timeout currently results in it being ignored
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = Opencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
@ -289,18 +294,20 @@ class TestOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
client.close()
async def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
async with httpx.AsyncClient() as http_client:
Opencode(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client))
def test_default_headers_option(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
test_client = Opencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
client2 = Opencode(
test_client2 = Opencode(
base_url=base_url,
_strict_response_validation=True,
default_headers={
@ -308,10 +315,13 @@ class TestOpencode:
"X-Stainless-Lang": "my-overriding-header",
},
)
request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
test_client.close()
test_client2.close()
def test_default_query_option(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"})
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@ -328,8 +338,10 @@ class TestOpencode:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
def test_request_extra_json(self) -> None:
request = self.client._build_request(
client.close()
def test_request_extra_json(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -340,7 +352,7 @@ class TestOpencode:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -351,7 +363,7 @@ class TestOpencode:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -362,8 +374,8 @@ class TestOpencode:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
def test_request_extra_headers(self) -> None:
request = self.client._build_request(
def test_request_extra_headers(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -373,7 +385,7 @@ class TestOpencode:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -384,8 +396,8 @@ class TestOpencode:
)
assert request.headers.get("X-Bar") == "false"
def test_request_extra_query(self) -> None:
request = self.client._build_request(
def test_request_extra_query(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -398,7 +410,7 @@ class TestOpencode:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -412,7 +424,7 @@ class TestOpencode:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -455,7 +467,7 @@ class TestOpencode:
]
@pytest.mark.respx(base_url=base_url)
def test_basic_union_response(self, respx_mock: MockRouter) -> None:
def test_basic_union_response(self, respx_mock: MockRouter, client: Opencode) -> None:
class Model1(BaseModel):
name: str
@ -464,12 +476,12 @@ class TestOpencode:
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
def test_union_response_different_types(self, respx_mock: MockRouter, client: Opencode) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@ -480,18 +492,18 @@ class TestOpencode:
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Opencode) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@ -507,7 +519,7 @@ class TestOpencode:
)
)
response = self.client.get("/foo", cast_to=Model)
response = client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
@ -519,6 +531,8 @@ class TestOpencode:
assert client.base_url == "https://example.com/from_setter/"
client.close()
def test_base_url_env(self) -> None:
with update_env(OPENCODE_BASE_URL="http://localhost:5000/from/env"):
client = Opencode(_strict_response_validation=True)
@ -545,6 +559,7 @@ class TestOpencode:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
client.close()
@pytest.mark.parametrize(
"client",
@ -567,6 +582,7 @@ class TestOpencode:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
client.close()
@pytest.mark.parametrize(
"client",
@ -589,35 +605,36 @@ class TestOpencode:
),
)
assert request.url == "https://myapi.com/foo"
client.close()
def test_copied_client_does_not_close_http(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True)
assert not client.is_closed()
test_client = Opencode(base_url=base_url, _strict_response_validation=True)
assert not test_client.is_closed()
copied = client.copy()
assert copied is not client
copied = test_client.copy()
assert copied is not test_client
del copied
assert not client.is_closed()
assert not test_client.is_closed()
def test_client_context_manager(self) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True)
with client as c2:
assert c2 is client
test_client = Opencode(base_url=base_url, _strict_response_validation=True)
with test_client as c2:
assert c2 is test_client
assert not c2.is_closed()
assert not client.is_closed()
assert client.is_closed()
assert not test_client.is_closed()
assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
def test_client_response_validation_error(self, respx_mock: MockRouter, client: Opencode) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
self.client.get("/foo", cast_to=Model)
client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@ -626,13 +643,13 @@ class TestOpencode:
Opencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None))
@pytest.mark.respx(base_url=base_url)
def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
def test_default_stream_cls(self, respx_mock: MockRouter, client: Opencode) -> None:
class Model(BaseModel):
name: str
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
stream = client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model])
assert isinstance(stream, Stream)
stream.response.close()
@ -648,11 +665,14 @@ class TestOpencode:
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
client = Opencode(base_url=base_url, _strict_response_validation=False)
non_strict_client = Opencode(base_url=base_url, _strict_response_validation=False)
response = client.get("/foo", cast_to=Model)
response = non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
strict_client.close()
non_strict_client.close()
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@ -675,9 +695,9 @@ class TestOpencode:
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
client = Opencode(base_url=base_url, _strict_response_validation=True)
def test_parse_retry_after_header(
self, remaining_retries: int, retry_after: str, timeout: float, client: Opencode
) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
@ -691,7 +711,7 @@ class TestOpencode:
with pytest.raises(APITimeoutError):
client.session.with_streaming_response.list().__enter__()
assert _get_open_connections(self.client) == 0
assert _get_open_connections(client) == 0
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@ -700,7 +720,7 @@ class TestOpencode:
with pytest.raises(APIStatusError):
client.session.with_streaming_response.list().__enter__()
assert _get_open_connections(self.client) == 0
assert _get_open_connections(client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@ -802,79 +822,73 @@ class TestOpencode:
)
@pytest.mark.respx(base_url=base_url)
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
def test_follow_redirects(self, respx_mock: MockRouter, client: Opencode) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Opencode) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
self.client.post(
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
)
client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
assert exc_info.value.response.status_code == 302
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
class TestAsyncOpencode:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_raw_response(self, respx_mock: MockRouter) -> None:
async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = await self.client.post("/foo", cast_to=httpx.Response)
response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
response = await self.client.post("/foo", cast_to=httpx.Response)
response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
def test_copy(self) -> None:
copied = self.client.copy()
assert id(copied) != id(self.client)
def test_copy(self, async_client: AsyncOpencode) -> None:
copied = async_client.copy()
assert id(copied) != id(async_client)
def test_copy_default_options(self) -> None:
def test_copy_default_options(self, async_client: AsyncOpencode) -> None:
# options that have a default are overridden correctly
copied = self.client.copy(max_retries=7)
copied = async_client.copy(max_retries=7)
assert copied.max_retries == 7
assert self.client.max_retries == 2
assert async_client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
assert isinstance(self.client.timeout, httpx.Timeout)
copied = self.client.copy(timeout=None)
assert isinstance(async_client.timeout, httpx.Timeout)
copied = async_client.copy(timeout=None)
assert copied.timeout is None
assert isinstance(self.client.timeout, httpx.Timeout)
assert isinstance(async_client.timeout, httpx.Timeout)
def test_copy_default_headers(self) -> None:
async def test_copy_default_headers(self) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
assert client.default_headers["X-Foo"] == "bar"
@ -905,8 +919,9 @@ class TestAsyncOpencode:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
await client.close()
def test_copy_default_query(self) -> None:
async def test_copy_default_query(self) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"})
assert _get_params(client)["foo"] == "bar"
@ -940,13 +955,15 @@ class TestAsyncOpencode:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
def test_copy_signature(self) -> None:
await client.close()
def test_copy_signature(self, async_client: AsyncOpencode) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
self.client.__init__, # type: ignore[misc]
async_client.__init__, # type: ignore[misc]
)
copy_signature = inspect.signature(self.client.copy)
copy_signature = inspect.signature(async_client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@ -957,12 +974,12 @@ class TestAsyncOpencode:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
def test_copy_build_request(self, async_client: AsyncOpencode) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
client = self.client.copy()
client._build_request(options)
client_copy = async_client.copy()
client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@ -1019,12 +1036,12 @@ class TestAsyncOpencode:
print(frame)
raise AssertionError()
async def test_request_timeout(self) -> None:
request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
async def test_request_timeout(self, async_client: AsyncOpencode) -> None:
request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
request = self.client._build_request(
request = async_client._build_request(
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
)
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
@ -1037,6 +1054,8 @@ class TestAsyncOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
await client.close()
async def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
async with httpx.AsyncClient(timeout=None) as http_client:
@ -1046,6 +1065,8 @@ class TestAsyncOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
await client.close()
# no timeout given to the httpx client should not use the httpx default
async with httpx.AsyncClient() as http_client:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
@ -1054,6 +1075,8 @@ class TestAsyncOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
await client.close()
# explicitly passing the default timeout currently results in it being ignored
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=http_client)
@ -1062,18 +1085,22 @@ class TestAsyncOpencode:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
await client.close()
def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
with httpx.Client() as http_client:
AsyncOpencode(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client))
def test_default_headers_option(self) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"})
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
async def test_default_headers_option(self) -> None:
test_client = AsyncOpencode(
base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
client2 = AsyncOpencode(
test_client2 = AsyncOpencode(
base_url=base_url,
_strict_response_validation=True,
default_headers={
@ -1081,11 +1108,14 @@ class TestAsyncOpencode:
"X-Stainless-Lang": "my-overriding-header",
},
)
request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
def test_default_query_option(self) -> None:
await test_client.close()
await test_client2.close()
async def test_default_query_option(self) -> None:
client = AsyncOpencode(
base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"}
)
@ -1103,8 +1133,10 @@ class TestAsyncOpencode:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
def test_request_extra_json(self) -> None:
request = self.client._build_request(
await client.close()
def test_request_extra_json(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1115,7 +1147,7 @@ class TestAsyncOpencode:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1126,7 +1158,7 @@ class TestAsyncOpencode:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1137,8 +1169,8 @@ class TestAsyncOpencode:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
def test_request_extra_headers(self) -> None:
request = self.client._build_request(
def test_request_extra_headers(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1148,7 +1180,7 @@ class TestAsyncOpencode:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1159,8 +1191,8 @@ class TestAsyncOpencode:
)
assert request.headers.get("X-Bar") == "false"
def test_request_extra_query(self) -> None:
request = self.client._build_request(
def test_request_extra_query(self, client: Opencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1173,7 +1205,7 @@ class TestAsyncOpencode:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1187,7 +1219,7 @@ class TestAsyncOpencode:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
request = self.client._build_request(
request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@ -1230,7 +1262,7 @@ class TestAsyncOpencode:
]
@pytest.mark.respx(base_url=base_url)
async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
class Model1(BaseModel):
name: str
@ -1239,12 +1271,12 @@ class TestAsyncOpencode:
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@ -1255,18 +1287,20 @@ class TestAsyncOpencode:
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
async def test_non_application_json_content_type_for_json_data(
self, respx_mock: MockRouter, async_client: AsyncOpencode
) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@ -1282,11 +1316,11 @@ class TestAsyncOpencode:
)
)
response = await self.client.get("/foo", cast_to=Model)
response = await async_client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
def test_base_url_setter(self) -> None:
async def test_base_url_setter(self) -> None:
client = AsyncOpencode(base_url="https://example.com/from_init", _strict_response_validation=True)
assert client.base_url == "https://example.com/from_init/"
@ -1294,7 +1328,9 @@ class TestAsyncOpencode:
assert client.base_url == "https://example.com/from_setter/"
def test_base_url_env(self) -> None:
await client.close()
async def test_base_url_env(self) -> None:
with update_env(OPENCODE_BASE_URL="http://localhost:5000/from/env"):
client = AsyncOpencode(_strict_response_validation=True)
assert client.base_url == "http://localhost:5000/from/env/"
@ -1311,7 +1347,7 @@ class TestAsyncOpencode:
],
ids=["standard", "custom http client"],
)
def test_base_url_trailing_slash(self, client: AsyncOpencode) -> None:
async def test_base_url_trailing_slash(self, client: AsyncOpencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@ -1320,6 +1356,7 @@ class TestAsyncOpencode:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
await client.close()
@pytest.mark.parametrize(
"client",
@ -1333,7 +1370,7 @@ class TestAsyncOpencode:
],
ids=["standard", "custom http client"],
)
def test_base_url_no_trailing_slash(self, client: AsyncOpencode) -> None:
async def test_base_url_no_trailing_slash(self, client: AsyncOpencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@ -1342,6 +1379,7 @@ class TestAsyncOpencode:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
await client.close()
@pytest.mark.parametrize(
"client",
@ -1355,7 +1393,7 @@ class TestAsyncOpencode:
],
ids=["standard", "custom http client"],
)
def test_absolute_request_url(self, client: AsyncOpencode) -> None:
async def test_absolute_request_url(self, client: AsyncOpencode) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@ -1364,37 +1402,37 @@ class TestAsyncOpencode:
),
)
assert request.url == "https://myapi.com/foo"
await client.close()
async def test_copied_client_does_not_close_http(self) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
assert not client.is_closed()
test_client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
assert not test_client.is_closed()
copied = client.copy()
assert copied is not client
copied = test_client.copy()
assert copied is not test_client
del copied
await asyncio.sleep(0.2)
assert not client.is_closed()
assert not test_client.is_closed()
async def test_client_context_manager(self) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
async with client as c2:
assert c2 is client
test_client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
async with test_client as c2:
assert c2 is test_client
assert not c2.is_closed()
assert not client.is_closed()
assert client.is_closed()
assert not test_client.is_closed()
assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
await self.client.get("/foo", cast_to=Model)
await async_client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@ -1403,19 +1441,17 @@ class TestAsyncOpencode:
AsyncOpencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None))
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_default_stream_cls(self, respx_mock: MockRouter) -> None:
async def test_default_stream_cls(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
class Model(BaseModel):
name: str
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
stream = await async_client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model])
assert isinstance(stream, AsyncStream)
await stream.response.aclose()
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str
@ -1427,11 +1463,14 @@ class TestAsyncOpencode:
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
client = AsyncOpencode(base_url=base_url, _strict_response_validation=False)
non_strict_client = AsyncOpencode(base_url=base_url, _strict_response_validation=False)
response = await client.get("/foo", cast_to=Model)
response = await non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
await strict_client.close()
await non_strict_client.close()
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@ -1454,13 +1493,12 @@ class TestAsyncOpencode:
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@pytest.mark.asyncio
async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
client = AsyncOpencode(base_url=base_url, _strict_response_validation=True)
async def test_parse_retry_after_header(
self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncOpencode
) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@ -1473,7 +1511,7 @@ class TestAsyncOpencode:
with pytest.raises(APITimeoutError):
await async_client.session.with_streaming_response.list().__aenter__()
assert _get_open_connections(self.client) == 0
assert _get_open_connections(async_client) == 0
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@ -1484,12 +1522,11 @@ class TestAsyncOpencode:
with pytest.raises(APIStatusError):
await async_client.session.with_streaming_response.list().__aenter__()
assert _get_open_connections(self.client) == 0
assert _get_open_connections(async_client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
@pytest.mark.parametrize("failure_mode", ["status", "exception"])
async def test_retries_taken(
self,
@ -1521,7 +1558,6 @@ class TestAsyncOpencode:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_omit_retry_count_header(
self, async_client: AsyncOpencode, failures_before_success: int, respx_mock: MockRouter
) -> None:
@ -1545,7 +1581,6 @@ class TestAsyncOpencode:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("opencode_ai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_overwrite_retry_count_header(
self, async_client: AsyncOpencode, failures_before_success: int, respx_mock: MockRouter
) -> None:
@ -1593,26 +1628,26 @@ class TestAsyncOpencode:
)
@pytest.mark.respx(base_url=base_url)
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncOpencode) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
await self.client.post(
await async_client.post(
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
)