Skip to content

Commit b19742e

Browse files
fix: generate valid code when a path parameter with a default precedes one without
1 parent 0a283fe commit b19742e

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

‎end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,79 @@ def test_path_params_with_hash_are_encoded(self, sync_detailed, Client):
149149
# Hash should be encoded as %23
150150
expected_url = "/items/item%231/details/detail%23id"
151151
assert call_kwargs["url"] == expected_url
152+
153+
154+
@with_generated_client_fixture(
155+
"""
156+
paths:
157+
"/test/{has_default}/{no_default}":
158+
get:
159+
operationId: getTest
160+
parameters:
161+
- name: has_default
162+
in: path
163+
required: true
164+
schema:
165+
type: string
166+
default: default_value
167+
- name: no_default
168+
in: path
169+
required: true
170+
schema:
171+
type: integer
172+
responses:
173+
"200":
174+
description: Success
175+
content:
176+
application/json:
177+
schema:
178+
type: object
179+
properties:
180+
result:
181+
type: string
182+
"""
183+
)
184+
@with_generated_code_import(".api.default.get_test.sync_detailed")
185+
@with_generated_code_import(".client.Client")
186+
class TestPathParameterWithDefaultBeforeRequired:
187+
"""A path parameter with a default may precede one without a default in the path.
188+
189+
The generated function must still be valid Python, and the default must be used
190+
when the parameter is not supplied.
191+
"""
192+
193+
def test_default_is_used_when_parameter_is_omitted(self, sync_detailed, Client):
194+
mock_httpx_client = MagicMock(spec=httpx.Client)
195+
mock_response = MagicMock(spec=httpx.Response)
196+
mock_response.status_code = 200
197+
mock_response.json.return_value = {"result": "ok"}
198+
mock_response.content = b'{"result": "ok"}'
199+
mock_response.headers = {}
200+
mock_httpx_client.request.return_value = mock_response
201+
202+
client = Client(base_url="https://api.example.com")
203+
client.set_httpx_client(mock_httpx_client)
204+
205+
sync_detailed(no_default=42, client=client)
206+
207+
mock_httpx_client.request.assert_called_once()
208+
call_kwargs = mock_httpx_client.request.call_args[1]
209+
assert call_kwargs["url"] == "/test/default_value/42"
210+
211+
def test_explicit_value_overrides_default(self, sync_detailed, Client):
212+
mock_httpx_client = MagicMock(spec=httpx.Client)
213+
mock_response = MagicMock(spec=httpx.Response)
214+
mock_response.status_code = 200
215+
mock_response.json.return_value = {"result": "ok"}
216+
mock_response.content = b'{"result": "ok"}'
217+
mock_response.headers = {}
218+
mock_httpx_client.request.return_value = mock_response
219+
220+
client = Client(base_url="https://api.example.com")
221+
client.set_httpx_client(mock_httpx_client)
222+
223+
sync_detailed(no_default=42, has_default="custom", client=client)
224+
225+
mock_httpx_client.request.assert_called_once()
226+
call_kwargs = mock_httpx_client.request.call_args[1]
227+
assert call_kwargs["url"] == "/test/custom/42"

‎openapi_python_client/parser/openapi.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ def sort_parameters(*, endpoint: "Endpoint") -> "Endpoint | ParseError":
376376
"""
377377
Sorts the path parameters of an `endpoint` so that they match the order declared in `endpoint.path`.
378378
379+
Parameters which declare a default are then moved after parameters which do not, preserving the
380+
path order within each group. Generated endpoint functions declare path parameters as
381+
positional-or-keyword arguments, so a parameter with a default followed by one without would be
382+
a `SyntaxError` in the generated code.
383+
379384
Args:
380385
endpoint: The endpoint to sort the parameters of.
381386
@@ -402,6 +407,8 @@ def sort_parameters(*, endpoint: "Endpoint") -> "Endpoint | ParseError":
402407
f"{{{parameter.name.get_untrusted_value()}}}", f"{{{parameter.python_name}}}"
403408
)
404409
)
410+
# Stable sort: parameters without a default must be declared before those with one
411+
endpoint.path_parameters.sort(key=lambda param: param.default is not None)
405412
return endpoint
406413

407414
@staticmethod

‎tests/test_parser/test_openapi.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from openapi_python_client.parser.errors import ParseError
88
from openapi_python_client.parser.openapi import Endpoint, EndpointCollection, import_string_from_class
99
from openapi_python_client.parser.properties import Class, IntProperty, Parameters, Schemas
10+
from openapi_python_client.parser.properties.protocol import Value
1011
from openapi_python_client.schema import DataType
1112
from openapi_python_client.schema.ref import Ref
1213
from openapi_python_client.schema.untrusted_string import UntrustedString
@@ -268,6 +269,22 @@ def test_sort_parameters(self, string_property_factory):
268269

269270
assert result_names == expected_names
270271

272+
def test_sort_parameters_moves_parameters_with_defaults_last(self, string_property_factory):
273+
endpoint = self.make_endpoint()
274+
endpoint.path = UntrustedString("/multiple-path-parameters/{param1}/{param2}/{param3}/{param4}")
275+
276+
endpoint.path_parameters.append(string_property_factory(name="param1", default=Value(PythonCode("'a'"), "a")))
277+
endpoint.path_parameters.append(string_property_factory(name="param2"))
278+
endpoint.path_parameters.append(string_property_factory(name="param3", default=Value(PythonCode("'c'"), "c")))
279+
endpoint.path_parameters.append(string_property_factory(name="param4"))
280+
281+
result = Endpoint.sort_parameters(endpoint=endpoint)
282+
283+
assert isinstance(result, Endpoint)
284+
result_names = [param.name for param in result.path_parameters]
285+
# Parameters without a default come first; path order is preserved within each group
286+
assert result_names == ["param2", "param4", "param1", "param3"]
287+
271288
def test_sort_parameters_missing_param(self, string_property_factory):
272289
endpoint = self.make_endpoint()
273290
endpoint.path = UntrustedString("/multiple-path-parameters/{param1}/{param2}")

0 commit comments

Comments
 (0)