Skip to content

Commit ff784ad

Browse files
committed
fix(parser): Attempt to deduplicate endpoint parameters based on name and location (fixes #305)
1 parent 59a5827 commit ff784ad

File tree

6 files changed

+160
-1
lines changed

6 files changed

+160
-1
lines changed

end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import Any, Dict, Union
2+
3+
import httpx
4+
5+
from ...client import Client
6+
from ...types import UNSET, Response, Unset
7+
8+
9+
def _get_kwargs(
10+
*,
11+
client: Client,
12+
param_path: Union[Unset, str] = UNSET,
13+
param: Union[Unset, str] = UNSET,
14+
) -> Dict[str, Any]:
15+
url = "{}/same-name-multiple-locations/{param}".format(client.base_url, param=param_path)
16+
17+
headers: Dict[str, Any] = client.get_headers()
18+
cookies: Dict[str, Any] = client.get_cookies()
19+
20+
params: Dict[str, Any] = {
21+
"param": param,
22+
}
23+
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
24+
25+
return {
26+
"url": url,
27+
"headers": headers,
28+
"cookies": cookies,
29+
"timeout": client.get_timeout(),
30+
"params": params,
31+
}
32+
33+
34+
def _build_response(*, response: httpx.Response) -> Response[None]:
35+
return Response(
36+
status_code=response.status_code,
37+
content=response.content,
38+
headers=response.headers,
39+
parsed=None,
40+
)
41+
42+
43+
def sync_detailed(
44+
*,
45+
client: Client,
46+
param_path: Union[Unset, str] = UNSET,
47+
param: Union[Unset, str] = UNSET,
48+
) -> Response[None]:
49+
kwargs = _get_kwargs(
50+
client=client,
51+
param_path=param_path,
52+
param=param,
53+
)
54+
55+
response = httpx.get(
56+
**kwargs,
57+
)
58+
59+
return _build_response(response=response)
60+
61+
62+
async def asyncio_detailed(
63+
*,
64+
client: Client,
65+
param_path: Union[Unset, str] = UNSET,
66+
param: Union[Unset, str] = UNSET,
67+
) -> Response[None]:
68+
kwargs = _get_kwargs(
69+
client=client,
70+
param_path=param_path,
71+
param=param,
72+
)
73+
74+
async with httpx.AsyncClient() as _client:
75+
response = await _client.get(**kwargs)
76+
77+
return _build_response(response=response)

end_to_end_tests/openapi.json

+27
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,33 @@
760760
"200": {"description": "Success"}
761761
}
762762
}
763+
},
764+
"/same-name-multiple-locations/{param}": {
765+
"description": "Test that if you have a property of the same name in multiple locations, it produces valid code",
766+
"get": {
767+
"tags": ["parameters"],
768+
"parameters": [
769+
{
770+
"name": "param",
771+
"in": "query",
772+
"schema": {
773+
"type": "string"
774+
}
775+
},
776+
{
777+
"name": "param",
778+
"in": "path",
779+
"schema": {
780+
"type": "string"
781+
}
782+
}
783+
],
784+
"responses": {
785+
"200": {
786+
"description": ""
787+
}
788+
}
789+
}
763790
}
764791
},
765792
"components": {

openapi_python_client/parser/openapi.py

+11
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def _add_parameters(
217217
*, endpoint: "Endpoint", data: Union[oai.Operation, oai.PathItem], schemas: Schemas, config: Config
218218
) -> Tuple[Union["Endpoint", ParseError], Schemas]:
219219
endpoint = deepcopy(endpoint)
220+
used_python_names = set()
220221
if data.parameters is None:
221222
return endpoint, schemas
222223
for param in data.parameters:
@@ -232,6 +233,16 @@ def _add_parameters(
232233
)
233234
if isinstance(prop, ParseError):
234235
return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas
236+
237+
if prop.python_name in used_python_names:
238+
prop.set_python_name(f"{prop.python_name}_{param.param_in}")
239+
240+
if prop.python_name in used_python_names:
241+
return (
242+
ParseError(detail=f"Encountered duplicate properties named {prop.python_name}", data=data),
243+
schemas,
244+
)
245+
used_python_names.add(prop.python_name)
235246
endpoint.relative_imports.update(prop.get_imports(prefix="..."))
236247

237248
if param.param_in == ParameterLocation.QUERY:

openapi_python_client/parser/properties/property.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ class Property:
3232
json_is_dict: ClassVar[bool] = False
3333

3434
def __attrs_post_init__(self) -> None:
35-
object.__setattr__(self, "python_name", utils.to_valid_python_identifier(utils.snake_case(self.name)))
35+
self.set_python_name(self.name)
36+
37+
def set_python_name(self, new_name: str) -> None:
38+
object.__setattr__(self, "python_name", utils.to_valid_python_identifier(utils.snake_case(new_name)))
3639

3740
def get_base_type_string(self) -> str:
3841
return self._type_string

tests/test_parser/test_openapi.py

+41
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,47 @@ def test__add_parameters_happy(self, mocker):
543543
assert endpoint.header_parameters == [header_prop]
544544
assert schemas == schemas_3
545545

546+
def test__add_parameters_duplicate_properties(self, mocker):
547+
from openapi_python_client.parser.openapi import Endpoint, Schemas
548+
549+
endpoint = self.make_endpoint()
550+
parsed_schemas = mocker.MagicMock()
551+
mocker.patch(
552+
f"{MODULE_NAME}.property_from_data", return_value=(mocker.MagicMock(python_name="test"), parsed_schemas)
553+
)
554+
param = oai.Parameter.construct(name="test", required=True, param_schema=mocker.MagicMock(), param_in="path")
555+
data = oai.Operation.construct(parameters=[param, param, param])
556+
schemas = Schemas()
557+
config = MagicMock()
558+
559+
result = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config)
560+
assert result == (ParseError(data=data, detail="Encountered duplicate properties named test"), parsed_schemas)
561+
562+
def test__add_parameters_duplicate_properties_different_location(self):
563+
from openapi_python_client.parser.openapi import Endpoint, Schemas
564+
565+
endpoint = self.make_endpoint()
566+
path_param = oai.Parameter.construct(
567+
name="test", required=True, param_schema=oai.Schema.construct(type="string"), param_in="path"
568+
)
569+
query_param = oai.Parameter.construct(
570+
name="test", required=True, param_schema=oai.Schema.construct(type="string"), param_in="query"
571+
)
572+
schemas = Schemas()
573+
config = MagicMock()
574+
575+
result = Endpoint._add_parameters(
576+
endpoint=endpoint,
577+
data=oai.Operation.construct(parameters=[path_param, query_param]),
578+
schemas=schemas,
579+
config=config,
580+
)[0]
581+
assert isinstance(result, Endpoint)
582+
assert result.path_parameters[0].python_name == "test"
583+
assert result.path_parameters[0].name == "test"
584+
assert result.query_parameters[0].python_name == "test_query"
585+
assert result.query_parameters[0].name == "test"
586+
546587
def test_from_data_bad_params(self, mocker):
547588
from openapi_python_client.parser.openapi import Endpoint
548589

0 commit comments

Comments
 (0)