Skip to content

Commit 2bbc573

Browse files
committed
fix: prevent schema names from shadowing endpoint imports
Alias endpoint-owned Client, AuthenticatedClient, Response and HTTPStatus imports while preserving generated model names and public SDK types. Cover resolved annotations, request execution and response identities in functional tests, and update generated fixtures and release notes.
1 parent 4a2f3db commit 2bbc573

68 files changed

Lines changed: 1039 additions & 796 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Fixed generated endpoints breaking when a schema is named `Client`, `AuthenticatedClient`, `Response` or `HTTPStatus`
6+
7+
Generated endpoint modules imported the SDK's `Client`, `AuthenticatedClient` and `Response` classes and the standard library's `HTTPStatus` under their bare names, so a model with one of those names shadowed them. Depending on the name this produced wrong type annotations, a failed response build, or a `Response.status_code` that held a model instance instead of an `HTTPStatus`. Endpoint modules now bind these generator-owned symbols under private aliases, leaving the public model names unchanged.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import asyncio
2+
import json
3+
from collections.abc import Iterator
4+
from http import HTTPStatus
5+
from typing import get_type_hints
6+
7+
import httpx
8+
import pytest
9+
10+
from end_to_end_tests.generated_client import GeneratedClientContext, generate_client_from_inline_spec
11+
12+
ISSUE_SPEC = """
13+
openapi: "3.0.3"
14+
info:
15+
title: Minimal repro
16+
version: "1.0"
17+
paths:
18+
/api/clients/{id}:
19+
patch:
20+
operationId: update_client
21+
tags:
22+
- client
23+
parameters:
24+
- name: id
25+
in: path
26+
required: true
27+
schema:
28+
type: string
29+
requestBody:
30+
content:
31+
application/json:
32+
schema:
33+
type: object
34+
properties:
35+
name:
36+
type: string
37+
responses:
38+
"200":
39+
description: OK
40+
content:
41+
application/json:
42+
schema:
43+
$ref: "#/components/schemas/Client"
44+
components:
45+
schemas:
46+
Client:
47+
type: object
48+
properties:
49+
name:
50+
type: string
51+
"""
52+
53+
54+
@pytest.fixture(params=["Client", "AuthenticatedClient", "Response", "HTTPStatus", "Customer"])
55+
def generated_client(request: pytest.FixtureRequest) -> Iterator[GeneratedClientContext]:
56+
# Client is the original issue document; Customer is a non-collision control.
57+
spec = ISSUE_SPEC.replace("Client", request.param)
58+
with generate_client_from_inline_spec(
59+
spec, base_module="minimal_repro_client", add_missing_sections=False
60+
) as generated:
61+
yield generated
62+
63+
64+
def test_endpoint_types_and_requests(generated_client: GeneratedClientContext) -> None:
65+
endpoint = generated_client.import_module(".api.client.update_client")
66+
transport = generated_client.import_module(".client")
67+
model = get_type_hints(endpoint._parse_response)["return"]
68+
models = generated_client.import_module(".models")
69+
response_type = generated_client.import_symbol(".types", "Response")
70+
body_type = generated_client.import_symbol(".models", "UpdateClientBody")
71+
package = generated_client.import_module("")
72+
73+
# Compare with public exports, independently of endpoint import aliases.
74+
schema_model = next(
75+
getattr(models, name)
76+
for name in ("Client", "AuthenticatedClient", "Response", "HTTPStatus", "Customer")
77+
if hasattr(models, name)
78+
)
79+
assert model == schema_model | None
80+
assert package.Client is transport.Client
81+
assert package.AuthenticatedClient is transport.AuthenticatedClient
82+
assert schema_model is not transport.Client
83+
assert schema_model is not transport.AuthenticatedClient
84+
assert schema_model is not response_type
85+
86+
for function_name in ("_parse_response", "_build_response", "sync", "sync_detailed", "asyncio", "asyncio_detailed"):
87+
hints = get_type_hints(getattr(endpoint, function_name))
88+
assert hints["client"] == transport.AuthenticatedClient | transport.Client
89+
detailed = function_name == "_build_response" or function_name.endswith("_detailed")
90+
assert hints["return"] == (response_type[schema_model] if detailed else schema_model | None)
91+
92+
def handle_request(request: httpx.Request) -> httpx.Response:
93+
assert request.method == "PATCH"
94+
assert request.url.path == "/api/clients/one"
95+
assert json.loads(request.content) == {"name": "Ada"}
96+
return httpx.Response(200, json={"name": "Ada"})
97+
98+
def check_result(result, detailed: bool) -> None:
99+
if detailed:
100+
assert type(result) is response_type
101+
assert result.status_code is HTTPStatus.OK
102+
result = result.parsed
103+
assert type(result) is schema_model
104+
assert result.name == "Ada"
105+
106+
with httpx.Client(base_url="https://example.test", transport=httpx.MockTransport(handle_request)) as http_client:
107+
client = transport.Client(base_url="https://example.test").set_httpx_client(http_client)
108+
for operation in (endpoint.sync, endpoint.sync_detailed):
109+
check_result(
110+
operation("one", client=client, body=body_type(name="Ada")), operation is endpoint.sync_detailed
111+
)
112+
113+
async def call_async() -> None:
114+
async with httpx.AsyncClient(
115+
base_url="https://example.test", transport=httpx.MockTransport(handle_request)
116+
) as http_client:
117+
client = transport.AuthenticatedClient(
118+
base_url="https://example.test", token="token"
119+
).set_async_httpx_client(http_client)
120+
for operation in (endpoint.asyncio, endpoint.asyncio_detailed):
121+
check_result(
122+
await operation("one", client=client, body=body_type(name="Ada")),
123+
operation is endpoint.asyncio_detailed,
124+
)
125+
126+
asyncio.run(call_async())
127+
128+
129+
def test_authenticated_endpoint_client_type() -> None:
130+
spec = (
131+
ISSUE_SPEC.replace("Client", "AuthenticatedClient")
132+
.replace("operationId: update_client", "operationId: update_client\n security: [{bearer: []}]")
133+
.replace("components:", "components:\n securitySchemes:\n bearer:\n type: http\n scheme: bearer")
134+
)
135+
with generate_client_from_inline_spec(
136+
spec, base_module="minimal_repro_client", add_missing_sections=False
137+
) as generated:
138+
endpoint = generated.import_module(".api.client.update_client")
139+
transport = generated.import_symbol(".client", "AuthenticatedClient")
140+
for function_name in ("sync", "sync_detailed", "asyncio", "asyncio_detailed"):
141+
assert get_type_hints(getattr(endpoint, function_name))["client"] is transport

