Skip to content

Commit 88ffbd6

Browse files
harabatharabatdbanty
authored
Fix hyphens in path parameters (#986)
Fixes #976 and #578, and replaces #978. @dbanty please choose your preferred approach between this and PR #987. The original issue is that `openapi-python-client` throws `incorrect path templating` warnings when the path has a parameter with a hyphen and consequently fails to generate the endpoints. --- The first commit ensures that hyphens are recognised as allowed delimiters in parameter path names. This allows the endpoints to be generated. However, this generates lines like these: ```python def _get_kwargs( user_id: int, ) -> Dict[str, Any]: _kwargs: Dict[str, Any] = { "method": "post", "url": "/activitypub/user-id/{user-id}/inbox".format(user-id=user_id,), } return _kwargs ``` Since Python variable names cannot contain hyphens, the `user-id` parameter name here will trigger errors (starting with `ruff`). --- The second commit replaces parameter names with their `python_name` in `__init__.py` and passes the modified path to `templates/endpoint_module.py.jinja`. This fixes the issue and allows endpoints to be generated correctly. --- #987 is a different option for the second commit which instead creates a custom Jinja filter in `utils.py` and so that the parameter names in `endpoint.path` can be converted to their python names directly in `templates/endpoint_module.py.jinja`. Both approaches are equivalent and have been tested with different parameter names (snake case, camel case, kebab case, mixed). --------- Co-authored-by: harabat <[email protected]> Co-authored-by: Dylan Anthony <[email protected]> Co-authored-by: Dylan Anthony <[email protected]>
1 parent b666ea2 commit 88ffbd6

File tree

7 files changed

+157
-3
lines changed

7 files changed

+157
-3
lines changed

Diff for: .changeset/allow_hyphens_in_path_parameters.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Allow hyphens in path parameters
6+
7+
Before now, path parameters which were invalid Python identifiers were not allowed, and would fail generation with an
8+
"Incorrect path templating" error. In particular, this meant that path parameters with hyphens were not allowed.
9+
This has now been fixed!
10+
11+
PR #986 fixed issue #976. Thanks @harabat!
12+
13+
> [!WARNING]
14+
> This change may break custom templates, see [this diff](https://github.com/openapi-generators/openapi-python-client/pull/986/files#diff-0de8437b26075d8fe8454cf47d8d95d4835c7f827fa87328e03f690412be803e)
15+
> if you have trouble upgrading.

Diff for: end_to_end_tests/baseline_openapi_3.0.json

+21
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,27 @@
14571457
}
14581458
}
14591459
},
1460+
"/naming/{hyphen-in-path}": {
1461+
"get": {
1462+
"tags": ["naming"],
1463+
"operationId": "hyphen_in_path",
1464+
"parameters": [
1465+
{
1466+
"name": "hyphen-in-path",
1467+
"in": "path",
1468+
"required": true,
1469+
"schema": {
1470+
"type": "string"
1471+
}
1472+
}
1473+
],
1474+
"responses": {
1475+
"200": {
1476+
"description": "Successful response"
1477+
}
1478+
}
1479+
}
1480+
},
14601481
"/parameter-references/{path_param}": {
14611482
"get": {
14621483
"tags": [

Diff for: end_to_end_tests/baseline_openapi_3.1.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,27 @@ info:
14511451
}
14521452
}
14531453
},
1454+
"/naming/{hyphen-in-path}": {
1455+
"get": {
1456+
"tags": [ "naming" ],
1457+
"operationId": "hyphen_in_path",
1458+
"parameters": [
1459+
{
1460+
"name": "hyphen-in-path",
1461+
"in": "path",
1462+
"required": true,
1463+
"schema": {
1464+
"type": "string"
1465+
}
1466+
}
1467+
],
1468+
"responses": {
1469+
"200": {
1470+
"description": "Successful response"
1471+
}
1472+
}
1473+
}
1474+
},
14541475
"/parameter-references/{path_param}": {
14551476
"get": {
14561477
"tags": [

Diff for: end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import types
44

5-
from . import mixed_case, post_naming_property_conflict_with_import
5+
from . import hyphen_in_path, mixed_case, post_naming_property_conflict_with_import
66

77

88
class NamingEndpoints:
@@ -13,3 +13,7 @@ def post_naming_property_conflict_with_import(cls) -> types.ModuleType:
1313
@classmethod
1414
def mixed_case(cls) -> types.ModuleType:
1515
return mixed_case
16+
17+
@classmethod
18+
def hyphen_in_path(cls) -> types.ModuleType:
19+
return hyphen_in_path
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from http import HTTPStatus
2+
from typing import Any, Dict, Optional, Union
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...types import Response
9+
10+
11+
def _get_kwargs(
12+
hyphen_in_path: str,
13+
) -> Dict[str, Any]:
14+
_kwargs: Dict[str, Any] = {
15+
"method": "get",
16+
"url": f"/naming/{hyphen_in_path}",
17+
}
18+
19+
return _kwargs
20+
21+
22+
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
23+
if response.status_code == HTTPStatus.OK:
24+
return None
25+
if client.raise_on_unexpected_status:
26+
raise errors.UnexpectedStatus(response.status_code, response.content)
27+
else:
28+
return None
29+
30+
31+
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
32+
return Response(
33+
status_code=HTTPStatus(response.status_code),
34+
content=response.content,
35+
headers=response.headers,
36+
parsed=_parse_response(client=client, response=response),
37+
)
38+
39+
40+
def sync_detailed(
41+
hyphen_in_path: str,
42+
*,
43+
client: Union[AuthenticatedClient, Client],
44+
) -> Response[Any]:
45+
"""
46+
Args:
47+
hyphen_in_path (str):
48+
49+
Raises:
50+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
51+
httpx.TimeoutException: If the request takes longer than Client.timeout.
52+
53+
Returns:
54+
Response[Any]
55+
"""
56+
57+
kwargs = _get_kwargs(
58+
hyphen_in_path=hyphen_in_path,
59+
)
60+
61+
response = client.get_httpx_client().request(
62+
**kwargs,
63+
)
64+
65+
return _build_response(client=client, response=response)
66+
67+
68+
async def asyncio_detailed(
69+
hyphen_in_path: str,
70+
*,
71+
client: Union[AuthenticatedClient, Client],
72+
) -> Response[Any]:
73+
"""
74+
Args:
75+
hyphen_in_path (str):
76+
77+
Raises:
78+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
79+
httpx.TimeoutException: If the request takes longer than Client.timeout.
80+
81+
Returns:
82+
Response[Any]
83+
"""
84+
85+
kwargs = _get_kwargs(
86+
hyphen_in_path=hyphen_in_path,
87+
)
88+
89+
response = await client.get_async_httpx_client().request(**kwargs)
90+
91+
return _build_response(client=client, response=response)

Diff for: openapi_python_client/parser/openapi.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .properties.schemas import parameter_from_reference
2727
from .responses import Response, response_from_data
2828

29-
_PATH_PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)}")
29+
_PATH_PARAM_REGEX = re.compile("{([a-zA-Z_-][a-zA-Z0-9_-]*)}")
3030

3131

3232
def import_string_from_class(class_: Class, prefix: str = "") -> str:
@@ -379,6 +379,8 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
379379
return ParseError(
380380
detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
381381
)
382+
for parameter in endpoint.path_parameters:
383+
endpoint.path = endpoint.path.replace(f"{{{parameter.name}}}", f"{{{parameter.python_name}}}")
382384
return endpoint
383385

384386
@staticmethod

Diff for: openapi_python_client/templates/endpoint_module.py.jinja

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _get_kwargs(
3131
{% if endpoint.path_parameters %}
3232
"url": "{{ endpoint.path }}".format(
3333
{%- for parameter in endpoint.path_parameters -%}
34-
{{parameter.name}}={{parameter.python_name}},
34+
{{parameter.python_name}}={{parameter.python_name}},
3535
{%- endfor -%}
3636
),
3737
{% else %}

0 commit comments

Comments
 (0)