‎end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from http import HTTPStatus
1+
from http import HTTPStatus as _HTTPStatus
22
from typing import Any
33

44
import httpx
55

66
from ... import errors
7-
from ...client import AuthenticatedClient, Client
8-
from ...types import Response
7+
from ...client import AuthenticatedClient as _AuthenticatedClient
8+
from ...client import Client as _Client
9+
from ...types import Response as _Response
910

1011

1112
def _get_kwargs() -> dict[str, Any]:
@@ -18,16 +19,16 @@ def _get_kwargs() -> dict[str, Any]:
1819
return _kwargs
1920

2021

21-
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None:
22+
def _parse_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> Any | None:
2223
if client.raise_on_unexpected_status:
2324
raise errors.UnexpectedStatus(response.status_code, response.content)
2425
else:
2526
return None
2627

2728

28-
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]:
29-
return Response(
30-
status_code=HTTPStatus(response.status_code),
29+
def _build_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> _Response[Any]:
30+
return _Response(
31+
status_code=_HTTPStatus(response.status_code),
3132
content=response.content,
3233
headers=response.headers,
3334
parsed=_parse_response(client=client, response=response),
@@ -36,8 +37,8 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res
3637

3738
def sync_detailed(
3839
*,
39-
client: AuthenticatedClient | Client,
40-
) -> Response[Any]:
40+
client: _AuthenticatedClient | _Client,
41+
) -> _Response[Any]:
4142
"""
4243
Raises:
4344
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -58,8 +59,8 @@ def sync_detailed(
5859

5960
async def asyncio_detailed(
6061
*,
61-
client: AuthenticatedClient | Client,
62-
) -> Response[Any]:
62+
client: _AuthenticatedClient | _Client,
63+
) -> _Response[Any]:
6364
"""
6465
Raises:
6566
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.

‎end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from http import HTTPStatus
1+
from http import HTTPStatus as _HTTPStatus
22
from typing import Any, cast
33

44
import httpx
55

66
from ... import errors
7-
from ...client import AuthenticatedClient, Client
7+
from ...client import AuthenticatedClient as _AuthenticatedClient
8+
from ...client import Client as _Client
89
from ...models.misc_metadata_escapes_body import MiscMetadataEscapesBody
9-
from ...types import Response
10+
from ...types import Response as _Response
1011

1112

1213
def _get_kwargs(
@@ -28,7 +29,7 @@ def _get_kwargs(
2829
return _kwargs
2930

3031

31-
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> str | None:
32+
def _parse_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> str | None:
3233
if response.status_code == 200:
3334
response_200 = cast(str, response.json())
3435
return response_200
@@ -39,9 +40,9 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
3940
return None
4041

4142

42-
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[str]:
43-
return Response(
44-
status_code=HTTPStatus(response.status_code),
43+
def _build_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> _Response[str]:
44+
return _Response(
45+
status_code=_HTTPStatus(response.status_code),
4546
content=response.content,
4647
headers=response.headers,
4748
parsed=_parse_response(client=client, response=response),
@@ -50,9 +51,9 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res
5051

5152
def sync_detailed(
5253
*,
53-
client: AuthenticatedClient | Client,
54+
client: _AuthenticatedClient | _Client,
5455
body: MiscMetadataEscapesBody,
55-
) -> Response[str]:
56+
) -> _Response[str]:
5657
"""
5758
Args:
5859
body (MiscMetadataEscapesBody):
@@ -78,7 +79,7 @@ def sync_detailed(
7879

7980
def sync(
8081
*,
81-
client: AuthenticatedClient | Client,
82+
client: _AuthenticatedClient | _Client,
8283
body: MiscMetadataEscapesBody,
8384
) -> str | None:
8485
"""
@@ -101,9 +102,9 @@ def sync(
101102

102103
async def asyncio_detailed(
103104
*,
104-
client: AuthenticatedClient | Client,
105+
client: _AuthenticatedClient | _Client,
105106
body: MiscMetadataEscapesBody,
106-
) -> Response[str]:
107+
) -> _Response[str]:
107108
"""
108109
Args:
109110
body (MiscMetadataEscapesBody):
@@ -127,7 +128,7 @@ async def asyncio_detailed(
127128

128129
async def asyncio(
129130
*,
130-
client: AuthenticatedClient | Client,
131+
client: _AuthenticatedClient | _Client,
131132
body: MiscMetadataEscapesBody,
132133
) -> str | None:
133134
"""

‎end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from http import HTTPStatus
1+
from http import HTTPStatus as _HTTPStatus
22
from typing import Any
33

44
import httpx
55

66
from ... import errors
7-
from ...client import AuthenticatedClient, Client
7+
from ...client import AuthenticatedClient as _AuthenticatedClient
8+
from ...client import Client as _Client
89
from ...models.non_string_example_body import NonStringExampleBody
9-
from ...types import Response
10+
from ...types import Response as _Response
1011

1112

1213
def _get_kwargs(
@@ -28,7 +29,7 @@ def _get_kwargs(
2829
return _kwargs
2930

3031

31-
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | None:
32+
def _parse_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> Any | None:
3233
if response.status_code == 200:
3334
return None
3435

@@ -38,9 +39,9 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
3839
return None
3940

4041

41-
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any]:
42-
return Response(
43-
status_code=HTTPStatus(response.status_code),
42+
def _build_response(*, client: _AuthenticatedClient | _Client, response: httpx.Response) -> _Response[Any]:
43+
return _Response(
44+
status_code=_HTTPStatus(response.status_code),
4445
content=response.content,
4546
headers=response.headers,
4647
parsed=_parse_response(client=client, response=response),
@@ -49,9 +50,9 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res
4950

5051
def sync_detailed(
5152
*,
52-
client: AuthenticatedClient | Client,
53+
client: _AuthenticatedClient | _Client,
5354
body: NonStringExampleBody,
54-
) -> Response[Any]:
55+
) -> _Response[Any]:
5556
"""
5657
Args:
5758
body (NonStringExampleBody):
@@ -77,9 +78,9 @@ def sync_detailed(
7778

7879
async def asyncio_detailed(
7980
*,
80-
client: AuthenticatedClient | Client,
81+
client: _AuthenticatedClient | _Client,
8182
body: NonStringExampleBody,
82-
) -> Response[Any]:
83+
) -> _Response[Any]:
8384
"""
8485
Args:
8586
body (NonStringExampleBody):

0 commit comments

Comments
 (0)